Commit 047640383e

Andrew Kelley <andrew@ziglang.org>
2024-07-24 05:49:00
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.
1 parent 6f37678
Changed files (5)
lib/compiler/build_runner.zig
@@ -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
lib/compiler/test_runner.zig
@@ -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);
     }
lib/std/Build/Step/Compile.zig
@@ -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;
     }
 }
lib/std/Build/Step/Run.zig
@@ -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);
lib/std/Build.zig
@@ -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| {