ReviewOS

also looking at this

zig-utils/zig-test-framework

Add retries, repeated runs, and flaky-test reporting

#25
Merged glennmichael123 wants to merge codex/issue-7-retries-repeat-flaky into main
15 files +719 -53
src/cli.zigmodified+70-0
Changes to src/cli.zig
@@ -45,6 +45,10 @@ pub const CLIOptions = struct {
4545 junit_output: ?[]const u8 = null,
4646 // Timeout options
4747 timeout: ?u64 = null, // Global timeout in milliseconds
48 // Retry and repetition options
49 retries: usize = 0,
50 repeat: usize = 1,
51 fail_on_flaky: bool = false,
4852};
4953
5054pub const version = build_options.version;
@@ -75,6 +79,9 @@ pub const CLI = struct {
7579 self.options.no_recursive = !config.test_options.recursive;
7680 self.options.filter = config.test_options.filter;
7781 self.options.timeout = config.test_options.timeout;
82 self.options.retries = config.test_options.retries;
83 self.options.repeat = config.test_options.repeat;
84 self.options.fail_on_flaky = config.test_options.fail_on_flaky;
7885 self.options.shard_index = config.sharding.index;
7986 self.options.shard_count = config.sharding.count;
8087 self.options.parallel = config.parallel.enabled;
@@ -107,6 +114,32 @@ pub const CLI = struct {
107114 self.options.version = true;
108115 } else if (std.mem.eql(u8, arg, "--bail") or std.mem.eql(u8, arg, "-b")) {
109116 self.options.bail = true;
117 } else if (std.mem.eql(u8, arg, "--retry") or std.mem.eql(u8, arg, "--retries")) {
118 if (i + 1 >= args.len) {
119 std.debug.print("Error: {s} requires a value\n", .{arg});
120 return CLIError.MissingValue;
121 }
122 i += 1;
123 self.options.retries = std.fmt.parseInt(usize, args[i], 10) catch {
124 std.debug.print("Error: {s} must be a valid number\n", .{arg});
125 return CLIError.InvalidArgument;
126 };
127 } else if (std.mem.eql(u8, arg, "--repeat")) {
128 if (i + 1 >= args.len) {
129 std.debug.print("Error: --repeat requires a value\n", .{});
130 return CLIError.MissingValue;
131 }
132 i += 1;
133 self.options.repeat = std.fmt.parseInt(usize, args[i], 10) catch {
134 std.debug.print("Error: --repeat must be a valid positive number\n", .{});
135 return CLIError.InvalidArgument;
136 };
137 if (self.options.repeat == 0) {
138 std.debug.print("Error: --repeat must be greater than zero\n", .{});
139 return CLIError.InvalidArgument;
140 }
141 } else if (std.mem.eql(u8, arg, "--fail-on-flaky")) {
142 self.options.fail_on_flaky = true;
110143 } else if (std.mem.eql(u8, arg, "--verbose")) {
111144 self.options.verbose = true;
112145 } else if (std.mem.eql(u8, arg, "--quiet") or std.mem.eql(u8, arg, "-q")) {
@@ -351,6 +384,9 @@ pub const CLI = struct {
351384 \\ -h, --help Show this help message
352385 \\ -v, --version Show version information
353386 \\ -b, --bail Stop test execution on first failure
387 \\ --retry <N> Retry each failed test up to N times
388 \\ --repeat <N> Require N successful runs of each selected test
389 \\ --fail-on-flaky Exit unsuccessfully when a retry recovers a failure
354390 \\ --filter <pattern> Run only tests matching pattern
355391 \\ --grep <pattern> Same as --filter (alias)
356392 \\ --reporter <name> Set reporter type (spec, dot, json, tap, junit)
@@ -414,6 +450,8 @@ pub const CLI = struct {
414450 \\ zig-test --update-snapshots Update all snapshots
415451 \\ zig-test --watch Run in watch mode
416452 \\ zig-test --timeout 10000 Set 10s timeout for all tests
453 \\ zig-test --retry 2 --fail-on-flaky Detect and reject flaky tests
454 \\ zig-test --repeat 100 --filter parser Stress-run selected tests
417455 \\ zig-test --profile-memory --fail-on-leak
418456 \\ zig-test --parallel --jobs 4 Run with 4 parallel workers
419457 \\ zig-test --config zig-test.json Load config from file
@@ -441,6 +479,9 @@ pub const CLI = struct {
441479 .n_jobs = self.options.jobs,
442480 .junit_output = self.options.junit_output orelse "test-results.xml",
443481 .timeout_ms = self.options.timeout,
482 .retries = self.options.retries,
483 .repeat = self.options.repeat,
484 .fail_on_flaky = self.options.fail_on_flaky,
444485 };
445486 }
446487};
@@ -662,6 +703,35 @@ test "CLI parse timeout" {
662703 try std.testing.expectEqual(@as(?u64, 5000), cli.options.timeout);
663704}
664705
706test "CLI parses retry repeat and flaky exit policy" {
707 var cli = CLI.init(std.testing.allocator);
708 const args = [_][]const u8{
709 "zig-test",
710 "--retry",
711 "2",
712 "--repeat",
713 "5",
714 "--fail-on-flaky",
715 };
716 try cli.parse(&args);
717 try std.testing.expectEqual(@as(usize, 2), cli.options.retries);
718 try std.testing.expectEqual(@as(usize, 5), cli.options.repeat);
719 try std.testing.expect(cli.options.fail_on_flaky);
720
721 const runner_options = cli.toRunnerOptions();
722 try std.testing.expectEqual(@as(usize, 2), runner_options.retries);
723 try std.testing.expectEqual(@as(usize, 5), runner_options.repeat);
724 try std.testing.expect(runner_options.fail_on_flaky);
725}
726
727test "CLI rejects a zero repeat count" {
728 var cli = CLI.init(std.testing.allocator);
729 try std.testing.expectError(
730 CLIError.InvalidArgument,
731 cli.parse(&.{ "zig-test", "--repeat", "0" }),
732 );
733}
734
665735test "CLI rejects invalid coverage tool" {
666736 var cli = CLI.init(std.testing.allocator);
667737 const args = [_][]const u8{ "zig-test", "--coverage-tool", "unknown" };