a73x

dcc68464

test: server_test_agent rides pumpUntil

a73x   2026-08-31 21:58

Commit message
test: server_test_agent rides pumpUntil

Every `while (spun < N …) pumpOnce` in this file asserted nothing: the
loop ended either because the daemon got there or because 200 rounds ran
out, and the `expect` after it could not tell those apart until it read
the state again. `pumpUntil` makes the condition the assertion, so a
failure names what did not happen and points at the predicate.

The conditions get names because they are the file's subject matter:
`ClientSlot.seated`/`.offering` (in the harness — half the sibling files
open with them), `Lead.taken` for the latest-active rule, `Refusals`,
`Wrote`, `Gone`. `Lead.taken` insists on a STRICT lead, which is what
the old loop's separate `expect` insisted on; waiting for it rather than
asserting it right after the loop also closes the race where the input
had not landed yet.

`pumpUntilReadable` takes milliseconds now instead of a round count —
its callers were passing 5 ms rounds wearing no unit, and its poll is
the one left in this file because it waits on raw agent-socket bytes,
not on a frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWxBL1HBULH1ZwTNzzKTja

src/server/server_test_agent.zig
Old New
@@ -8,6 +8,8 @@ const srv_mod = @import("server.zig");
8 const Server = srv_mod.Server; 8 const Server = srv_mod.Server;
9 const max_agent_chans = srv_mod.max_agent_chans; 9 const max_agent_chans = srv_mod.max_agent_chans;
10 const attachNamed = h.attachNamed; 10 const attachNamed = h.attachNamed;
11 const ClientSlot = h.ClientSlot;
12 const pumpUntil = h.pumpUntil;
11 const awaitFrame = h.awaitFrame; 13 const awaitFrame = h.awaitFrame;
12 const awaitGridText = h.awaitGridText; 14 const awaitGridText = h.awaitGridText;
13 const findFrame = h.findFrame; 15 const findFrame = h.findFrame;
@@ -95,17 +97,14 @@ test "Server: agent_offer flags the slot, and an unknown type leaves the client
95 97
96 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, ""); 98 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
97 defer c.close(); 99 defer c.close();
98 var spun: usize = 0; 100 const slot: ClientSlot = .{ .srv = &td.srv, .n = 0 };
99 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5); 101 try std.testing.expect(try pumpUntil(&td.srv, 2000, slot, ClientSlot.seated));
100 try std.testing.expect(td.srv.clients[0] != null);
101 // The offer is opt-in, so the flag must start false or every client 102 // The offer is opt-in, so the flag must start false or every client
102 // would look like it had offered. 103 // would look like it had offered.
103 try std.testing.expect(!td.srv.clients[0].?.agent_offer); 104 try std.testing.expect(!slot.offering());
104 105
105 try proto.writeFrame(c.handle, .agent_offer, ""); 106 try proto.writeFrame(c.handle, .agent_offer, "");
106 spun = 0; 107 try std.testing.expect(try pumpUntil(&td.srv, 2000, slot, ClientSlot.offering));
107 while (spun < 200 and !td.srv.clients[0].?.agent_offer) : (spun += 1) try td.srv.pumpOnce(5);
108 try std.testing.expect(td.srv.clients[0].?.agent_offer);
109 108
110 // 0x7e is unmapped in MsgType. Skipping it rather than dropping the 109 // 0x7e is unmapped in MsgType. Skipping it rather than dropping the
111 // client is what lets a client offering agent forwarding talk to a 110 // client is what lets a client offering agent forwarding talk to a
@@ -130,33 +129,47 @@ test "Server: agent_offer flags the slot, and an unknown type leaves the client
130 /// Test helper: pump until `fd` has something to say, then take it once. 129 /// Test helper: pump until `fd` has something to say, then take it once.
131 /// Null means it never spoke inside the budget, which the absence probes 130 /// Null means it never spoke inside the budget, which the absence probes
132 /// below read as "and never would have"; 0 is EOF, an answer in its own 131 /// below read as "and never would have"; 0 is EOF, an answer in its own
133 /// right. 132 /// right. The budget is milliseconds of pumping, not a round count — the
134 fn pumpUntilReadable(srv: *Server, fd: std.posix.fd_t, buf: []u8, iters: usize) !?usize { 133 /// counts it replaced were 5 ms rounds wearing no unit at all.
135 var i: usize = 0; 134 fn pumpUntilReadable(srv: *Server, fd: std.posix.fd_t, buf: []u8, budget_ms: u64) !?usize {
136 while (i < iters) : (i += 1) { 135 const Readable = struct {
137 try srv.pumpOnce(5); 136 fd: std.posix.fd_t,
138 var pfd = [_]std.posix.pollfd{ 137 fn yes(self: @This()) bool {
139 .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }, 138 var pfd = [_]std.posix.pollfd{
140 }; 139 .{ .fd = self.fd, .events = std.posix.POLL.IN, .revents = 0 },
141 if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue; 140 };
142 return std.posix.read(fd, buf) catch 0; 141 return (std.posix.poll(&pfd, 0) catch 0) != 0;
143 } 142 }
144 return null; 143 };
144 if (!try pumpUntil(srv, budget_ms, Readable{ .fd = fd }, Readable.yes)) return null;
145 return std.posix.read(fd, buf) catch 0;
145 } 146 }
146 147
148 /// Which of two client slots is the latest-active one. STRICTLY ahead, not
149 /// merely tied: the rule under test is "the most recently active client
150 /// answers", and equal activity would let a wait finish before the lead had
151 /// actually changed hands.
152 const Lead = struct {
153 srv: *Server,
154 ahead: usize,
155 behind: usize,
156 fn taken(self: Lead) bool {
157 const a = self.srv.clients[self.ahead] orelse return false;
158 const b = self.srv.clients[self.behind] orelse return false;
159 return a.activity > b.activity;
160 }
161 };
162
147 /// Test helper: seat a client in `slot` and have it volunteer an agent, 163 /// Test helper: seat a client in `slot` and have it volunteer an agent,
148 /// pumping until the daemon holds both facts. Attaches one at a time by 164 /// pumping until the daemon holds both facts. Attaches one at a time by
149 /// contract — freeClientSlot hands out the lowest free slot, so the caller's 165 /// contract — freeClientSlot hands out the lowest free slot, so the caller's
150 /// slot number is only the attach order if nobody attaches concurrently. 166 /// slot number is only the attach order if nobody attaches concurrently.
151 fn attachOffering(srv: *Server, fd: std.posix.fd_t, slot: usize, name: []const u8) !void { 167 fn attachOffering(srv: *Server, fd: std.posix.fd_t, slot: usize, name: []const u8) !void {
152 try attachNamed(fd, 80, 24, name); 168 try attachNamed(fd, 80, 24, name);
153 var spun: usize = 0; 169 const s: ClientSlot = .{ .srv = srv, .n = slot };
154 while (spun < 200 and srv.clients[slot] == null) : (spun += 1) try srv.pumpOnce(5); 170 if (!try pumpUntil(srv, 2000, s, ClientSlot.seated)) return error.ClientNeverSeated;
155 if (srv.clients[slot] == null) return error.ClientNeverSeated;
156 try proto.writeFrame(fd, .agent_offer, ""); 171 try proto.writeFrame(fd, .agent_offer, "");
157 spun = 0; 172 if (!try pumpUntil(srv, 2000, s, ClientSlot.offering)) return error.OfferNeverLanded;
158 while (spun < 200 and !srv.clients[slot].?.agent_offer) : (spun += 1) try srv.pumpOnce(5);
159 if (!srv.clients[slot].?.agent_offer) return error.OfferNeverLanded;
160 } 173 }
161 174
162 test "Server: a session with no agent socket does not inherit the daemon's" { 175 test "Server: a session with no agent socket does not inherit the daemon's" {
@@ -206,18 +219,21 @@ test "Server: a session with no agent socket does not inherit the daemon's" {
206 219
207 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nosock"); 220 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nosock");
208 defer c.close(); 221 defer c.close();
209 var spun: usize = 0; 222 if (!try pumpUntil(&td.srv, 2000, ClientSlot{ .srv = &td.srv, .n = 0 }, ClientSlot.seated))
210 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5); 223 return error.ClientNeverSeated;
211 const si = td.srv.clients[0].?.session orelse return error.ClientNeverSeated; 224 const si = td.srv.clients[0].?.session orelse return error.ClientNeverSeated;
212 try std.testing.expect(td.srv.sessions.table[si].?.agentPath() == null); 225 try std.testing.expect(td.srv.sessions.table[si].?.agentPath() == null);
213 226
214 try proto.writeFrame(c.handle, .input, cmd); 227 try proto.writeFrame(c.handle, .input, cmd);
215 spun = 0; 228 const Wrote = struct {
216 while (spun < 600) : (spun += 1) { 229 path: []const u8,
217 try td.srv.pumpOnce(5); 230 fn yes(self: @This()) bool {
218 std.fs.accessAbsolute(done, .{}) catch continue; 231 std.fs.accessAbsolute(self.path, .{}) catch return false;
219 break; 232 return true;
220 } 233 }
234 };
235 if (!try pumpUntil(&td.srv, 3000, Wrote{ .path = done }, Wrote.yes))
236 return error.ShellNeverAnswered;
221 const got = std.fs.cwd().readFileAlloc(alloc, probe, 4096) catch 237 const got = std.fs.cwd().readFileAlloc(alloc, probe, 4096) catch
222 return error.ShellNeverAnswered; 238 return error.ShellNeverAnswered;
223 defer alloc.free(got); 239 defer alloc.free(got);
@@ -245,8 +261,14 @@ test "Server: a full channel table refuses the newest dial and says so once" {
245 for (&dials) |*d| d.* = try dial.dial(path); 261 for (&dials) |*d| d.* = try dial.dial(path);
246 defer for (dials) |d| d.close(); 262 defer for (dials) |d| d.close();
247 263
248 var spun: usize = 0; 264 const Refusals = struct {
249 while (spun < 400 and td.srv.agents.refused_full == 0) : (spun += 1) try td.srv.pumpOnce(5); 265 srv: *Server,
266 want: u32,
267 fn reached(self: @This()) bool {
268 return self.srv.agents.refused_full >= self.want;
269 }
270 };
271 try std.testing.expect(try pumpUntil(&td.srv, 2000, Refusals{ .srv = &td.srv, .want = 1 }, Refusals.reached));
250 for (td.srv.agents.chans) |slot| try std.testing.expect(slot != null); 272 for (td.srv.agents.chans) |slot| try std.testing.expect(slot != null);
251 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_full); 273 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_full);
252 try std.testing.expectEqual(@as(u32, 0), td.srv.agents.refused_no_offer); 274 try std.testing.expectEqual(@as(u32, 0), td.srv.agents.refused_no_offer);
@@ -255,7 +277,7 @@ test "Server: a full channel table refuses the newest dial and says so once" {
255 // hangup as "agent refused" and moves on, where silence costs it a 277 // hangup as "agent refused" and moves on, where silence costs it a
256 // timeout on every dial. 278 // timeout on every dial.
257 var buf: [8]u8 = undefined; 279 var buf: [8]u8 = undefined;
258 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, dials[max_agent_chans].handle, &buf, 60)); 280 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, dials[max_agent_chans].handle, &buf, 300));
259 281
260 // Ids are unique across the live table, which is the only property both 282 // Ids are unique across the live table, which is the only property both
261 // ends rely on — and the counter's wrap must not break it. 283 // ends rely on — and the counter's wrap must not break it.
@@ -272,8 +294,7 @@ test "Server: a full channel table refuses the newest dial and says so once" {
272 try std.testing.expect(td.srv.agents.full_said); 294 try std.testing.expect(td.srv.agents.full_said);
273 const again = try dial.dial(path); 295 const again = try dial.dial(path);
274 defer again.close(); 296 defer again.close();
275 spun = 0; 297 try std.testing.expect(try pumpUntil(&td.srv, 2000, Refusals{ .srv = &td.srv, .want = 2 }, Refusals.reached));
276 while (spun < 400 and td.srv.agents.refused_full < 2) : (spun += 1) try td.srv.pumpOnce(5);
277 try std.testing.expectEqual(@as(u32, 2), td.srv.agents.refused_full); 298 try std.testing.expectEqual(@as(u32, 2), td.srv.agents.refused_full);
278 try std.testing.expect(td.srv.agents.full_said); 299 try std.testing.expect(td.srv.agents.full_said);
279 td.srv.agents.closeChan(&td.srv, 0, .notify); 300 td.srv.agents.closeChan(&td.srv, 0, .notify);
@@ -288,12 +309,11 @@ test "Server: an agent connection with nobody offering is refused fast" {
288 309
289 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, ""); 310 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
290 defer c.close(); 311 defer c.close();
291 var spun: usize = 0; 312 const slot0: ClientSlot = .{ .srv = &td.srv, .n = 0 };
292 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5); 313 try std.testing.expect(try pumpUntil(&td.srv, 2000, slot0, ClientSlot.seated));
293 try std.testing.expect(td.srv.clients[0] != null);
294 // Attached but never offered — the case this test is about. The socket 314 // Attached but never offered — the case this test is about. The socket
295 // exists for as long as the session does; only the answer comes and goes. 315 // exists for as long as the session does; only the answer comes and goes.
296 try std.testing.expect(!td.srv.clients[0].?.agent_offer); 316 try std.testing.expect(!slot0.offering());
297 317
298 // An offerer, but watching a DIFFERENT session. A socket per session 318 // An offerer, but watching a DIFFERENT session. A socket per session
299 // exists so a dial can be attributed to one shell; an answerer taken 319 // exists so a dial can be attributed to one shell; an answerer taken
@@ -313,7 +333,7 @@ test "Server: an agent connection with nobody offering is refused fast" {
313 // where a connection left open and silent would make it wait out a 333 // where a connection left open and silent would make it wait out a
314 // timeout on every dial instead. 334 // timeout on every dial instead.
315 var buf: [16]u8 = undefined; 335 var buf: [16]u8 = undefined;
316 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200); 336 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000);
317 try std.testing.expectEqual(@as(?usize, 0), n); 337 try std.testing.expectEqual(@as(?usize, 0), n);
318 } 338 }
319 339
@@ -347,7 +367,7 @@ test "Server: agent bytes pump both ways through a channel" {
347 @memcpy(req[proto.agent_id_len..], "req"); 367 @memcpy(req[proto.agent_id_len..], "req");
348 try proto.writeFrame(c.handle, .agent_data, &req); 368 try proto.writeFrame(c.handle, .agent_data, &req);
349 var buf: [64]u8 = undefined; 369 var buf: [64]u8 = undefined;
350 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse 370 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000)) orelse
351 return error.NoAgentRequest; 371 return error.NoAgentRequest;
352 try std.testing.expectEqualSlices(u8, "req", buf[0..n]); 372 try std.testing.expectEqualSlices(u8, "req", buf[0..n]);
353 373
@@ -390,11 +410,7 @@ test "Server: an agent connection is routed to the latest-active offerer" {
390 // each answer a single connection identically to latest-wins. Only the 410 // each answer a single connection identically to latest-wins. Only the
391 // pair of answers disagrees. 411 // pair of answers disagrees.
392 try proto.writeFrame(cb.handle, .input, "x"); 412 try proto.writeFrame(cb.handle, .input, "x");
393 var spun: usize = 0; 413 try std.testing.expect(try pumpUntil(&td.srv, 2000, Lead{ .srv = &td.srv, .ahead = 1, .behind = 0 }, Lead.taken));
394 while (spun < 200 and td.srv.clients[1].?.activity < td.srv.clients[0].?.activity) : (spun += 1) {
395 try td.srv.pumpOnce(5);
396 }
397 try std.testing.expect(td.srv.clients[1].?.activity > td.srv.clients[0].?.activity);
398 414
399 const first = try dial.dial(path); 415 const first = try dial.dial(path);
400 defer first.close(); 416 defer first.close();
@@ -413,11 +429,7 @@ test "Server: an agent connection is routed to the latest-active offerer" {
413 // already open stays B's regardless — it is mid-exchange with an ssh 429 // already open stays B's regardless — it is mid-exchange with an ssh
414 // that would fail the signature rather than change identity. 430 // that would fail the signature rather than change identity.
415 try proto.writeFrame(ca.handle, .input, "y"); 431 try proto.writeFrame(ca.handle, .input, "y");
416 spun = 0; 432 try std.testing.expect(try pumpUntil(&td.srv, 2000, Lead{ .srv = &td.srv, .ahead = 0, .behind = 1 }, Lead.taken));
417 while (spun < 200 and td.srv.clients[0].?.activity < td.srv.clients[1].?.activity) : (spun += 1) {
418 try td.srv.pumpOnce(5);
419 }
420 try std.testing.expect(td.srv.clients[0].?.activity > td.srv.clients[1].?.activity);
421 433
422 const second = try dial.dial(path); 434 const second = try dial.dial(path);
423 defer second.close(); 435 defer second.close();
@@ -466,7 +478,7 @@ test "Server: agent_data for an unknown or another client's channel is dropped"
466 unknown[proto.agent_id_len] = 'x'; 478 unknown[proto.agent_id_len] = 'x';
467 try proto.writeFrame(cb.handle, .agent_data, &unknown); 479 try proto.writeFrame(cb.handle, .agent_data, &unknown);
468 var buf: [64]u8 = undefined; 480 var buf: [64]u8 = undefined;
469 try std.testing.expect((try pumpUntilReadable(&td.srv, agent.handle, &buf, 60)) == null); 481 try std.testing.expect((try pumpUntilReadable(&td.srv, agent.handle, &buf, 300)) == null);
470 // Dropped, not filed: an id nobody holds must not be an id anybody can 482 // Dropped, not filed: an id nobody holds must not be an id anybody can
471 // conjure a channel with. 483 // conjure a channel with.
472 try std.testing.expect(td.srv.agents.chans[1] == null); 484 try std.testing.expect(td.srv.agents.chans[1] == null);
@@ -477,7 +489,7 @@ test "Server: agent_data for an unknown or another client's channel is dropped"
477 @memcpy(mine[0..proto.agent_id_len], &proto.encodeAgentId(id)); 489 @memcpy(mine[0..proto.agent_id_len], &proto.encodeAgentId(id));
478 @memcpy(mine[proto.agent_id_len..], "ok"); 490 @memcpy(mine[proto.agent_id_len..], "ok");
479 try proto.writeFrame(cb.handle, .agent_data, &mine); 491 try proto.writeFrame(cb.handle, .agent_data, &mine);
480 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse 492 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000)) orelse
481 return error.NoAgentRequest; 493 return error.NoAgentRequest;
482 try std.testing.expectEqualSlices(u8, "ok", buf[0..n]); 494 try std.testing.expectEqualSlices(u8, "ok", buf[0..n]);
483 495
@@ -529,7 +541,7 @@ test "Server: an agent_data frame past the cap hangs the channel up" {
529 // Not one byte of it reached the agent: a truncated request is worse 541 // Not one byte of it reached the agent: a truncated request is worse
530 // than none, and EOF here is the hangup rather than an idle socket. 542 // than none, and EOF here is the hangup rather than an idle socket.
531 var buf: [64]u8 = undefined; 543 var buf: [64]u8 = undefined;
532 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 60)); 544 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 300));
533 545
534 // And the client itself survives — the frame was refused, not the peer. 546 // And the client itself survives — the frame was refused, not the peer.
535 try proto.writeFrame(c.handle, .stats_req, ""); 547 try proto.writeFrame(c.handle, .stats_req, "");
@@ -563,7 +575,7 @@ test "Server: a client closing a channel hangs up the agent connection without a
563 var buf: [16]u8 = undefined; 575 var buf: [16]u8 = undefined;
564 try std.testing.expectEqual( 576 try std.testing.expectEqual(
565 @as(?usize, 0), 577 @as(?usize, 0),
566 try pumpUntilReadable(&td.srv, agent.handle, &buf, 200), 578 try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000),
567 ); 579 );
568 try std.testing.expect(td.srv.agents.chans[0] == null); 580 try std.testing.expect(td.srv.agents.chans[0] == null);
569 581
@@ -641,7 +653,7 @@ test "Server: a client's agent channels die with the client" {
641 // EOF now, not a channel held open against a client that is gone. 653 // EOF now, not a channel held open against a client that is gone.
642 c.close(); 654 c.close();
643 var buf: [16]u8 = undefined; 655 var buf: [16]u8 = undefined;
644 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200); 656 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000);
645 try std.testing.expectEqual(@as(?usize, 0), n); 657 try std.testing.expectEqual(@as(?usize, 0), n);
646 try std.testing.expect(td.srv.clients[0] == null); 658 try std.testing.expect(td.srv.clients[0] == null);
647 try std.testing.expect(td.srv.agents.chans[0] == null); 659 try std.testing.expect(td.srv.agents.chans[0] == null);
@@ -699,7 +711,7 @@ test "Server: a QUIC client's agent channels die with the client" {
699 var abuf: [16]u8 = undefined; 711 var abuf: [16]u8 = undefined;
700 try std.testing.expectEqual( 712 try std.testing.expectEqual(
701 @as(?usize, 0), 713 @as(?usize, 0),
702 try pumpUntilReadable(&td.srv, agent.handle, &abuf, 200), 714 try pumpUntilReadable(&td.srv, agent.handle, &abuf, 1000),
703 ); 715 );
704 try std.testing.expect(td.srv.agents.chans[0] == null); 716 try std.testing.expect(td.srv.agents.chans[0] == null);
705 } 717 }
@@ -743,7 +755,7 @@ test "Server: an offerer that never answers its first request is hung up on and
743 // and closes it falls through to its next auth method in 2ms. So the 755 // and closes it falls through to its next auth method in 2ms. So the
744 // daemon's answer to silence is the close the client should have sent. 756 // daemon's answer to silence is the close the client should have sent.
745 var buf: [16]u8 = undefined; 757 var buf: [16]u8 = undefined;
746 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200); 758 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000);
747 try std.testing.expectEqual(@as(?usize, 0), n); 759 try std.testing.expectEqual(@as(?usize, 0), n);
748 try std.testing.expect(td.srv.agents.chans[0] == null); 760 try std.testing.expect(td.srv.agents.chans[0] == null);
749 // The client is told, like any other far-end close it did not ask for. 761 // The client is told, like any other far-end close it did not ask for.
@@ -756,7 +768,7 @@ test "Server: an offerer that never answers its first request is hung up on and
756 try std.testing.expect(!td.srv.clients[0].?.agent_offer); 768 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
757 const again = try dial.dial(path); 769 const again = try dial.dial(path);
758 defer again.close(); 770 defer again.close();
759 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, again.handle, &buf, 60)); 771 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, again.handle, &buf, 300));
760 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_no_offer); 772 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_no_offer);
761 } 773 }
762 774
@@ -782,7 +794,7 @@ test "Server: a channel that has answered once is never timed out" {
782 @memcpy(resp[proto.agent_id_len..], "resp"); 794 @memcpy(resp[proto.agent_id_len..], "resp");
783 try proto.writeFrame(c.handle, .agent_data, &resp); 795 try proto.writeFrame(c.handle, .agent_data, &resp);
784 var buf: [64]u8 = undefined; 796 var buf: [64]u8 = undefined;
785 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse return error.NoReply; 797 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000)) orelse return error.NoReply;
786 798
787 // A second request the client takes its time over — a SIGN against a 799 // A second request the client takes its time over — a SIGN against a
788 // token waiting for a touch. The bound is for a peer that cannot 800 // token waiting for a touch. The bound is for a peer that cannot
@@ -793,7 +805,7 @@ test "Server: a channel that has answered once is never timed out" {
793 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse 805 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse
794 return error.RequestNeverForwarded; 806 return error.RequestNeverForwarded;
795 fwd.deinit(alloc); 807 fwd.deinit(alloc);
796 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 100)); 808 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 500));
797 try std.testing.expect(td.srv.agents.chans[0] != null); 809 try std.testing.expect(td.srv.agents.chans[0] != null);
798 try std.testing.expect(td.srv.clients[0].?.agent_offer); 810 try std.testing.expect(td.srv.clients[0].?.agent_offer);
799 } 811 }
@@ -825,14 +837,14 @@ test "Server: bytes a client sends before it was asked prove nothing" {
825 @memcpy(eager[proto.agent_id_len..], "hi"); 837 @memcpy(eager[proto.agent_id_len..], "hi");
826 try proto.writeFrame(c.handle, .agent_data, &eager); 838 try proto.writeFrame(c.handle, .agent_data, &eager);
827 var buf: [64]u8 = undefined; 839 var buf: [64]u8 = undefined;
828 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse return error.NotForwarded; 840 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000)) orelse return error.NotForwarded;
829 841
830 // Then ssh asks and the client goes quiet: the clock must still run. 842 // Then ssh asks and the client goes quiet: the clock must still run.
831 try proto.writeAllFd(agent.handle, "req"); 843 try proto.writeAllFd(agent.handle, "req");
832 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse 844 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse
833 return error.RequestNeverForwarded; 845 return error.RequestNeverForwarded;
834 fwd.deinit(alloc); 846 fwd.deinit(alloc);
835 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)); 847 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 1000));
836 try std.testing.expect(!td.srv.clients[0].?.agent_offer); 848 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
837 } 849 }
838 850
@@ -858,7 +870,7 @@ test "Server: a channel nobody has asked anything on is not timed out" {
858 // has been handed a request, so a clock anchored on the open would 870 // has been handed a request, so a clock anchored on the open would
859 // hang up on a working client for the peer's pause. 871 // hang up on a working client for the peer's pause.
860 var buf: [16]u8 = undefined; 872 var buf: [16]u8 = undefined;
861 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 100)); 873 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 500));
862 try std.testing.expect(td.srv.agents.chans[0] != null); 874 try std.testing.expect(td.srv.agents.chans[0] != null);
863 try std.testing.expect(td.srv.clients[0].?.agent_offer); 875 try std.testing.expect(td.srv.clients[0].?.agent_offer);
864 } 876 }
@@ -905,12 +917,13 @@ test "Server: ending one session unlinks ITS agent socket and leaves every other
905 try std.testing.expect((proto.parseEndReply(r.payload) orelse 917 try std.testing.expect((proto.parseEndReply(r.payload) orelse
906 return error.BadEndReply).accepted); 918 return error.BadEndReply).accepted);
907 919
908 var waited: u32 = 0; 920 const Gone = struct {
909 while (waited < 3000) : (waited += 50) { 921 path: [:0]const u8,
910 try td.srv.pumpOnce(20); 922 fn yes(self: @This()) bool {
911 if (!isSocketAt(dp)) break; 923 return !isSocketAt(self.path);
912 std.Thread.sleep(30 * std.time.ns_per_ms); 924 }
913 } 925 };
926 try std.testing.expect(try pumpUntil(&td.srv, 3000, Gone{ .path = dp }, Gone.yes));
914 927
915 // Asked of the filesystem, not of the daemon: a table that has forgotten 928 // Asked of the filesystem, not of the daemon: a table that has forgotten
916 // a session says nothing about whether its name left the directory, and 929 // a session says nothing about whether its name left the directory, and
src/server/server_test_harness.zig
Old New
@@ -45,6 +45,26 @@ pub fn pumpUntil(
45 } 45 }
46 } 46 }
47 47
48 /// One client slot on a daemon, as a `pumpUntil` context. Here rather than
49 /// in a test file because "the client is seated" and "the client has
50 /// offered an agent" are the two conditions most of the sibling files open
51 /// with, and a predicate copied per file is a predicate that drifts per file.
52 pub const ClientSlot = struct {
53 srv: *Server,
54 n: usize,
55
56 pub fn seated(s: ClientSlot) bool {
57 return s.srv.clients[s.n] != null;
58 }
59
60 /// False rather than a panic on an empty slot: a wait for the offer can
61 /// start before the accept lands, and "not yet" is the honest answer.
62 pub fn offering(s: ClientSlot) bool {
63 const c = s.srv.clients[s.n] orelse return false;
64 return c.agent_offer;
65 }
66 };
67
48 /// A daemon on a socket of its own, which is what nearly every test in this 68 /// A daemon on a socket of its own, which is what nearly every test in this
49 /// folder opens with: a short-path temp directory (`testtmp`, because a unix 69 /// folder opens with: a short-path temp directory (`testtmp`, because a unix
50 /// socket path caps at 108 bytes), a socket named inside it, and a `Server` 70 /// socket path caps at 108 bytes), a socket named inside it, and a `Server`