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/sharding.zigadded+87-0
Changes to src/sharding.zig
@@ -0,0 +1,87 @@
1const std = ;
2
3pub const ShardError = error{
4 InvalidShardCount,
5 InvalidShardIndex,
6};
7
8/// A one-based shard selection suitable for CI matrix values.
9pub const ShardOptions = struct {
10 index: usize,
11 count: usize,
12
13 pub fn validate(self: ShardOptions) ShardError!void {
14 if (self.count == 0) return ShardError.InvalidShardCount;
15 if (self.index == 0 or self.index > self.count) return ShardError.InvalidShardIndex;
16 }
17
18 pub fn includes(self: ShardOptions, identity: []const u8) ShardError!bool {
19 try self.validate();
20 return (try shardIndex(identity, self.count)) + 1 == self.index;
21 }
22};
23
24/// Assign a normalized test identity to a zero-based shard using FNV-1a 64.
25/// Path separators are normalized so the same relative path maps identically
26/// on Unix and Windows. The algorithm and seed are intentionally fixed.
27pub fn shardIndex(identity: []const u8, shard_count: usize) ShardError!usize {
28 if (shard_count == 0) return ShardError.InvalidShardCount;
29
30 var hash: u64 = 14_695_981_039_346_656_037;
31 for (identity) |byte| {
32 const normalized = if (byte == '\\') @as(u8, '/') else byte;
33 hash ^= normalized;
34 hash *%= 1_099_511_628_211;
35 }
36
37 const count: u64 = @intCast(shard_count);
38 return @intCast(hash % count);
39}
40
41test "fixed identities have stable shard assignments" {
42 try std.testing.expectEqual(@as(usize, 1), try shardIndex("tests/api.test.zig", 4));
43 try std.testing.expectEqual(@as(usize, 1), try shardIndex("tests/db.test.zig", 4));
44 try std.testing.expectEqual(@as(usize, 2), try shardIndex("tests/unit/math.test.zig", 4));
45}
46
47test "path separators do not change shard assignment" {
48 try std.testing.expectEqual(
49 try shardIndex("tests/unit/math.test.zig", 7),
50 try shardIndex("tests\\unit\\math.test.zig", 7),
51 );
52}
53
54test "every identity belongs to exactly one shard" {
55 const identities = [_][]const u8{
56 "tests/api.test.zig",
57 "tests/db.test.zig",
58 "tests/unit/math.test.zig",
59 "tests/unit/string.test.zig",
60 };
61
62 for (identities) |identity| {
63 var matches: usize = 0;
64 for (1..5) |index| {
65 if (try (ShardOptions{ .index = index, .count = 4 }).includes(identity)) {
66 matches += 1;
67 }
68 }
69 try std.testing.expectEqual(@as(usize, 1), matches);
70 }
71}
72
73test "invalid shard options are rejected" {
74 try std.testing.expectError(
75 ShardError.InvalidShardCount,
76 (ShardOptions{ .index = 1, .count = 0 }).validate(),
77 );
78 try std.testing.expectError(ShardError.InvalidShardCount, shardIndex("test.zig", 0));
79 try std.testing.expectError(
80 ShardError.InvalidShardIndex,
81 (ShardOptions{ .index = 0, .count = 2 }).validate(),
82 );
83 try std.testing.expectError(
84 ShardError.InvalidShardIndex,
85 (ShardOptions{ .index = 3, .count = 2 }).validate(),
86 );
87}