mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
446 lines
16 KiB
Zig
446 lines
16 KiB
Zig
//! Default test runner for unit tests.
|
|
const builtin = @import("builtin");
|
|
|
|
const std = @import("std");
|
|
const Io = std.Io;
|
|
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;
|
|
var runner_threaded_io: Io.Threaded = .init_single_threaded;
|
|
|
|
/// Keep in sync with logic in `std.Build.addRunArtifact` which decides whether
|
|
/// the test runner will communicate with the build runner via `std.zig.Server`.
|
|
const need_simple = 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 (need_simple) {
|
|
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));
|
|
}
|
|
|
|
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(runner_threaded_io.io(), &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 = .{};
|
|
testing.io_instance = .init(testing.allocator);
|
|
log_err_count = 0;
|
|
const index = try server.receiveBody_u32();
|
|
const test_fn = builtin.test_functions[index];
|
|
is_fuzz_test = false;
|
|
|
|
// let the build server know we're starting the test now
|
|
try server.serveStringMessage(.test_started, &.{});
|
|
|
|
const TestResults = std.zig.Server.Message.TestResults;
|
|
const status: TestResults.Status = if (test_fn.func()) |v| s: {
|
|
v;
|
|
break :s .pass;
|
|
} else |err| switch (err) {
|
|
error.SkipZigTest => .skip,
|
|
else => s: {
|
|
if (@errorReturnTrace()) |trace| {
|
|
std.debug.dumpStackTrace(trace.*);
|
|
}
|
|
break :s .fail;
|
|
},
|
|
};
|
|
testing.io_instance.deinit();
|
|
const leak_count = testing.allocator_instance.detectLeaks();
|
|
testing.allocator_instance.deinitWithoutLeakChecks();
|
|
try server.serveTestResults(.{
|
|
.index = index,
|
|
.flags = .{
|
|
.status = status,
|
|
.fuzz = is_fuzz_test,
|
|
.log_err_count = std.math.lossyCast(
|
|
@FieldType(TestResults.Flags, "log_err_count"),
|
|
log_err_count,
|
|
),
|
|
.leak_count = std.math.lossyCast(
|
|
@FieldType(TestResults.Flags, "leak_count"),
|
|
leak_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 leaks: usize = 0;
|
|
for (test_fn_list, 0..) |test_fn, i| {
|
|
testing.allocator_instance = .{};
|
|
testing.io_instance = .init(testing.allocator);
|
|
defer {
|
|
testing.io_instance.deinit();
|
|
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 `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 (need_simple) 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, "");
|
|
}
|