670a80f5
test: the last counted waits — modes, await, deliver
a73x 2026-08-31 21:58
Commit message
src/server/server_test_await.zig
| Old | New | ||
|---|---|---|---|
| @@ -83,6 +83,26 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" { | |||
| 83 | // pty → engine → tracker → wire, and no part of it is stubbed. | 83 | // pty → engine → tracker → wire, and no part of it is stubbed. |
| 84 | // --------------------------------------------------------------------------- | 84 | // --------------------------------------------------------------------------- |
| 85 | 85 | ||
| 86 | /// A session's command phase, as a `pumpUntil` context. | ||
| 87 | const Phase = struct { | ||
| 88 | srv: *Server, | ||
| 89 | si: usize, | ||
| 90 | want: proto.CmdPhase, | ||
| 91 | fn reached(self: Phase) bool { | ||
| 92 | return self.srv.sessions.table[self.si].?.cmd.phase == self.want; | ||
| 93 | } | ||
| 94 | }; | ||
| 95 | |||
| 96 | /// A session by name, for the waits that open with "the daemon has seated | ||
| 97 | /// this one". | ||
| 98 | const Named = struct { | ||
| 99 | srv: *Server, | ||
| 100 | name: []const u8, | ||
| 101 | fn exists(self: Named) bool { | ||
| 102 | return self.srv.sessions.find(self.name) != null; | ||
| 103 | } | ||
| 104 | }; | ||
| 105 | |||
| 86 | /// The absence half of the phantom-mark assertions: run nothing, claim | 106 | /// The absence half of the phantom-mark assertions: run nothing, claim |
| 87 | /// nothing. | 107 | /// nothing. |
| 88 | fn anyReturnWithin( | 108 | fn anyReturnWithin( |
| @@ -570,11 +590,7 @@ test "Server: a return is still answerable once the next command is running" { | |||
| 570 | // until the daemon has actually seen it open. | 590 | // until the daemon has actually seen it open. |
| 571 | try proto.writeFrame(c.handle, .input, "go\n"); | 591 | try proto.writeFrame(c.handle, .input, "go\n"); |
| 572 | try proto.writeFrame(c.handle, .input, "go\n"); | 592 | try proto.writeFrame(c.handle, .input, "go\n"); |
| 573 | var spun: usize = 0; | 593 | try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Phase{ .srv = &td.srv, .si = 0, .want = .running }, Phase.reached)); |
| 574 | while (spun < 500 and td.srv.sessions.table[0].?.cmd.phase != .running) : (spun += 1) { | ||
| 575 | try td.srv.pumpOnce(5); | ||
| 576 | } | ||
| 577 | try std.testing.expectEqual(proto.CmdPhase.running, td.srv.sessions.table[0].?.cmd.phase); | ||
| 578 | 594 | ||
| 579 | // Now ask about everything since the beginning of time. The honest answer | 595 | // Now ask about everything since the beginning of time. The honest answer |
| 580 | // is the FIRST command's return — the client asked what had returned | 596 | // is the FIRST command's return — the client asked what had returned |
| @@ -780,8 +796,7 @@ test "Server: re-attaching to another session drops the await it left behind" { | |||
| 780 | 796 | ||
| 781 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "a"); | 797 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "a"); |
| 782 | defer c.close(); | 798 | defer c.close(); |
| 783 | var spun: usize = 0; | 799 | try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Named{ .srv = &td.srv, .name = "a" }, Named.exists)); |
| 784 | while (spun < 200 and td.srv.sessions.find("a") == null) : (spun += 1) try td.srv.pumpOnce(5); | ||
| 785 | const si_a = td.srv.sessions.find("a") orelse return error.NoSessionA; | 800 | const si_a = td.srv.sessions.find("a") orelse return error.NoSessionA; |
| 786 | 801 | ||
| 787 | // An await is a question about ONE session's seq series: since_seq is a | 802 | // An await is a question about ONE session's seq series: since_seq is a |
| @@ -791,19 +806,20 @@ test "Server: re-attaching to another session drops the await it left behind" { | |||
| 791 | .settle_ms = 0, | 806 | .settle_ms = 0, |
| 792 | .timeout_ms = 60_000, | 807 | .timeout_ms = 60_000, |
| 793 | })); | 808 | })); |
| 794 | spun = 0; | 809 | const Awaiting = struct { |
| 795 | while (spun < 200) : (spun += 1) { | 810 | srv: *Server, |
| 796 | try td.srv.pumpOnce(5); | 811 | si: usize, |
| 797 | if (slotAwaiting(&td.srv, si_a)) break; | 812 | fn yes(self: @This()) bool { |
| 798 | } | 813 | return slotAwaiting(self.srv, self.si); |
| 799 | try std.testing.expect(slotAwaiting(&td.srv, si_a)); | 814 | } |
| 815 | }; | ||
| 816 | try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Awaiting{ .srv = &td.srv, .si = si_a }, Awaiting.yes)); | ||
| 800 | 817 | ||
| 801 | // Re-attach the SAME connection to a different session. Carrying the | 818 | // Re-attach the SAME connection to a different session. Carrying the |
| 802 | // watermark across would compare session a's seq against session b's | 819 | // watermark across would compare session a's seq against session b's |
| 803 | // last_return — an await that answers instantly or never, arbitrarily. | 820 | // last_return — an await that answers instantly or never, arbitrarily. |
| 804 | try attachNamed(c.handle, 80, 24, "b"); | 821 | try attachNamed(c.handle, 80, 24, "b"); |
| 805 | spun = 0; | 822 | try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Named{ .srv = &td.srv, .name = "b" }, Named.exists)); |
| 806 | while (spun < 200 and td.srv.sessions.find("b") == null) : (spun += 1) try td.srv.pumpOnce(5); | ||
| 807 | const si_b = td.srv.sessions.find("b") orelse return error.NoSessionB; | 823 | const si_b = td.srv.sessions.find("b") orelse return error.NoSessionB; |
| 808 | try std.testing.expect(si_a != si_b); | 824 | try std.testing.expect(si_a != si_b); |
| 809 | 825 | ||
| @@ -929,11 +945,14 @@ test "Server: an await resolves against the awaiting client's session" { | |||
| 929 | // b's shell returns a command, and the daemon has demonstrably seen it. | 945 | // b's shell returns a command, and the daemon has demonstrably seen it. |
| 930 | const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing; | 946 | const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing; |
| 931 | try proto.writeFrame(b.handle, .input, "go\n"); | 947 | try proto.writeFrame(b.handle, .input, "go\n"); |
| 932 | var spun: usize = 0; | 948 | const Returned = struct { |
| 933 | while (spun < 500 and td.srv.sessions.table[si_b].?.last_return == null) : (spun += 1) { | 949 | srv: *Server, |
| 934 | try td.srv.pumpOnce(5); | 950 | si: usize, |
| 935 | } | 951 | fn yes(self: @This()) bool { |
| 936 | try std.testing.expect(td.srv.sessions.table[si_b].?.last_return != null); | 952 | return self.srv.sessions.table[self.si].?.last_return != null; |
| 953 | } | ||
| 954 | }; | ||
| 955 | try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Returned{ .srv = &td.srv, .si = si_b }, Returned.yes)); | ||
| 937 | 956 | ||
| 938 | // That return is another session's and must not resolve a's await. | 957 | // That return is another session's and must not resolve a's await. |
| 939 | if (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 40)) |early| { | 958 | if (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 40)) |early| { |
src/server/server_test_deliver.zig
| Old | New | ||
|---|---|---|---|
| @@ -36,6 +36,12 @@ test "Server: a stalled client does not block delivery to others" { | |||
| 36 | // Snapshot everyone until A backs up. A blocking write into A's full | 36 | // Snapshot everyone until A backs up. A blocking write into A's full |
| 37 | // buffer would hang here rather than fail, so reaching the assertions | 37 | // buffer would hang here rather than fail, so reaching the assertions |
| 38 | // at all is itself the "never stalls" half of this test. | 38 | // at all is itself the "never stalls" half of this test. |
| 39 | // | ||
| 40 | // The five counted loops in this file are all this shape and none is a | ||
| 41 | // `pumpUntil`: each round DOES work — it feeds the engine and | ||
| 42 | // resnapshots — so the loop applies load until the daemon reacts rather | ||
| 43 | // than waiting for something already in flight. The cap loop below goes | ||
| 44 | // further and asserts on the count itself. | ||
| 39 | var rounds: usize = 0; | 45 | var rounds: usize = 0; |
| 40 | while (rounds < 64) : (rounds += 1) { | 46 | while (rounds < 64) : (rounds += 1) { |
| 41 | if (td.srv.clients[0]) |slot| { | 47 | if (td.srv.clients[0]) |slot| { |
| @@ -93,7 +99,8 @@ test "Server: a client exceeding the pending cap is dropped" { | |||
| 93 | } | 99 | } |
| 94 | 100 | ||
| 95 | // Past the cap the daemon drops the client rather than buffering for it | 101 | // Past the cap the daemon drops the client rather than buffering for it |
| 96 | // without bound. | 102 | // without bound. `rounds` is read after the loop, so the count is part |
| 103 | // of the verdict here and not a budget. | ||
| 97 | try std.testing.expect(td.srv.clients[0] == null); | 104 | try std.testing.expect(td.srv.clients[0] == null); |
| 98 | // The cap is what ended it, not the loop bound: a four-frame cap behind | 105 | // The cap is what ended it, not the loop bound: a four-frame cap behind |
| 99 | // a socket buffer of about one frame cannot absorb many more than five. | 106 | // a socket buffer of about one frame cannot absorb many more than five. |
src/server/server_test_modes.zig
| Old | New | ||
|---|---|---|---|
| @@ -17,6 +17,41 @@ fn pumpAndCollectModes( | |||
| 17 | try drainModes(alloc, fd, out); | 17 | try drainModes(alloc, fd, out); |
| 18 | } | 18 | } |
| 19 | 19 | ||
| 20 | /// Pump until `out` holds `want` mode frames, keeping every one of them in | ||
| 21 | /// order. A sink rather than a `pumpUntil`, because the condition is | ||
| 22 | /// counted out of the frames themselves: the predicate would have to read | ||
| 23 | /// the socket to answer, and a predicate with a side effect is a worse | ||
| 24 | /// thing to read than this. | ||
| 25 | fn collectModes( | ||
| 26 | alloc: std.mem.Allocator, | ||
| 27 | srv: *Server, | ||
| 28 | fd: std.posix.fd_t, | ||
| 29 | out: *std.ArrayList(proto.PtyModeFlags), | ||
| 30 | want: usize, | ||
| 31 | iters: usize, | ||
| 32 | ) !bool { | ||
| 33 | const Collect = struct { | ||
| 34 | alloc: std.mem.Allocator, | ||
| 35 | out: *std.ArrayList(proto.PtyModeFlags), | ||
| 36 | want: usize, | ||
| 37 | fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void { | ||
| 38 | if (frame.type != .pty_mode) return; | ||
| 39 | const self: *@This() = @ptrCast(@alignCast(ctx.?)); | ||
| 40 | try self.out.append(self.alloc, try proto.decodePtyMode(frame.payload)); | ||
| 41 | if (self.out.items.len >= self.want) return error.ModesCollected; | ||
| 42 | } | ||
| 43 | }; | ||
| 44 | var collect: Collect = .{ .alloc = alloc, .out = out, .want = want }; | ||
| 45 | _ = h.awaitFrameSink(alloc, srv, fd, h.never_from_daemon, iters, .{ | ||
| 46 | .ctx = &collect, | ||
| 47 | .on = Collect.on, | ||
| 48 | }) catch |err| switch (err) { | ||
| 49 | error.ModesCollected => return true, | ||
| 50 | else => return err, | ||
| 51 | }; | ||
| 52 | return false; | ||
| 53 | } | ||
| 54 | |||
| 20 | /// No pump: for paths that answer synchronously, where pumping would blur what | 55 | /// No pump: for paths that answer synchronously, where pumping would blur what |
| 21 | /// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it | 56 | /// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it |
| 22 | /// does not WAIT at all — it takes what is already readable and returns. | 57 | /// does not WAIT at all — it takes what is already readable and returns. |
| @@ -120,10 +155,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when | |||
| 120 | // Attaching is enough on its own: nothing about the session has changed, | 155 | // Attaching is enough on its own: nothing about the session has changed, |
| 121 | // so a client that only heard about changes would never be told what the | 156 | // so a client that only heard about changes would never be told what the |
| 122 | // terminal is doing. | 157 | // terminal is doing. |
| 123 | var rounds: usize = 0; | 158 | try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 1, 250)); |
| 124 | while (rounds < 250 and modes.items.len == 0) : (rounds += 1) { | ||
| 125 | try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); | ||
| 126 | } | ||
| 127 | try std.testing.expectEqual(@as(usize, 1), modes.items.len); | 159 | try std.testing.expectEqual(@as(usize, 1), modes.items.len); |
| 128 | try std.testing.expect(modes.items[0].icanon); | 160 | try std.testing.expect(modes.items[0].icanon); |
| 129 | try std.testing.expect(modes.items[0].echo); | 161 | try std.testing.expect(modes.items[0].echo); |
| @@ -133,10 +165,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when | |||
| 133 | // poll cannot be folded into the "the pty printed something" arm. | 165 | // poll cannot be folded into the "the pty printed something" arm. |
| 134 | try proto.writeFrame(c.handle, .input, "go\n"); | 166 | try proto.writeFrame(c.handle, .input, "go\n"); |
| 135 | 167 | ||
| 136 | rounds = 0; | 168 | try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 2, 500)); |
| 137 | while (rounds < 500 and modes.items.len < 2) : (rounds += 1) { | ||
| 138 | try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); | ||
| 139 | } | ||
| 140 | try std.testing.expectEqual(@as(usize, 2), modes.items.len); | 169 | try std.testing.expectEqual(@as(usize, 2), modes.items.len); |
| 141 | try std.testing.expect(!modes.items[1].echo); | 170 | try std.testing.expect(!modes.items[1].echo); |
| 142 | // -echo alone: the line discipline is still canonical, and reporting | 171 | // -echo alone: the line discipline is still canonical, and reporting |
| @@ -149,18 +178,17 @@ test "Server: the pty's mode bits reach a client on attach, and again only when | |||
| 149 | // the mode only after reading pty output sits here forever. | 178 | // the mode only after reading pty output sits here forever. |
| 150 | try proto.writeFrame(c.handle, .input, "silent\n"); | 179 | try proto.writeFrame(c.handle, .input, "silent\n"); |
| 151 | 180 | ||
| 152 | rounds = 0; | 181 | try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 3, 500)); |
| 153 | while (rounds < 500 and modes.items.len < 3) : (rounds += 1) { | ||
| 154 | try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); | ||
| 155 | } | ||
| 156 | try std.testing.expectEqual(@as(usize, 3), modes.items.len); | 182 | try std.testing.expectEqual(@as(usize, 3), modes.items.len); |
| 157 | try std.testing.expect(!modes.items[2].icanon); | 183 | try std.testing.expect(!modes.items[2].icanon); |
| 158 | try std.testing.expect(!modes.items[2].echo); | 184 | try std.testing.expect(!modes.items[2].echo); |
| 159 | 185 | ||
| 160 | // And then nothing: the pty is in a blocking read with its mode unchanged, | 186 | // And then nothing: the pty is in a blocking read with its mode unchanged, |
| 161 | // so a daemon that re-sent what it already said shows up as a fourth frame | 187 | // so a daemon that re-sent what it already said shows up as a fourth frame |
| 162 | // on the very NEXT pump. 25 rounds, because round one catches it. | 188 | // on the very NEXT pump. 25 rounds, because round one catches it — a |
| 163 | rounds = 0; | 189 | // fixed count on purpose, since the verdict is that a FOURTH frame never |
| 190 | // came and there is no arrival to wait for. | ||
| 191 | var rounds: usize = 0; | ||
| 164 | while (rounds < 25) : (rounds += 1) { | 192 | while (rounds < 25) : (rounds += 1) { |
| 165 | try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); | 193 | try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); |
| 166 | } | 194 | } |