also looking at this
Unify discovery and programmatic test execution
#23
12 files
+656
-169
| @@ -0,0 +1,169 @@ | ||
| 1 | const std = ; | |
| 2 | const discovery = ; | |
| 3 | const reporter = ; | |
| 4 | const suite = ; | |
| 5 | ||
| 6 | pub const PlanOrigin = enum { | |
| 7 | registered, | |
| 8 | discovered, | |
| 9 | }; | |
| 10 | ||
| 11 | pub const RegisteredTest = struct { | |
| 12 | test_suite: *suite.TestSuite, | |
| 13 | test_case: *suite.TestCase, | |
| 14 | }; | |
| 15 | ||
| 16 | pub const PlanItem = union(PlanOrigin) { | |
| 17 | registered: RegisteredTest, | |
| 18 | discovered: *const discovery.TestFile, | |
| 19 | ||
| 20 | pub fn identity(self: PlanItem) []const u8 { | |
| 21 | return switch (self) { | |
| 22 | .registered => |item| item.test_case.name, | |
| 23 | .discovered => |file| file.relative_path, | |
| 24 | }; | |
| 25 | } | |
| 26 | }; | |
| 27 | ||
| 28 | /// Executor-neutral plan produced by registration or file discovery. | |
| 29 | pub const TestPlan = struct { | |
| 30 | allocator: std.mem.Allocator, | |
| 31 | items: std.ArrayList(PlanItem) = .empty, | |
| 32 | ||
| 33 | pub fn init(allocator: std.mem.Allocator) TestPlan { | |
| 34 | return .{ .allocator = allocator }; | |
| 35 | } | |
| 36 | ||
| 37 | pub fn deinit(self: *TestPlan) void { | |
| 38 | self.items.deinit(self.allocator); | |
| 39 | } | |
| 40 | ||
| 41 | pub fn fromRegistry( | |
| 42 | allocator: std.mem.Allocator, | |
| 43 | registry: *suite.TestRegistry, | |
| 44 | ) !TestPlan { | |
| 45 | var plan = TestPlan.init(allocator); | |
| 46 | errdefer plan.deinit(); | |
| 47 | for (registry.root_suites.items) |test_suite| { | |
| 48 | try plan.appendSuite(test_suite); | |
| 49 | } | |
| 50 | return plan; | |
| 51 | } | |
| 52 | ||
| 53 | pub fn fromDiscovery( | |
| 54 | allocator: std.mem.Allocator, | |
| 55 | discovered: *const discovery.DiscoveryResult, | |
| 56 | ) !TestPlan { | |
| 57 | var plan = TestPlan.init(allocator); | |
| 58 | errdefer plan.deinit(); | |
| 59 | for (discovered.files.items) |*file| { | |
| 60 | try plan.appendDiscovered(file); | |
| 61 | } | |
| 62 | return plan; | |
| 63 | } | |
| 64 | ||
| 65 | pub fn appendDiscovered(self: *TestPlan, file: *const discovery.TestFile) !void { | |
| 66 | try self.items.append(self.allocator, .{ .discovered = file }); | |
| 67 | } | |
| 68 | ||
| 69 | fn appendSuite(self: *TestPlan, test_suite: *suite.TestSuite) !void { | |
| 70 | for (test_suite.tests.items) |*test_case| { | |
| 71 | try self.items.append(self.allocator, .{ .registered = .{ | |
| 72 | .test_suite = test_suite, | |
| 73 | .test_case = test_case, | |
| 74 | } }); | |
| 75 | } | |
| 76 | for (test_suite.suites.items) |nested| { | |
| 77 | try self.appendSuite(nested); | |
| 78 | } | |
| 79 | } | |
| 80 | }; | |
| 81 | ||
| 82 | /// Shared execution policy. Executors apply it at their natural granularity: | |
| 83 | /// registered test names or filters forwarded to an external Zig test process. | |
| 84 | pub const ExecutionPolicy = struct { | |
| 85 | bail: bool = false, | |
| 86 | filter: ?[]const u8 = null, | |
| 87 | timeout_ms: ?u64 = null, | |
| 88 | ||
| 89 | pub fn matches(self: ExecutionPolicy, name: []const u8) bool { | |
| 90 | const filter = self.filter orelse return true; | |
| 91 | return std.mem.indexOf(u8, name, filter) != null; | |
| 92 | } | |
| 93 | ||
| 94 | pub fn shouldStop(self: ExecutionPolicy, results: *const reporter.TestResults) bool { | |
| 95 | return self.bail and results.failed > 0; | |
| 96 | } | |
| 97 | ||
| 98 | pub fn timedOut(self: ExecutionPolicy, execution_time_ns: u64) bool { | |
| 99 | const timeout_ms = self.timeout_ms orelse return false; | |
| 100 | return execution_time_ns > timeout_ms * std.time.ns_per_ms; | |
| 101 | } | |
| 102 | }; | |
| 103 | ||
| 104 | pub const LifecycleEvent = union(enum) { | |
| 105 | run_started: usize, | |
| 106 | run_finished: *reporter.TestResults, | |
| 107 | suite_started: []const u8, | |
| 108 | suite_finished: []const u8, | |
| 109 | test_started: []const u8, | |
| 110 | test_finished: *const suite.TestCase, | |
| 111 | }; | |
| 112 | ||
| 113 | /// Routes the typed lifecycle shared by every executor into existing reporter | |
| 114 | /// callbacks. Additional consumers can be added without changing executors. | |
| 115 | pub const EventStream = struct { | |
| 116 | rep: *reporter.Reporter, | |
| 117 | ||
| 118 | pub fn emit(self: EventStream, event: LifecycleEvent) !void { | |
| 119 | switch (event) { | |
| 120 | .run_started => |total| try self.rep.onRunStart(total), | |
| 121 | .run_finished => |results| try self.rep.onRunEnd(results), | |
| 122 | .suite_started => |name| try self.rep.onSuiteStart(name), | |
| 123 | .suite_finished => |name| try self.rep.onSuiteEnd(name), | |
| 124 | .test_started => |name| try self.rep.onTestStart(name), | |
| 125 | .test_finished => |test_case| try self.rep.onTestEnd(test_case), | |
| 126 | } | |
| 127 | } | |
| 128 | }; | |
| 129 | ||
| 130 | fn passingTest(_: std.mem.Allocator) !void {} | |
| 131 | ||
| 132 | test "registered and discovered inputs produce one plan model" { | |
| 133 | const allocator = std.testing.allocator; | |
| 134 | ||
| 135 | var registry = suite.TestRegistry.init(allocator); | |
| 136 | defer registry.deinit(); | |
| 137 | const test_suite = try suite.TestSuite.init(allocator, "unit"); | |
| 138 | try test_suite.addTest(suite.TestCase.init("passes", passingTest)); | |
| 139 | try registry.registerSuite(test_suite); | |
| 140 | ||
| 141 | var registered = try TestPlan.fromRegistry(allocator, ®istry); | |
| 142 | defer registered.deinit(); | |
| 143 | try std.testing.expectEqual(@as(usize, 1), registered.items.items.len); | |
| 144 | try std.testing.expectEqualStrings("passes", registered.items.items[0].identity()); | |
| 145 | ||
| 146 | var discovered = discovery.DiscoveryResult.init(allocator); | |
| 147 | defer discovered.deinit(); | |
| 148 | try discovered.addFile("tests/sample.test.zig", "sample.test.zig", "sample.test.zig"); | |
| 149 | ||
| 150 | var external = try TestPlan.fromDiscovery(allocator, &discovered); | |
| 151 | defer external.deinit(); | |
| 152 | try std.testing.expectEqual(@as(usize, 1), external.items.items.len); | |
| 153 | try std.testing.expectEqualStrings("sample.test.zig", external.items.items[0].identity()); | |
| 154 | } | |
| 155 | ||
| 156 | test "execution policy shares matching and bail semantics" { | |
| 157 | const policy = ExecutionPolicy{ .bail = true, .filter = "selected", .timeout_ms = 500 }; | |
| 158 | try std.testing.expect(policy.matches("selected test")); | |
| 159 | try std.testing.expect(!policy.matches("other test")); | |
| 160 | try std.testing.expectEqual(@as(?u64, 500), policy.timeout_ms); | |
| 161 | try std.testing.expect(!policy.timedOut(499 * std.time.ns_per_ms)); | |
| 162 | try std.testing.expect(policy.timedOut(501 * std.time.ns_per_ms)); | |
| 163 | ||
| 164 | var results = reporter.TestResults.init(std.testing.allocator); | |
| 165 | defer results.deinit(); | |
| 166 | try std.testing.expect(!policy.shouldStop(&results)); | |
| 167 | results.failed = 1; | |
| 168 | try std.testing.expect(policy.shouldStop(&results)); | |
| 169 | } | |