Skip to content

Instantly share code, notes, and snippets.

@nurpax
Last active February 5, 2025 00:26
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 = b.path("test_runner.zig"), // add this line
// .root_source_file = b.path("src/main.zig"),
// });
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 {
out: std.fs.File.Writer,
fn init() Printer {
return .{
.out = std.io.getStdErr().writer(),
};
}
fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
std.fmt.format(self.out, format, args) catch unreachable;
}
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 => "",
};
const out = self.out;
out.writeAll(color) catch @panic("writeAll failed?!");
std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!");
self.fmt("\x1b[0m", .{});
}
};
@fanyang89
Copy link

std.os.exit is not portable. A simple fix is using std.process.exit

Replace

std.os.exit(if (fail == 0) 0 else 1);

To

std.process.exit(if (fail == 0) 0 else 1);

@fanyang89
Copy link

And another fix:

const test_name = t.name[5..];

to

const test_name = t.name;

@bahodge
Copy link

bahodge commented Jul 15, 2024

After adding the comments that @fanyang89 suggests, the runner works great. This is pretty helpful on smaller projects. Great work @nurpax

One thing that also needs to change is in zig 0.13.0 you need to change test_runner.zig to b.path("test_runner.zig")

Here is an example

    const exe_unit_tests = b.addTest(.{
        .root_source_file = b.path("src/test.zig"),
        .target = target,
        .optimize = optimize,
        .test_runner = b.path("src/test_runner.zig"),
    });

@nurpax
Copy link
Author

nurpax commented Oct 8, 2024

Thanks everyone for the suggestions! I updated the original source with the fixes from comments.

@SimonMeskens
Copy link

What's the license on this?

@nurpax
Copy link
Author

nurpax commented Feb 4, 2025

Good question. It's adapted from https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b. I'd be ok with MIT.

I wish something like this was included in Zig's own test runner.

@SimonMeskens
Copy link

I asked there too. Without a license I can't really use it for anything real unfortunately, but it's nice to know that this one will become MIT if the other one picks a compatible license.

@SimonMeskens
Copy link

Upstream project was clarified as being MIT \o/

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