Let’s say I am writing a CLI utility, which, say, prints hello world:
// ./main.zig
const std = @import("std");
pub fn main() !void {
try std.io.getStdOut().writeAll("hello world");
}
Now, I want to write an integration test to run the utility and check its output, something like this:
// ./tests.zig
const std = @import("std");
test "it prints hello world" {
const exe_path: []const u8 = get_exe_path();
const exec_result = try std.ChildProcess.exec(.{
.allocator = std.testing.allocator,
.argv = &.{exe_path},
});
try std.testing.expectEqualStrings(
"hello world",
exec_result.stdout,
);
}
fn get_exe_path() []const u8 {
???
}
What’s the best way to get a path to exe file? That is, how should I write my build.zig such that it compiles the exe before the tests are run, and somehow injects path to the exe into tests?
I can write
// ./build.zig
const std = @import("std");
pub fn build(b: *std.build.Builder) !void {
const hello = b.addExecutable(.{
.name = "hello",
.root_source_file = .{ .path = "./main.zig" },
});
const tests = b.addTest(.{
.root_source_file = .{ .path = "./tests.zig" },
});
const run_unit_tests = b.addRunArtifact(tests);
{ // Key Point: injecting path to exe
run_unit_tests.addArg("--exe");
run_unit_tests.addArtifactArg(hello);
}
const test_tls = b.step("test", "run the tests");
test_tls.dependOn(&hello.step);
test_tls.dependOn(&run_unit_tests.step);
}
but this doesn’t work, as the test harness doesn’t expect the --exe
argument.
Another option is adding an environmental variable, but setEnvironmentVariable
requires a string, not a LazyPath or Artifact.
The third option is to install the binary into a well-known location, but that doesn’t feel right: eg, I might want to run debug and release tests concurrently, but, with a hard-coded path, I can’t.
How should this work?