mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-04-27 19:09:47 +03:00
d8ba173e5e
- New Features -- Multiprocess Fuzzing The fuzzer now is able to utilize multiple cores. This is controllable with the `-j` build option. Limited fuzzing still uses one core. -- Fuzzing Infinite Mode When provided multiple tests, the fuzzer now switches between them and prioritizes the most effective and interesting ones. Over time already explored tests will become barely run compared to tests yielding new inputs. -- Crash Dumps Crashing inputs are now saved to a file indicated by the crash message. It is recommended to use these files to reproduce the crash using `std.testing.FuzzInputOptions.corpus` and @embedFile. - Design Each fuzzing process is assigned an instance id which has the following uses: * In conjunction with the pc hash and running test index, they uniquely identify input files in the case of a crash. * It is combined with the test seed for a unique rng seed. * Instance 0 is solely responsible for syncing the filesystem corpus. When new inputs are found, they are sent to the build server. It then distributes the new input to the other instances. Each instance has a concurrent poller managed by the test runner which sends received inputs to libfuzzer. (note that this is affected by #31718 and so can (rarely) deadlock) For fuzzing infinite mode, the test runner now receives a list of tests from the build server. The fuzzer runs tests in batches of one second, approximated in cycles by the previous batch's run speed. Tests finding new inputs or with few runs are given a higher run chance. The baseline run chance is based off the recency of the last find and the number of pcs the test has hit.
608 lines
23 KiB
Zig
608 lines
23 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 panic = std.debug.panic;
|
|
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 stdin_reader: Io.File.Reader = undefined;
|
|
var stdout_writer: Io.File.Writer = undefined;
|
|
const runner_threaded_io: Io = Io.Threaded.global_single_threaded.io();
|
|
|
|
/// 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(init: std.process.Init.Minimal) void {
|
|
@disableInstrumentation();
|
|
|
|
if (builtin.cpu.arch.isSpirV()) {
|
|
// SPIR-V needs an special test-runner
|
|
return;
|
|
}
|
|
|
|
if (need_simple) {
|
|
return mainSimple() catch |err| panic("test failure: {t}", .{err});
|
|
}
|
|
|
|
const args = init.args.toSlice(fba.allocator()) catch |err| panic("unable to parse command line args: {t}", .{err});
|
|
|
|
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: {s}", .{arg});
|
|
}
|
|
}
|
|
|
|
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(init) catch |err| panic("internal test runner failure: {t}", .{err});
|
|
} else {
|
|
return mainTerminal(init);
|
|
}
|
|
}
|
|
|
|
fn mainServer(init: std.process.Init.Minimal) !void {
|
|
@disableInstrumentation();
|
|
stdin_reader = .initStreaming(.stdin(), runner_threaded_io, &stdin_buffer);
|
|
stdout_writer = .initStreaming(.stdout(), runner_threaded_io, &stdout_buffer);
|
|
var server = try std.zig.Server.init(.{
|
|
.in = &stdin_reader.interface,
|
|
.out = &stdout_writer.interface,
|
|
.zig_version = builtin.zig_version_string,
|
|
});
|
|
|
|
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.ArrayList(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.environ = init.environ;
|
|
testing.allocator_instance = .{};
|
|
testing.io_instance = .init(testing.allocator, .{
|
|
.argv0 = .init(init.args),
|
|
.environ = init.environ,
|
|
});
|
|
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;
|
|
|
|
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
|
|
defer if (gpa_instance.deinit() == .leak) {
|
|
@panic("internal test runner memory leak");
|
|
};
|
|
const gpa = gpa_instance.allocator();
|
|
var io_instance: Io.Threaded = .init(gpa, .{
|
|
.argv0 = .init(init.args),
|
|
.environ = init.environ,
|
|
});
|
|
defer io_instance.deinit();
|
|
const io = io_instance.io();
|
|
|
|
const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
|
|
const amount_or_instance = try server.receiveBody_u64();
|
|
const main_instance = mode == .iterations or amount_or_instance == 0;
|
|
|
|
if (main_instance) {
|
|
const coverage = fuzz_abi.fuzzer_coverage();
|
|
try server.serveCoverageIdMessage(
|
|
coverage.id,
|
|
coverage.runs,
|
|
coverage.unique,
|
|
coverage.seen,
|
|
);
|
|
}
|
|
|
|
const n_tests: u32 = try server.receiveBody_u32();
|
|
const test_indexes = try gpa.alloc(u32, n_tests);
|
|
defer gpa.free(test_indexes);
|
|
fuzz_runner = .{
|
|
.indexes = test_indexes,
|
|
.server = &server,
|
|
.gpa = gpa,
|
|
.io = io,
|
|
.input_poller = undefined,
|
|
};
|
|
|
|
{
|
|
var large_name_buf: std.ArrayList(u8) = .empty;
|
|
defer large_name_buf.deinit(gpa);
|
|
for (test_indexes) |*i| {
|
|
const name_len = try server.receiveBody_u32();
|
|
const name = if (name_len <= server.in.buffer.len)
|
|
try server.in.take(name_len)
|
|
else large_name: {
|
|
try large_name_buf.resize(gpa, name_len);
|
|
try server.in.readSliceAll(large_name_buf.items);
|
|
break :large_name large_name_buf.items;
|
|
};
|
|
|
|
for (0.., builtin.test_functions) |test_i, test_fn| {
|
|
if (std.mem.eql(u8, name, test_fn.name)) {
|
|
i.* = @intCast(test_i);
|
|
break;
|
|
}
|
|
} else {
|
|
panic("fuzz test {s} no longer exists", .{name});
|
|
}
|
|
|
|
if (main_instance) {
|
|
const relocated_entry_addr = @intFromPtr(builtin.test_functions[i.*].func);
|
|
const entry_addr = fuzz_abi.fuzzer_unslide_address(relocated_entry_addr);
|
|
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
|
}
|
|
}
|
|
}
|
|
|
|
fuzz_abi.fuzzer_main(n_tests, testing.random_seed, mode, amount_or_instance);
|
|
|
|
assert(mode != .forever);
|
|
std.process.exit(0);
|
|
},
|
|
|
|
else => {
|
|
std.debug.print("unsupported message: {x}\n", .{@intFromEnum(hdr.tag)});
|
|
std.process.exit(1);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mainTerminal(init: std.process.Init.Minimal) 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(runner_threaded_io, .{
|
|
.root_name = "Test",
|
|
.estimated_total_items = test_fn_list.len,
|
|
});
|
|
const have_tty = Io.File.stderr().isTty(runner_threaded_io) catch unreachable;
|
|
|
|
var leaks: usize = 0;
|
|
for (test_fn_list, 0..) |test_fn, i| {
|
|
testing.allocator_instance = .{};
|
|
testing.io_instance = .init(testing.allocator, .{
|
|
.argv0 = .init(init.args),
|
|
.environ = init.environ,
|
|
});
|
|
defer {
|
|
testing.io_instance.deinit();
|
|
if (testing.allocator_instance.deinit() == .leak) leaks += 1;
|
|
}
|
|
testing.log_level = .warn;
|
|
testing.environ = init.environ;
|
|
|
|
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: @EnumLiteral(),
|
|
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 `Io.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,
|
|
};
|
|
|
|
testing.io_instance = .init(testing.allocator, .{});
|
|
|
|
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) Io.File.stdout() else {};
|
|
|
|
for (builtin.test_functions) |test_fn| {
|
|
if (enable_write) {
|
|
stdout.writeStreamingAll(runner_threaded_io, test_fn.name) catch {};
|
|
stdout.writeStreamingAll(runner_threaded_io, "... ") catch {};
|
|
}
|
|
if (test_fn.func()) |_| {
|
|
if (enable_write) stdout.writeStreamingAll(runner_threaded_io, "PASS\n") catch {};
|
|
} else |err| {
|
|
if (err != error.SkipZigTest) {
|
|
if (enable_write) stdout.writeStreamingAll(runner_threaded_io, "FAIL\n") catch {};
|
|
failed += 1;
|
|
if (!enable_write) return err;
|
|
continue;
|
|
}
|
|
if (enable_write) stdout.writeStreamingAll(runner_threaded_io, "SKIP\n") catch {};
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
passed += 1;
|
|
}
|
|
if (enable_print) {
|
|
var unbuffered_stdout_writer = stdout.writer(runner_threaded_io, &.{});
|
|
unbuffered_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_runner: if (builtin.fuzz) struct {
|
|
indexes: []u32,
|
|
server: *std.zig.Server,
|
|
gpa: std.mem.Allocator,
|
|
io: Io,
|
|
input_poller: Io.Future(Io.Cancelable!void),
|
|
|
|
comptime {
|
|
assert(builtin.fuzz); // `fuzz_runner` was analyzed in non-fuzzing compilation
|
|
}
|
|
|
|
export fn runner_test_run(i: u32) void {
|
|
@disableInstrumentation();
|
|
|
|
fuzz_runner.server.serveU32Message(.fuzz_test_change, i) catch |e| switch (e) {
|
|
error.WriteFailed => panic("failed to write to stdout: {t}", .{stdout_writer.err.?}),
|
|
};
|
|
|
|
testing.allocator_instance = .{};
|
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
|
is_fuzz_test = false;
|
|
|
|
builtin.test_functions[fuzz_runner.indexes[i]].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");
|
|
}
|
|
|
|
export fn runner_test_name(i: u32) fuzz_abi.Slice {
|
|
@disableInstrumentation();
|
|
return .fromSlice(builtin.test_functions[fuzz_runner.indexes[i]].name);
|
|
}
|
|
|
|
export fn runner_broadcast_input(test_i: u32, bytes_slice: fuzz_abi.Slice) void {
|
|
@disableInstrumentation();
|
|
const bytes = bytes_slice.toSlice();
|
|
fuzz_runner.server.serveBroadcastFuzzInputMessage(test_i, bytes) catch |e| switch (e) {
|
|
error.WriteFailed => panic("failed to write to stdout: {t}", .{stdout_writer.err.?}),
|
|
};
|
|
}
|
|
|
|
export fn runner_start_input_poller() void {
|
|
@disableInstrumentation();
|
|
const future = fuzz_runner.io.concurrent(inputPoller, .{}) catch |e| switch (e) {
|
|
error.ConcurrencyUnavailable => @panic("failed to spawn concurrent fuzz input poller"),
|
|
};
|
|
fuzz_runner.input_poller = future;
|
|
}
|
|
|
|
export fn runner_stop_input_poller() void {
|
|
@disableInstrumentation();
|
|
assert(fuzz_runner.input_poller.cancel(fuzz_runner.io) == error.Canceled);
|
|
}
|
|
|
|
export fn runner_futex_wait(ptr: *const u32, expected: u32) bool {
|
|
@disableInstrumentation();
|
|
return fuzz_runner.io.futexWait(u32, ptr, expected) == error.Canceled;
|
|
}
|
|
|
|
export fn runner_futex_wake(ptr: *const u32, waiters: u32) void {
|
|
@disableInstrumentation();
|
|
fuzz_runner.io.futexWake(u32, ptr, waiters);
|
|
}
|
|
|
|
fn inputPoller() Io.Cancelable!void {
|
|
@disableInstrumentation();
|
|
switch (inputPollerInner()) {
|
|
error.Canceled => return error.Canceled,
|
|
error.ReadFailed => {
|
|
if (stdin_reader.err.? == error.Canceled) return error.Canceled;
|
|
panic("failed to read from stdin: {t}", .{stdin_reader.err.?});
|
|
},
|
|
error.EndOfStream => @panic("unexpected end of stdin"),
|
|
}
|
|
}
|
|
|
|
fn inputPollerInner() (Io.Cancelable || Io.Reader.Error) {
|
|
@disableInstrumentation();
|
|
const server = fuzz_runner.server;
|
|
var large_bytes_list: std.ArrayList(u8) = .empty;
|
|
defer large_bytes_list.deinit(fuzz_runner.gpa);
|
|
while (true) {
|
|
const hdr = try server.receiveMessage();
|
|
if (hdr.tag != .new_fuzz_input) {
|
|
panic("unexpected message: {x}\n", .{@intFromEnum(hdr.tag)});
|
|
}
|
|
const test_i = try server.receiveBody_u32();
|
|
const input_len = hdr.bytes_len - 4;
|
|
const bytes = if (input_len <= server.in.buffer.len)
|
|
try server.in.take(input_len)
|
|
else bytes: {
|
|
large_bytes_list.resize(fuzz_runner.gpa, @intCast(input_len)) catch @panic("OOM");
|
|
try server.in.readSliceAll(large_bytes_list.items);
|
|
break :bytes large_bytes_list.items;
|
|
};
|
|
if (fuzz_abi.fuzzer_receive_input(test_i, .fromSlice(bytes))) {
|
|
return error.Canceled;
|
|
}
|
|
}
|
|
}
|
|
} else void = undefined;
|
|
|
|
pub fn fuzz(
|
|
context: anytype,
|
|
comptime testOne: fn (context: @TypeOf(context), *std.testing.Smith) 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() callconv(.c) bool {
|
|
@disableInstrumentation();
|
|
testing.allocator_instance = .{};
|
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
|
log_err_count = 0;
|
|
testOne(ctx, @constCast(&testing.Smith{ .in = null })) catch |err| switch (err) {
|
|
error.SkipZigTest => return true,
|
|
else => {
|
|
const stderr = std.debug.lockStderr(&.{}).terminal();
|
|
p: {
|
|
if (@errorReturnTrace()) |trace| {
|
|
std.debug.writeStackTrace(trace, stderr) catch break :p;
|
|
}
|
|
stderr.writer.print("failed with error.{t}\n", .{err}) catch break :p;
|
|
}
|
|
std.process.exit(1);
|
|
},
|
|
};
|
|
if (log_err_count != 0) {
|
|
const stderr = std.debug.lockStderr(&.{}).terminal();
|
|
stderr.writer.print("error logs detected\n", .{}) catch {};
|
|
std.process.exit(1);
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if (builtin.fuzz) {
|
|
// Preserve the calling test's allocator state
|
|
const prev_allocator_state = testing.allocator_instance;
|
|
testing.allocator_instance = .{};
|
|
defer testing.allocator_instance = prev_allocator_state;
|
|
|
|
global.ctx = context;
|
|
fuzz_abi.fuzzer_set_test(&global.test_one);
|
|
for (options.corpus) |elem|
|
|
fuzz_abi.fuzzer_new_input(.fromSlice(elem));
|
|
fuzz_abi.fuzzer_start_test();
|
|
return;
|
|
}
|
|
|
|
// When the unit test executable is not built in fuzz mode, only run the
|
|
// provided corpus.
|
|
for (options.corpus) |input| {
|
|
var smith: testing.Smith = .{ .in = input };
|
|
try testOne(context, &smith);
|
|
}
|
|
|
|
// In case there is no provided corpus, also use an empty
|
|
// string as a smoke test.
|
|
var smith: testing.Smith = .{ .in = "" };
|
|
try testOne(context, &smith);
|
|
}
|