ReviewOS

also looking at this

zig-utils/zig-test-framework

Add reproducible randomized test order

#29
Merged glennmichael123 wants to merge codex/issue-14-random-order into main
18 files +487 -42
src/test_runner.zigmodified+133-13
Changes to src/test_runner.zig
@@ -4,6 +4,7 @@ const reporter_mod = ;
44const pipeline = ;
55const parallel = ;
66const compat = ;
7const random_order = ;
78
89pub const RunnerError = error{
910 NoTestsFound,
@@ -25,6 +26,10 @@ pub const RunnerOptions = struct {
2526 repeat: usize = 1,
2627 /// Treat tests that pass after a retry as an unsuccessful run.
2728 fail_on_flaky: bool = false,
29 /// Shuffle suites and tests. A generated seed is printed for replay.
30 shuffle: bool = false,
31 /// Reproduce a shuffled run. Supplying a seed implicitly enables shuffle.
32 seed: ?u64 = null,
2833 /// Optional output supplied by a CLI host. Embedded/test callers default
2934 /// to stderr so they do not interfere with Zig's stdout test protocol.
3035 reporter_writer: ?*std.Io.Writer = null,
@@ -71,6 +76,9 @@ pub const TestRunner = struct {
7176 );
7277 defer reporters.deinit();
7378 const current_reporter = reporters.selected();
79 const random_seed = random_order.resolveSeed(self.options.shuffle, self.options.seed);
80 current_reporter.random_seed = random_seed;
81 self.results.random_seed = random_seed;
7482 const events = pipeline.EventStream{ .rep = current_reporter };
7583
7684 var plan = try pipeline.TestPlan.fromRegistry(self.allocator, self.registry);
@@ -87,6 +95,7 @@ pub const TestRunner = struct {
8795 .n_jobs = self.options.n_jobs,
8896 .filter = self.options.filter,
8997 .timeout_ms = self.options.timeout_ms,
98 .random_seed = random_seed,
9099 };
91100
92101 const all_passed = try parallel.runTestsParallel(
@@ -106,9 +115,19 @@ pub const TestRunner = struct {
106115 }
107116
108117 // Sequential execution (original behavior)
118 var shuffler: random_order.Shuffler = undefined;
119 const shuffler_ptr: ?*random_order.Shuffler = if (random_seed) |seed| blk: {
120 shuffler = .init(seed);
121 break :blk &shuffler;
122 } else null;
123 var root_suites: std.ArrayList(*suite.TestSuite) = .empty;
124 defer root_suites.deinit(self.allocator);
125 try root_suites.appendSlice(self.allocator, self.registry.root_suites.items);
126 if (shuffler_ptr) |randomizer| randomizer.shuffle(*suite.TestSuite, root_suites.items);
127
109128 try events.emit(.{ .run_started = total_tests });
110 for (self.registry.root_suites.items) |test_suite| {
111 try self.runSuite(test_suite, events);
129 for (root_suites.items) |test_suite| {
130 try self.runSuite(test_suite, events, shuffler_ptr);
112131 if (self.policy().shouldStop(&self.results)) {
113132 break;
114133 }
@@ -130,15 +149,20 @@ pub const TestRunner = struct {
130149 }
131150
132151 /// Run a single test suite
133 fn runSuite(self: *Self, test_suite: *suite.TestSuite, events: pipeline.EventStream) !void {
152 fn runSuite(
153 self: *Self,
154 test_suite: *suite.TestSuite,
155 events: pipeline.EventStream,
156 shuffler: ?*random_order.Shuffler,
157 ) !void {
134158 // Skip if marked as skip or if has_only and this isn't marked as only
135159 if (test_suite.shouldSkip()) {
136 try self.skipAllTests(test_suite, events);
160 try self.skipAllTests(test_suite, events, shuffler);
137161 return;
138162 }
139163
140164 if (self.registry.has_only and !test_suite.hasOnly()) {
141 try self.skipAllTests(test_suite, events);
165 try self.skipAllTests(test_suite, events, shuffler);
142166 return;
143167 }
144168
@@ -148,13 +172,17 @@ pub const TestRunner = struct {
148172 // Run beforeAll hooks
149173 test_suite.runBeforeAllHooks(self.allocator) catch |err| {
150174 std.debug.print("beforeAll hook failed: {any}\n", .{err});
151 try self.skipAllTests(test_suite, events);
175 try self.skipAllTests(test_suite, events, shuffler);
152176 try events.emit(.{ .suite_finished = test_suite.name });
153177 return;
154178 };
155179
156180 // Run tests in this suite
157 for (test_suite.tests.items) |*test_case| {
181 var tests: std.ArrayList(*suite.TestCase) = .empty;
182 defer tests.deinit(self.allocator);
183 for (test_suite.tests.items) |*test_case| try tests.append(self.allocator, test_case);
184 if (shuffler) |randomizer| randomizer.shuffle(*suite.TestCase, tests.items);
185 for (tests.items) |test_case| {
158186 if (test_case.skip or (self.registry.has_only and !test_case.only)) {
159187 test_case.status = .skipped;
160188 try events.emit(.{ .test_finished = test_case });
@@ -178,8 +206,12 @@ pub const TestRunner = struct {
178206 }
179207
180208 // Run nested suites
181 for (test_suite.suites.items) |nested_suite| {
182 try self.runSuite(nested_suite, events);
209 var nested_suites: std.ArrayList(*suite.TestSuite) = .empty;
210 defer nested_suites.deinit(self.allocator);
211 try nested_suites.appendSlice(self.allocator, test_suite.suites.items);
212 if (shuffler) |randomizer| randomizer.shuffle(*suite.TestSuite, nested_suites.items);
213 for (nested_suites.items) |nested_suite| {
214 try self.runSuite(nested_suite, events, shuffler);
183215 if (self.policy().shouldStop(&self.results)) {
184216 break;
185217 }
@@ -298,15 +330,28 @@ pub const TestRunner = struct {
298330 }
299331
300332 /// Skip all tests in a suite
301 fn skipAllTests(self: *Self, test_suite: *suite.TestSuite, events: pipeline.EventStream) !void {
302 for (test_suite.tests.items) |*test_case| {
333 fn skipAllTests(
334 self: *Self,
335 test_suite: *suite.TestSuite,
336 events: pipeline.EventStream,
337 shuffler: ?*random_order.Shuffler,
338 ) !void {
339 var tests: std.ArrayList(*suite.TestCase) = .empty;
340 defer tests.deinit(self.allocator);
341 for (test_suite.tests.items) |*test_case| try tests.append(self.allocator, test_case);
342 if (shuffler) |randomizer| randomizer.shuffle(*suite.TestCase, tests.items);
343 for (tests.items) |test_case| {
303344 test_case.status = .skipped;
304345 try self.results.addTest(test_case);
305346 try events.emit(.{ .test_finished = test_case });
306347 }
307348
308 for (test_suite.suites.items) |nested_suite| {
309 try self.skipAllTests(nested_suite, events);
349 var nested_suites: std.ArrayList(*suite.TestSuite) = .empty;
350 defer nested_suites.deinit(self.allocator);
351 try nested_suites.appendSlice(self.allocator, test_suite.suites.items);
352 if (shuffler) |randomizer| randomizer.shuffle(*suite.TestSuite, nested_suites.items);
353 for (nested_suites.items) |nested_suite| {
354 try self.skipAllTests(nested_suite, events, shuffler);
310355 }
311356 }
312357
@@ -355,6 +400,8 @@ test "RunnerOptions default values" {
355400 try std.testing.expectEqual(@as(usize, 0), options.retries);
356401 try std.testing.expectEqual(@as(usize, 1), options.repeat);
357402 try std.testing.expect(!options.fail_on_flaky);
403 try std.testing.expect(!options.shuffle);
404 try std.testing.expect(options.seed == null);
358405}
359406
360407test "RunnerOptions custom values" {
@@ -376,6 +423,37 @@ var failing_fixture_runs: usize = 0;
376423var skipped_by_bail_runs: usize = 0;
377424var repeated_fixture_runs: usize = 0;
378425var timeout_fixture_runs: usize = 0;
426var randomized_log: [6]u8 = undefined;
427var randomized_log_len: usize = 0;
428
429fn recordRandomized(id: u8) void {
430 randomized_log[randomized_log_len] = id;
431 randomized_log_len += 1;
432}
433
434fn randomizedOne(_: std.mem.Allocator) !void {
435 recordRandomized(1);
436}
437
438fn randomizedTwo(_: std.mem.Allocator) !void {
439 recordRandomized(2);
440}
441
442fn randomizedThree(_: std.mem.Allocator) !void {
443 recordRandomized(3);
444}
445
446fn randomizedFour(_: std.mem.Allocator) !void {
447 recordRandomized(4);
448}
449
450fn randomizedFive(_: std.mem.Allocator) !void {
451 recordRandomized(5);
452}
453
454fn randomizedSix(_: std.mem.Allocator) !void {
455 recordRandomized(6);
456}
379457
380458fn flakyFixture(_: std.mem.Allocator) !void {
381459 flaky_fixture_runs += 1;
@@ -391,6 +469,48 @@ fn skippedByBailFixture(_: std.mem.Allocator) !void {
391469 skipped_by_bail_runs += 1;
392470}
393471
472test "sequential order is reproducible with a fixed seed" {
473 const allocator = std.testing.allocator;
474 var registry = suite.TestRegistry.init(allocator);
475 defer registry.deinit();
476 const test_suite = try suite.TestSuite.init(allocator, "randomized");
477 try test_suite.addTest(suite.TestCase.init("one", randomizedOne));
478 try test_suite.addTest(suite.TestCase.init("two", randomizedTwo));
479 try test_suite.addTest(suite.TestCase.init("three", randomizedThree));
480 try test_suite.addTest(suite.TestCase.init("four", randomizedFour));
481 try test_suite.addTest(suite.TestCase.init("five", randomizedFive));
482 try test_suite.addTest(suite.TestCase.init("six", randomizedSix));
483 try registry.registerSuite(test_suite);
484
485 var first_output: [4096]u8 = undefined;
486 var first_writer: std.Io.Writer = .fixed(&first_output);
487 randomized_log_len = 0;
488 var first_runner = TestRunner.init(allocator, &registry, .{
489 .seed = 112358,
490 .use_colors = false,
491 .reporter_writer = &first_writer,
492 });
493 defer first_runner.deinit();
494 try std.testing.expect(try first_runner.run());
495 const first_order = randomized_log;
496
497 var second_output: [4096]u8 = undefined;
498 var second_writer: std.Io.Writer = .fixed(&second_output);
499 randomized_log_len = 0;
500 var second_runner = TestRunner.init(allocator, &registry, .{
501 .seed = 112358,
502 .use_colors = false,
503 .reporter_writer = &second_writer,
504 });
505 defer second_runner.deinit();
506 try std.testing.expect(try second_runner.run());
507
508 try std.testing.expectEqual(@as(usize, randomized_log.len), randomized_log_len);
509 try std.testing.expectEqualSlices(u8, &first_order, &randomized_log);
510 try std.testing.expectEqual(@as(?u64, 112358), first_runner.results.random_seed);
511 try std.testing.expectEqual(@as(?u64, 112358), second_runner.results.random_seed);
512}
513
394514fn repeatedFixture(_: std.mem.Allocator) !void {
395515 repeated_fixture_runs += 1;
396516}