mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
For now, there is a flag to `zig build` called `--test-timeout-ms` which accepts a value in milliseconds. If the execution time of any individual unit test exceeds that number of milliseconds, the test is terminated and marked as timed out. In the future, we may want to increase the granularity of this feature by allowing timeouts to be specified per-step or even per-test. However, a global option is actually very useful. In particular, it can be used in CI scripts to ensure that no individual unit test exceeds some reasonable limit (e.g. 60 seconds) without having to assign limits to every individual test step in the build script. Also, individual unit test durations are now shown in the time report web interface -- this was fairly trivial to add since we're timing tests (to check for timeouts) anyway. This commit makes progress on #19821, but does not close it, because that proposal includes a more sophisticated mechanism for setting timeouts. Co-Authored-By: David Rubin <david@vortan.dev>
442 lines
16 KiB
Zig
442 lines
16 KiB
Zig
//! Default test runner for unit tests.
|
|
const builtin = @import("builtin");
|
|
|
|
const std = @import("std");
|
|
const fatal = std.process.fatal;
|
|
const testing = std.testing;
|
|
const assert = std.debug.assert;
|
|
const fuzz_abi = std.Build.abi.fuzz;
|
|
|
|
pub const std_options: std.Options = .{
|
|
.logFn = log,
|
|
};
|
|
|
|
var log_err_count: usize = 0;
|
|
var fba = std.heap.FixedBufferAllocator.init(&fba_buffer);
|
|
var fba_buffer: [8192]u8 = undefined;
|
|
var stdin_buffer: [4096]u8 = undefined;
|
|
var stdout_buffer: [4096]u8 = undefined;
|
|
|
|
const crippled = switch (builtin.zig_backend) {
|
|
.stage2_aarch64,
|
|
.stage2_powerpc,
|
|
.stage2_riscv64,
|
|
=> true,
|
|
else => false,
|
|
};
|
|
|
|
pub fn main() void {
|
|
@disableInstrumentation();
|
|
|
|
if (builtin.cpu.arch.isSpirV()) {
|
|
// SPIR-V needs an special test-runner
|
|
return;
|
|
}
|
|
|
|
if (crippled) {
|
|
return mainSimple() catch @panic("test failure\n");
|
|
}
|
|
|
|
const args = std.process.argsAlloc(fba.allocator()) catch
|
|
@panic("unable to parse command line args");
|
|
|
|
var listen = false;
|
|
var opt_cache_dir: ?[]const u8 = null;
|
|
|
|
for (args[1..]) |arg| {
|
|
if (std.mem.eql(u8, arg, "--listen=-")) {
|
|
listen = true;
|
|
} else if (std.mem.startsWith(u8, arg, "--seed=")) {
|
|
testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch
|
|
@panic("unable to parse --seed command line argument");
|
|
} else if (std.mem.startsWith(u8, arg, "--cache-dir")) {
|
|
opt_cache_dir = arg["--cache-dir=".len..];
|
|
} else {
|
|
@panic("unrecognized command line argument");
|
|
}
|
|
}
|
|
|
|
if (builtin.fuzz) {
|
|
const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
|
|
fuzz_abi.fuzzer_init(.fromSlice(cache_dir));
|
|
}
|
|
|
|
fba.reset();
|
|
|
|
if (listen) {
|
|
return mainServer() catch @panic("internal test runner failure");
|
|
} else {
|
|
return mainTerminal();
|
|
}
|
|
}
|
|
|
|
fn mainServer() !void {
|
|
@disableInstrumentation();
|
|
var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer);
|
|
var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer);
|
|
var server = try std.zig.Server.init(.{
|
|
.in = &stdin_reader.interface,
|
|
.out = &stdout_writer.interface,
|
|
.zig_version = builtin.zig_version_string,
|
|
});
|
|
|
|
if (builtin.fuzz) {
|
|
const coverage = fuzz_abi.fuzzer_coverage();
|
|
try server.serveCoverageIdMessage(
|
|
coverage.id,
|
|
coverage.runs,
|
|
coverage.unique,
|
|
coverage.seen,
|
|
);
|
|
}
|
|
|
|
while (true) {
|
|
const hdr = try server.receiveMessage();
|
|
switch (hdr.tag) {
|
|
.exit => {
|
|
return std.process.exit(0);
|
|
},
|
|
.query_test_metadata => {
|
|
testing.allocator_instance = .{};
|
|
defer if (testing.allocator_instance.deinit() == .leak) {
|
|
@panic("internal test runner memory leak");
|
|
};
|
|
|
|
var string_bytes: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer string_bytes.deinit(testing.allocator);
|
|
try string_bytes.append(testing.allocator, 0); // Reserve 0 for null.
|
|
|
|
const test_fns = builtin.test_functions;
|
|
const names = try testing.allocator.alloc(u32, test_fns.len);
|
|
defer testing.allocator.free(names);
|
|
const expected_panic_msgs = try testing.allocator.alloc(u32, test_fns.len);
|
|
defer testing.allocator.free(expected_panic_msgs);
|
|
|
|
for (test_fns, names, expected_panic_msgs) |test_fn, *name, *expected_panic_msg| {
|
|
name.* = @intCast(string_bytes.items.len);
|
|
try string_bytes.ensureUnusedCapacity(testing.allocator, test_fn.name.len + 1);
|
|
string_bytes.appendSliceAssumeCapacity(test_fn.name);
|
|
string_bytes.appendAssumeCapacity(0);
|
|
expected_panic_msg.* = 0;
|
|
}
|
|
|
|
try server.serveTestMetadata(.{
|
|
.names = names,
|
|
.expected_panic_msgs = expected_panic_msgs,
|
|
.string_bytes = string_bytes.items,
|
|
});
|
|
},
|
|
|
|
.run_test => {
|
|
testing.allocator_instance = .{};
|
|
log_err_count = 0;
|
|
const index = try server.receiveBody_u32();
|
|
const test_fn = builtin.test_functions[index];
|
|
var fail = false;
|
|
var skip = false;
|
|
is_fuzz_test = false;
|
|
|
|
// let the build server know we're starting the test now
|
|
try server.serveStringMessage(.test_started, &.{});
|
|
|
|
test_fn.func() catch |err| switch (err) {
|
|
error.SkipZigTest => skip = true,
|
|
else => {
|
|
fail = true;
|
|
if (@errorReturnTrace()) |trace| {
|
|
std.debug.dumpStackTrace(trace);
|
|
}
|
|
},
|
|
};
|
|
const leak = testing.allocator_instance.deinit() == .leak;
|
|
try server.serveTestResults(.{
|
|
.index = index,
|
|
.flags = .{
|
|
.fail = fail,
|
|
.skip = skip,
|
|
.leak = leak,
|
|
.fuzz = is_fuzz_test,
|
|
.log_err_count = std.math.lossyCast(
|
|
@FieldType(std.zig.Server.Message.TestResults.Flags, "log_err_count"),
|
|
log_err_count,
|
|
),
|
|
},
|
|
});
|
|
},
|
|
.start_fuzzing => {
|
|
// This ensures that this code won't be analyzed and hence reference fuzzer symbols
|
|
// since they are not present.
|
|
if (!builtin.fuzz) unreachable;
|
|
|
|
const index = try server.receiveBody_u32();
|
|
const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
|
|
const amount_or_instance = try server.receiveBody_u64();
|
|
|
|
const test_fn = builtin.test_functions[index];
|
|
const entry_addr = @intFromPtr(test_fn.func);
|
|
|
|
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
|
is_fuzz_test = false;
|
|
fuzz_test_index = index;
|
|
fuzz_mode = mode;
|
|
fuzz_amount_or_instance = amount_or_instance;
|
|
|
|
test_fn.func() catch |err| switch (err) {
|
|
error.SkipZigTest => return,
|
|
else => {
|
|
if (@errorReturnTrace()) |trace| {
|
|
std.debug.dumpStackTrace(trace);
|
|
}
|
|
std.debug.print("failed with error.{t}\n", .{err});
|
|
std.process.exit(1);
|
|
},
|
|
};
|
|
if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
|
|
if (log_err_count != 0) @panic("error logs detected");
|
|
assert(mode != .forever);
|
|
std.process.exit(0);
|
|
},
|
|
|
|
else => {
|
|
std.debug.print("unsupported message: {x}\n", .{@intFromEnum(hdr.tag)});
|
|
std.process.exit(1);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mainTerminal() void {
|
|
@disableInstrumentation();
|
|
if (builtin.fuzz) @panic("fuzz test requires server");
|
|
|
|
const test_fn_list = builtin.test_functions;
|
|
var ok_count: usize = 0;
|
|
var skip_count: usize = 0;
|
|
var fail_count: usize = 0;
|
|
var fuzz_count: usize = 0;
|
|
const root_node = if (builtin.fuzz) std.Progress.Node.none else std.Progress.start(.{
|
|
.root_name = "Test",
|
|
.estimated_total_items = test_fn_list.len,
|
|
});
|
|
const have_tty = std.fs.File.stderr().isTty();
|
|
|
|
var async_frame_buffer: []align(builtin.target.stackAlignment()) u8 = undefined;
|
|
// TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly
|
|
// ignores the alignment of the slice.
|
|
async_frame_buffer = &[_]u8{};
|
|
|
|
var leaks: usize = 0;
|
|
for (test_fn_list, 0..) |test_fn, i| {
|
|
testing.allocator_instance = .{};
|
|
defer {
|
|
if (testing.allocator_instance.deinit() == .leak) {
|
|
leaks += 1;
|
|
}
|
|
}
|
|
testing.log_level = .warn;
|
|
|
|
const test_node = root_node.start(test_fn.name, 0);
|
|
if (!have_tty) {
|
|
std.debug.print("{d}/{d} {s}...", .{ i + 1, test_fn_list.len, test_fn.name });
|
|
}
|
|
is_fuzz_test = false;
|
|
if (test_fn.func()) |_| {
|
|
ok_count += 1;
|
|
test_node.end();
|
|
if (!have_tty) std.debug.print("OK\n", .{});
|
|
} else |err| switch (err) {
|
|
error.SkipZigTest => {
|
|
skip_count += 1;
|
|
if (have_tty) {
|
|
std.debug.print("{d}/{d} {s}...SKIP\n", .{ i + 1, test_fn_list.len, test_fn.name });
|
|
} else {
|
|
std.debug.print("SKIP\n", .{});
|
|
}
|
|
test_node.end();
|
|
},
|
|
else => {
|
|
fail_count += 1;
|
|
if (have_tty) {
|
|
std.debug.print("{d}/{d} {s}...FAIL ({t})\n", .{
|
|
i + 1, test_fn_list.len, test_fn.name, err,
|
|
});
|
|
} else {
|
|
std.debug.print("FAIL ({t})\n", .{err});
|
|
}
|
|
if (@errorReturnTrace()) |trace| {
|
|
std.debug.dumpStackTrace(trace);
|
|
}
|
|
test_node.end();
|
|
},
|
|
}
|
|
fuzz_count += @intFromBool(is_fuzz_test);
|
|
}
|
|
root_node.end();
|
|
if (ok_count == test_fn_list.len) {
|
|
std.debug.print("All {d} tests passed.\n", .{ok_count});
|
|
} else {
|
|
std.debug.print("{d} passed; {d} skipped; {d} failed.\n", .{ ok_count, skip_count, fail_count });
|
|
}
|
|
if (log_err_count != 0) {
|
|
std.debug.print("{d} errors were logged.\n", .{log_err_count});
|
|
}
|
|
if (leaks != 0) {
|
|
std.debug.print("{d} tests leaked memory.\n", .{leaks});
|
|
}
|
|
if (fuzz_count != 0) {
|
|
std.debug.print("{d} fuzz tests found.\n", .{fuzz_count});
|
|
}
|
|
if (leaks != 0 or log_err_count != 0 or fail_count != 0) {
|
|
std.process.exit(1);
|
|
}
|
|
}
|
|
|
|
pub fn log(
|
|
comptime message_level: std.log.Level,
|
|
comptime scope: @Type(.enum_literal),
|
|
comptime format: []const u8,
|
|
args: anytype,
|
|
) void {
|
|
@disableInstrumentation();
|
|
if (@intFromEnum(message_level) <= @intFromEnum(std.log.Level.err)) {
|
|
log_err_count +|= 1;
|
|
}
|
|
if (@intFromEnum(message_level) <= @intFromEnum(testing.log_level)) {
|
|
std.debug.print(
|
|
"[" ++ @tagName(scope) ++ "] (" ++ @tagName(message_level) ++ "): " ++ format ++ "\n",
|
|
args,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Simpler main(), exercising fewer language features, so that
|
|
/// work-in-progress backends can handle it.
|
|
pub fn mainSimple() anyerror!void {
|
|
@disableInstrumentation();
|
|
// is the backend capable of calling `std.fs.File.writeAll`?
|
|
const enable_write = switch (builtin.zig_backend) {
|
|
.stage2_aarch64, .stage2_riscv64 => true,
|
|
else => false,
|
|
};
|
|
// is the backend capable of calling `std.Io.Writer.print`?
|
|
const enable_print = switch (builtin.zig_backend) {
|
|
.stage2_aarch64, .stage2_riscv64 => true,
|
|
else => false,
|
|
};
|
|
|
|
var passed: u64 = 0;
|
|
var skipped: u64 = 0;
|
|
var failed: u64 = 0;
|
|
|
|
// we don't want to bring in File and Writer if the backend doesn't support it
|
|
const stdout = if (enable_write) std.fs.File.stdout() else {};
|
|
|
|
for (builtin.test_functions) |test_fn| {
|
|
if (enable_write) {
|
|
stdout.writeAll(test_fn.name) catch {};
|
|
stdout.writeAll("... ") catch {};
|
|
}
|
|
if (test_fn.func()) |_| {
|
|
if (enable_write) stdout.writeAll("PASS\n") catch {};
|
|
} else |err| {
|
|
if (err != error.SkipZigTest) {
|
|
if (enable_write) stdout.writeAll("FAIL\n") catch {};
|
|
failed += 1;
|
|
if (!enable_write) return err;
|
|
continue;
|
|
}
|
|
if (enable_write) stdout.writeAll("SKIP\n") catch {};
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
passed += 1;
|
|
}
|
|
if (enable_print) {
|
|
var stdout_writer = stdout.writer(&.{});
|
|
stdout_writer.interface.print("{} passed, {} skipped, {} failed\n", .{ passed, skipped, failed }) catch {};
|
|
}
|
|
if (failed != 0) std.process.exit(1);
|
|
}
|
|
|
|
var is_fuzz_test: bool = undefined;
|
|
var fuzz_test_index: u32 = undefined;
|
|
var fuzz_mode: fuzz_abi.LimitKind = undefined;
|
|
var fuzz_amount_or_instance: u64 = undefined;
|
|
|
|
pub fn fuzz(
|
|
context: anytype,
|
|
comptime testOne: fn (context: @TypeOf(context), []const u8) anyerror!void,
|
|
options: testing.FuzzInputOptions,
|
|
) anyerror!void {
|
|
// Prevent this function from confusing the fuzzer by omitting its own code
|
|
// coverage from being considered.
|
|
@disableInstrumentation();
|
|
|
|
// Some compiler backends are not capable of handling fuzz testing yet but
|
|
// we still want CI test coverage enabled.
|
|
if (crippled) return;
|
|
|
|
// Smoke test to ensure the test did not use conditional compilation to
|
|
// contradict itself by making it not actually be a fuzz test when the test
|
|
// is built in fuzz mode.
|
|
is_fuzz_test = true;
|
|
|
|
// Ensure no test failure occurred before starting fuzzing.
|
|
if (log_err_count != 0) @panic("error logs detected");
|
|
|
|
// libfuzzer is in a separate compilation unit so that its own code can be
|
|
// excluded from code coverage instrumentation. It needs a function pointer
|
|
// it can call for checking exactly one input. Inside this function we do
|
|
// our standard unit test checks such as memory leaks, and interaction with
|
|
// error logs.
|
|
const global = struct {
|
|
var ctx: @TypeOf(context) = undefined;
|
|
|
|
fn test_one(input: fuzz_abi.Slice) callconv(.c) void {
|
|
@disableInstrumentation();
|
|
testing.allocator_instance = .{};
|
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
|
log_err_count = 0;
|
|
testOne(ctx, input.toSlice()) catch |err| switch (err) {
|
|
error.SkipZigTest => return,
|
|
else => {
|
|
std.debug.lockStdErr();
|
|
if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace);
|
|
std.debug.print("failed with error.{t}\n", .{err});
|
|
std.process.exit(1);
|
|
},
|
|
};
|
|
if (log_err_count != 0) {
|
|
std.debug.lockStdErr();
|
|
std.debug.print("error logs detected\n", .{});
|
|
std.process.exit(1);
|
|
}
|
|
}
|
|
};
|
|
if (builtin.fuzz) {
|
|
const prev_allocator_state = testing.allocator_instance;
|
|
testing.allocator_instance = .{};
|
|
defer testing.allocator_instance = prev_allocator_state;
|
|
|
|
global.ctx = context;
|
|
fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name));
|
|
|
|
for (options.corpus) |elem|
|
|
fuzz_abi.fuzzer_new_input(.fromSlice(elem));
|
|
|
|
fuzz_abi.fuzzer_main(fuzz_mode, fuzz_amount_or_instance);
|
|
return;
|
|
}
|
|
|
|
// When the unit test executable is not built in fuzz mode, only run the
|
|
// provided corpus.
|
|
for (options.corpus) |input| {
|
|
try testOne(context, input);
|
|
}
|
|
|
|
// In case there is no provided corpus, also use an empty
|
|
// string as a smoke test.
|
|
try testOne(context, "");
|
|
}
|