Hi everyone (this is actually my first post here)!
Since no one has done it yet, I just want to point out that it’s not very complicated to generate those tests the old-school way: generating a text file.
This is my example main.zig (without the actual main function because we only care about tests). It includes the hello-world test from zig init-exe as well as a few test cases that I created.
const std = @import("std");
pub const tests = .{
struct {
pub const name = "simple_test";
pub fn run() !void {
var list = std.ArrayList(i32).init(std.testing.allocator);
defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
try list.append(42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}
},
ParamTest_isSmallNumber("isSmallNumber", 1, true),
ParamTest_isSmallNumber("isSmallNumber", 10, true),
ParamTest_isSmallNumber("isSmallNumber", 100, false),
};
fn isSmallNumber(number: u32) bool {
return number < 101;
}
fn ParamTest_isSmallNumber(comptime base_name: []const u8, comptime input: u32, comptime expected_result: bool) type {
comptime var buffer: [base_name.len + 10]u8 = undefined;
comptime var name_with_input = try std.fmt.bufPrint(&buffer, "{s} ({d})", .{base_name, input});
return struct {
pub const name = name_with_input;
pub fn run() !void {
const result = isSmallNumber(input);
try std.testing.expectEqual(expected_result, result);
}
};
}
This is my generate_tests.zig which does what the name says.
const std = @import("std");
const source_tuples = .{
.{ "main", @import("main.zig") },
};
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var text = std.ArrayList(u8).init(arena.allocator());
inline for (source_tuples) |tuple| {
inline for (tuple[1].tests, 0..) |test_struct, index| {
try text.writer().print("test \"{s}\" {{\n try @import(\"{s}.zig\").tests[{d}].run();\n}}\n",
.{ test_struct.name, tuple[0], index });
}
}
try std.fs.cwd().writeFile("src/generated_tests.zig", text.items);
}
In my build.zig, I set it up so that tests are generated before being executed.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const generate_tests = b.addExecutable(.{
.name = "generate_tests",
.root_source_file = .{ .path = "src/generate_tests.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const generate_tests_cmd = b.addRunArtifact(generate_tests);
generate_tests_cmd.step.dependOn(b.getInstallStep());
const generate_tests_step = b.step("generate_tests", "Generate the tests");
generate_tests_step.dependOn(&generate_tests_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/generated_tests.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(generate_tests_step);
test_step.dependOn(&run_unit_tests.step);
}
When I now just type zig build test
, the following generated_tests.zig file is created before being used for testing.
test "simple_test" {
try @import("main.zig").tests[0].run();
}
test "isSmallNumber (1)" {
try @import("main.zig").tests[1].run();
}
test "isSmallNumber (10)" {
try @import("main.zig").tests[2].run();
}
test "isSmallNumber (100)" {
try @import("main.zig").tests[3].run();
}
And the output of running the tests looks like this (one of the test fails, of course).
run test: error: 'test.isSmallNumber (100)' failed: expected false, found true
<some_path>/testing.zig:84:17: 0x224fe2 in expectEqual__anon_2816 (test)
return error.TestExpectedEqual;
^
<some_path>/src/main.zig:34:13: 0x2251a0 in run (test)
try std.testing.expectEqual(expected_result, result);
^
<some_path>/src/generated_tests.zig:11:5: 0x2251e3 in test.isSmallNumber (100) (test)
try @import("main.zig").tests[3].run();
^
run test: error: while executing test 'test.isSmallNumber (100)', the following test command failed:
<some_path>/test --listen=-
Build Summary: 8/10 steps succeeded; 1 failed; 3/4 tests passed; 1 failed (disable with --summary none)
test transitive failure
└─ run test 3/4 passed, 1 failed
error: the following build command failed with exit code 1:
<some_path>/build <some_path>/zig <some_path> <some_path>/zig-cache <some_path>/zig test
I just typed this down in a few minutes so there is probably a lot of stuff that can be improved or scenarios where it doesn’t work. It does seem rather simple and effective to me though.
Update: I have to admit that it doesn’t simple very simple to create that tests tuple dynamically from an enum (or list of values in any form) like the OP wants to.