Skip to content

Instantly share code, notes, and snippets.

@nurpax
Last active July 21, 2025 06:54
Show Gist options
  • Save nurpax/4afcb6e4ef3f03f0d282f7c462005f12 to your computer and use it in GitHub Desktop.
Save nurpax/4afcb6e4ef3f03f0d282f7c462005f12 to your computer and use it in GitHub Desktop.
zig test runner
// Modded from from https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b
// in your build.zig, you can specify a custom test runner:
// const tests = b.addTest(.{
// .target = target,
// .optimize = optimize,
// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple }, // add this line
// .root_source_file = b.path("src/main.zig"),
// });
//
// Tested on versions:
// - zig-0.14
// - zig-0.15.0-dev.1092+d772c0627.
//
// Note: doesn't support std.testing.fuzz()
const std = @import("std");
const builtin = @import("builtin");
const BORDER = "=" ** 80;
const Status = enum {
pass,
fail,
skip,
text,
};
fn getenvOwned(alloc: std.mem.Allocator, key: []const u8) ?[]u8 {
const v = std.process.getEnvVarOwned(alloc, key) catch |err| {
if (err == error.EnvironmentVariableNotFound) {
return null;
}
std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
return null;
};
return v;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
const alloc = gpa.allocator();
const fail_first = blk: {
if (getenvOwned(alloc, "TEST_FAIL_FIRST")) |e| {
defer alloc.free(e);
break :blk std.mem.eql(u8, e, "true");
}
break :blk false;
};
const filter = getenvOwned(alloc, "TEST_FILTER");
defer if (filter) |f| alloc.free(f);
const printer = Printer.init();
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
var pass: usize = 0;
var fail: usize = 0;
var skip: usize = 0;
var leak: usize = 0;
for (builtin.test_functions) |t| {
std.testing.allocator_instance = .{};
var status = Status.pass;
if (filter) |f| {
if (std.mem.indexOf(u8, t.name, f) == null) {
continue;
}
}
printer.fmt("Testing {s}: ", .{t.name});
const result = t.func();
if (std.testing.allocator_instance.deinit() == .leak) {
leak += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, t.name, BORDER });
}
if (result) |_| {
pass += 1;
} else |err| {
switch (err) {
error.SkipZigTest => {
skip += 1;
status = .skip;
},
else => {
status = .fail;
fail += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, t.name, @errorName(err), BORDER });
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
if (fail_first) {
break;
}
},
}
}
printer.status(status, "[{s}]\n", .{@tagName(status)});
}
const total_tests = pass + fail;
const status: Status = if (fail == 0) .pass else .fail;
printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
if (skip > 0) {
printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
}
if (leak > 0) {
printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
}
std.process.exit(if (fail == 0) 0 else 1);
}
const Printer = struct {
fn init() Printer {
return .{};
}
fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
_ = self; // autofix
std.debug.print(format, args);
}
fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void {
const color = switch (s) {
.pass => "\x1b[32m",
.fail => "\x1b[31m",
.skip => "\x1b[33m",
else => "",
};
std.debug.print("{s}", .{color});
std.debug.print(format, args);
self.fmt("\x1b[0m", .{});
}
};
@utensil
Copy link

utensil commented Jul 14, 2025

This no longer works for a recent zig master version (mine: 0.15.0-dev.1034+bd97b6618). My adapted, ported and working version is here: https://github.com/utensil/native-land/blob/main/yard-zig/basic-xp/test_runner.zig

It also added some features and tweaks, before the port. It looks like this for zig build test --summary all:

// omitted....
simd.test.vector swizzle operations [pass]
simd.test.vector min/max operations [pass]
simd.test.vector bitwise operations [pass]
simd.test.vector floating point operations [pass]
simd.test.vector load and store operations [pass]
simd: 12 of 12 tests passed
string_literals.test.multiline string literals vs regular strings [pass]
string_literals.test.multiline strings preserve backslashes [pass]
string_literals.test.multiline strings for code generation [pass]
string_literals.test.multiline strings preserve trailing whitespace [pass]
string_literals.test.@embedFile vs multiline strings [pass]
string_literals: 5 of 5 tests passed
Total: 83 of 83 tests passed
Build Summary: 5/5 steps succeeded
test success
+- run test success 243ms MaxRSS:2M
|  +- compile test Debug native success 842ms MaxRSS:282M
+- run test success 239ms MaxRSS:1M
   +- compile test Debug native success 719ms MaxRSS:244M

Disclaimer: Some code is coded with the help of a coding agent. It figured out how to fix the errors by trying a few different ways. I checked the latest document and nudged it in the right direction.

@nurpax
Copy link
Author

nurpax commented Jul 21, 2025

Thanks @utensil! I updated to gist to use std.debug.print() (like in your version) and it should now work on zig-0.14 and 0.15.0-dev.1092+d772c0627. Using std.debug.print is probably a safer path to keep this compiling than updating to use the new Io interface. (Honestly, I couldn't get the old std.fs.File based code to compile. ;))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment