ReviewOS

also looking at this

zig-utils/zig-test-framework

Add deterministic test sharding for distributed CI

#22
Merged glennmichael123 wants to merge codex/issue-4-deterministic-sharding into main
10 files +322 -5
src/cli.zigmodified+65-0
Changes to src/cli.zig
@@ -16,6 +16,8 @@ pub const CLIOptions = struct {
1616 test_dir: ?[]const u8 = ".",
1717 pattern: []const u8 = "*.test.zig",
1818 no_recursive: bool = false,
19 shard_index: ?usize = null,
20 shard_count: ?usize = null,
1921 // Coverage options
2022 coverage: bool = false,
2123 coverage_dir: []const u8 = "coverage",
@@ -73,6 +75,8 @@ pub const CLI = struct {
7375 self.options.no_recursive = !config.test_options.recursive;
7476 self.options.filter = config.test_options.filter;
7577 self.options.timeout = config.test_options.timeout;
78 self.options.shard_index = config.sharding.index;
79 self.options.shard_count = config.sharding.count;
7680 self.options.parallel = config.parallel.enabled;
7781 self.options.jobs = config.parallel.jobs;
7882 self.options.reporter = try reporterFromName(config.reporter.reporter);
@@ -144,6 +148,26 @@ pub const CLI = struct {
144148 self.options.pattern = args[i];
145149 } else if (std.mem.eql(u8, arg, "--no-recursive")) {
146150 self.options.no_recursive = true;
151 } else if (std.mem.eql(u8, arg, "--shard-index")) {
152 if (i + 1 >= args.len) {
153 std.debug.print("Error: --shard-index requires a value\n", .{});
154 return CLIError.MissingValue;
155 }
156 i += 1;
157 self.options.shard_index = std.fmt.parseInt(usize, args[i], 10) catch {
158 std.debug.print("Error: --shard-index must be a valid number\n", .{});
159 return CLIError.InvalidArgument;
160 };
161 } else if (std.mem.eql(u8, arg, "--shard-count")) {
162 if (i + 1 >= args.len) {
163 std.debug.print("Error: --shard-count requires a value\n", .{});
164 return CLIError.MissingValue;
165 }
166 i += 1;
167 self.options.shard_count = std.fmt.parseInt(usize, args[i], 10) catch {
168 std.debug.print("Error: --shard-count must be a valid number\n", .{});
169 return CLIError.InvalidArgument;
170 };
147171 } else if (std.mem.eql(u8, arg, "--coverage")) {
148172 self.options.coverage = true;
149173 } else if (std.mem.eql(u8, arg, "--coverage-dir")) {
@@ -273,6 +297,24 @@ pub const CLI = struct {
273297 self.options.test_dir = arg;
274298 }
275299 }
300
301 const has_shard_index = self.options.shard_index != null;
302 const has_shard_count = self.options.shard_count != null;
303 if (has_shard_index != has_shard_count) {
304 std.debug.print("Error: --shard-index and --shard-count must be provided together\n", .{});
305 return CLIError.InvalidArgument;
306 }
307 if (self.options.shard_count) |count| {
308 const index = self.options.shard_index.?;
309 if (count == 0) {
310 std.debug.print("Error: --shard-count must be greater than zero\n", .{});
311 return CLIError.InvalidArgument;
312 }
313 if (index == 0 or index > count) {
314 std.debug.print("Error: --shard-index must be between 1 and --shard-count\n", .{});
315 return CLIError.InvalidArgument;
316 }
317 }
276318 }
277319
278320 /// Return the first option that discovery mode cannot currently honor.
@@ -325,6 +367,8 @@ pub const CLI = struct {
325367 \\ --test-dir <dir> Directory to search for tests (default: .)
326368 \\ --pattern <pattern> Test file pattern (default: *.test.zig)
327369 \\ --no-recursive Disable recursive directory search
370 \\ --shard-index <N> One-based shard to run
371 \\ --shard-count <N> Total number of shards
328372 \\
329373 \\COVERAGE:
330374 \\ --coverage Enable code coverage collection
@@ -458,6 +502,27 @@ test "CLI parse coverage flag" {
458502 try std.testing.expect(cli.options.coverage);
459503}
460504
505test "CLI parse shard options" {
506 var cli = CLI.init(std.testing.allocator);
507 try cli.parse(&.{ "zig-test", "--shard-count", "5", "--shard-index", "3" });
508 try std.testing.expectEqual(@as(?usize, 3), cli.options.shard_index);
509 try std.testing.expectEqual(@as(?usize, 5), cli.options.shard_count);
510}
511
512test "CLI rejects incomplete and out-of-range shards" {
513 var incomplete = CLI.init(std.testing.allocator);
514 try std.testing.expectError(
515 CLIError.InvalidArgument,
516 incomplete.parse(&.{ "zig-test", "--shard-count", "2" }),
517 );
518
519 var out_of_range = CLI.init(std.testing.allocator);
520 try std.testing.expectError(
521 CLIError.InvalidArgument,
522 out_of_range.parse(&.{ "zig-test", "--shard-index", "0", "--shard-count", "2" }),
523 );
524}
525
461526test "CLI parse coverage-dir" {
462527 var cli = CLI.init(std.testing.allocator);
463528 const args = [_][]const u8{ "zig-test", "--coverage-dir", "my-coverage" };