6ec04455
test: the send test names its cause instead of wedging the runner
a73x 2026-09-03 20:08
Commit message
src/os/server_os.zig
| Old | New | ||
|---|---|---|---|
| @@ -472,10 +472,9 @@ test "server_os.sendNoSigNoWait: a full buffer is WouldBlock, not a stall" { | |||
| 472 | // the wait for buffer SPACE tests the socket's own SS_NBIO bit, which is | 472 | // the wait for buffer SPACE tests the socket's own SS_NBIO bit, which is |
| 473 | // O_NONBLOCK on the file descriptor and nothing a send flag can reach. | 473 | // O_NONBLOCK on the file descriptor and nothing a send flag can reach. |
| 474 | // An arm that only passes the flag therefore SLEEPS here, waiting for a | 474 | // An arm that only passes the flag therefore SLEEPS here, waiting for a |
| 475 | // peer that never reads, and this test hangs rather than failing — a | 475 | // peer that never reads — the same stall a client that stopped reading |
| 476 | // `zig build test` that prints nothing for minutes is what that looks | 476 | // would impose on the daemon's only pump. The watchdog below is what |
| 477 | // like, and it is the same stall a client that stopped reading would | 477 | // turns that sleep into a sentence. |
| 478 | // impose on the daemon's only pump. | ||
| 479 | // | 478 | // |
| 480 | // The daemon's own fds are all non-blocking from their accept | 479 | // The daemon's own fds are all non-blocking from their accept |
| 481 | // (`Server.setNonblocking`), so this asks the operation the question the | 480 | // (`Server.setNonblocking`), so this asks the operation the question the |
| @@ -484,6 +483,44 @@ test "server_os.sendNoSigNoWait: a full buffer is WouldBlock, not a stall" { | |||
| 484 | try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp)); | 483 | try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp)); |
| 485 | defer std.posix.close(sp[0]); | 484 | defer std.posix.close(sp[0]); |
| 486 | defer std.posix.close(sp[1]); | 485 | defer std.posix.close(sp[1]); |
| 486 | // A WATCHDOG, because the failure this test exists to catch is a thread | ||
| 487 | // that never runs again: an arm that waits for the peer sleeps inside | ||
| 488 | // `send`, so no deadline checked between iterations would ever be read. | ||
| 489 | // Another thread has to be the one holding the clock. It cannot unblock | ||
| 490 | // the send — draining the peer would make the test pass for the wrong | ||
| 491 | // reason — so it prints the diagnosis and ends the process, which the | ||
| 492 | // build runner reports as a failed test command. That is a sentence | ||
| 493 | // naming the cause instead of a runner that goes quiet for its whole | ||
| 494 | // timeout, which CLAUDE.md names as the worst failure mode here. | ||
| 495 | const Watchdog = struct { | ||
| 496 | done: std.atomic.Value(bool) = .init(false), | ||
| 497 | fn run(self: *@This()) void { | ||
| 498 | var waited_ms: usize = 0; | ||
| 499 | // Ten seconds against a loop of at most a few hundred syscalls: | ||
| 500 | // slack enough that a loaded machine cannot trip it, short enough | ||
| 501 | // to be an answer rather than a wait. | ||
| 502 | while (waited_ms < 10_000) : (waited_ms += 50) { | ||
| 503 | if (self.done.load(.acquire)) return; | ||
| 504 | std.Thread.sleep(50 * std.time.ns_per_ms); | ||
| 505 | } | ||
| 506 | std.debug.print( | ||
| 507 | \\ | ||
| 508 | \\server_os.sendNoSigNoWait blocked on a full send buffer instead of | ||
| 509 | \\answering WouldBlock, so the NoWait half of its name is not true on | ||
| 510 | \\this OS. On Darwin that is MSG_DONTWAIT without O_NONBLOCK on the fd: | ||
| 511 | \\xnu tests the socket's own SS_NBIO bit when it waits for buffer space. | ||
| 512 | \\ | ||
| 513 | , .{}); | ||
| 514 | exitNow(1); | ||
| 515 | } | ||
| 516 | }; | ||
| 517 | var watchdog: Watchdog = .{}; | ||
| 518 | const watcher = try std.Thread.spawn(.{}, Watchdog.run, .{&watchdog}); | ||
| 519 | defer { | ||
| 520 | watchdog.done.store(true, .release); | ||
| 521 | watcher.join(); | ||
| 522 | } | ||
| 523 | |||
| 487 | // Nobody ever reads sp[1]. A socket send buffer is a few hundred KB at | 524 | // Nobody ever reads sp[1]. A socket send buffer is a few hundred KB at |
| 488 | // most, so 32 MB of 64 KB writes is two orders of magnitude of slack and | 525 | // most, so 32 MB of 64 KB writes is two orders of magnitude of slack and |
| 489 | // still finishes in well under a second; reaching the bound means the | 526 | // still finishes in well under a second; reaching the bound means the |