f1e1cc8f
test: the QUIC absence checks wait long enough to be a claim
a73x 2026-09-03 20:08
Commit message
src/server/quic_server.zig
| Old | New | ||
|---|---|---|---|
| @@ -1391,17 +1391,25 @@ test "Listener: a packet addressed to any advertised CID reaches its connection" | |||
| 1391 | var probe_len: std.posix.socklen_t = @sizeOf(@TypeOf(probe_addr)); | 1391 | var probe_len: std.posix.socklen_t = @sizeOf(@TypeOf(probe_addr)); |
| 1392 | try std.posix.getsockname(probe, @ptrCast(&probe_addr), &probe_len); | 1392 | try std.posix.getsockname(probe, @ptrCast(&probe_addr), &probe_len); |
| 1393 | 1393 | ||
| 1394 | // The baseline: nothing has been routed, so nothing may arrive. It waits | ||
| 1395 | // the same budget the real absence check does, so the two are the same | ||
| 1396 | // question asked twice and a budget too short to matter would show up | ||
| 1397 | // here as well. | ||
| 1394 | var pkt: [1300]u8 = undefined; | 1398 | var pkt: [1300]u8 = undefined; |
| 1395 | try std.testing.expect(!probeGotAnything(probe, 0)); | 1399 | try std.testing.expect(!probeGotAnything(probe, probe_absence_ms)); |
| 1396 | 1400 | ||
| 1397 | // Addressed to a CID this connection advertised: delivered, not | 1401 | // Addressed to a CID this connection advertised: delivered, not |
| 1398 | // answered. | 1402 | // answered. This is the test's central negative and it has to be a WAIT, |
| 1403 | // not a glance — a read that returns before any datagram could have | ||
| 1404 | // crossed loopback is true of a listener that answered wrongly. | ||
| 1399 | buildInitial(&pkt, sec.data[0..sec.datalen]); | 1405 | buildInitial(&pkt, sec.data[0..sec.datalen]); |
| 1400 | setup.l.route(&pkt, &probe_addr, probe_len); | 1406 | setup.l.route(&pkt, &probe_addr, probe_len); |
| 1401 | try std.testing.expect(!probeGotAnything(probe, 0)); | 1407 | try std.testing.expect(!probeGotAnything(probe, probe_absence_ms)); |
| 1402 | 1408 | ||
| 1403 | // Addressed to nobody: `accept` answers with a Retry, which is what | 1409 | // Addressed to nobody: `accept` answers with a Retry, which is what |
| 1404 | // every migrated packet used to get. | 1410 | // every migrated packet used to get. The budget here is generous because |
| 1411 | // a MISSING reply is the failure this checks for; how long the reply | ||
| 1412 | // really takes is what `probe_absence_ms` is sized against. | ||
| 1405 | buildInitial(&pkt, &stranger_cid); | 1413 | buildInitial(&pkt, &stranger_cid); |
| 1406 | setup.l.route(&pkt, &probe_addr, probe_len); | 1414 | setup.l.route(&pkt, &probe_addr, probe_len); |
| 1407 | try std.testing.expect(probeGotAnything(probe, 2000)); | 1415 | try std.testing.expect(probeGotAnything(probe, 2000)); |
| @@ -1432,19 +1440,32 @@ fn buildInitial(pkt: *[1300]u8, dcid: []const u8) void { | |||
| 1432 | std.mem.writeInt(u16, pkt[i..][0..2], rest | 0x4000, .big); | 1440 | std.mem.writeInt(u16, pkt[i..][0..2], rest | 0x4000, .big); |
| 1433 | } | 1441 | } |
| 1434 | 1442 | ||
| 1443 | /// True when a datagram arrives within `budget_ms`. Every caller waits, | ||
| 1444 | /// including the ones asserting ABSENCE: loopback UDP is not synchronous | ||
| 1445 | /// everywhere — Linux hands the datagram over inside the `sendto`, so an | ||
| 1446 | /// immediate recv finds it, and Darwin does not. A read with no budget | ||
| 1447 | /// therefore CANNOT SEE a Retry on a Mac, which would make "nothing came | ||
| 1448 | /// back" true of a listener that answered wrongly. The absence budget is | ||
| 1449 | /// what makes the negative a claim rather than a race the Mac always wins; | ||
| 1450 | /// `probe_absence_ms` says how it was chosen. | ||
| 1435 | fn probeGotAnything(fd: std.posix.fd_t, budget_ms: i32) bool { | 1451 | fn probeGotAnything(fd: std.posix.fd_t, budget_ms: i32) bool { |
| 1436 | var buf: [2048]u8 = undefined; | 1452 | var buf: [2048]u8 = undefined; |
| 1437 | var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }}; | 1453 | var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }}; |
| 1438 | // A budget, because loopback UDP is not synchronous everywhere: Linux | ||
| 1439 | // hands the datagram over inside the sendto, so an immediate recv finds | ||
| 1440 | // it, and Darwin does not — the Retry this test is waiting for arrived | ||
| 1441 | // after the read that was looking for it. A caller asserting ABSENCE | ||
| 1442 | // passes 0 and keeps the immediate read it always did. | ||
| 1443 | if (budget_ms > 0) _ = std.posix.poll(&fds, budget_ms) catch return false; | 1454 | if (budget_ms > 0) _ = std.posix.poll(&fds, budget_ms) catch return false; |
| 1444 | const n = std.posix.recv(fd, &buf, 0) catch return false; | 1455 | const n = std.posix.recv(fd, &buf, 0) catch return false; |
| 1445 | return n > 0; | 1456 | return n > 0; |
| 1446 | } | 1457 | } |
| 1447 | 1458 | ||
| 1459 | /// How long "nothing came back" waits before it is believed. Sized against | ||
| 1460 | /// the presence check at the end of the same test, on the same socket: | ||
| 1461 | /// timed on both systems 2026-09-03, a Retry this listener DOES send is | ||
| 1462 | /// readable in under a millisecond once the reader yields to a poll. Half a | ||
| 1463 | /// second is three orders of magnitude of slack, so a wrongly-emitted one has | ||
| 1464 | /// had every chance to arrive. What it is NOT sized against is `sendto` | ||
| 1465 | /// itself: Linux delivers inside the call and Darwin does not, which is why | ||
| 1466 | /// a glance with no budget at all could never fail on a Mac. | ||
| 1467 | const probe_absence_ms: i32 = 500; | ||
| 1468 | |||
| 1448 | test "Listener: a reply queued just before a close still reaches the peer" { | 1469 | test "Listener: a reply queued just before a close still reaches the peer" { |
| 1449 | const alloc = std.testing.allocator; | 1470 | const alloc = std.testing.allocator; |
| 1450 | const key: quic.Key = .{ .bytes = [_]u8{0x5C} ** quic.key_len }; | 1471 | const key: quic.Key = .{ .bytes = [_]u8{0x5C} ** quic.key_len }; |