ReviewOS

also looking at this

zig-utils/zig-test-framework

Unify discovery and programmatic test execution

#23
Merged glennmichael123 wants to merge codex/issue-5-unified-test-pipeline into main
12 files +656 -169
src/reporter.zigmodified+182-56
Changes to src/reporter.zig
@@ -2,6 +2,14 @@ const std = ;
22const suite = ;
33const compat = ;
44
5pub const ReporterType = enum {
6 spec,
7 dot,
8 json,
9 tap,
10 junit,
11};
12
513/// ANSI color codes
614pub const Colors = struct {
715 pub const reset = "\x1b[0m";
@@ -101,6 +109,7 @@ pub const SpecReporter = struct {
101109 reporter: Reporter,
102110 indent_level: usize = 0,
103111 writer: std.Io.Writer,
112 writer_ref: ?*std.Io.Writer = null,
104113
105114 const Self = @This();
106115
@@ -114,6 +123,16 @@ pub const SpecReporter = struct {
114123 };
115124 }
116125
126 pub fn initRef(allocator: std.mem.Allocator, writer: *std.Io.Writer) Self {
127 var result = init(allocator, writer.*);
128 result.writer_ref = writer;
129 return result;
130 }
131
132 fn output(instance: *Self) *std.Io.Writer {
133 return instance.writer_ref orelse &instance.writer;
134 }
135
117136 const vtable = Reporter.VTable{
118137 .onRunStart = onRunStart,
119138 .onRunEnd = onRunEnd,
@@ -129,64 +148,64 @@ pub const SpecReporter = struct {
129148
130149 fn onRunStart(reporter: *Reporter, total_tests: usize) !void {
131150 const s = self(reporter);
132 try s.writer.print("\n", .{});
151 try s.output().print("\n", .{});
133152 if (reporter.use_colors) {
134 try s.writer.print("{s}Running {d} test(s)...{s}\n\n", .{ Colors.bold, total_tests, Colors.reset });
153 try s.output().print("{s}Running {d} test(s)...{s}\n\n", .{ Colors.bold, total_tests, Colors.reset });
135154 } else {
136 try s.writer.print("Running {d} test(s)...\n\n", .{total_tests});
155 try s.output().print("Running {d} test(s)...\n\n", .{total_tests});
137156 }
138157 }
139158
140159 fn onRunEnd(reporter: *Reporter, results: *TestResults) !void {
141160 const s = self(reporter);
142 try s.writer.print("\n", .{});
161 try s.output().print("\n", .{});
143162
144163 // Print failed tests details
145164 if (results.failed > 0) {
146165 if (reporter.use_colors) {
147 try s.writer.print("{s}Failed Tests:{s}\n\n", .{ Colors.bold ++ Colors.red, Colors.reset });
166 try s.output().print("{s}Failed Tests:{s}\n\n", .{ Colors.bold ++ Colors.red, Colors.reset });
148167 } else {
149 try s.writer.print("Failed Tests:\n\n", .{});
168 try s.output().print("Failed Tests:\n\n", .{});
150169 }
151170
152171 for (results.failed_tests.items) |test_case| {
153172 if (reporter.use_colors) {
154 try s.writer.print(" {s}✗{s} {s}\n", .{ Colors.red, Colors.reset, test_case.name });
173 try s.output().print(" {s}✗{s} {s}\n", .{ Colors.red, Colors.reset, test_case.name });
155174 } else {
156 try s.writer.print(" ✗ {s}\n", .{test_case.name});
175 try s.output().print(" ✗ {s}\n", .{test_case.name});
157176 }
158177 if (test_case.error_message) |msg| {
159 try s.writer.print(" {s}\n", .{msg});
178 try s.output().print(" {s}\n", .{msg});
160179 }
161180 }
162 try s.writer.print("\n", .{});
181 try s.output().print("\n", .{});
163182 }
164183
165184 // Print summary
166185 const total_time_ms = @as(f64, @floatFromInt(results.total_time_ns)) / 1_000_000.0;
167186
168187 if (reporter.use_colors) {
169 try s.writer.print("{s}Test Summary:{s}\n", .{ Colors.bold, Colors.reset });
170 try s.writer.print(" Total: {d}\n", .{results.total});
171 try s.writer.print(" {s}Passed: {d}{s}\n", .{ Colors.green, results.passed, Colors.reset });
188 try s.output().print("{s}Test Summary:{s}\n", .{ Colors.bold, Colors.reset });
189 try s.output().print(" Total: {d}\n", .{results.total});
190 try s.output().print(" {s}Passed: {d}{s}\n", .{ Colors.green, results.passed, Colors.reset });
172191 if (results.failed > 0) {
173 try s.writer.print(" {s}Failed: {d}{s}\n", .{ Colors.red, results.failed, Colors.reset });
192 try s.output().print(" {s}Failed: {d}{s}\n", .{ Colors.red, results.failed, Colors.reset });
174193 }
175194 if (results.skipped > 0) {
176 try s.writer.print(" {s}Skipped: {d}{s}\n", .{ Colors.yellow, results.skipped, Colors.reset });
195 try s.output().print(" {s}Skipped: {d}{s}\n", .{ Colors.yellow, results.skipped, Colors.reset });
177196 }
178 try s.writer.print(" Time: {d:.2}ms\n", .{total_time_ms});
197 try s.output().print(" Time: {d:.2}ms\n", .{total_time_ms});
179198 } else {
180 try s.writer.print("Test Summary:\n", .{});
181 try s.writer.print(" Total: {d}\n", .{results.total});
182 try s.writer.print(" Passed: {d}\n", .{results.passed});
199 try s.output().print("Test Summary:\n", .{});
200 try s.output().print(" Total: {d}\n", .{results.total});
201 try s.output().print(" Passed: {d}\n", .{results.passed});
183202 if (results.failed > 0) {
184 try s.writer.print(" Failed: {d}\n", .{results.failed});
203 try s.output().print(" Failed: {d}\n", .{results.failed});
185204 }
186205 if (results.skipped > 0) {
187 try s.writer.print(" Skipped: {d}\n", .{results.skipped});
206 try s.output().print(" Skipped: {d}\n", .{results.skipped});
188207 }
189 try s.writer.print(" Time: {d:.2}ms\n", .{total_time_ms});
208 try s.output().print(" Time: {d:.2}ms\n", .{total_time_ms});
190209 }
191210 }
192211
@@ -196,13 +215,13 @@ pub const SpecReporter = struct {
196215 // Print indent
197216 var i: usize = 0;
198217 while (i < s.indent_level) : (i += 1) {
199 try s.writer.print(" ", .{});
218 try s.output().print(" ", .{});
200219 }
201220
202221 if (reporter.use_colors) {
203 try s.writer.print("{s}{s}{s}\n", .{ Colors.bold, suite_name, Colors.reset });
222 try s.output().print("{s}{s}{s}\n", .{ Colors.bold, suite_name, Colors.reset });
204223 } else {
205 try s.writer.print("{s}\n", .{suite_name});
224 try s.output().print("{s}\n", .{suite_name});
206225 }
207226 s.indent_level += 1;
208227 }
@@ -226,7 +245,7 @@ pub const SpecReporter = struct {
226245 // Print indent
227246 var i: usize = 0;
228247 while (i < s.indent_level) : (i += 1) {
229 try s.writer.print(" ", .{});
248 try s.output().print(" ", .{});
230249 }
231250
232251 const time_ms = @as(f64, @floatFromInt(test_case.execution_time_ns)) / 1_000_000.0;
@@ -234,7 +253,7 @@ pub const SpecReporter = struct {
234253 switch (test_case.status) {
235254 .passed => {
236255 if (reporter.use_colors) {
237 try s.writer.print("{s}✓{s} {s} {s}({d:.2}ms){s}\n", .{
256 try s.output().print("{s}✓{s} {s} {s}({d:.2}ms){s}\n", .{
238257 Colors.green,
239258 Colors.reset,
240259 test_case.name,
@@ -243,12 +262,12 @@ pub const SpecReporter = struct {
243262 Colors.reset,
244263 });
245264 } else {
246 try s.writer.print("✓ {s} ({d:.2}ms)\n", .{ test_case.name, time_ms });
265 try s.output().print("✓ {s} ({d:.2}ms)\n", .{ test_case.name, time_ms });
247266 }
248267 },
249268 .failed => {
250269 if (reporter.use_colors) {
251 try s.writer.print("{s}✗{s} {s} {s}({d:.2}ms){s}\n", .{
270 try s.output().print("{s}✗{s} {s} {s}({d:.2}ms){s}\n", .{
252271 Colors.red,
253272 Colors.reset,
254273 test_case.name,
@@ -257,12 +276,12 @@ pub const SpecReporter = struct {
257276 Colors.reset,
258277 });
259278 } else {
260 try s.writer.print("✗ {s} ({d:.2}ms)\n", .{ test_case.name, time_ms });
279 try s.output().print("✗ {s} ({d:.2}ms)\n", .{ test_case.name, time_ms });
261280 }
262281 },
263282 .skipped => {
264283 if (reporter.use_colors) {
265 try s.writer.print("{s}⊘{s} {s} {s}(skipped){s}\n", .{
284 try s.output().print("{s}⊘{s} {s} {s}(skipped){s}\n", .{
266285 Colors.yellow,
267286 Colors.reset,
268287 test_case.name,
@@ -270,7 +289,7 @@ pub const SpecReporter = struct {
270289 Colors.reset,
271290 });
272291 } else {
273 try s.writer.print("⊘ {s} (skipped)\n", .{test_case.name});
292 try s.output().print("⊘ {s} (skipped)\n", .{test_case.name});
274293 }
275294 },
276295 else => {},
@@ -282,6 +301,7 @@ pub const SpecReporter = struct {
282301pub const DotReporter = struct {
283302 reporter: Reporter,
284303 writer: std.Io.Writer,
304 writer_ref: ?*std.Io.Writer = null,
285305 tests_per_line: usize = 80,
286306 current_line_count: usize = 0,
287307
@@ -297,6 +317,16 @@ pub const DotReporter = struct {
297317 };
298318 }
299319
320 pub fn initRef(allocator: std.mem.Allocator, writer: *std.Io.Writer) Self {
321 var result = init(allocator, writer.*);
322 result.writer_ref = writer;
323 return result;
324 }
325
326 fn output(instance: *Self) *std.Io.Writer {
327 return instance.writer_ref orelse &instance.writer;
328 }
329
300330 const vtable = Reporter.VTable{
301331 .onRunStart = onRunStart,
302332 .onRunEnd = onRunEnd,
@@ -312,21 +342,21 @@ pub const DotReporter = struct {
312342
313343 fn onRunStart(reporter: *Reporter, total_tests: usize) !void {
314344 const s = self(reporter);
315 try s.writer.print("\nRunning {d} tests:\n", .{total_tests});
345 try s.output().print("\nRunning {d} tests:\n", .{total_tests});
316346 }
317347
318348 fn onRunEnd(reporter: *Reporter, results: *TestResults) !void {
319349 const s = self(reporter);
320 try s.writer.print("\n\n", .{});
350 try s.output().print("\n\n", .{});
321351
322352 const total_time_ms = @as(f64, @floatFromInt(results.total_time_ns)) / 1_000_000.0;
323353
324354 if (reporter.use_colors) {
325 try s.writer.print("{s}Passed: {d}{s}, ", .{ Colors.green, results.passed, Colors.reset });
326 try s.writer.print("{s}Failed: {d}{s}, ", .{ Colors.red, results.failed, Colors.reset });
327 try s.writer.print("Total: {d} ({d:.2}ms)\n", .{ results.total, total_time_ms });
355 try s.output().print("{s}Passed: {d}{s}, ", .{ Colors.green, results.passed, Colors.reset });
356 try s.output().print("{s}Failed: {d}{s}, ", .{ Colors.red, results.failed, Colors.reset });
357 try s.output().print("Total: {d} ({d:.2}ms)\n", .{ results.total, total_time_ms });
328358 } else {
329 try s.writer.print("Passed: {d}, Failed: {d}, Total: {d} ({d:.2}ms)\n", .{
359 try s.output().print("Passed: {d}, Failed: {d}, Total: {d} ({d:.2}ms)\n", .{
330360 results.passed,
331361 results.failed,
332362 results.total,
@@ -356,23 +386,23 @@ pub const DotReporter = struct {
356386 switch (test_case.status) {
357387 .passed => {
358388 if (reporter.use_colors) {
359 try s.writer.print("{s}.{s}", .{ Colors.green, Colors.reset });
389 try s.output().print("{s}.{s}", .{ Colors.green, Colors.reset });
360390 } else {
361 try s.writer.print(".", .{});
391 try s.output().print(".", .{});
362392 }
363393 },
364394 .failed => {
365395 if (reporter.use_colors) {
366 try s.writer.print("{s}F{s}", .{ Colors.red, Colors.reset });
396 try s.output().print("{s}F{s}", .{ Colors.red, Colors.reset });
367397 } else {
368 try s.writer.print("F", .{});
398 try s.output().print("F", .{});
369399 }
370400 },
371401 .skipped => {
372402 if (reporter.use_colors) {
373 try s.writer.print("{s}S{s}", .{ Colors.yellow, Colors.reset });
403 try s.output().print("{s}S{s}", .{ Colors.yellow, Colors.reset });
374404 } else {
375 try s.writer.print("S", .{});
405 try s.output().print("S", .{});
376406 }
377407 },
378408 else => {},
@@ -380,7 +410,7 @@ pub const DotReporter = struct {
380410
381411 s.current_line_count += 1;
382412 if (s.current_line_count >= s.tests_per_line) {
383 try s.writer.print("\n", .{});
413 try s.output().print("\n", .{});
384414 s.current_line_count = 0;
385415 }
386416 }
@@ -390,6 +420,7 @@ pub const DotReporter = struct {
390420pub const JsonReporter = struct {
391421 reporter: Reporter,
392422 writer: std.Io.Writer,
423 writer_ref: ?*std.Io.Writer = null,
393424 suites: std.ArrayList([]const u8),
394425
395426 const Self = @This();
@@ -405,6 +436,16 @@ pub const JsonReporter = struct {
405436 };
406437 }
407438
439 pub fn initRef(allocator: std.mem.Allocator, writer: *std.Io.Writer) Self {
440 var result = init(allocator, writer.*);
441 result.writer_ref = writer;
442 return result;
443 }
444
445 fn output(instance: *Self) *std.Io.Writer {
446 return instance.writer_ref orelse &instance.writer;
447 }
448
408449 pub fn deinit(s: *Self) void {
409450 s.suites.deinit(s.reporter.allocator);
410451 }
@@ -424,14 +465,14 @@ pub const JsonReporter = struct {
424465
425466 fn onRunStart(reporter: *Reporter, total_tests: usize) !void {
426467 const s = self(reporter);
427 try s.writer.print("{{\"totalTests\":{d},\"tests\":[\n", .{total_tests});
468 try s.output().print("{{\"totalTests\":{d},\"tests\":[\n", .{total_tests});
428469 }
429470
430471 fn onRunEnd(reporter: *Reporter, results: *TestResults) !void {
431472 const s = self(reporter);
432473 const total_time_ms = @as(f64, @floatFromInt(results.total_time_ns)) / 1_000_000.0;
433474
434 try s.writer.print("],\"summary\":{{\"total\":{d},\"passed\":{d},\"failed\":{d},\"skipped\":{d},\"time\":{d:.2}}}}}\n", .{
475 try s.output().print("],\"summary\":{{\"total\":{d},\"passed\":{d},\"failed\":{d},\"skipped\":{d},\"time\":{d:.2}}}}}\n", .{
435476 results.total,
436477 results.passed,
437478 results.failed,
@@ -470,17 +511,17 @@ pub const JsonReporter = struct {
470511 };
471512
472513 // Note: In a real implementation, you'd want to properly escape JSON strings
473 try s.writer.print(" {{\"name\":\"{s}\",\"status\":\"{s}\",\"time\":{d:.2}", .{
514 try s.output().print(" {{\"name\":\"{s}\",\"status\":\"{s}\",\"time\":{d:.2}", .{
474515 test_case.name,
475516 status_str,
476517 time_ms,
477518 });
478519
479520 if (test_case.error_message) |msg| {
480 try s.writer.print(",\"error\":\"{s}\"", .{msg});
521 try s.output().print(",\"error\":\"{s}\"", .{msg});
481522 }
482523
483 try s.writer.print("}},\n", .{});
524 try s.output().print("}},\n", .{});
484525 }
485526};
486527
@@ -488,6 +529,7 @@ pub const JsonReporter = struct {
488529pub const TAPReporter = struct {
489530 reporter: Reporter,
490531 writer: std.Io.Writer,
532 writer_ref: ?*std.Io.Writer = null,
491533 test_count: usize = 0,
492534
493535 const Self = @This();
@@ -510,9 +552,19 @@ pub const TAPReporter = struct {
510552 };
511553 }
512554
555 pub fn initRef(allocator: std.mem.Allocator, writer: *std.Io.Writer) Self {
556 var result = init(allocator, writer.*);
557 result.writer_ref = writer;
558 return result;
559 }
560
561 fn output(instance: *Self) *std.Io.Writer {
562 return instance.writer_ref orelse &instance.writer;
563 }
564
513565 fn onRunStart(reporter: *Reporter, total: usize) !void {
514566 const self: *Self = @fieldParentPtr("reporter", reporter);
515 try self.writer.print("TAP version 14\n1..{d}\n", .{total});
567 try self.output().print("TAP version 14\n1..{d}\n", .{total});
516568 }
517569
518570 fn onRunEnd(reporter: *Reporter, results: *TestResults) !void {
@@ -522,7 +574,7 @@ pub const TAPReporter = struct {
522574
523575 fn onSuiteStart(reporter: *Reporter, suite_name: []const u8) !void {
524576 const self: *Self = @fieldParentPtr("reporter", reporter);
525 try self.writer.print("# Subtest: {s}\n", .{suite_name});
577 try self.output().print("# Subtest: {s}\n", .{suite_name});
526578 }
527579
528580 fn onSuiteEnd(reporter: *Reporter, suite_name: []const u8) !void {
@@ -546,14 +598,14 @@ pub const TAPReporter = struct {
546598 else => "not ok",
547599 };
548600
549 try self.writer.print("{s} {d} - {s}", .{ status, self.test_count, test_case.name });
601 try self.output().print("{s} {d} - {s}", .{ status, self.test_count, test_case.name });
550602
551603 if (test_case.status == .skipped) {
552 try self.writer.print(" # SKIP\n", .{});
604 try self.output().print(" # SKIP\n", .{});
553605 } else if (test_case.status == .failed and test_case.error_message != null) {
554 try self.writer.print("\n ---\n message: {s}\n ...\n", .{test_case.error_message.?});
606 try self.output().print("\n ---\n message: {s}\n ...\n", .{test_case.error_message.?});
555607 } else {
556 try self.writer.print("\n", .{});
608 try self.output().print("\n", .{});
557609 }
558610 }
559611};
@@ -735,6 +787,80 @@ pub const JUnitReporter = struct {
735787 }
736788};
737789
790/// Owns every built-in reporter and selects one through a shared interface.
791/// Keeping construction here lets every executor expose identical reporters.
792pub const ReporterSet = struct {
793 kind: ReporterType,
794 spec: SpecReporter,
795 dot: DotReporter,
796 json: JsonReporter,
797 tap: TAPReporter,
798 junit: JUnitReporter,
799
800 pub fn init(
801 allocator: std.mem.Allocator,
802 writer: std.Io.Writer,
803 kind: ReporterType,
804 junit_output: []const u8,
805 use_colors: bool,
806 ) ReporterSet {
807 var reporters = ReporterSet{
808 .kind = kind,
809 .spec = SpecReporter.init(allocator, writer),
810 .dot = DotReporter.init(allocator, writer),
811 .json = JsonReporter.init(allocator, writer),
812 .tap = TAPReporter.init(allocator, writer),
813 .junit = JUnitReporter.init(allocator, junit_output),
814 };
815 reporters.selected().use_colors = use_colors;
816 return reporters;
817 }
818
819 pub fn initRef(
820 allocator: std.mem.Allocator,
821 writer: *std.Io.Writer,
822 kind: ReporterType,
823 junit_output: []const u8,
824 use_colors: bool,
825 ) ReporterSet {
826 var reporters = ReporterSet{
827 .kind = kind,
828 .spec = SpecReporter.initRef(allocator, writer),
829 .dot = DotReporter.initRef(allocator, writer),
830 .json = JsonReporter.initRef(allocator, writer),
831 .tap = TAPReporter.initRef(allocator, writer),
832 .junit = JUnitReporter.init(allocator, junit_output),
833 };
834 reporters.selected().use_colors = use_colors;
835 return reporters;
836 }
837
838 pub fn deinit(self: *ReporterSet) void {
839 self.json.deinit();
840 self.junit.deinit();
841 }
842
843 pub fn selected(self: *ReporterSet) *Reporter {
844 return switch (self.kind) {
845 .spec => &self.spec.reporter,
846 .dot => &self.dot.reporter,
847 .json => &self.json.reporter,
848 .tap => &self.tap.reporter,
849 .junit => &self.junit.reporter,
850 };
851 }
852
853 pub fn flush(self: *ReporterSet) !void {
854 switch (self.kind) {
855 .spec => try self.spec.output().flush(),
856 .dot => try self.dot.output().flush(),
857 .json => try self.json.output().flush(),
858 .tap => try self.tap.output().flush(),
859 .junit => {},
860 }
861 }
862};
863
738864// Tests
739865test "Colors constants exist" {
740866 // Just verify the constants are defined