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/parallel.zigmodified+74-8
Changes to src/parallel.zig
@@ -3,6 +3,7 @@ const suite = ;
33const reporter = ;
44const pipeline = ;
55const compat = ;
6const random_order = ;
67
78/// Options for bounded parallel test execution.
89pub const ParallelOptions = struct {
@@ -12,6 +13,7 @@ pub const ParallelOptions = struct {
1213 enabled: bool = false,
1314 filter: ?[]const u8 = null,
1415 timeout_ms: ?u64 = null,
16 random_seed: ?u64 = null,
1517};
1618
1719pub const ParallelError = error{
@@ -111,6 +113,7 @@ const RunContext = struct {
111113 rep: *reporter.Reporter,
112114 options: ParallelOptions,
113115 worker_limit: usize,
116 shuffler: ?*random_order.Shuffler,
114117
115118 fn runSuite(self: *RunContext, test_suite: *suite.TestSuite) !bool {
116119 try self.rep.onSuiteStart(test_suite.name);
@@ -131,7 +134,12 @@ const RunContext = struct {
131134 var scheduled: std.ArrayList(*suite.TestCase) = .empty;
132135 defer scheduled.deinit(self.allocator);
133136
134 for (test_suite.tests.items) |*test_case| {
137 var ordered_tests: std.ArrayList(*suite.TestCase) = .empty;
138 defer ordered_tests.deinit(self.allocator);
139 for (test_suite.tests.items) |*test_case| try ordered_tests.append(self.allocator, test_case);
140 if (self.shuffler) |randomizer| randomizer.shuffle(*suite.TestCase, ordered_tests.items);
141
142 for (ordered_tests.items) |test_case| {
135143 if (!self.shouldRun(test_suite, test_case)) {
136144 test_case.status = .skipped;
137145 continue;
@@ -143,12 +151,16 @@ const RunContext = struct {
143151 try self.runBatch(test_suite, scheduled.items);
144152
145153 var all_passed = true;
146 for (test_suite.tests.items) |*test_case| {
154 for (ordered_tests.items) |test_case| {
147155 try self.rep.onTestEnd(test_case);
148156 if (test_case.status == .failed) all_passed = false;
149157 }
150158
151 for (test_suite.suites.items) |nested_suite| {
159 var nested_suites: std.ArrayList(*suite.TestSuite) = .empty;
160 defer nested_suites.deinit(self.allocator);
161 try nested_suites.appendSlice(self.allocator, test_suite.suites.items);
162 if (self.shuffler) |randomizer| randomizer.shuffle(*suite.TestSuite, nested_suites.items);
163 for (nested_suites.items) |nested_suite| {
152164 if (!try self.runSuite(nested_suite)) all_passed = false;
153165 }
154166
@@ -201,11 +213,19 @@ const RunContext = struct {
201213 }
202214
203215 fn reportSkippedSuite(self: *RunContext, test_suite: *suite.TestSuite) !void {
204 for (test_suite.tests.items) |*test_case| {
216 var tests: std.ArrayList(*suite.TestCase) = .empty;
217 defer tests.deinit(self.allocator);
218 for (test_suite.tests.items) |*test_case| try tests.append(self.allocator, test_case);
219 if (self.shuffler) |randomizer| randomizer.shuffle(*suite.TestCase, tests.items);
220 for (tests.items) |test_case| {
205221 test_case.status = .skipped;
206222 try self.rep.onTestEnd(test_case);
207223 }
208 for (test_suite.suites.items) |nested| {
224 var nested_suites: std.ArrayList(*suite.TestSuite) = .empty;
225 defer nested_suites.deinit(self.allocator);
226 try nested_suites.appendSlice(self.allocator, test_suite.suites.items);
227 if (self.shuffler) |randomizer| randomizer.shuffle(*suite.TestSuite, nested_suites.items);
228 for (nested_suites.items) |nested| {
209229 try self.rep.onSuiteStart(nested.name);
210230 try self.reportSkippedSuite(nested);
211231 try self.rep.onSuiteEnd(nested.name);
@@ -213,9 +233,9 @@ const RunContext = struct {
213233 }
214234};
215235
216/// Run suites in declaration order while executing each suite's direct tests
236/// Run suites in the selected deterministic order while executing each suite's direct tests
217237/// with a bounded worker set. Reporter callbacks are serialized and emitted in
218/// declaration order after each batch completes.
238/// the selected order after each batch completes.
219239pub fn runTestsParallel(
220240 allocator: std.mem.Allocator,
221241 test_registry: *suite.TestRegistry,
@@ -226,23 +246,36 @@ pub fn runTestsParallel(
226246 const worker_limit = try resolveWorkerCount(options.n_jobs);
227247
228248 const total_tests = test_registry.countAllTests();
249 rep.random_seed = options.random_seed;
229250 try rep.onRunStart(total_tests);
230251
252 var shuffler: random_order.Shuffler = undefined;
253 const shuffler_ptr: ?*random_order.Shuffler = if (options.random_seed) |seed| blk: {
254 shuffler = .init(seed);
255 break :blk &shuffler;
256 } else null;
257
231258 var context = RunContext{
232259 .allocator = allocator,
233260 .registry = test_registry,
234261 .rep = rep,
235262 .options = options,
236263 .worker_limit = worker_limit,
264 .shuffler = shuffler_ptr,
237265 };
238266
267 var root_suites: std.ArrayList(*suite.TestSuite) = .empty;
268 defer root_suites.deinit(allocator);
269 try root_suites.appendSlice(allocator, test_registry.root_suites.items);
270 if (shuffler_ptr) |randomizer| randomizer.shuffle(*suite.TestSuite, root_suites.items);
239271 var all_passed = true;
240 for (test_registry.root_suites.items) |test_suite| {
272 for (root_suites.items) |test_suite| {
241273 if (!try context.runSuite(test_suite)) all_passed = false;
242274 }
243275
244276 var results = reporter.TestResults.init(allocator);
245277 defer results.deinit();
278 results.random_seed = options.random_seed;
246279 for (test_registry.root_suites.items) |test_suite| {
247280 try addSuiteResults(&results, test_suite);
248281 }
@@ -347,6 +380,7 @@ const RecordingReporter = struct {
347380 passed: usize = 0,
348381 failed: usize = 0,
349382 skipped: usize = 0,
383 random_seed: ?u64 = null,
350384
351385 fn init(allocator: std.mem.Allocator) RecordingReporter {
352386 return .{
@@ -379,6 +413,7 @@ const RecordingReporter = struct {
379413 recording.passed = results.passed;
380414 recording.failed = results.failed;
381415 recording.skipped = results.skipped;
416 recording.random_seed = results.random_seed;
382417 }
383418
384419 fn onSuiteStart(rep: *reporter.Reporter, _: []const u8) !void {
@@ -468,6 +503,37 @@ test "parallel reporting is serialized, ordered, and complete" {
468503 try std.testing.expectEqual(@as(usize, 1), recording.skipped);
469504}
470505
506test "parallel order is reproducible with a fixed seed" {
507 const allocator = std.heap.smp_allocator;
508 var registry = suite.TestRegistry.init(allocator);
509 defer registry.deinit();
510 const test_suite = try suite.TestSuite.init(allocator, "randomized");
511 inline for (.{ "one", "two", "three", "four", "five", "six" }) |name| {
512 try test_suite.addTest(suite.TestCase.init(name, passingTest));
513 }
514 try registry.registerSuite(test_suite);
515
516 var first = RecordingReporter.init(allocator);
517 try std.testing.expect(try runTestsParallel(allocator, &registry, &first.reporter, .{
518 .enabled = true,
519 .n_jobs = 2,
520 .random_seed = 8675309,
521 }));
522 var second = RecordingReporter.init(allocator);
523 try std.testing.expect(try runTestsParallel(allocator, &registry, &second.reporter, .{
524 .enabled = true,
525 .n_jobs = 2,
526 .random_seed = 8675309,
527 }));
528
529 try std.testing.expectEqual(first.ended_count, second.ended_count);
530 for (first.ended_names[0..first.ended_count], second.ended_names[0..second.ended_count]) |a, b| {
531 try std.testing.expectEqualStrings(a, b);
532 }
533 try std.testing.expectEqual(@as(?u64, 8675309), first.random_seed);
534 try std.testing.expectEqual(@as(?u64, 8675309), second.random_seed);
535}
536
471537test "parallel executor preserves nested hook lifecycles" {
472538 const counters = [_]*std.atomic.Value(usize){
473539 &parent_before_each,