A poolable string builder (aka string buffer) for Zig
0

Configure Feed

Select the types of activity you want to include in your feed.

buffer.zig / test_runner.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}