a73x

6ec04455

test: the send test names its cause instead of wedging the runner

a73x   2026-09-03 20:08

Commit message
test: the send test names its cause instead of wedging the runner

Without the fix the new root send test sleeps inside send, and a zig build
test that prints nothing is the worst failure mode this repo has. No
deadline checked between iterations can help, because the thread never runs
again — so a watchdog thread holds the clock. It cannot unblock the send
without making the test pass for the wrong reason, so after ten seconds it
prints what went wrong and ends the process, which the build runner reports
as a failed test command.

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

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