39c1f074
scenario: close TickError set + extract blink_period_ns const
a73x 2026-04-19 09:26
Commit message
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 | } | ||