zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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 }