also looking at this
Add deterministic test sharding for distributed CI
#22
10 files
+322
-5
| @@ -16,6 +16,8 @@ pub const CLIOptions = struct { | ||
| 16 | 16 | test_dir: ?[]const u8 = ".", |
| 17 | 17 | pattern: []const u8 = "*.test.zig", |
| 18 | 18 | no_recursive: bool = false, |
| 19 | shard_index: ?usize = null, | |
| 20 | shard_count: ?usize = null, | |
| 19 | 21 | // Coverage options |
| 20 | 22 | coverage: bool = false, |
| 21 | 23 | coverage_dir: []const u8 = "coverage", |
| @@ -73,6 +75,8 @@ pub const CLI = struct { | ||
| 73 | 75 | self.options.no_recursive = !config.test_options.recursive; |
| 74 | 76 | self.options.filter = config.test_options.filter; |
| 75 | 77 | self.options.timeout = config.test_options.timeout; |
| 78 | self.options.shard_index = config.sharding.index; | |
| 79 | self.options.shard_count = config.sharding.count; | |
| 76 | 80 | self.options.parallel = config.parallel.enabled; |
| 77 | 81 | self.options.jobs = config.parallel.jobs; |
| 78 | 82 | self.options.reporter = try reporterFromName(config.reporter.reporter); |
| @@ -144,6 +148,26 @@ pub const CLI = struct { | ||
| 144 | 148 | self.options.pattern = args[i]; |
| 145 | 149 | } else if (std.mem.eql(u8, arg, "--no-recursive")) { |
| 146 | 150 | 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 | }; | |
| 147 | 171 | } else if (std.mem.eql(u8, arg, "--coverage")) { |
| 148 | 172 | self.options.coverage = true; |
| 149 | 173 | } else if (std.mem.eql(u8, arg, "--coverage-dir")) { |
| @@ -273,6 +297,24 @@ pub const CLI = struct { | ||
| 273 | 297 | self.options.test_dir = arg; |
| 274 | 298 | } |
| 275 | 299 | } |
| 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 | } | |
| 276 | 318 | } |
| 277 | 319 | |
| 278 | 320 | /// Return the first option that discovery mode cannot currently honor. |
| @@ -325,6 +367,8 @@ pub const CLI = struct { | ||
| 325 | 367 | \\ --test-dir <dir> Directory to search for tests (default: .) |
| 326 | 368 | \\ --pattern <pattern> Test file pattern (default: *.test.zig) |
| 327 | 369 | \\ --no-recursive Disable recursive directory search |
| 370 | \\ --shard-index <N> One-based shard to run | |
| 371 | \\ --shard-count <N> Total number of shards | |
| 328 | 372 | \\ |
| 329 | 373 | \\COVERAGE: |
| 330 | 374 | \\ --coverage Enable code coverage collection |
| @@ -458,6 +502,27 @@ test "CLI parse coverage flag" { | ||
| 458 | 502 | try std.testing.expect(cli.options.coverage); |
| 459 | 503 | } |
| 460 | 504 | |
| 505 | test "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 | ||
| 512 | test "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 | ||
| 461 | 526 | test "CLI parse coverage-dir" { |
| 462 | 527 | var cli = CLI.init(std.testing.allocator); |
| 463 | 528 | const args = [_][]const u8{ "zig-test", "--coverage-dir", "my-coverage" }; |