ReviewOS

also looking at this

zig-utils/zig-test-framework

Define stable JSON event protocol

#27
Merged glennmichael123 wants to merge codex/issue-10-json-events into main
11 files +442 -177
src/ui_server.zigmodified+33-34
Changes to src/ui_server.zig
@@ -3,6 +3,7 @@ const reporter_mod = ;
33const suite = ;
44const test_history = ;
55const compat = ;
6const protocol = ;
67
78/// Options for the UI server
89pub const UIServerOptions = struct {
@@ -282,13 +283,19 @@ pub const UIServer = struct {
282283 index += 1;
283284 }
284285 }
286
287 /// Serialize and broadcast one versioned protocol event.
288 pub fn broadcastEvent(self: *Self, allocator: std.mem.Allocator, event: protocol.Event) !void {
289 const encoded = try protocol.encodeAlloc(allocator, event);
290 defer allocator.free(encoded);
291 try self.broadcast(event.eventName(), encoded);
292 }
285293};
286294
287295/// UI Reporter - sends test events to the UI server
288296pub const UIReporter = struct {
289297 reporter: reporter_mod.Reporter,
290298 server: *UIServer,
291 buffer: std.ArrayList(u8),
292299
293300 const Self = @This();
294301
@@ -307,71 +314,66 @@ pub const UIReporter = struct {
307314 .use_colors = false,
308315 },
309316 .server = server,
310 .buffer = .empty,
311317 };
312318 }
313319
314 pub fn deinit(self: *Self) void {
315 self.buffer.deinit(self.reporter.allocator);
316 }
317
318 fn encode(self: *Self, value: anytype) !void {
319 self.buffer.clearRetainingCapacity();
320 var writer: std.Io.Writer.Allocating = .fromArrayList(self.reporter.allocator, &self.buffer);
321 defer self.buffer = writer.toArrayList();
322 try std.json.Stringify.value(value, .{}, &writer.writer);
323 }
320 pub fn deinit(_: *Self) void {}
324321
325322 fn onRunStart(reporter: *reporter_mod.Reporter, total: usize) !void {
326323 const self: *Self = @fieldParentPtr("reporter", reporter);
327 try self.encode(.{ .total = total });
328 try self.server.broadcast("run_start", self.buffer.items);
324 try self.server.broadcastEvent(reporter.allocator, .{ .run_start = .{ .total = total } });
329325 }
330326
331327 fn onRunEnd(reporter: *reporter_mod.Reporter, results: *reporter_mod.TestResults) !void {
332328 const self: *Self = @fieldParentPtr("reporter", reporter);
333329
334 try self.encode(.{
330 try self.server.broadcastEvent(reporter.allocator, .{ .run_end = .{
335331 .total = results.total,
336332 .passed = results.passed,
337333 .flaky = results.flaky,
338334 .failed = results.failed,
339335 .skipped = results.skipped,
340 });
341 try self.server.broadcast("run_end", self.buffer.items);
336 .duration_ns = results.total_time_ns,
337 } });
342338 }
343339
344340 fn onSuiteStart(reporter: *reporter_mod.Reporter, suite_name: []const u8) !void {
345341 const self: *Self = @fieldParentPtr("reporter", reporter);
346342
347 try self.encode(.{ .name = suite_name });
348 try self.server.broadcast("suite_start", self.buffer.items);
343 try self.server.broadcastEvent(reporter.allocator, .{ .suite_start = .{ .name = suite_name } });
349344 }
350345
351346 fn onSuiteEnd(reporter: *reporter_mod.Reporter, suite_name: []const u8) !void {
352347 const self: *Self = @fieldParentPtr("reporter", reporter);
353348
354 try self.encode(.{ .name = suite_name });
355 try self.server.broadcast("suite_end", self.buffer.items);
349 try self.server.broadcastEvent(reporter.allocator, .{ .suite_end = .{ .name = suite_name } });
356350 }
357351
358352 fn onTestStart(reporter: *reporter_mod.Reporter, test_name: []const u8) !void {
359353 const self: *Self = @fieldParentPtr("reporter", reporter);
360354
361 try self.encode(.{ .name = test_name });
362 try self.server.broadcast("test_start", self.buffer.items);
355 try self.server.broadcastEvent(reporter.allocator, .{ .test_start = .{ .name = test_name } });
363356 }
364357
365358 fn onTestEnd(reporter: *reporter_mod.Reporter, test_case: *const suite.TestCase) !void {
366359 const self: *Self = @fieldParentPtr("reporter", reporter);
367360
368 try self.encode(.{
361 for (test_case.attempts.items) |attempt| {
362 try self.server.broadcastEvent(reporter.allocator, .{ .retry = .{
363 .name = test_case.name,
364 .attempt = attempt.number,
365 .repetition = attempt.repetition,
366 .status = protocol.status(attempt.status),
367 .duration_ns = attempt.duration_ns,
368 .error_message = attempt.error_message,
369 } });
370 }
371 try self.server.broadcastEvent(reporter.allocator, .{ .test_end = .{
369372 .name = test_case.name,
370 .status = @tagName(test_case.status),
371 .execution_time_ns = test_case.execution_time_ns,
372 .error_message = test_case.error_message orelse "",
373 });
374 try self.server.broadcast("test_end", self.buffer.items);
373 .status = protocol.status(test_case.status),
374 .duration_ns = test_case.execution_time_ns,
375 .error_message = test_case.error_message,
376 } });
375377 }
376378};
377379
@@ -471,10 +473,7 @@ test "UIReporter initialization" {
471473 defer ui_reporter.deinit();
472474
473475 try std.testing.expectEqual(false, ui_reporter.reporter.use_colors);
474 try std.testing.expectEqual(@as(usize, 0), ui_reporter.buffer.items.len);
475
476476 try ui_reporter.reporter.onSuiteStart("quoted \"suite\"\nname");
477 try std.testing.expectEqualStrings("{\"name\":\"quoted \\\"suite\\\"\\nname\"}", ui_reporter.buffer.items);
478477}
479478
480479fn writeTestRequest(io: std.Io, stream: std.Io.net.Stream, target: []const u8) !void {
@@ -535,9 +534,9 @@ test "UI server streams broadcast events over SSE" {
535534 try expectLine(&stream_reader.interface, "data: {}");
536535 try expectLine(&stream_reader.interface, "");
537536
538 try server.broadcast("run_start", "{\"total\":2}");
537 try server.broadcastEvent(std.testing.allocator, .{ .run_start = .{ .total = 2 } });
539538 try expectLine(&stream_reader.interface, "event: run_start");
540 try expectLine(&stream_reader.interface, "data: {\"total\":2}");
539 try expectLine(&stream_reader.interface, "data: {\"protocol_version\":1,\"type\":\"run_start\",\"data\":{\"total\":2}}");
541540 try expectLine(&stream_reader.interface, "");
542541}
543542