also looking at this
Define stable JSON event protocol
#27
11 files
+442
-177
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | const std = ; |
| 2 | 2 | const suite = ; |
| 3 | 3 | const compat = ; |
| 4 | const protocol = ; | |
| 4 | 5 | |
| 5 | 6 | pub const ReporterType = enum { |
| 6 | 7 | spec, |
| @@ -464,7 +465,7 @@ pub const JsonReporter = struct { | ||
| 464 | 465 | writer: std.Io.Writer, |
| 465 | 466 | writer_ref: ?*std.Io.Writer = null, |
| 466 | 467 | suites: std.ArrayList([]const u8), |
| 467 | emitted_tests: bool = false, | |
| 468 | emitted_events: bool = false, | |
| 468 | 469 | |
| 469 | 470 | const Self = @This(); |
| 470 | 471 | |
| @@ -508,87 +509,72 @@ pub const JsonReporter = struct { | ||
| 508 | 509 | |
| 509 | 510 | fn onRunStart(reporter: *Reporter, total_tests: usize) !void { |
| 510 | 511 | const s = self(reporter); |
| 511 | try s.output().print("{{\"totalTests\":{d},\"tests\":[\n", .{total_tests}); | |
| 512 | s.emitted_events = false; | |
| 513 | s.suites.clearRetainingCapacity(); | |
| 514 | try s.output().writeAll("[\n"); | |
| 515 | try s.emit(.{ .run_start = .{ .total = total_tests } }); | |
| 512 | 516 | } |
| 513 | 517 | |
| 514 | 518 | fn onRunEnd(reporter: *Reporter, results: *TestResults) !void { |
| 515 | 519 | const s = self(reporter); |
| 516 | const total_time_ms = @as(f64, @floatFromInt(results.total_time_ns)) / 1_000_000.0; | |
| 517 | ||
| 518 | try s.output().print("\n],\"summary\":{{\"total\":{d},\"passed\":{d},\"flaky\":{d},\"failed\":{d},\"skipped\":{d},\"time\":{d:.2}}}}}\n", .{ | |
| 519 | results.total, | |
| 520 | results.passed, | |
| 521 | results.flaky, | |
| 522 | results.failed, | |
| 523 | results.skipped, | |
| 524 | total_time_ms, | |
| 525 | }); | |
| 520 | try s.emit(.{ .run_end = .{ | |
| 521 | .total = results.total, | |
| 522 | .passed = results.passed, | |
| 523 | .flaky = results.flaky, | |
| 524 | .failed = results.failed, | |
| 525 | .skipped = results.skipped, | |
| 526 | .duration_ns = results.total_time_ns, | |
| 527 | } }); | |
| 528 | try s.output().writeAll("\n]\n"); | |
| 526 | 529 | } |
| 527 | 530 | |
| 528 | 531 | fn onSuiteStart(reporter: *Reporter, suite_name: []const u8) !void { |
| 529 | 532 | const s = self(reporter); |
| 530 | 533 | try s.suites.append(reporter.allocator, suite_name); |
| 534 | try s.emit(.{ .suite_start = .{ .name = suite_name } }); | |
| 531 | 535 | } |
| 532 | 536 | |
| 533 | 537 | fn onSuiteEnd(reporter: *Reporter, suite_name: []const u8) !void { |
| 534 | _ = suite_name; | |
| 535 | 538 | const s = self(reporter); |
| 539 | try s.emit(.{ .suite_end = .{ .name = suite_name } }); | |
| 536 | 540 | if (s.suites.items.len > 0) { |
| 537 | 541 | _ = s.suites.pop(); |
| 538 | 542 | } |
| 539 | 543 | } |
| 540 | 544 | |
| 541 | 545 | fn onTestStart(reporter: *Reporter, test_name: []const u8) !void { |
| 542 | _ = reporter; | |
| 543 | _ = test_name; | |
| 546 | const s = self(reporter); | |
| 547 | try s.emit(.{ .test_start = .{ | |
| 548 | .name = test_name, | |
| 549 | .suite = if (s.suites.getLastOrNull()) |name| name else null, | |
| 550 | } }); | |
| 544 | 551 | } |
| 545 | 552 | |
| 546 | 553 | fn onTestEnd(reporter: *Reporter, test_case: *const suite.TestCase) !void { |
| 547 | 554 | const s = self(reporter); |
| 548 | const time_ms = @as(f64, @floatFromInt(test_case.execution_time_ns)) / 1_000_000.0; | |
| 549 | ||
| 550 | const status_str = switch (test_case.status) { | |
| 551 | .passed => "passed", | |
| 552 | .flaky => "flaky", | |
| 553 | .failed => "failed", | |
| 554 | .skipped => "skipped", | |
| 555 | else => "unknown", | |
| 556 | }; | |
| 557 | ||
| 558 | if (s.emitted_tests) try s.output().print(",\n", .{}); | |
| 559 | s.emitted_tests = true; | |
| 560 | ||
| 561 | // Note: In a real implementation, you'd want to properly escape JSON strings | |
| 562 | try s.output().print(" {{\"name\":\"{s}\",\"status\":\"{s}\",\"time\":{d:.2}", .{ | |
| 563 | test_case.name, | |
| 564 | status_str, | |
| 565 | time_ms, | |
| 566 | }); | |
| 567 | ||
| 568 | if (test_case.error_message) |msg| { | |
| 569 | try s.output().print(",\"error\":\"{s}\"", .{msg}); | |
| 570 | } | |
| 571 | ||
| 572 | try s.output().print(",\"attempts\":[", .{}); | |
| 573 | for (test_case.attempts.items, 0..) |attempt, index| { | |
| 574 | if (index > 0) try s.output().print(",", .{}); | |
| 575 | const attempt_status = switch (attempt.status) { | |
| 576 | .passed => "passed", | |
| 577 | .flaky => "flaky", | |
| 578 | .failed => "failed", | |
| 579 | .skipped => "skipped", | |
| 580 | else => "unknown", | |
| 581 | }; | |
| 582 | try s.output().print( | |
| 583 | "{{\"number\":{d},\"repetition\":{d},\"status\":\"{s}\",\"durationNs\":{d}", | |
| 584 | .{ attempt.number, attempt.repetition, attempt_status, attempt.duration_ns }, | |
| 585 | ); | |
| 586 | if (attempt.error_message) |message| { | |
| 587 | try s.output().print(",\"error\":\"{s}\"", .{message}); | |
| 588 | } | |
| 589 | try s.output().print("}}", .{}); | |
| 555 | for (test_case.attempts.items) |attempt| { | |
| 556 | try s.emit(.{ .retry = .{ | |
| 557 | .name = test_case.name, | |
| 558 | .attempt = attempt.number, | |
| 559 | .repetition = attempt.repetition, | |
| 560 | .status = protocol.status(attempt.status), | |
| 561 | .duration_ns = attempt.duration_ns, | |
| 562 | .error_message = attempt.error_message, | |
| 563 | } }); | |
| 590 | 564 | } |
| 591 | try s.output().print("]}}", .{}); | |
| 565 | try s.emit(.{ .test_end = .{ | |
| 566 | .name = test_case.name, | |
| 567 | .suite = if (s.suites.getLastOrNull()) |name| name else null, | |
| 568 | .status = protocol.status(test_case.status), | |
| 569 | .duration_ns = test_case.execution_time_ns, | |
| 570 | .error_message = test_case.error_message, | |
| 571 | } }); | |
| 572 | } | |
| 573 | ||
| 574 | fn emit(s: *Self, event: protocol.Event) !void { | |
| 575 | if (s.emitted_events) try s.output().writeAll(",\n"); | |
| 576 | s.emitted_events = true; | |
| 577 | try std.json.Stringify.value(event, .{}, s.output()); | |
| 592 | 578 | } |
| 593 | 579 | }; |
| 594 | 580 | |