cmdtest v0.2.0
This blog post highlight my experience and stuff that I have learned while
writing cmdtest.
cmdtest is a Zig package that you can use to test your CLI app.
Here is how you can install and use the cmdtest:
-
Fetch the latest release:
Shellzig fetch --save=cmdtest https://github.com/pyk/cmdtest/archive/v0.2.0.tar.gzThis updates
build.zig.zon. -
Write your test file. Example:
test/mycli.zig.Zigconst std = @import("std"); const cmdtest = @import("cmdtest"); const testing = std.testing; test "via exe name" { const argv = &[_][]const u8{"mycli"}; var result = try cmdtest.run(.{ .argv = argv }); defer result.deinit(); try testing.expectEqualStrings("project-exe", result.stderr); } test "via path" { const argv = &[_][]const u8{"./zig-out/bin/mycli"}; var result = try cmdtest.run(.{ .argv = argv }); defer result.deinit(); try testing.expectEqualStrings("project-exe", result.stderr); } -
Register the test in
build.zig:Zigconst std = @import("std"); const cmdtest = @import("cmdtest"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); // Your CLI const cli = b.addExecutable(.{ .name = "mycli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, }), }); b.installArtifact(cli); // Register new test const cli_test = cmdtest.add(b, .{ .name = "mycli", .test_file = b.path("test/mycli.zig"), }); const test_step = b.step("test", "Run tests"); test_step.dependOn(&cli_test.step); } -
Run the tests:
Shellzig build test --summary all
See minimal Zig project cmdtest-example.
Learning about I/O
The first thing that I have learned while publishing the package is about the I/O.
Zig 0.15.2 introduces std.Io.Writer which if you are dealing with the stdin,
stdout and stderr, the std.Io.Writer is your friend.
Here is how to write to stdout using std.Io.Writer:
var buf: [1024]u8 = undefined;
var writer = std.fs.File.stdout().writer(&buf);
const io = &writer.interface;
try io.writeAll("Hello\n");
try io.flush();The writer.interface is caught me offguard. I thought I can simply do this:
var writer = std.fs.File.stdout().writer(&buf);
try writer.writeAll("Hello");
try writer.flush();however its not the case.
I also learned that the buf is not the output, its like staging area before
bytes got flushed to the target.
Comptime vs Runtime Values
The second thing that I have learned is about comptime value vs runtime value.
In cmdtest I have use case where I want to write syntax sugar for array of
string literal for argv.
Here is how to write array of string literal in zig:
const argv = &[_][]const u8{"echo", "hello"};I thought, can I simply write cmd("echo", "hello") and automatically in zig?
The answer is Zig does not support variadic arguments. So it does not work.
Then lets try to use tuple: cmd(.{"echo", "hello"}).
Well it looks better, but can we implement it?
pub fn cmd(comptime argv: anytype) []const []const u8 {
const len = comptime {
// Get length at compile time for tuple here
}
var arr: [len][]const u8 = undefined;
comptime var i: usize = 0;
while (i < len) : (i += 1) {
arr[i] = argv[i];
}
return arr[0..len];
}This trick work if the tuple have only one element, however it will not work if
the tuple have more than one element. It’s segfault. I believe this caused by
the fact that arr lifetime is tied to the cmd function.
build.zig Import Behavior
Third thing that I have learned is about the import behaviour in build.zig.
I thought when I write the @import("cmdtest") in build.zig it will
automatically uses the src/root.zig of my package.
Turns out I was wrong. If you import package from your build.zig it will get
the build.zig of the package.
So previously I defined the add function inside src/root.zig, now the add
function is in build.zig
pub const AddOptions = struct {
/// Name of the test target
name: []const u8,
/// Path to the test source file
test_file: Build.LazyPath,
/// The `cmdtest` build module to import into the test
cmdtest_mod: ?*Build.Module = null,
};
/// Register new test
pub fn add(b: *Build, options: AddOptions) *Build.Step.Run {
const cmdtest_mod = if (options.cmdtest_mod) |mod|
mod
else
b.dependency("cmdtest", .{
.target = b.graph.host,
}).module("cmdtest");
// Create the test module that imports the runtime module
const test_mod = b.createModule(.{
.root_source_file = options.test_file,
.target = b.graph.host,
.imports = &.{
.{
.name = "cmdtest",
.module = cmdtest_mod,
},
},
});
// Create the test executable compilation step
const test_exe = b.addTest(.{
.name = options.name,
.root_module = test_mod,
});
const run_test_exe = b.addRunArtifact(test_exe);
// IMPORTANT: Make sure all exe are installed first
run_test_exe.step.dependOn(b.getInstallStep());
const original_path = b.graph.env_map.get("PATH") orelse "";
const path = b.fmt("{s}{c}{s}", .{
b.exe_dir,
std.fs.path.delimiter,
original_path,
});
run_test_exe.setEnvironmentVariable("PATH", path);
return run_test_exe;
}And that’s it. I love zig because its very explicit about the memory allocation
and how they provide std.testing.allocator to catch memory management mistake
early in the development.
| Tags | zig , testing , cli |
|---|