EDIT (2024/11/03):
バージョン0.14以降、std.Build.initialized_deps
フィールドは廃止された。
かわりにstd.Build.graph.dependency_cache
を走査する。
このフィールドの型はstd.HashMapUnmanaged
であり、値の型は*std.Build.Dependency
で0.13の時と同じ
あらまし
たとえば以下のようなフォルダ構成。
root
├── 📁sub-proj_1
│ ├── 📁src
│ └── 📃build.zig
├── 📁sub-proj_2
│ ├── 📁src
│ └── 📃build.zig
├── 📁sub-proj_3
│ ├── 📁src
│ └── 📃build.zig
├── 📃build.zig
root
がメインプロジェクトで直下にbuild.zig
を持つ。
そしてサブプロジェクトが配下に置かれている。
そんな構成。
各サブプロジェクトは、ユニットテスト用のビルド設定が定義されていて、zig build test
で実行できるものとする。
このような環境でメインプロジェクトから全てのサブプロジェクトのテストを一括に実行するにはどうするのがスマートか。
その一例を思いついたので記録として残す。
方法(あかんやつ)
一番簡単な方法は、各サブプロジェクトのテスト定義をメインプロジェクトのbuild.zig
にまるっとコピーすることである。
しかしこの方法は、サブプロジェクトの変更を常にトラックしてメインプロジェクトに反映させる必要があるため煩雑である。何より美しくない。
方法
zig init
でプロジェクトを作成した場合に、以下のようなテスト構成でテンプレートから作成される
-
std.Build.addTest
を呼んでテスト用のコンパイル構成を作成する。 -
std.Build.addRunArtifact
を呼んでテストランナーを作成する。
となっている。
そしてこの作成されるテストランナーは、プロジェクト内に閉じたものであるため、外側のプロジェクトからは呼べない1。
また、テストのコンパイル結果はプロジェクト内の.zig-cache
にいるため、特定は容易ではない2。
実施手順
1. サブプロジェクトの構成
事前準備として、サブプロジェクトでのテスト構成を作成する際、以下のような記述にする。
const exe_unit_tests = b.addTest(.{
.name = "test-sub-proj_1{s}-{s}",,
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// テスト構成は省略
// ランナーを作成する
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
// 成果物としてインストールする
test_step.dependOn(&b.addInstallArtifact(exe_unit_tests, .{.dest_sub_path = "../test/sub-proj_1"}).step);
ポイントは2点。
- テストに名前をつけること
これは、後でメインプロジェクトからテスト構成を収集するが、その際デフォの設定の構成を省くためである。
たいていの公開されたパッケージは、名前を付け替えるまではしないため、これを行うことでサブプロジェクトのみを効果的に収集できるようになる(と思ってる)。 - 成果物として、
zig-out
下にインストールすること
メインプロジェクトからの収集の目印として使用する。
2. サブプロジェクトをメインプロジェクトに追加
サブプロジェクトをメインプロジェクトのbuild.zig.zon
に追加する。
.{
.name = "main_proj",
.version = "0.0.0",
.dependencies = .{
.sub_proj_1 = .{ .path = ".sub-proj_1" },
.sub_proj_2 = .{ .path = ".sub-proj_2" },
.sub_proj_3 = .{ .path = ".sub-proj_3" },
}
.paths = .{}
}
この際.path
で指定するのが便利。
3. サブプロジェクトのテストを収集
メインプロジェクトのzig.build
に以下の関数を作成する
fn addTestAll(b: *std.Build) void {
const run_step = b.step("test", "Run all unit tests");
// 0.13系までの書き方
// var deps_iter = b.initialized_deps.valueIterator();
// 以下は0.14系の書き方
var deps_iter = b.graph.dependency_cache.valueIterator();
while(deps_iter.next()) |dep| {
var tls_iter = dep.*.builder.top_level_steps.iterator();
while (tls_iter.next()) |entry| {
const tls = entry.value_ptr.*;
if (! std.mem.eql(u8, tls.step.name, "test")) continue;
for (tls.step.dependencies.items) |dep_step| {
if (dep_step.id != .install_artifact) continue;
const inst: *std.Build.Step.InstallArtifact = dep_step.cast(std.Build.Step.InstallArtifact) orelse continue;
if (inst.artifact.kind == .@"test") {
const path = b.pathResolve(&.{"test/", inst.artifact.name});
std.debug.print("Test found: {s}\n", .{path});
// install test artifact
const install_step = b.addInstallArtifact(
inst.artifact,
.{
.dest_sub_path = path,
.dest_dir = .{.override = .prefix}
}
);
// invoke test
const invoke_step = b.addSystemCommand(&.{b.pathResolve(&.{b.install_prefix, path})});
invoke_step.step.dependOn(&install_step.step);
run_step.dependOn(&invoke_step.step);
}
}
}
}
}
この関数では以下のことを行なっている。
-
zig build test
用のランナーを作成する -
build.zig.zon
に追加された全ての依存パッケージを列挙 - そのなかで、ステップ名が
test
となっているものを省く - 依存パッケージの中から、さらに
std.Build.Step.InstallArtifact
以外のステップを省く - 成果物の
kind
フィールドが@test
以外を省く3 - メインプロジェクトに成果物としてインストールする
その際、zig-out/test
下にインストールすることで混在することを避けている
.std.Build.addSystemCommand
でインストールしたテストを実行するよう構成する - メインプロジェクトのランナーに関連づける
4. addTestAllを呼ぶ
build()
関数の最後で呼んでおけば良いかと。
pub fn build(b: std.Build) void {
// 省略
addTestAll();
}
5. 実行
zig build test
の実行で全てのサブプロジェクトのテストが実行される。
この記述の良いところは、新たなサブプロジェクトを追加する際に、同じような記述を行っておくと自動的にトラックしてくれることである。
おまけ
サブプロジェクトでテストを成果物として登録した効果として、VS Code
等でデバック構成として登録しておくことができる。
困った時デバッガ頼りのときにスムーズに開始できて嬉しい。