39c1f074
scenario: close TickError set + extract blink_period_ns const
a73x 2026-04-19 09:26
Removes the `|| anyerror` tail from TickError so callers can exhaustively match. Callback failures from write_bytes / capture now wrap to error.CallbackFailed with a log line. blink_period_ns moves to a module-local pub const. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/src/scenario.zig b/src/scenario.zig index 0fd4e7c..42c766c 100644 --- a/src/scenario.zig +++ b/src/scenario.zig @@ -8,6 +8,11 @@ const std = @import("std"); const png = @import("png"); const imgdiff = @import("imgdiff"); /// Matches the cursor-blink period in the main runtime. Kept as a /// module-local constant for now; if/when the runtime exports it /// publicly, replace this with the imported symbol. pub const blink_period_ns: i128 = 500 * std.time.ns_per_ms; // --------------------------------------------------------------- // Directive types // --------------------------------------------------------------- @@ -777,7 +782,8 @@ pub const TickError = error{ SleepUntilFlipTimeout, AssertFailed, PredicateOnMissingLabel, } || std.mem.Allocator.Error || anyerror; // callbacks can surface anyerror CallbackFailed, } || std.mem.Allocator.Error; pub const TickOutcome = enum { working, // directives remain; call tick again later @@ -855,7 +861,7 @@ pub const ScenarioState = struct { // Continue the loop to execute the next directive. } else { const started = self.sleep_until_flip_started_ns.?; const blink_timeout_ns: i128 = 500 * std.time.ns_per_ms * 2; const blink_timeout_ns: i128 = 2 * blink_period_ns; if (now_ns - started > blink_timeout_ns) { return error.SleepUntilFlipTimeout; } @@ -863,11 +869,17 @@ pub const ScenarioState = struct { } }, .bytes => |slice| { try io.write_bytes(io.ctx, slice); io.write_bytes(io.ctx, slice) catch |err| { std.log.warn("scenario: write_bytes callback failed: {s}", .{@errorName(err)}); return error.CallbackFailed; }; self.cursor += 1; }, .capture => |label| { const img = try io.capture(io.ctx, label); const img = io.capture(io.ctx, label) catch |err| { std.log.warn("scenario: capture callback failed: {s}", .{@errorName(err)}); return error.CallbackFailed; }; // Dupe the label into ScenarioState.alloc so captures outlive the arena. const label_copy = try self.alloc.dupe(u8, label); errdefer self.alloc.free(label_copy); @@ -1078,6 +1090,45 @@ test "tick: sleep-until-flip times out after 2x blink_period_ns of real wait" { // First tick enters the directive, stamps the start time. _ = try state.tick(0, tio.io()); // Second tick at t=1.1s — over the 2x blink_period budget. const r = state.tick(1_100 * std.time.ns_per_ms, tio.io()); const r = state.tick(2 * blink_period_ns + 100 * std.time.ns_per_ms, tio.io()); try std.testing.expectError(error.SleepUntilFlipTimeout, r); } const FailingIO = struct { pub fn writeBytes(ctx: *anyopaque, bytes: []const u8) anyerror!void { _ = ctx; _ = bytes; return error.DiskFull; } pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image { _ = ctx; _ = label; unreachable; } pub fn flipCb(ctx: *anyopaque) bool { _ = ctx; return false; } }; test "tick: write_bytes callback failure surfaces as CallbackFailed" { var s = try parseOk( \\size 80 24 \\timeout 1000ms \\bytes "x" ); defer s.deinit(); var state = ScenarioState.init(std.testing.allocator, &s, 0); defer state.deinit(); var ctx: u8 = 0; const io: TickIO = .{ .ctx = &ctx, .write_bytes = FailingIO.writeBytes, .capture = FailingIO.captureCb, .blink_just_flipped = FailingIO.flipCb, }; const r = state.tick(0, io); try std.testing.expectError(error.CallbackFailed, r); }