add --fuzz CLI argument to zig build

This flag makes the build runner rebuild unit tests after the pipeline
finishes, if it finds any unit tests.

I did not make this integrate with file system watching yet.

The test runner is updated to detect which tests are fuzz tests.

Run step is updated to track which test indexes are fuzz tests.
This commit is contained in:
Andrew Kelley 2024-07-23 20:49:00 -07:00
parent 6f3767862d
commit 047640383e
5 changed files with 97 additions and 17 deletions

View file

@ -10,7 +10,8 @@ const File = std.fs.File;
const Step = std.Build.Step;
const Watch = std.Build.Watch;
const Allocator = std.mem.Allocator;
const fatal = std.zig.fatal;
const fatal = std.process.fatal;
const runner = @This();
pub const root = @import("@build");
pub const dependencies = @import("@dependencies");
@ -102,6 +103,7 @@ pub fn main() !void {
var steps_menu = false;
var output_tmp_nonce: ?[16]u8 = null;
var watch = false;
var fuzz = false;
var debounce_interval_ms: u16 = 50;
while (nextArg(args, &arg_idx)) |arg| {
@ -234,6 +236,8 @@ pub fn main() !void {
prominent_compile_errors = true;
} else if (mem.eql(u8, arg, "--watch")) {
watch = true;
} else if (mem.eql(u8, arg, "--fuzz")) {
fuzz = true;
} else if (mem.eql(u8, arg, "-fincremental")) {
graph.incremental = true;
} else if (mem.eql(u8, arg, "-fno-incremental")) {
@ -353,6 +357,7 @@ pub fn main() !void {
.max_rss_mutex = .{},
.skip_oom_steps = skip_oom_steps,
.watch = watch,
.fuzz = fuzz,
.memory_blocked_steps = std.ArrayList(*Step).init(arena),
.step_stack = .{},
.prominent_compile_errors = prominent_compile_errors,
@ -394,6 +399,10 @@ pub fn main() !void {
},
else => return err,
};
if (fuzz) {
startFuzzing(&run.thread_pool, run.step_stack.keys(), main_progress_node);
}
if (!watch) return cleanExit();
switch (builtin.os.tag) {
@ -430,6 +439,43 @@ pub fn main() !void {
}
}
fn startFuzzing(thread_pool: *std.Thread.Pool, all_steps: []const *Step, prog_node: std.Progress.Node) void {
{
const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
defer rebuild_node.end();
var count: usize = 0;
var wait_group: std.Thread.WaitGroup = .{};
defer wait_group.wait();
for (all_steps) |step| {
const run = step.cast(Step.Run) orelse continue;
if (run.fuzz_tests.items.len > 0 and run.producer != null) {
thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, prog_node });
count += 1;
}
}
if (count == 0) {
std.debug.lockStdErr();
std.debug.print("no fuzz tests found\n", .{});
process.exit(2);
}
rebuild_node.setEstimatedTotalItems(count);
}
@panic("TODO do something with the rebuilt unit tests");
}
fn rebuildTestsWorkerRun(run: *Step.Run, parent_prog_node: std.Progress.Node) void {
const compile_step = run.producer.?;
const prog_node = parent_prog_node.start(compile_step.step.name, 0);
defer prog_node.end();
const rebuilt_bin_path = compile_step.rebuildInFuzzMode(prog_node) catch |err| {
std.debug.print("failed to rebuild {s} in fuzz mode: {s}", .{
compile_step.step.name, @errorName(err),
});
return;
};
std.debug.print("rebuilt binary: '{s}'\n", .{rebuilt_bin_path});
}
fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
for (all_steps) |step| switch (step.state) {
.dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
@ -457,6 +503,7 @@ const Run = struct {
max_rss_mutex: std.Thread.Mutex,
skip_oom_steps: bool,
watch: bool,
fuzz: bool,
memory_blocked_steps: std.ArrayList(*Step),
step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
prominent_compile_errors: bool,
@ -466,6 +513,11 @@ const Run = struct {
summary: Summary,
ttyconf: std.io.tty.Config,
stderr: File,
fn cleanExit(run: Run) void {
if (run.watch or run.fuzz) return;
return runner.cleanExit();
}
};
fn prepare(
@ -614,8 +666,7 @@ fn runStepNames(
else => false,
};
if (failure_count == 0 and failures_only) {
if (!run.watch) cleanExit();
return;
return run.cleanExit();
}
const ttyconf = run.ttyconf;
@ -672,8 +723,7 @@ fn runStepNames(
}
if (failure_count == 0) {
if (!run.watch) cleanExit();
return;
return run.cleanExit();
}
// Finally, render compile errors at the bottom of the terminal.
@ -1226,6 +1276,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
\\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
\\ --fetch Exit after fetching dependency tree
\\ --watch Continuously rebuild when source files are modified
\\ --fuzz Continuously search for unit test failures
\\ --debounce <ms> Delay before rebuilding after changed file detected
\\ -fincremental Enable incremental compilation
\\ -fno-incremental Disable incremental compilation

View file

@ -143,6 +143,7 @@ fn mainTerminal() void {
var ok_count: usize = 0;
var skip_count: usize = 0;
var fail_count: usize = 0;
var fuzz_count: usize = 0;
const root_node = std.Progress.start(.{
.root_name = "Test",
.estimated_total_items = test_fn_list.len,
@ -168,7 +169,7 @@ fn mainTerminal() void {
if (!have_tty) {
std.debug.print("{d}/{d} {s}...", .{ i + 1, test_fn_list.len, test_fn.name });
}
// Track in a global variable so that `fuzzInput` can see it.
is_fuzz_test = false;
if (test_fn.func()) |_| {
ok_count += 1;
test_node.end();
@ -198,6 +199,7 @@ fn mainTerminal() void {
test_node.end();
},
}
fuzz_count += @intFromBool(is_fuzz_test);
}
root_node.end();
if (ok_count == test_fn_list.len) {
@ -211,6 +213,9 @@ fn mainTerminal() void {
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);
}

View file

@ -977,6 +977,7 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {
// Consider that this is declarative; the run step may not be run unless a user
// option is supplied.
const run_step = Step.Run.create(b, b.fmt("run {s}", .{exe.name}));
run_step.producer = exe;
if (exe.kind == .@"test") {
if (exe.exec_cmd_args) |exec_cmd_args| {
for (exec_cmd_args) |cmd_arg| {

View file

@ -1004,7 +1004,7 @@ fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking
return path;
}
fn getZigArgs(compile: *Compile) ![][]const u8 {
fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
const step = &compile.step;
const b = step.owner;
const arena = b.allocator;
@ -1055,6 +1055,10 @@ fn getZigArgs(compile: *Compile) ![][]const u8 {
try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));
}
if (fuzz) {
try zig_args.append("-ffuzz");
}
{
// Stores system libraries that have already been seen for at least one
// module, along with any arguments that need to be passed to the
@ -1757,7 +1761,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
const b = step.owner;
const compile: *Compile = @fieldParentPtr("step", step);
const zig_args = try getZigArgs(compile);
const zig_args = try getZigArgs(compile, false);
const maybe_output_bin_path = step.evalZigProcess(
zig_args,
@ -1835,6 +1839,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
}
}
pub fn rebuildInFuzzMode(c: *Compile, progress_node: std.Progress.Node) ![]const u8 {
const zig_args = try getZigArgs(c, true);
const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false);
return maybe_output_bin_path.?;
}
pub fn doAtomicSymLinks(
step: *Step,
output_path: []const u8,
@ -1861,10 +1871,10 @@ pub fn doAtomicSymLinks(
};
}
fn execPkgConfigList(compile: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
const pkg_config_exe = compile.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
const stdout = try compile.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
var list = ArrayList(PkgConfigPkg).init(compile.allocator);
fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
var list = ArrayList(PkgConfigPkg).init(b.allocator);
errdefer list.deinit();
var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
while (line_it.next()) |line| {
@ -1878,13 +1888,13 @@ fn execPkgConfigList(compile: *std.Build, out_code: *u8) (PkgConfigError || RunE
return list.toOwnedSlice();
}
fn getPkgConfigList(compile: *std.Build) ![]const PkgConfigPkg {
if (compile.pkg_config_pkg_list) |res| {
fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
if (b.pkg_config_pkg_list) |res| {
return res;
}
var code: u8 = undefined;
if (execPkgConfigList(compile, &code)) |list| {
compile.pkg_config_pkg_list = list;
if (execPkgConfigList(b, &code)) |list| {
b.pkg_config_pkg_list = list;
return list;
} else |err| {
const result = switch (err) {
@ -1896,7 +1906,7 @@ fn getPkgConfigList(compile: *std.Build) ![]const PkgConfigPkg {
error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
else => return err,
};
compile.pkg_config_pkg_list = result;
b.pkg_config_pkg_list = result;
return result;
}
}

View file

@ -86,6 +86,13 @@ dep_output_file: ?*Output,
has_side_effects: bool,
/// If this is a Zig unit test binary, this tracks the indexes of the unit
/// tests that are also fuzz tests.
fuzz_tests: std.ArrayListUnmanaged(u32),
/// If this Run step was produced by a Compile step, it is tracked here.
producer: ?*Step.Compile,
pub const StdIn = union(enum) {
none,
bytes: []const u8,
@ -175,6 +182,8 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {
.captured_stderr = null,
.dep_output_file = null,
.has_side_effects = false,
.fuzz_tests = .{},
.producer = null,
};
return run;
}
@ -1347,6 +1356,8 @@ fn evalZigTest(
var sub_prog_node: ?std.Progress.Node = null;
defer if (sub_prog_node) |n| n.end();
run.fuzz_tests.clearRetainingCapacity();
poll: while (true) {
while (stdout.readableLength() < @sizeOf(Header)) {
if (!(try poller.poll())) break :poll;
@ -1404,6 +1415,8 @@ fn evalZigTest(
leak_count +|= @intFromBool(tr_hdr.flags.leak);
log_err_count +|= tr_hdr.flags.log_err_count;
if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index);
if (tr_hdr.flags.fail or tr_hdr.flags.leak or tr_hdr.flags.log_err_count > 0) {
const name = std.mem.sliceTo(md.string_bytes[md.names[tr_hdr.index]..], 0);
const orig_msg = stderr.readableSlice(0);