[Zig] メインプロジェクトからサブプロジェクトのテストを一括実行する


:::note info
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`でプロジェクトを作成した場合に、以下のようなテスト構成でテンプレートから作成される

1. `std.Build.addTest`を呼んでテスト用のコンパイル構成を作成する。
2. `std.Build.addRunArtifact`を呼んでテストランナーを作成する。

となっている。

そしてこの作成されるテストランナーは、プロジェクト内に閉じたものであるため、外側のプロジェクトからは呼べない[^1]。
[^1]: 正確には外部コマンドを叩く構成を追加し、カレントディレクトリをサブプロジェクトに移して`zig build test`を実行させればできなくもない。

また、テストのコンパイル結果はプロジェクト内の`.zig-cache`にいるため、特定は容易ではない[^2]。
[^2]: `zig build test --verbose`でパスを知ることは一応できる。

### 実施手順

#### 1\. サブプロジェクトの構成

事前準備として、サブプロジェクトでのテスト構成を作成する際、以下のような記述にする。

```zig
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`に追加する。

```zig
.{
    .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`に以下の関数を作成する

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

この関数では以下のことを行なっている。

1. `zig build test`用のランナーを作成する
2. `build.zig.zon`に追加された全ての依存パッケージを列挙
3. そのなかで、ステップ名が`test`となっているものを省く
4. 依存パッケージの中から、さらに`std.Build.Step.InstallArtifact`以外のステップを省く
5. 成果物の`kind`フィールドが`@test`以外を省く[^3]
6. メインプロジェクトに成果物としてインストールする
その際、`zig-out/test`下にインストールすることで混在することを避けている
. `std.Build.addSystemCommand`でインストールしたテストを実行するよう構成する
8. メインプロジェクトのランナーに関連づける

[^3]: `std.Build.addTest`は`kind`フィールドが`@test`で作成される。

#### 4\. addTestAllを呼ぶ

`build()`関数の最後で呼んでおけば良いかと。

```
pub fn build(b: std.Build) void {
    // 省略
    addTestAll();
}
```

#### 5\. 実行

`zig build test`の実行で全てのサブプロジェクトのテストが実行される。
この記述の良いところは、新たなサブプロジェクトを追加する際に、同じような記述を行っておくと自動的にトラックしてくれることである。

### おまけ

サブプロジェクトでテストを成果物として登録した効果として、`VS Code`等でデバック構成として登録しておくことができる。

困った時デバッガ頼りのときにスムーズに開始できて嬉しい。