A poolable string builder (aka string buffer) for Zig
9.1 kB
294 lines
1// in your build.zig, you can specify a custom test runner:
2// const tests = b.addTest(.{
3// .root_module = $MODULE_BEING_TESTED,
4// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple },
5// });
6
7const std = @import("std");
8const builtin = @import("builtin");
9
10const Allocator = std.mem.Allocator;
11
12const BORDER = "=" ** 80;
13
14// use in custom panic handler
15var current_test: ?[]const u8 = null;
16
17pub fn main() !void {
18 var mem: [8192]u8 = undefined;
19 var fba = std.heap.FixedBufferAllocator.init(&mem);
20
21 const allocator = fba.allocator();
22
23 const env = Env.init(allocator);
24 defer env.deinit(allocator);
25
26 var slowest = SlowTracker.init(allocator, 5);
27 defer slowest.deinit();
28
29 var pass: usize = 0;
30 var fail: usize = 0;
31 var skip: usize = 0;
32 var leak: usize = 0;
33
34 Printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
35
36 for (builtin.test_functions) |t| {
37 if (isSetup(t)) {
38 t.func() catch |err| {
39 Printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err });
40 return err;
41 };
42 }
43 }
44
45 for (builtin.test_functions) |t| {
46 if (isSetup(t) or isTeardown(t)) {
47 continue;
48 }
49
50 var status = Status.pass;
51 slowest.startTiming();
52
53 const is_unnamed_test = isUnnamed(t);
54 if (env.filter) |f| {
55 if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
56 continue;
57 }
58 }
59
60 const friendly_name = blk: {
61 const name = t.name;
62 var it = std.mem.splitScalar(u8, name, '.');
63 while (it.next()) |value| {
64 if (std.mem.eql(u8, value, "test")) {
65 const rest = it.rest();
66 break :blk if (rest.len > 0) rest else name;
67 }
68 }
69 break :blk name;
70 };
71
72 current_test = friendly_name;
73 std.testing.allocator_instance = .{};
74 const result = t.func();
75 current_test = null;
76
77 const ns_taken = slowest.endTiming(friendly_name);
78
79 if (std.testing.allocator_instance.deinit() == .leak) {
80 leak += 1;
81 Printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
82 }
83
84 if (result) |_| {
85 pass += 1;
86 } else |err| switch (err) {
87 error.SkipZigTest => {
88 skip += 1;
89 status = .skip;
90 },
91 else => {
92 status = .fail;
93 fail += 1;
94 Printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
95 if (@errorReturnTrace()) |trace| {
96 std.debug.dumpStackTrace(trace.*);
97 }
98 if (env.fail_first) {
99 break;
100 }
101 },
102 }
103
104 if (env.verbose) {
105 const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
106 Printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
107 } else {
108 Printer.status(status, ".", .{});
109 }
110 }
111
112 for (builtin.test_functions) |t| {
113 if (isTeardown(t)) {
114 t.func() catch |err| {
115 Printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err });
116 return err;
117 };
118 }
119 }
120
121 const total_tests = pass + fail;
122 const status = if (fail == 0) Status.pass else Status.fail;
123 Printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
124 if (skip > 0) {
125 Printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
126 }
127 if (leak > 0) {
128 Printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
129 }
130 Printer.fmt("\n", .{});
131 try slowest.display();
132 Printer.fmt("\n", .{});
133 std.posix.exit(if (fail == 0) 0 else 1);
134}
135
136const Printer = struct {
137 fn fmt(comptime format: []const u8, args: anytype) void {
138 std.debug.print(format, args);
139 }
140
141 fn status(s: Status, comptime format: []const u8, args: anytype) void {
142 switch (s) {
143 .pass => std.debug.print("\x1b[32m", .{}),
144 .fail => std.debug.print("\x1b[31m", .{}),
145 .skip => std.debug.print("\x1b[33m", .{}),
146 else => {},
147 }
148 std.debug.print(format ++ "\x1b[0m", args);
149 }
150};
151
152const Status = enum {
153 pass,
154 fail,
155 skip,
156 text,
157};
158
159const SlowTracker = struct {
160 const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
161 max: usize,
162 slowest: SlowestQueue,
163 timer: std.time.Timer,
164
165 fn init(allocator: Allocator, count: u32) SlowTracker {
166 const timer = std.time.Timer.start() catch @panic("failed to start timer");
167 var slowest = SlowestQueue.init(allocator, {});
168 slowest.ensureTotalCapacity(count) catch @panic("OOM");
169 return .{
170 .max = count,
171 .timer = timer,
172 .slowest = slowest,
173 };
174 }
175
176 const TestInfo = struct {
177 ns: u64,
178 name: []const u8,
179 };
180
181 fn deinit(self: SlowTracker) void {
182 self.slowest.deinit();
183 }
184
185 fn startTiming(self: *SlowTracker) void {
186 self.timer.reset();
187 }
188
189 fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
190 var timer = self.timer;
191 const ns = timer.lap();
192
193 var slowest = &self.slowest;
194
195 if (slowest.count() < self.max) {
196 // Capacity is fixed to the # of slow tests we want to track
197 // If we've tracked fewer tests than this capacity, than always add
198 slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
199 return ns;
200 }
201
202 {
203 // Optimization to avoid shifting the dequeue for the common case
204 // where the test isn't one of our slowest.
205 const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
206 if (fastest_of_the_slow.ns > ns) {
207 // the test was faster than our fastest slow test, don't add
208 return ns;
209 }
210 }
211
212 // the previous fastest of our slow tests, has been pushed off.
213 _ = slowest.removeMin();
214 slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
215 return ns;
216 }
217
218 fn display(self: *SlowTracker) !void {
219 var slowest = self.slowest;
220 const count = slowest.count();
221 Printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
222 while (slowest.removeMinOrNull()) |info| {
223 const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
224 Printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
225 }
226 }
227
228 fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
229 _ = context;
230 return std.math.order(a.ns, b.ns);
231 }
232};
233
234const Env = struct {
235 verbose: bool,
236 fail_first: bool,
237 filter: ?[]const u8,
238
239 fn init(allocator: Allocator) Env {
240 return .{
241 .verbose = readEnvBool(allocator, "TEST_VERBOSE", true),
242 .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false),
243 .filter = readEnv(allocator, "TEST_FILTER"),
244 };
245 }
246
247 fn deinit(self: Env, allocator: Allocator) void {
248 if (self.filter) |f| {
249 allocator.free(f);
250 }
251 }
252
253 fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {
254 const v = std.process.getEnvVarOwned(allocator, key) catch |err| {
255 if (err == error.EnvironmentVariableNotFound) {
256 return null;
257 }
258 std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
259 return null;
260 };
261 return v;
262 }
263
264 fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {
265 const value = readEnv(allocator, key) orelse return deflt;
266 defer allocator.free(value);
267 return std.ascii.eqlIgnoreCase(value, "true");
268 }
269};
270
271pub const panic = std.debug.FullPanic(struct {
272 pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
273 if (current_test) |ct| {
274 std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER });
275 }
276 std.debug.defaultPanic(msg, first_trace_addr);
277 }
278}.panicFn);
279
280fn isUnnamed(t: std.builtin.TestFn) bool {
281 const marker = ".test_";
282 const test_name = t.name;
283 const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
284 _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
285 return true;
286}
287
288fn isSetup(t: std.builtin.TestFn) bool {
289 return std.mem.endsWith(u8, t.name, "tests:beforeAll");
290}
291
292fn isTeardown(t: std.builtin.TestFn) bool {
293 return std.mem.endsWith(u8, t.name, "tests:afterAll");
294}