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

## はじめに

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

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

[^1]: 個人の感想です。

## catch2

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

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

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

## zigからcatch2を呼び出す

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

https://github.com/catchorg/Catch2/blob/4e8d92bf02f7d1c8006a0e7a5ecabd8e62d98502/examples/232-Cfg-CustomMain.cpp#L12

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

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

c++側

```cpp
#include <catch2/catch_session.hpp>

extern "C" {

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

}
```

zig側

```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]

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

解決案として、

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

とのこと。

前者は

```cpp
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);
}
```

後者は

```cpp
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;
}
```

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

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

:::

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

```zig
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`でくくると、テストコードをプロダクトコードから排除できる

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

// ここにテストを書く

#endif
```

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

```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`にあげました。

https://github.com/ritalin/catch2-from-zig