authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-23 20:49:00-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-25 18:52:20-07:00
log047640383e5e635ffe52ab360e03dbe08e73d025
tree47b7062d2f80b34655481bf91f06bef4d132cfe7
parent6f3767862d6886d5fde7e3734455a30f168ba80b

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.

5 files changed, 97 insertions(+), 17 deletions(-)

lib/compiler/build_runner.zig+56-5
...@@ -10,7 +10,8 @@ const File = std.fs.File;...@@ -10,7 +10,8 @@ const File = std.fs.File;
10const Step = std.Build.Step;10const Step = std.Build.Step;
11const Watch = std.Build.Watch;11const Watch = std.Build.Watch;
12const Allocator = std.mem.Allocator;12const Allocator = std.mem.Allocator;
13const fatal = std.zig.fatal;13const fatal = std.process.fatal;
14const runner = @This();
1415
15pub const root = @import("@build");16pub const root = @import("@build");
16pub const dependencies = @import("@dependencies");17pub const dependencies = @import("@dependencies");
...@@ -102,6 +103,7 @@ pub fn main() !void {...@@ -102,6 +103,7 @@ pub fn main() !void {
102 var steps_menu = false;103 var steps_menu = false;
103 var output_tmp_nonce: ?[16]u8 = null;104 var output_tmp_nonce: ?[16]u8 = null;
104 var watch = false;105 var watch = false;
106 var fuzz = false;
105 var debounce_interval_ms: u16 = 50;107 var debounce_interval_ms: u16 = 50;
106108
107 while (nextArg(args, &arg_idx)) |arg| {109 while (nextArg(args, &arg_idx)) |arg| {
...@@ -234,6 +236,8 @@ pub fn main() !void {...@@ -234,6 +236,8 @@ pub fn main() !void {
234 prominent_compile_errors = true;236 prominent_compile_errors = true;
235 } else if (mem.eql(u8, arg, "--watch")) {237 } else if (mem.eql(u8, arg, "--watch")) {
236 watch = true;238 watch = true;
239 } else if (mem.eql(u8, arg, "--fuzz")) {
240 fuzz = true;
237 } else if (mem.eql(u8, arg, "-fincremental")) {241 } else if (mem.eql(u8, arg, "-fincremental")) {
238 graph.incremental = true;242 graph.incremental = true;
239 } else if (mem.eql(u8, arg, "-fno-incremental")) {243 } else if (mem.eql(u8, arg, "-fno-incremental")) {
...@@ -353,6 +357,7 @@ pub fn main() !void {...@@ -353,6 +357,7 @@ pub fn main() !void {
353 .max_rss_mutex = .{},357 .max_rss_mutex = .{},
354 .skip_oom_steps = skip_oom_steps,358 .skip_oom_steps = skip_oom_steps,
355 .watch = watch,359 .watch = watch,
360 .fuzz = fuzz,
356 .memory_blocked_steps = std.ArrayList(*Step).init(arena),361 .memory_blocked_steps = std.ArrayList(*Step).init(arena),
357 .step_stack = .{},362 .step_stack = .{},
358 .prominent_compile_errors = prominent_compile_errors,363 .prominent_compile_errors = prominent_compile_errors,
...@@ -394,6 +399,10 @@ pub fn main() !void {...@@ -394,6 +399,10 @@ pub fn main() !void {
394 },399 },
395 else => return err,400 else => return err,
396 };401 };
402 if (fuzz) {
403 startFuzzing(&run.thread_pool, run.step_stack.keys(), main_progress_node);
404 }
405
397 if (!watch) return cleanExit();406 if (!watch) return cleanExit();
398407
399 switch (builtin.os.tag) {408 switch (builtin.os.tag) {
...@@ -430,6 +439,43 @@ pub fn main() !void {...@@ -430,6 +439,43 @@ pub fn main() !void {
430 }439 }
431}440}
432441
442fn startFuzzing(thread_pool: *std.Thread.Pool, all_steps: []const *Step, prog_node: std.Progress.Node) void {
443 {
444 const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
445 defer rebuild_node.end();
446 var count: usize = 0;
447 var wait_group: std.Thread.WaitGroup = .{};
448 defer wait_group.wait();
449 for (all_steps) |step| {
450 const run = step.cast(Step.Run) orelse continue;
451 if (run.fuzz_tests.items.len > 0 and run.producer != null) {
452 thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, prog_node });
453 count += 1;
454 }
455 }
456 if (count == 0) {
457 std.debug.lockStdErr();
458 std.debug.print("no fuzz tests found\n", .{});
459 process.exit(2);
460 }
461 rebuild_node.setEstimatedTotalItems(count);
462 }
463 @panic("TODO do something with the rebuilt unit tests");
464}
465
466fn rebuildTestsWorkerRun(run: *Step.Run, parent_prog_node: std.Progress.Node) void {
467 const compile_step = run.producer.?;
468 const prog_node = parent_prog_node.start(compile_step.step.name, 0);
469 defer prog_node.end();
470 const rebuilt_bin_path = compile_step.rebuildInFuzzMode(prog_node) catch |err| {
471 std.debug.print("failed to rebuild {s} in fuzz mode: {s}", .{
472 compile_step.step.name, @errorName(err),
473 });
474 return;
475 };
476 std.debug.print("rebuilt binary: '{s}'\n", .{rebuilt_bin_path});
477}
478
433fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {479fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
434 for (all_steps) |step| switch (step.state) {480 for (all_steps) |step| switch (step.state) {
435 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),481 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
...@@ -457,6 +503,7 @@ const Run = struct {...@@ -457,6 +503,7 @@ const Run = struct {
457 max_rss_mutex: std.Thread.Mutex,503 max_rss_mutex: std.Thread.Mutex,
458 skip_oom_steps: bool,504 skip_oom_steps: bool,
459 watch: bool,505 watch: bool,
506 fuzz: bool,
460 memory_blocked_steps: std.ArrayList(*Step),507 memory_blocked_steps: std.ArrayList(*Step),
461 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),508 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
462 prominent_compile_errors: bool,509 prominent_compile_errors: bool,
...@@ -466,6 +513,11 @@ const Run = struct {...@@ -466,6 +513,11 @@ const Run = struct {
466 summary: Summary,513 summary: Summary,
467 ttyconf: std.io.tty.Config,514 ttyconf: std.io.tty.Config,
468 stderr: File,515 stderr: File,
516
517 fn cleanExit(run: Run) void {
518 if (run.watch or run.fuzz) return;
519 return runner.cleanExit();
520 }
469};521};
470522
471fn prepare(523fn prepare(
...@@ -614,8 +666,7 @@ fn runStepNames(...@@ -614,8 +666,7 @@ fn runStepNames(
614 else => false,666 else => false,
615 };667 };
616 if (failure_count == 0 and failures_only) {668 if (failure_count == 0 and failures_only) {
617 if (!run.watch) cleanExit();669 return run.cleanExit();
618 return;
619 }670 }
620671
621 const ttyconf = run.ttyconf;672 const ttyconf = run.ttyconf;
...@@ -672,8 +723,7 @@ fn runStepNames(...@@ -672,8 +723,7 @@ fn runStepNames(
672 }723 }
673724
674 if (failure_count == 0) {725 if (failure_count == 0) {
675 if (!run.watch) cleanExit();726 return run.cleanExit();
676 return;
677 }727 }
678728
679 // Finally, render compile errors at the bottom of the terminal.729 // Finally, render compile errors at the bottom of the terminal.
...@@ -1226,6 +1276,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {...@@ -1226,6 +1276,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
1226 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss1276 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1227 \\ --fetch Exit after fetching dependency tree1277 \\ --fetch Exit after fetching dependency tree
1228 \\ --watch Continuously rebuild when source files are modified1278 \\ --watch Continuously rebuild when source files are modified
1279 \\ --fuzz Continuously search for unit test failures
1229 \\ --debounce <ms> Delay before rebuilding after changed file detected1280 \\ --debounce <ms> Delay before rebuilding after changed file detected
1230 \\ -fincremental Enable incremental compilation1281 \\ -fincremental Enable incremental compilation
1231 \\ -fno-incremental Disable incremental compilation1282 \\ -fno-incremental Disable incremental compilation
lib/compiler/test_runner.zig+6-1
...@@ -143,6 +143,7 @@ fn mainTerminal() void {...@@ -143,6 +143,7 @@ fn mainTerminal() void {
143 var ok_count: usize = 0;143 var ok_count: usize = 0;
144 var skip_count: usize = 0;144 var skip_count: usize = 0;
145 var fail_count: usize = 0;145 var fail_count: usize = 0;
146 var fuzz_count: usize = 0;
146 const root_node = std.Progress.start(.{147 const root_node = std.Progress.start(.{
147 .root_name = "Test",148 .root_name = "Test",
148 .estimated_total_items = test_fn_list.len,149 .estimated_total_items = test_fn_list.len,
...@@ -168,7 +169,7 @@ fn mainTerminal() void {...@@ -168,7 +169,7 @@ fn mainTerminal() void {
168 if (!have_tty) {169 if (!have_tty) {
169 std.debug.print("{d}/{d} {s}...", .{ i + 1, test_fn_list.len, test_fn.name });170 std.debug.print("{d}/{d} {s}...", .{ i + 1, test_fn_list.len, test_fn.name });
170 }171 }
171 // Track in a global variable so that `fuzzInput` can see it.172 is_fuzz_test = false;
172 if (test_fn.func()) |_| {173 if (test_fn.func()) |_| {
173 ok_count += 1;174 ok_count += 1;
174 test_node.end();175 test_node.end();
...@@ -198,6 +199,7 @@ fn mainTerminal() void {...@@ -198,6 +199,7 @@ fn mainTerminal() void {
198 test_node.end();199 test_node.end();
199 },200 },
200 }201 }
202 fuzz_count += @intFromBool(is_fuzz_test);
201 }203 }
202 root_node.end();204 root_node.end();
203 if (ok_count == test_fn_list.len) {205 if (ok_count == test_fn_list.len) {
...@@ -211,6 +213,9 @@ fn mainTerminal() void {...@@ -211,6 +213,9 @@ fn mainTerminal() void {
211 if (leaks != 0) {213 if (leaks != 0) {
212 std.debug.print("{d} tests leaked memory.\n", .{leaks});214 std.debug.print("{d} tests leaked memory.\n", .{leaks});
213 }215 }
216 if (fuzz_count != 0) {
217 std.debug.print("{d} fuzz tests found.\n", .{fuzz_count});
218 }
214 if (leaks != 0 or log_err_count != 0 or fail_count != 0) {219 if (leaks != 0 or log_err_count != 0 or fail_count != 0) {
215 std.process.exit(1);220 std.process.exit(1);
216 }221 }
lib/std/Build.zig+1
...@@ -977,6 +977,7 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {...@@ -977,6 +977,7 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {
977 // Consider that this is declarative; the run step may not be run unless a user977 // Consider that this is declarative; the run step may not be run unless a user
978 // option is supplied.978 // option is supplied.
979 const run_step = Step.Run.create(b, b.fmt("run {s}", .{exe.name}));979 const run_step = Step.Run.create(b, b.fmt("run {s}", .{exe.name}));
980 run_step.producer = exe;
980 if (exe.kind == .@"test") {981 if (exe.kind == .@"test") {
981 if (exe.exec_cmd_args) |exec_cmd_args| {982 if (exe.exec_cmd_args) |exec_cmd_args| {
982 for (exec_cmd_args) |cmd_arg| {983 for (exec_cmd_args) |cmd_arg| {
lib/std/Build/Step/Compile.zig+21-11
...@@ -1004,7 +1004,7 @@ fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking...@@ -1004,7 +1004,7 @@ fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking
1004 return path;1004 return path;
1005}1005}
10061006
1007fn getZigArgs(compile: *Compile) ![][]const u8 {1007fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
1008 const step = &compile.step;1008 const step = &compile.step;
1009 const b = step.owner;1009 const b = step.owner;
1010 const arena = b.allocator;1010 const arena = b.allocator;
...@@ -1055,6 +1055,10 @@ fn getZigArgs(compile: *Compile) ![][]const u8 {...@@ -1055,6 +1055,10 @@ fn getZigArgs(compile: *Compile) ![][]const u8 {
1055 try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));1055 try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));
1056 }1056 }
10571057
1058 if (fuzz) {
1059 try zig_args.append("-ffuzz");
1060 }
1061
1058 {1062 {
1059 // Stores system libraries that have already been seen for at least one1063 // Stores system libraries that have already been seen for at least one
1060 // module, along with any arguments that need to be passed to the1064 // module, along with any arguments that need to be passed to the
...@@ -1757,7 +1761,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -1757,7 +1761,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
1757 const b = step.owner;1761 const b = step.owner;
1758 const compile: *Compile = @fieldParentPtr("step", step);1762 const compile: *Compile = @fieldParentPtr("step", step);
17591763
1760 const zig_args = try getZigArgs(compile);1764 const zig_args = try getZigArgs(compile, false);
17611765
1762 const maybe_output_bin_path = step.evalZigProcess(1766 const maybe_output_bin_path = step.evalZigProcess(
1763 zig_args,1767 zig_args,
...@@ -1835,6 +1839,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -1835,6 +1839,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
1835 }1839 }
1836}1840}
18371841
1842pub fn rebuildInFuzzMode(c: *Compile, progress_node: std.Progress.Node) ![]const u8 {
1843 const zig_args = try getZigArgs(c, true);
1844 const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false);
1845 return maybe_output_bin_path.?;
1846}
1847
1838pub fn doAtomicSymLinks(1848pub fn doAtomicSymLinks(
1839 step: *Step,1849 step: *Step,
1840 output_path: []const u8,1850 output_path: []const u8,
...@@ -1861,10 +1871,10 @@ pub fn doAtomicSymLinks(...@@ -1861,10 +1871,10 @@ pub fn doAtomicSymLinks(
1861 };1871 };
1862}1872}
18631873
1864fn execPkgConfigList(compile: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {1874fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
1865 const pkg_config_exe = compile.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";1875 const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
1866 const stdout = try compile.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);1876 const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
1867 var list = ArrayList(PkgConfigPkg).init(compile.allocator);1877 var list = ArrayList(PkgConfigPkg).init(b.allocator);
1868 errdefer list.deinit();1878 errdefer list.deinit();
1869 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");1879 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
1870 while (line_it.next()) |line| {1880 while (line_it.next()) |line| {
...@@ -1878,13 +1888,13 @@ fn execPkgConfigList(compile: *std.Build, out_code: *u8) (PkgConfigError || RunE...@@ -1878,13 +1888,13 @@ fn execPkgConfigList(compile: *std.Build, out_code: *u8) (PkgConfigError || RunE
1878 return list.toOwnedSlice();1888 return list.toOwnedSlice();
1879}1889}
18801890
1881fn getPkgConfigList(compile: *std.Build) ![]const PkgConfigPkg {1891fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
1882 if (compile.pkg_config_pkg_list) |res| {1892 if (b.pkg_config_pkg_list) |res| {
1883 return res;1893 return res;
1884 }1894 }
1885 var code: u8 = undefined;1895 var code: u8 = undefined;
1886 if (execPkgConfigList(compile, &code)) |list| {1896 if (execPkgConfigList(b, &code)) |list| {
1887 compile.pkg_config_pkg_list = list;1897 b.pkg_config_pkg_list = list;
1888 return list;1898 return list;
1889 } else |err| {1899 } else |err| {
1890 const result = switch (err) {1900 const result = switch (err) {
...@@ -1896,7 +1906,7 @@ fn getPkgConfigList(compile: *std.Build) ![]const PkgConfigPkg {...@@ -1896,7 +1906,7 @@ fn getPkgConfigList(compile: *std.Build) ![]const PkgConfigPkg {
1896 error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,1906 error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
1897 else => return err,1907 else => return err,
1898 };1908 };
1899 compile.pkg_config_pkg_list = result;1909 b.pkg_config_pkg_list = result;
1900 return result;1910 return result;
1901 }1911 }
1902}1912}
lib/std/Build/Step/Run.zig+13
...@@ -86,6 +86,13 @@ dep_output_file: ?*Output,...@@ -86,6 +86,13 @@ dep_output_file: ?*Output,
8686
87has_side_effects: bool,87has_side_effects: bool,
8888
89/// If this is a Zig unit test binary, this tracks the indexes of the unit
90/// tests that are also fuzz tests.
91fuzz_tests: std.ArrayListUnmanaged(u32),
92
93/// If this Run step was produced by a Compile step, it is tracked here.
94producer: ?*Step.Compile,
95
89pub const StdIn = union(enum) {96pub const StdIn = union(enum) {
90 none,97 none,
91 bytes: []const u8,98 bytes: []const u8,
...@@ -175,6 +182,8 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {...@@ -175,6 +182,8 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {
175 .captured_stderr = null,182 .captured_stderr = null,
176 .dep_output_file = null,183 .dep_output_file = null,
177 .has_side_effects = false,184 .has_side_effects = false,
185 .fuzz_tests = .{},
186 .producer = null,
178 };187 };
179 return run;188 return run;
180}189}
...@@ -1347,6 +1356,8 @@ fn evalZigTest(...@@ -1347,6 +1356,8 @@ fn evalZigTest(
1347 var sub_prog_node: ?std.Progress.Node = null;1356 var sub_prog_node: ?std.Progress.Node = null;
1348 defer if (sub_prog_node) |n| n.end();1357 defer if (sub_prog_node) |n| n.end();
13491358
1359 run.fuzz_tests.clearRetainingCapacity();
1360
1350 poll: while (true) {1361 poll: while (true) {
1351 while (stdout.readableLength() < @sizeOf(Header)) {1362 while (stdout.readableLength() < @sizeOf(Header)) {
1352 if (!(try poller.poll())) break :poll;1363 if (!(try poller.poll())) break :poll;
...@@ -1404,6 +1415,8 @@ fn evalZigTest(...@@ -1404,6 +1415,8 @@ fn evalZigTest(
1404 leak_count +|= @intFromBool(tr_hdr.flags.leak);1415 leak_count +|= @intFromBool(tr_hdr.flags.leak);
1405 log_err_count +|= tr_hdr.flags.log_err_count;1416 log_err_count +|= tr_hdr.flags.log_err_count;
14061417
1418 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index);
1419
1407 if (tr_hdr.flags.fail or tr_hdr.flags.leak or tr_hdr.flags.log_err_count > 0) {1420 if (tr_hdr.flags.fail or tr_hdr.flags.leak or tr_hdr.flags.log_err_count > 0) {
1408 const name = std.mem.sliceTo(md.string_bytes[md.names[tr_hdr.index]..], 0);1421 const name = std.mem.sliceTo(md.string_bytes[md.names[tr_hdr.index]..], 0);
1409 const orig_msg = stderr.readableSlice(0);1422 const orig_msg = stderr.readableSlice(0);