const std = @import("std");const debug_trace = @import("../core/shared/debug_trace.zig");const record_tape = @import("../core/workspace/record_tape.zig");const shell_runtime = @import("shell_runtime.zig"); pub const EventLoopCallbacks = struct { ctx: *anyopaque, collect_facts: *const fn (*anyopaque) anyerror!void, next_collected_byte: *const fn (*anyopaque) ?u8, handle_byte: *const fn (*anyopaque, u8) anyerror!void, settle_delivery_epoch: *const fn (*anyopaque) anyerror!void, commit_frame: *const fn (*anyopaque) anyerror!void, poll_timeout_ms: ?*const fn (*anyopaque, i32) i32 = null,}; const max_input_reads_per_fact_collection = 32; fn attemptFinalFrameCommit(callbacks: EventLoopCallbacks, reason: []const u8) void { callbacks.commit_frame(callbacks.ctx) catch |err| debug_trace.logf( "event_loop", "final_frame_commit_failed reason={s} err={s}", .{ reason, @errorName(err) }, );} pub const ExitCause = enum { requested_exit, input_closed,}; pub fn pump_ready_input(terminal: anytype, should_exit: *bool, callbacks: EventLoopCallbacks) !?ExitCause { var buf: [128]u8 = undefined; var handled_input = false; while (callbacks.next_collected_byte(callbacks.ctx)) |byte| { handled_input = true; try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } var poll_result = try terminal.pollInput(0); var input_reads: usize = 0; while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) { const read_len = try terminal.read(&buf); if (read_len == 0) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof"); return .input_closed; } handled_input = true; record_tape.recordStdin(buf[0..read_len]); for (buf[0..read_len]) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } poll_result = try terminal.pollInput(0); } if (poll_result.readable) { try callbacks.commit_frame(callbacks.ctx); return null; } if (poll_result.closed()) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup"); return .input_closed; } if (handled_input) { try callbacks.settle_delivery_epoch(callbacks.ctx); try callbacks.commit_frame(callbacks.ctx); } return null;} pub fn run(terminal: anytype, should_exit: *bool, poll_timeout_ms: i32, callbacks: EventLoopCallbacks) !ExitCause { var buf: [128]u8 = undefined; while (!should_exit.*) { try callbacks.collect_facts(callbacks.ctx); if (should_exit.*) return .requested_exit; while (callbacks.next_collected_byte(callbacks.ctx)) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } const timeout_ms = if (callbacks.poll_timeout_ms) |resolve| resolve(callbacks.ctx, poll_timeout_ms) else poll_timeout_ms; var poll_result = try terminal.pollInput(timeout_ms); var input_reads: usize = 0; while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) { const read_len = try terminal.read(&buf); if (read_len == 0) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof"); return .input_closed; } record_tape.recordStdin(buf[0..read_len]); for (buf[0..read_len]) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } poll_result = try terminal.pollInput(0); } if (poll_result.readable) { try callbacks.commit_frame(callbacks.ctx); continue; } if (poll_result.closed()) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup"); return .input_closed; } try callbacks.settle_delivery_epoch(callbacks.ctx); try callbacks.commit_frame(callbacks.ctx); } return .requested_exit;} test "event loop exposes a post-input commit callback" { try std.testing.expect(@hasField(EventLoopCallbacks, "commit_frame")); try std.testing.expect(@hasField(EventLoopCallbacks, "settle_delivery_epoch"));} const EventLoopTestTrace = struct { bytes: [8]u8 = undefined, len: usize = 0, collected: [2]u8 = undefined, collected_len: usize = 0, collected_index: usize = 0, should_exit: *bool, fn append(self: *EventLoopTestTrace, byte: u8) void { self.bytes[self.len] = byte; self.len += 1; }}; const EventLoopTestTerminal = struct { polls: *usize, burst_reads: ?*usize = null, observed_timeout_ms: ?*i32 = null, fn pollInput(self: EventLoopTestTerminal, timeout_ms: i32) !shell_runtime.PollResult { self.polls.* += 1; if (self.observed_timeout_ms) |observed| { if (self.polls.* == 1) observed.* = timeout_ms; } if (self.burst_reads) |reads| { if (reads.* < 2) return .{ .readable = true }; return if (timeout_ms == 0) .{} else .{ .hung_up = true }; } if (self.polls.* > 1) return .{ .hung_up = true }; return .{ .readable = true }; } fn read(self: EventLoopTestTerminal, buf: []u8) !usize { const bytes = if (self.burst_reads) |reads| blk: { const chunk = if (reads.* == 0) "ab" else "de"; reads.* += 1; break :blk chunk; } else "ab"; @memcpy(buf[0..bytes.len], bytes); return bytes.len; }}; fn collectEventLoopTestFacts(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('t');} fn noCollectedEventLoopByte(_: *anyopaque) ?u8 { return null;} fn nextCollectedEventLoopByte(ctx: *anyopaque) ?u8 { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); if (trace.collected_index >= trace.collected_len) return null; const byte = trace.collected[trace.collected_index]; trace.collected_index += 1; return byte;} fn handleEventLoopTestByte(ctx: *anyopaque, byte: u8) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append(byte);} fn commitEventLoopTestFrame(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c'); trace.should_exit.* = true;} fn commitEventLoopTestFrameWithoutExit(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c');} fn settleEventLoopTestDeliveryEpoch(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('s');} fn collectAndExitEventLoopTest(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('t'); trace.should_exit.* = true;} fn handleAndExitEventLoopTestByte(ctx: *anyopaque, byte: u8) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append(byte); trace.should_exit.* = true;} fn rejectUnexpectedEventLoopByte(_: *anyopaque, _: u8) !void { return error.UnexpectedByte;} fn rejectUnexpectedEventLoopCommit(_: *anyopaque) !void { return error.UnexpectedCommit;} fn failEventLoopTestCommit(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c'); return error.FinalCommitFailed;} fn useLongerPollTimeout(_: *anyopaque, default_timeout_ms: i32) i32 { return default_timeout_ms * 2;} test "event loop commits once after every byte from the current read" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqualStrings("tabc", trace.bytes[0..trace.len]);} test "event loop batches already readable input before committing a frame" { var should_exit = false; var polls: usize = 0; var reads: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls, .burst_reads = &reads }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("tabdesc", trace.bytes[0..trace.len]);} test "cooperative input pump renders ready input without collecting facts" { var should_exit = false; var polls: usize = 0; var reads: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try pump_ready_input(EventLoopTestTerminal{ .polls = &polls, .burst_reads = &reads, }, &should_exit, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrameWithoutExit, }); try std.testing.expectEqual(@as(?ExitCause, null), exit_cause); try std.testing.expect(!should_exit); try std.testing.expectEqualStrings("abdesc", trace.bytes[0..trace.len]);} test "cooperative input pump is idle when no input is ready" { const Terminal = struct { fn pollInput(_: @This(), timeout_ms: i32) !shell_runtime.PollResult { try std.testing.expectEqual(@as(i32, 0), timeout_ms); return .{}; } fn read(_: @This(), _: []u8) !usize { return error.UnexpectedRead; } }; var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try pump_ready_input(Terminal{}, &should_exit, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = rejectUnexpectedEventLoopCommit, .commit_frame = rejectUnexpectedEventLoopCommit, }); try std.testing.expectEqual(@as(?ExitCause, null), exit_cause); try std.testing.expect(!should_exit); try std.testing.expectEqual(@as(usize, 0), trace.len);} test "event loop settles a facts-only delivery epoch before committing" { const Terminal = struct { fn pollInput(_: @This(), _: i32) !shell_runtime.PollResult { return .{}; } fn read(_: @This(), _: []u8) !usize { return error.UnexpectedRead; } }; var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(Terminal{}, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("tsc", trace.bytes[0..trace.len]);} test "event loop resolves a dynamic poll timeout" { var should_exit = false; var polls: usize = 0; var observed_timeout_ms: i32 = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls, .observed_timeout_ms = &observed_timeout_ms, }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, .poll_timeout_ms = useLongerPollTimeout, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqual(@as(i32, 16), observed_timeout_ms);} test "event loop dispatches probe-collected input before polled input" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .collected = .{ 'x', 0 }, .collected_len = 1, .should_exit = &should_exit, }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = nextCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqualStrings("txabc", trace.bytes[0..trace.len]);} test "event loop stops after fact collection requests exit" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectAndExitEventLoopTest, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqual(@as(usize, 0), polls); try std.testing.expectEqualStrings("t", trace.bytes[0..trace.len]);} test "event loop skips frame commit after input requests exit" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run( EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleAndExitEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }, ); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("ta", trace.bytes[0..trace.len]);} const EventLoopClosedTerminal = struct { poll_result: shell_runtime.PollResult, fn pollInput(self: EventLoopClosedTerminal, _: i32) !shell_runtime.PollResult { return self.poll_result; } fn read(_: EventLoopClosedTerminal, _: []u8) !usize { return 0; }}; test "event loop distinguishes requested exit from input closure" { var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const callbacks = EventLoopCallbacks{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }; try std.testing.expectEqual( ExitCause.input_closed, try run( EventLoopClosedTerminal{ .poll_result = .{ .readable = true } }, &should_exit, 8, callbacks, ), ); try std.testing.expect(!should_exit); try std.testing.expectEqual( ExitCause.input_closed, try run( EventLoopClosedTerminal{ .poll_result = .{ .hung_up = true } }, &should_exit, 8, callbacks, ), ); try std.testing.expect(!should_exit);} test "event loop treats a failed final commit after hangup as a clean close" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = failEventLoopTestCommit, });Lines 1–500 of 600. Load more
const std = @import("std"); const debug_trace = @import("../core/shared/debug_trace.zig"); const record_tape = @import("../core/workspace/record_tape.zig"); const shell_runtime = @import("shell_runtime.zig"); pub const EventLoopCallbacks = struct { ctx: *anyopaque, collect_facts: *const fn (*anyopaque) anyerror!void, next_collected_byte: *const fn (*anyopaque) ?u8, handle_byte: *const fn (*anyopaque, u8) anyerror!void, settle_delivery_epoch: *const fn (*anyopaque) anyerror!void, commit_frame: *const fn (*anyopaque) anyerror!void, poll_timeout_ms: ?*const fn (*anyopaque, i32) i32 = null, }; const max_input_reads_per_fact_collection = 32; fn attemptFinalFrameCommit(callbacks: EventLoopCallbacks, reason: []const u8) void { callbacks.commit_frame(callbacks.ctx) catch |err| debug_trace.logf( "event_loop", "final_frame_commit_failed reason={s} err={s}", .{ reason, @errorName(err) }, ); } pub const ExitCause = enum { requested_exit, input_closed, }; pub fn pump_ready_input(terminal: anytype, should_exit: *bool, callbacks: EventLoopCallbacks) !?ExitCause { var buf: [128]u8 = undefined; var handled_input = false; while (callbacks.next_collected_byte(callbacks.ctx)) |byte| { handled_input = true; try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } var poll_result = try terminal.pollInput(0); var input_reads: usize = 0; while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) { const read_len = try terminal.read(&buf); if (read_len == 0) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof"); return .input_closed; } handled_input = true; record_tape.recordStdin(buf[0..read_len]); for (buf[0..read_len]) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } poll_result = try terminal.pollInput(0); } if (poll_result.readable) { try callbacks.commit_frame(callbacks.ctx); return null; } if (poll_result.closed()) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup"); return .input_closed; } if (handled_input) { try callbacks.settle_delivery_epoch(callbacks.ctx); try callbacks.commit_frame(callbacks.ctx); } return null; } pub fn run(terminal: anytype, should_exit: *bool, poll_timeout_ms: i32, callbacks: EventLoopCallbacks) !ExitCause { var buf: [128]u8 = undefined; while (!should_exit.*) { try callbacks.collect_facts(callbacks.ctx); if (should_exit.*) return .requested_exit; while (callbacks.next_collected_byte(callbacks.ctx)) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } const timeout_ms = if (callbacks.poll_timeout_ms) |resolve| resolve(callbacks.ctx, poll_timeout_ms) else poll_timeout_ms; var poll_result = try terminal.pollInput(timeout_ms); var input_reads: usize = 0; while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) { const read_len = try terminal.read(&buf); if (read_len == 0) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof"); return .input_closed; } record_tape.recordStdin(buf[0..read_len]); for (buf[0..read_len]) |byte| { try callbacks.handle_byte(callbacks.ctx, byte); if (should_exit.*) return .requested_exit; } poll_result = try terminal.pollInput(0); } if (poll_result.readable) { try callbacks.commit_frame(callbacks.ctx); continue; } if (poll_result.closed()) { if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup"); return .input_closed; } try callbacks.settle_delivery_epoch(callbacks.ctx); try callbacks.commit_frame(callbacks.ctx); } return .requested_exit; } test "event loop exposes a post-input commit callback" { try std.testing.expect(@hasField(EventLoopCallbacks, "commit_frame")); try std.testing.expect(@hasField(EventLoopCallbacks, "settle_delivery_epoch")); } const EventLoopTestTrace = struct { bytes: [8]u8 = undefined, len: usize = 0, collected: [2]u8 = undefined, collected_len: usize = 0, collected_index: usize = 0, should_exit: *bool, fn append(self: *EventLoopTestTrace, byte: u8) void { self.bytes[self.len] = byte; self.len += 1; } }; const EventLoopTestTerminal = struct { polls: *usize, burst_reads: ?*usize = null, observed_timeout_ms: ?*i32 = null, fn pollInput(self: EventLoopTestTerminal, timeout_ms: i32) !shell_runtime.PollResult { self.polls.* += 1; if (self.observed_timeout_ms) |observed| { if (self.polls.* == 1) observed.* = timeout_ms; } if (self.burst_reads) |reads| { if (reads.* < 2) return .{ .readable = true }; return if (timeout_ms == 0) .{} else .{ .hung_up = true }; } if (self.polls.* > 1) return .{ .hung_up = true }; return .{ .readable = true }; } fn read(self: EventLoopTestTerminal, buf: []u8) !usize { const bytes = if (self.burst_reads) |reads| blk: { const chunk = if (reads.* == 0) "ab" else "de"; reads.* += 1; break :blk chunk; } else "ab"; @memcpy(buf[0..bytes.len], bytes); return bytes.len; } }; fn collectEventLoopTestFacts(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('t'); } fn noCollectedEventLoopByte(_: *anyopaque) ?u8 { return null; } fn nextCollectedEventLoopByte(ctx: *anyopaque) ?u8 { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); if (trace.collected_index >= trace.collected_len) return null; const byte = trace.collected[trace.collected_index]; trace.collected_index += 1; return byte; } fn handleEventLoopTestByte(ctx: *anyopaque, byte: u8) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append(byte); } fn commitEventLoopTestFrame(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c'); trace.should_exit.* = true; } fn commitEventLoopTestFrameWithoutExit(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c'); } fn settleEventLoopTestDeliveryEpoch(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('s'); } fn collectAndExitEventLoopTest(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('t'); trace.should_exit.* = true; } fn handleAndExitEventLoopTestByte(ctx: *anyopaque, byte: u8) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append(byte); trace.should_exit.* = true; } fn rejectUnexpectedEventLoopByte(_: *anyopaque, _: u8) !void { return error.UnexpectedByte; } fn rejectUnexpectedEventLoopCommit(_: *anyopaque) !void { return error.UnexpectedCommit; } fn failEventLoopTestCommit(ctx: *anyopaque) !void { const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx)); trace.append('c'); return error.FinalCommitFailed; } fn useLongerPollTimeout(_: *anyopaque, default_timeout_ms: i32) i32 { return default_timeout_ms * 2; } test "event loop commits once after every byte from the current read" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqualStrings("tabc", trace.bytes[0..trace.len]); } test "event loop batches already readable input before committing a frame" { var should_exit = false; var polls: usize = 0; var reads: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls, .burst_reads = &reads }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("tabdesc", trace.bytes[0..trace.len]); } test "cooperative input pump renders ready input without collecting facts" { var should_exit = false; var polls: usize = 0; var reads: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try pump_ready_input(EventLoopTestTerminal{ .polls = &polls, .burst_reads = &reads, }, &should_exit, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrameWithoutExit, }); try std.testing.expectEqual(@as(?ExitCause, null), exit_cause); try std.testing.expect(!should_exit); try std.testing.expectEqualStrings("abdesc", trace.bytes[0..trace.len]); } test "cooperative input pump is idle when no input is ready" { const Terminal = struct { fn pollInput(_: @This(), timeout_ms: i32) !shell_runtime.PollResult { try std.testing.expectEqual(@as(i32, 0), timeout_ms); return .{}; } fn read(_: @This(), _: []u8) !usize { return error.UnexpectedRead; } }; var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try pump_ready_input(Terminal{}, &should_exit, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = rejectUnexpectedEventLoopCommit, .commit_frame = rejectUnexpectedEventLoopCommit, }); try std.testing.expectEqual(@as(?ExitCause, null), exit_cause); try std.testing.expect(!should_exit); try std.testing.expectEqual(@as(usize, 0), trace.len); } test "event loop settles a facts-only delivery epoch before committing" { const Terminal = struct { fn pollInput(_: @This(), _: i32) !shell_runtime.PollResult { return .{}; } fn read(_: @This(), _: []u8) !usize { return error.UnexpectedRead; } }; var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(Terminal{}, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("tsc", trace.bytes[0..trace.len]); } test "event loop resolves a dynamic poll timeout" { var should_exit = false; var polls: usize = 0; var observed_timeout_ms: i32 = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls, .observed_timeout_ms = &observed_timeout_ms, }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, .poll_timeout_ms = useLongerPollTimeout, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqual(@as(i32, 16), observed_timeout_ms); } test "event loop dispatches probe-collected input before polled input" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .collected = .{ 'x', 0 }, .collected_len = 1, .should_exit = &should_exit, }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = nextCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = commitEventLoopTestFrame, }); try std.testing.expectEqual(ExitCause.input_closed, exit_cause); try std.testing.expectEqualStrings("txabc", trace.bytes[0..trace.len]); } test "event loop stops after fact collection requests exit" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectAndExitEventLoopTest, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqual(@as(usize, 0), polls); try std.testing.expectEqualStrings("t", trace.bytes[0..trace.len]); } test "event loop skips frame commit after input requests exit" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run( EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleAndExitEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }, ); try std.testing.expectEqual(ExitCause.requested_exit, exit_cause); try std.testing.expectEqualStrings("ta", trace.bytes[0..trace.len]); } const EventLoopClosedTerminal = struct { poll_result: shell_runtime.PollResult, fn pollInput(self: EventLoopClosedTerminal, _: i32) !shell_runtime.PollResult { return self.poll_result; } fn read(_: EventLoopClosedTerminal, _: []u8) !usize { return 0; } }; test "event loop distinguishes requested exit from input closure" { var should_exit = false; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const callbacks = EventLoopCallbacks{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = rejectUnexpectedEventLoopByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = rejectUnexpectedEventLoopCommit, }; try std.testing.expectEqual( ExitCause.input_closed, try run( EventLoopClosedTerminal{ .poll_result = .{ .readable = true } }, &should_exit, 8, callbacks, ), ); try std.testing.expect(!should_exit); try std.testing.expectEqual( ExitCause.input_closed, try run( EventLoopClosedTerminal{ .poll_result = .{ .hung_up = true } }, &should_exit, 8, callbacks, ), ); try std.testing.expect(!should_exit); } test "event loop treats a failed final commit after hangup as a clean close" { var should_exit = false; var polls: usize = 0; var trace = EventLoopTestTrace{ .should_exit = &should_exit }; const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{ .ctx = &trace, .collect_facts = collectEventLoopTestFacts, .next_collected_byte = noCollectedEventLoopByte, .handle_byte = handleEventLoopTestByte, .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch, .commit_frame = failEventLoopTestCommit, });