a73x

39c1f074

scenario: close TickError set + extract blink_period_ns const

a73x   2026-04-19 09:26

Commit message
scenario: close TickError set + extract blink_period_ns const

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>

src/scenario.zig
Old New
@@ -8,6 +8,11 @@ const std = @import("std");
8 const png = @import("png"); 8 const png = @import("png");
9 const imgdiff = @import("imgdiff"); 9 const imgdiff = @import("imgdiff");
10 10
11 /// Matches the cursor-blink period in the main runtime. Kept as a
12 /// module-local constant for now; if/when the runtime exports it
13 /// publicly, replace this with the imported symbol.
14 pub const blink_period_ns: i128 = 500 * std.time.ns_per_ms;
15
11 // --------------------------------------------------------------- 16 // ---------------------------------------------------------------
12 // Directive types 17 // Directive types
13 // --------------------------------------------------------------- 18 // ---------------------------------------------------------------
@@ -777,7 +782,8 @@ pub const TickError = error{
777 SleepUntilFlipTimeout, 782 SleepUntilFlipTimeout,
778 AssertFailed, 783 AssertFailed,
779 PredicateOnMissingLabel, 784 PredicateOnMissingLabel,
780 } || std.mem.Allocator.Error || anyerror; // callbacks can surface anyerror 785 CallbackFailed,
786 } || std.mem.Allocator.Error;
781 787
782 pub const TickOutcome = enum { 788 pub const TickOutcome = enum {
783 working, // directives remain; call tick again later 789 working, // directives remain; call tick again later
@@ -855,7 +861,7 @@ pub const ScenarioState = struct {
855 // Continue the loop to execute the next directive. 861 // Continue the loop to execute the next directive.
856 } else { 862 } else {
857 const started = self.sleep_until_flip_started_ns.?; 863 const started = self.sleep_until_flip_started_ns.?;
858 const blink_timeout_ns: i128 = 500 * std.time.ns_per_ms * 2; 864 const blink_timeout_ns: i128 = 2 * blink_period_ns;
859 if (now_ns - started > blink_timeout_ns) { 865 if (now_ns - started > blink_timeout_ns) {
860 return error.SleepUntilFlipTimeout; 866 return error.SleepUntilFlipTimeout;
861 } 867 }
@@ -863,11 +869,17 @@ pub const ScenarioState = struct {
863 } 869 }
864 }, 870 },
865 .bytes => |slice| { 871 .bytes => |slice| {
866 try io.write_bytes(io.ctx, slice); 872 io.write_bytes(io.ctx, slice) catch |err| {
873 std.log.warn("scenario: write_bytes callback failed: {s}", .{@errorName(err)});
874 return error.CallbackFailed;
875 };
867 self.cursor += 1; 876 self.cursor += 1;
868 }, 877 },
869 .capture => |label| { 878 .capture => |label| {
870 const img = try io.capture(io.ctx, label); 879 const img = io.capture(io.ctx, label) catch |err| {
880 std.log.warn("scenario: capture callback failed: {s}", .{@errorName(err)});
881 return error.CallbackFailed;
882 };
871 // Dupe the label into ScenarioState.alloc so captures outlive the arena. 883 // Dupe the label into ScenarioState.alloc so captures outlive the arena.
872 const label_copy = try self.alloc.dupe(u8, label); 884 const label_copy = try self.alloc.dupe(u8, label);
873 errdefer self.alloc.free(label_copy); 885 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" {
1078 // First tick enters the directive, stamps the start time. 1090 // First tick enters the directive, stamps the start time.
1079 _ = try state.tick(0, tio.io()); 1091 _ = try state.tick(0, tio.io());
1080 // Second tick at t=1.1s — over the 2x blink_period budget. 1092 // Second tick at t=1.1s — over the 2x blink_period budget.
1081 const r = state.tick(1_100 * std.time.ns_per_ms, tio.io()); 1093 const r = state.tick(2 * blink_period_ns + 100 * std.time.ns_per_ms, tio.io());
1082 try std.testing.expectError(error.SleepUntilFlipTimeout, r); 1094 try std.testing.expectError(error.SleepUntilFlipTimeout, r);
1083 } 1095 }
1096
1097 const FailingIO = struct {
1098 pub fn writeBytes(ctx: *anyopaque, bytes: []const u8) anyerror!void {
1099 _ = ctx;
1100 _ = bytes;
1101 return error.DiskFull;
1102 }
1103 pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image {
1104 _ = ctx;
1105 _ = label;
1106 unreachable;
1107 }
1108 pub fn flipCb(ctx: *anyopaque) bool {
1109 _ = ctx;
1110 return false;
1111 }
1112 };
1113
1114 test "tick: write_bytes callback failure surfaces as CallbackFailed" {
1115 var s = try parseOk(
1116 \\size 80 24
1117 \\timeout 1000ms
1118 \\bytes "x"
1119 );
1120 defer s.deinit();
1121 var state = ScenarioState.init(std.testing.allocator, &s, 0);
1122 defer state.deinit();
1123
1124 var ctx: u8 = 0;
1125 const io: TickIO = .{
1126 .ctx = &ctx,
1127 .write_bytes = FailingIO.writeBytes,
1128 .capture = FailingIO.captureCb,
1129 .blink_just_flipped = FailingIO.flipCb,
1130 };
1131
1132 const r = state.tick(0, io);
1133 try std.testing.expectError(error.CallbackFailed, r);
1134 }