0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zig言語からC++テストフレームワーク:Catch2を呼ぶ

Last updated at Posted at 2024-06-18

はじめに

extern "C"を中継する必要はあるが、Zig言語から、C++のコードを呼ぶのは結構便利だったりする。
理由は、C++童貞が難解なCMakeの記法を理解するのが難解。1

それに比べZigのビルドスクリプトは、インクルードファイルとソース、(場合によっては静的ライブラリ)のパスを加えておくだけですむので楽ちん。 1

catch2

C++側で定義する型情報をzig側で再定義するのはメンドイので、できればユニットテストもC++側で完結したい。

Wikipediaのユニットテストのページを舐め回してたところ、直感でcatch2が刺さった。
理由としては、

  • インサイドコードテストが書きやすそう
  • BDDもいける口

zigからcatch2を呼び出す

公式レポジトリのサンプルを見たところ、

Catch::Sessionインスタンスを用意し、run()メソッドを呼べば良さそう

ってことで、以下のようなコードを書いた。

c++側

#include <catch2/catch_session.hpp>

extern "C" {

auto run_catch2_test() -> int {
    Catch::Session session;
    return session.run();
}

}

zig側

const std = @import("std");

const run_catch2_test = @import("./root.zig").run_catch2_test;

test "call catch2 test" {
    run_catch2_test();
}

zig build testを実行したところ、session.run()でブロックされテストが終了しなかった。

CharGPT先生と壁打ちしたところ、どうも標準出力、標準エラーがzigのテストランタイムにブロックされてるっぽいとの指摘をもらった。2

解決案として、

  • コマンドラインオプションとして、-o <FILE>を渡す
  • 標準出力をリダイレクトする

とのこと。

前者は

auto run_catch2_test() -> int {
    int argc = 3;
    const char* argv[] = { "your_program_name", "-o", "result.txt"};

    Catch::Session session;
    return sesson.run(argc, argv);
}

後者は

auto run_catch2_test() -> int {
    // 標準出力をファイルにリダイレクト
    std::ofstream out("output.txt");
    std::streambuf* coutbuf = std::cout.rdbuf(); // 標準出力のバッファを保存
    std::cout.rdbuf(out.rdbuf()); // 標準出力をリダイレクト

    int argc = 2;
    const char* argv[] = { "your_program_name", "--success" };

    Catch::Session session;

    // Apply the command line arguments
    int returnCode = session.applyCommandLine(argc, argv);
    if (returnCode != 0) { // Indicates a command line error
        std::cout << "Error parsing command line arguments\n";
        return -1;
    }

    // Run the tests and capture the result
    int result = session.run();
    
    std::cout.rdbuf(coutbuf); // 標準出力を元に戻す
    return result;
}

いずれの方法でもブロックが回避できた。

プロダクトコードで標準出力を行っている場合、-o <FILE>は出力がブロックされてしまう。
この場合後者の、標準出力の差し替えが必要となる。

最終的には、zig側でcatch2::Session::run()の戻り値を検証して、結果を表示するようにした。

test "call catch2 test" {
    const allocator = std.testing.allocator;

    const err = run_catch2_test();
    
    if (err > 0) {
        // ファイルからCatch2の結果を読み取る
        const file = try std.fs.cwd().openFile("output.txt", .{});
        defer file.close();

        const meta = try file.metadata();
        const data = try file.readToEndAlloc(allocator, meta.size());
        defer allocator.free(data);

        std.debug.print("Catch2 output:\n{s}\n", .{data}); 
    }

    try testing.expectEqual(0, err);
}

追記 2024-06-19

インサイドコードテストとして記述した場合、そのままだとテストコードがプロダクトコードのリンク対象となってしまう。

そのため、テストコードを#ifdef/#endifでくくると、テストコードをプロダクトコードから排除できる

#ifdef CATCH2_TEST
#include <catch2/catch_test_macros.hpp>

// ここにテストを書く

#endif

定数はbuild.zigで、ユニットテスト用のコンパイル定義にて指定する

// build.zig
    const exe_unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe_unit_tests.defineCMacro("CATCH2_TEST", "1");

追記 2024-06-19#2

上の方法でも問題なくビルド&テストできるが、デフォでマクロの値が定義されていないため、VS Codeでソースを表示すると無効状態扱いになり精神安定上良くない。

上記とは逆に`DISABLE_CATCH2_TESTを用意し、

まとめ

zig言語からcatch2を使う利用者が、天の川銀河系に自分以外に1人いればめっけもの。

おまけ

確認用のzigプロジェクトをgithubにあげました。

  1. 個人の感想です。 2

  2. 別途通常の実行モジュールから呼び出したところ、問題なく成功したことも決定的な要因

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?