e97d10f9
test: server_test_await and server_test_deliver ride the harness awaits
a73x 2026-08-31 21:58
Commit message
src/server/server_test_await.zig
| Old | New | ||
|---|---|---|---|
| @@ -91,18 +91,32 @@ fn anyReturnWithin( | |||
| 91 | fd: std.posix.fd_t, | 91 | fd: std.posix.fd_t, |
| 92 | budget_ms: i64, | 92 | budget_ms: i64, |
| 93 | ) !?proto.CmdState { | 93 | ) !?proto.CmdState { |
| 94 | // The answer comes out of the SINK, because a cmd_state in any other | ||
| 95 | // phase is not it: `want` would take the first one and call it the | ||
| 96 | // return. The wall clock stays the budget — the harness's pumping await | ||
| 97 | // counts rounds, and this one's contract is "inside this many | ||
| 98 | // milliseconds, nothing came back". | ||
| 99 | const Returned = struct { | ||
| 100 | st: proto.CmdState = undefined, | ||
| 101 | fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void { | ||
| 102 | if (frame.type != .cmd_state) return; | ||
| 103 | const self: *@This() = @ptrCast(@alignCast(ctx.?)); | ||
| 104 | const st = try proto.decodeCmdState(frame.payload); | ||
| 105 | if (st.phase != .returned) return; | ||
| 106 | self.st = st; | ||
| 107 | return error.CommandReturned; | ||
| 108 | } | ||
| 109 | }; | ||
| 110 | var seen: Returned = .{}; | ||
| 94 | const deadline = std.time.milliTimestamp() + budget_ms; | 111 | const deadline = std.time.milliTimestamp() + budget_ms; |
| 95 | while (std.time.milliTimestamp() < deadline) { | 112 | while (std.time.milliTimestamp() < deadline) { |
| 96 | try srv.pumpOnce(5); | 113 | _ = h.awaitFrameSink(alloc, srv, fd, h.never_from_daemon, 1, .{ |
| 97 | var pfd = [_]std.posix.pollfd{ | 114 | .ctx = &seen, |
| 98 | .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }, | 115 | .on = Returned.on, |
| 116 | }) catch |err| switch (err) { | ||
| 117 | error.CommandReturned => return seen.st, | ||
| 118 | else => return err, | ||
| 99 | }; | 119 | }; |
| 100 | if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue; | ||
| 101 | const frame = (try proto.readFrame(alloc, fd)) orelse return null; | ||
| 102 | defer frame.deinit(alloc); | ||
| 103 | if (frame.type != .cmd_state) continue; | ||
| 104 | const st = try proto.decodeCmdState(frame.payload); | ||
| 105 | if (st.phase == .returned) return st; | ||
| 106 | } | 120 | } |
| 107 | return null; | 121 | return null; |
| 108 | } | 122 | } |
| @@ -412,6 +426,10 @@ test "Server: an unknown shell is not injected into at all — no directory, no | |||
| 412 | /// of `want` turns up or the socket goes quiet — the "did the daemon answer | 426 | /// of `want` turns up or the socket goes quiet — the "did the daemon answer |
| 413 | /// inside that one pump" observable. An answer needing another pump reads as | 427 | /// inside that one pump" observable. An answer needing another pump reads as |
| 414 | /// absent, which is the distinction the immediate-answer test makes. | 428 | /// absent, which is the distinction the immediate-answer test makes. |
| 429 | /// | ||
| 430 | /// Its own poll rather than `Link.awaitFrame` for both halves of that: it | ||
| 431 | /// must not pump, and it ends on SILENCE, which the primitive does not | ||
| 432 | /// report — a poll that timed out inside it looks the same as a deadline. | ||
| 415 | fn readQueued(alloc: std.mem.Allocator, fd: std.posix.fd_t, want: proto.MsgType) !?proto.Frame { | 433 | fn readQueued(alloc: std.mem.Allocator, fd: std.posix.fd_t, want: proto.MsgType) !?proto.Frame { |
| 416 | var guard: usize = 0; | 434 | var guard: usize = 0; |
| 417 | while (guard < 16) : (guard += 1) { | 435 | while (guard < 16) : (guard += 1) { |
| @@ -963,6 +981,9 @@ test "Server: a promoted-but-unattached slot receives nothing" { | |||
| 963 | // The slot was told nothing — nothing queued, nothing on the wire. | 981 | // The slot was told nothing — nothing queued, nothing on the wire. |
| 964 | try std.testing.expect(td.srv.clients[0] != null); | 982 | try std.testing.expect(td.srv.clients[0] != null); |
| 965 | try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.pending.items.len); | 983 | try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.pending.items.len); |
| 984 | // A bare poll, not a wait: the claim is that nothing is readable at this | ||
| 985 | // instant, and any budget at all would turn it into a slower claim about | ||
| 986 | // a window instead. | ||
| 966 | var pfd = [_]std.posix.pollfd{ | 987 | var pfd = [_]std.posix.pollfd{ |
| 967 | .{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 }, | 988 | .{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 }, |
| 968 | }; | 989 | }; |
src/server/server_test_deliver.zig
| Old | New | ||
|---|---|---|---|
| @@ -125,7 +125,8 @@ test "Server: a writable backlog is flushed by poll, not mistaken for input" { | |||
| 125 | defer td.deinit(); | 125 | defer td.deinit(); |
| 126 | 126 | ||
| 127 | // Drain the shell's startup output first, so the pump below has exactly | 127 | // Drain the shell's startup output first, so the pump below has exactly |
| 128 | // one thing to react to: the client's writability. | 128 | // one thing to react to: the client's writability. Raw bytes off the pty |
| 129 | // master, so there is no Link here to await on. | ||
| 129 | while (true) { | 130 | while (true) { |
| 130 | var pfd = [_]std.posix.pollfd{ | 131 | var pfd = [_]std.posix.pollfd{ |
| 131 | .{ .fd = td.srv.sessions.table[0].?.pty.master, .events = std.posix.POLL.IN, .revents = 0 }, | 132 | .{ .fd = td.srv.sessions.table[0].?.pty.master, .events = std.posix.POLL.IN, .revents = 0 }, |
| @@ -192,6 +193,9 @@ test "Server: a partially flushed queue delivers every byte exactly once" { | |||
| 192 | try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0); | 193 | try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0); |
| 193 | 194 | ||
| 194 | // Drain and flush alternately: each flush starts where the last stopped. | 195 | // Drain and flush alternately: each flush starts where the last stopped. |
| 196 | // Byte-level on purpose, all the way down: the verdict below is that | ||
| 197 | // what the peer read equals `expect` BYTE for byte, so these two loops | ||
| 198 | // read a stream rather than frames and cannot be a frame await. | ||
| 195 | var got: std.ArrayList(u8) = .empty; | 199 | var got: std.ArrayList(u8) = .empty; |
| 196 | defer got.deinit(alloc); | 200 | defer got.deinit(alloc); |
| 197 | var spins: usize = 0; | 201 | var spins: usize = 0; |
| @@ -232,7 +236,10 @@ test "Server: the shell's exit status reaches an attached client" { | |||
| 232 | defer c.close(); | 236 | defer c.close(); |
| 233 | 237 | ||
| 234 | // Settle first, so the shell is ready for input and the exit below is | 238 | // Settle first, so the shell is ready for input and the exit below is |
| 235 | // the next thing that happens. | 239 | // the next thing that happens. Its own poll, like the quiet-drains in |
| 240 | // server_test_attach.zig: the condition is SILENCE, and inside | ||
| 241 | // `Link.awaitFrame` a poll that timed out is indistinguishable from the | ||
| 242 | // deadline running out. | ||
| 236 | var quiet_ms: u64 = 0; | 243 | var quiet_ms: u64 = 0; |
| 237 | var deadline_ms: u64 = 10_000; | 244 | var deadline_ms: u64 = 10_000; |
| 238 | while (deadline_ms > 0 and quiet_ms < 500) { | 245 | while (deadline_ms > 0 and quiet_ms < 500) { |
| @@ -266,7 +273,8 @@ test "Server: the shell's exit status reaches an attached client" { | |||
| 266 | 273 | ||
| 267 | /// Test helper: read one fd until it has been quiet for `quiet_ms`, keeping | 274 | /// Test helper: read one fd until it has been quiet for `quiet_ms`, keeping |
| 268 | /// everything. Runs on its own thread so a drain under test has a peer that | 275 | /// everything. Runs on its own thread so a drain under test has a peer that |
| 269 | /// is actually consuming. | 276 | /// is actually consuming. Raw bytes and a silence condition, so neither half |
| 277 | /// of it is a frame await. | ||
| 270 | const PeerDrainer = struct { | 278 | const PeerDrainer = struct { |
| 271 | fd: std.posix.fd_t, | 279 | fd: std.posix.fd_t, |
| 272 | alloc: std.mem.Allocator, | 280 | alloc: std.mem.Allocator, |