test_runner.zig (15678B) - Raw
1 //! Default test runner for unit tests. 2 const builtin = @import("builtin"); 3 4 const std = @import("std"); 5 const testing = std.testing; 6 const assert = std.debug.assert; 7 8 pub const std_options: std.Options = .{ 9 .logFn = log, 10 }; 11 12 var log_err_count: usize = 0; 13 var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); 14 var fba_buffer: [8192]u8 = undefined; 15 var stdin_buffer: [4096]u8 = undefined; 16 var stdout_buffer: [4096]u8 = undefined; 17 18 const crippled = switch (builtin.zig_backend) { 19 .stage2_aarch64, 20 .stage2_powerpc, 21 .stage2_riscv64, 22 => true, 23 else => false, 24 }; 25 26 pub fn main() void { 27 @disableInstrumentation(); 28 29 if (builtin.cpu.arch.isSpirV()) { 30 // SPIR-V needs an special test-runner 31 return; 32 } 33 34 if (crippled) { 35 return mainSimple() catch @panic("test failure\n"); 36 } 37 38 const args = std.process.argsAlloc(fba.allocator()) catch 39 @panic("unable to parse command line args"); 40 41 var listen = false; 42 var opt_cache_dir: ?[]const u8 = null; 43 44 for (args[1..]) |arg| { 45 if (std.mem.eql(u8, arg, "--listen=-")) { 46 listen = true; 47 } else if (std.mem.startsWith(u8, arg, "--seed=")) { 48 testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch 49 @panic("unable to parse --seed command line argument"); 50 } else if (std.mem.startsWith(u8, arg, "--cache-dir")) { 51 opt_cache_dir = arg["--cache-dir=".len..]; 52 } else { 53 @panic("unrecognized command line argument"); 54 } 55 } 56 57 fba.reset(); 58 if (builtin.fuzz) { 59 const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument"); 60 fuzzer_init(FuzzerSlice.fromSlice(cache_dir)); 61 } 62 63 if (listen) { 64 return mainServer() catch @panic("internal test runner failure"); 65 } else { 66 return mainTerminal(); 67 } 68 } 69 70 fn mainServer() !void { 71 @disableInstrumentation(); 72 var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); 73 var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); 74 var server = try std.zig.Server.init(.{ 75 .in = &stdin_reader.interface, 76 .out = &stdout_writer.interface, 77 .zig_version = builtin.zig_version_string, 78 }); 79 80 if (builtin.fuzz) { 81 const coverage_id = fuzzer_coverage_id(); 82 try server.serveU64Message(.coverage_id, coverage_id); 83 } 84 85 while (true) { 86 const hdr = try server.receiveMessage(); 87 switch (hdr.tag) { 88 .exit => { 89 return std.process.exit(0); 90 }, 91 .query_test_metadata => { 92 testing.allocator_instance = .{}; 93 defer if (testing.allocator_instance.deinit() == .leak) { 94 @panic("internal test runner memory leak"); 95 }; 96 97 var string_bytes: std.ArrayListUnmanaged(u8) = .empty; 98 defer string_bytes.deinit(testing.allocator); 99 try string_bytes.append(testing.allocator, 0); // Reserve 0 for null. 100 101 const test_fns = builtin.test_functions; 102 const names = try testing.allocator.alloc(u32, test_fns.len); 103 defer testing.allocator.free(names); 104 const expected_panic_msgs = try testing.allocator.alloc(u32, test_fns.len); 105 defer testing.allocator.free(expected_panic_msgs); 106 107 for (test_fns, names, expected_panic_msgs) |test_fn, *name, *expected_panic_msg| { 108 name.* = @intCast(string_bytes.items.len); 109 try string_bytes.ensureUnusedCapacity(testing.allocator, test_fn.name.len + 1); 110 string_bytes.appendSliceAssumeCapacity(test_fn.name); 111 string_bytes.appendAssumeCapacity(0); 112 expected_panic_msg.* = 0; 113 } 114 115 try server.serveTestMetadata(.{ 116 .names = names, 117 .expected_panic_msgs = expected_panic_msgs, 118 .string_bytes = string_bytes.items, 119 }); 120 }, 121 122 .run_test => { 123 testing.allocator_instance = .{}; 124 log_err_count = 0; 125 const index = try server.receiveBody_u32(); 126 const test_fn = builtin.test_functions[index]; 127 var fail = false; 128 var skip = false; 129 is_fuzz_test = false; 130 test_fn.func() catch |err| switch (err) { 131 error.SkipZigTest => skip = true, 132 else => { 133 fail = true; 134 if (@errorReturnTrace()) |trace| { 135 std.debug.dumpStackTrace(trace.*); 136 } 137 }, 138 }; 139 const leak = testing.allocator_instance.deinit() == .leak; 140 try server.serveTestResults(.{ 141 .index = index, 142 .flags = .{ 143 .fail = fail, 144 .skip = skip, 145 .leak = leak, 146 .fuzz = is_fuzz_test, 147 .log_err_count = std.math.lossyCast( 148 @FieldType(std.zig.Server.Message.TestResults.Flags, "log_err_count"), 149 log_err_count, 150 ), 151 }, 152 }); 153 }, 154 .start_fuzzing => { 155 if (!builtin.fuzz) unreachable; 156 const index = try server.receiveBody_u32(); 157 const test_fn = builtin.test_functions[index]; 158 const entry_addr = @intFromPtr(test_fn.func); 159 try server.serveU64Message(.fuzz_start_addr, entry_addr); 160 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); 161 is_fuzz_test = false; 162 fuzzer_set_name(test_fn.name.ptr, test_fn.name.len); 163 test_fn.func() catch |err| switch (err) { 164 error.SkipZigTest => return, 165 else => { 166 if (@errorReturnTrace()) |trace| { 167 std.debug.dumpStackTrace(trace.*); 168 } 169 std.debug.print("failed with error.{s}\n", .{@errorName(err)}); 170 std.process.exit(1); 171 }, 172 }; 173 if (!is_fuzz_test) @panic("missed call to std.testing.fuzz"); 174 if (log_err_count != 0) @panic("error logs detected"); 175 }, 176 177 else => { 178 std.debug.print("unsupported message: {x}\n", .{@intFromEnum(hdr.tag)}); 179 std.process.exit(1); 180 }, 181 } 182 } 183 } 184 185 fn mainTerminal() void { 186 @disableInstrumentation(); 187 const test_fn_list = builtin.test_functions; 188 var ok_count: usize = 0; 189 var skip_count: usize = 0; 190 var fail_count: usize = 0; 191 var fuzz_count: usize = 0; 192 const root_node = if (builtin.fuzz) std.Progress.Node.none else std.Progress.start(.{ 193 .root_name = "Test", 194 .estimated_total_items = test_fn_list.len, 195 }); 196 const have_tty = std.fs.File.stderr().isTty(); 197 198 var async_frame_buffer: []align(builtin.target.stackAlignment()) u8 = undefined; 199 // TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly 200 // ignores the alignment of the slice. 201 async_frame_buffer = &[_]u8{}; 202 203 var leaks: usize = 0; 204 for (test_fn_list, 0..) |test_fn, i| { 205 testing.allocator_instance = .{}; 206 defer { 207 if (testing.allocator_instance.deinit() == .leak) { 208 leaks += 1; 209 } 210 } 211 testing.log_level = .warn; 212 213 const test_node = root_node.start(test_fn.name, 0); 214 if (!have_tty) { 215 std.debug.print("{d}/{d} {s}...", .{ i + 1, test_fn_list.len, test_fn.name }); 216 } 217 is_fuzz_test = false; 218 if (test_fn.func()) |_| { 219 ok_count += 1; 220 test_node.end(); 221 if (!have_tty) std.debug.print("OK\n", .{}); 222 } else |err| switch (err) { 223 error.SkipZigTest => { 224 skip_count += 1; 225 if (have_tty) { 226 std.debug.print("{d}/{d} {s}...SKIP\n", .{ i + 1, test_fn_list.len, test_fn.name }); 227 } else { 228 std.debug.print("SKIP\n", .{}); 229 } 230 test_node.end(); 231 }, 232 else => { 233 fail_count += 1; 234 if (have_tty) { 235 std.debug.print("{d}/{d} {s}...FAIL ({s})\n", .{ 236 i + 1, test_fn_list.len, test_fn.name, @errorName(err), 237 }); 238 } else { 239 std.debug.print("FAIL ({s})\n", .{@errorName(err)}); 240 } 241 if (@errorReturnTrace()) |trace| { 242 std.debug.dumpStackTrace(trace.*); 243 } 244 test_node.end(); 245 }, 246 } 247 fuzz_count += @intFromBool(is_fuzz_test); 248 } 249 root_node.end(); 250 if (ok_count == test_fn_list.len) { 251 std.debug.print("All {d} tests passed.\n", .{ok_count}); 252 } else { 253 std.debug.print("{d} passed; {d} skipped; {d} failed.\n", .{ ok_count, skip_count, fail_count }); 254 } 255 if (log_err_count != 0) { 256 std.debug.print("{d} errors were logged.\n", .{log_err_count}); 257 } 258 if (leaks != 0) { 259 std.debug.print("{d} tests leaked memory.\n", .{leaks}); 260 } 261 if (fuzz_count != 0) { 262 std.debug.print("{d} fuzz tests found.\n", .{fuzz_count}); 263 } 264 if (leaks != 0 or log_err_count != 0 or fail_count != 0) { 265 std.process.exit(1); 266 } 267 } 268 269 pub fn log( 270 comptime message_level: std.log.Level, 271 comptime scope: @Type(.enum_literal), 272 comptime format: []const u8, 273 args: anytype, 274 ) void { 275 @disableInstrumentation(); 276 if (@intFromEnum(message_level) <= @intFromEnum(std.log.Level.err)) { 277 log_err_count +|= 1; 278 } 279 if (@intFromEnum(message_level) <= @intFromEnum(testing.log_level)) { 280 std.debug.print( 281 "[" ++ @tagName(scope) ++ "] (" ++ @tagName(message_level) ++ "): " ++ format ++ "\n", 282 args, 283 ); 284 } 285 } 286 287 /// Simpler main(), exercising fewer language features, so that 288 /// work-in-progress backends can handle it. 289 pub fn mainSimple() anyerror!void { 290 @disableInstrumentation(); 291 // is the backend capable of calling `std.fs.File.writeAll`? 292 const enable_write = switch (builtin.zig_backend) { 293 .stage2_aarch64, .stage2_riscv64 => true, 294 else => false, 295 }; 296 // is the backend capable of calling `std.Io.Writer.print`? 297 const enable_print = switch (builtin.zig_backend) { 298 .stage2_aarch64, .stage2_riscv64 => true, 299 else => false, 300 }; 301 302 var passed: u64 = 0; 303 var skipped: u64 = 0; 304 var failed: u64 = 0; 305 306 // we don't want to bring in File and Writer if the backend doesn't support it 307 const stdout = if (enable_write) std.fs.File.stdout() else {}; 308 309 for (builtin.test_functions) |test_fn| { 310 if (enable_write) { 311 stdout.writeAll(test_fn.name) catch {}; 312 stdout.writeAll("... ") catch {}; 313 } 314 if (test_fn.func()) |_| { 315 if (enable_write) stdout.writeAll("PASS\n") catch {}; 316 } else |err| { 317 if (err != error.SkipZigTest) { 318 if (enable_write) stdout.writeAll("FAIL\n") catch {}; 319 failed += 1; 320 if (!enable_write) return err; 321 continue; 322 } 323 if (enable_write) stdout.writeAll("SKIP\n") catch {}; 324 skipped += 1; 325 continue; 326 } 327 passed += 1; 328 } 329 if (enable_print) { 330 var stdout_writer = stdout.writer(&.{}); 331 stdout_writer.interface.print("{} passed, {} skipped, {} failed\n", .{ passed, skipped, failed }) catch {}; 332 } 333 if (failed != 0) std.process.exit(1); 334 } 335 336 const FuzzerSlice = extern struct { 337 ptr: [*]const u8, 338 len: usize, 339 340 /// Inline to avoid fuzzer instrumentation. 341 inline fn toSlice(s: FuzzerSlice) []const u8 { 342 return s.ptr[0..s.len]; 343 } 344 345 /// Inline to avoid fuzzer instrumentation. 346 inline fn fromSlice(s: []const u8) FuzzerSlice { 347 return .{ .ptr = s.ptr, .len = s.len }; 348 } 349 }; 350 351 var is_fuzz_test: bool = undefined; 352 353 extern fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void; 354 extern fn fuzzer_init(cache_dir: FuzzerSlice) void; 355 extern fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void; 356 extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.c) void) void; 357 extern fn fuzzer_coverage_id() u64; 358 359 pub fn fuzz( 360 context: anytype, 361 comptime testOne: fn (context: @TypeOf(context), []const u8) anyerror!void, 362 options: testing.FuzzInputOptions, 363 ) anyerror!void { 364 // Prevent this function from confusing the fuzzer by omitting its own code 365 // coverage from being considered. 366 @disableInstrumentation(); 367 368 // Some compiler backends are not capable of handling fuzz testing yet but 369 // we still want CI test coverage enabled. 370 if (crippled) return; 371 372 // Smoke test to ensure the test did not use conditional compilation to 373 // contradict itself by making it not actually be a fuzz test when the test 374 // is built in fuzz mode. 375 is_fuzz_test = true; 376 377 // Ensure no test failure occurred before starting fuzzing. 378 if (log_err_count != 0) @panic("error logs detected"); 379 380 // libfuzzer is in a separate compilation unit so that its own code can be 381 // excluded from code coverage instrumentation. It needs a function pointer 382 // it can call for checking exactly one input. Inside this function we do 383 // our standard unit test checks such as memory leaks, and interaction with 384 // error logs. 385 const global = struct { 386 var ctx: @TypeOf(context) = undefined; 387 388 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.c) void { 389 @disableInstrumentation(); 390 testing.allocator_instance = .{}; 391 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); 392 log_err_count = 0; 393 testOne(ctx, input_ptr[0..input_len]) catch |err| switch (err) { 394 error.SkipZigTest => return, 395 else => { 396 std.debug.lockStdErr(); 397 if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*); 398 std.debug.print("failed with error.{s}\n", .{@errorName(err)}); 399 std.process.exit(1); 400 }, 401 }; 402 if (log_err_count != 0) { 403 std.debug.lockStdErr(); 404 std.debug.print("error logs detected\n", .{}); 405 std.process.exit(1); 406 } 407 } 408 }; 409 if (builtin.fuzz) { 410 const prev_allocator_state = testing.allocator_instance; 411 testing.allocator_instance = .{}; 412 defer testing.allocator_instance = prev_allocator_state; 413 414 for (options.corpus) |elem| fuzzer_init_corpus_elem(elem.ptr, elem.len); 415 416 global.ctx = context; 417 fuzzer_start(&global.fuzzer_one); 418 return; 419 } 420 421 // When the unit test executable is not built in fuzz mode, only run the 422 // provided corpus. 423 for (options.corpus) |input| { 424 try testOne(context, input); 425 } 426 427 // In case there is no provided corpus, also use an empty 428 // string as a smoke test. 429 try testOne(context, ""); 430 }