dcc68464
test: server_test_agent rides pumpUntil
a73x 2026-08-31 21:58
Commit message
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` |