はじめに
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
にあげました。