a73x

d2cc2334

test: byte fidelity, non-vacuity, and the third sun_path victim

a73x   2026-08-08 14:08

Commit message
test: byte fidelity, non-vacuity, and the third sun_path victim

Three tests that passed for weaker reasons than they claimed.

**The 3MB payload test counted bytes and never looked at them.** A transport
that echoed three megabytes of zeros satisfied it exactly. The payload is now
verified as it arrives, which matters here more than most places: this file
has already shipped a bug whose entire signature was the right number of the
wrong bytes, and the test that should have caught it was this one.

**The wrong-key test proved a handshake failed, not that the key failed it.**
A client refused earlier — a Retry token that never validated, an Initial the
server would not parse — satisfied every assertion in it. It now also
requires that a connection id was handed out, which only happens once a
token-bearing Initial has been accepted: the witness that this client got all
the way to the point where the PSK was the only thing left to disagree about.
Returning early from accept() before the key is ever consulted now fails it.

**The sun_path hazard, retired.** std.testing.tmpDir lives under .zig-cache,
so a socket path is as long as wherever the repository was checked out, and
sun_path caps it at 108 bytes. A checkout a couple of directories deeper than
usual turned every socket-binding test into NameTooLong raised from inside
std.net — a failure that reads like a bug in the code under test. It has cost
three people a gate cycle each. src/testtmp.zig hands out ~21-character
directories under /tmp instead, with the same shape as the thing it replaces
so the 35 call sites changed by one line each. Verified by building from a
128-character path, where the suite used to fail and now passes 109/109.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -90,6 +90,15 @@ pub fn build(b: *std.Build) void {
90 .link_libc = true, 90 .link_libc = true,
91 }); 91 });
92 92
93 // Test-only: short temp paths for the tests that bind unix sockets.
94 // Imported by every module that has such a test, which is why it is a
95 // module rather than three copies.
96 const testtmp_mod = b.createModule(.{
97 .root_source_file = b.path("src/testtmp.zig"),
98 .target = target,
99 .optimize = optimize,
100 });
101
93 const server_mod = b.createModule(.{ 102 const server_mod = b.createModule(.{
94 .root_source_file = b.path("src/server.zig"), 103 .root_source_file = b.path("src/server.zig"),
95 .target = target, 104 .target = target,
@@ -100,6 +109,7 @@ pub fn build(b: *std.Build) void {
100 server_mod.addImport("pty", pty_mod); 109 server_mod.addImport("pty", pty_mod);
101 server_mod.addImport("protocol", protocol_mod); 110 server_mod.addImport("protocol", protocol_mod);
102 server_mod.addImport("quic", quic_mod); 111 server_mod.addImport("quic", quic_mod);
112 server_mod.addImport("testtmp", testtmp_mod);
103 113
104 const client_mod = b.createModule(.{ 114 const client_mod = b.createModule(.{
105 .root_source_file = b.path("src/client.zig"), 115 .root_source_file = b.path("src/client.zig"),
@@ -109,6 +119,7 @@ pub fn build(b: *std.Build) void {
109 }); 119 });
110 client_mod.addImport("engine", engine_mod); 120 client_mod.addImport("engine", engine_mod);
111 client_mod.addImport("protocol", protocol_mod); 121 client_mod.addImport("protocol", protocol_mod);
122 client_mod.addImport("testtmp", testtmp_mod);
112 123
113 const mux_mod = b.createModule(.{ 124 const mux_mod = b.createModule(.{
114 .root_source_file = b.path("src/mux_main.zig"), 125 .root_source_file = b.path("src/mux_main.zig"),
@@ -118,14 +129,17 @@ pub fn build(b: *std.Build) void {
118 }); 129 });
119 mux_mod.addImport("client", client_mod); 130 mux_mod.addImport("client", client_mod);
120 131
121 // No imports, deliberately: the proxy is a byte pump that knows nothing 132 // No imports that teach it anything, deliberately: the proxy is a byte
122 // about the protocol it carries. 133 // pump that knows nothing about the protocol it carries. `testtmp` is
134 // the one exception and does not weaken that — it hands its tests a
135 // short directory to put a socket in and knows nothing about the bytes.
123 const proxy_mod = b.createModule(.{ 136 const proxy_mod = b.createModule(.{
124 .root_source_file = b.path("src/proxy.zig"), 137 .root_source_file = b.path("src/proxy.zig"),
125 .target = target, 138 .target = target,
126 .optimize = optimize, 139 .optimize = optimize,
127 .link_libc = true, 140 .link_libc = true,
128 }); 141 });
142 proxy_mod.addImport("testtmp", testtmp_mod);
129 143
130 const exe_mod = b.createModule(.{ 144 const exe_mod = b.createModule(.{
131 .root_source_file = b.path("src/main.zig"), 145 .root_source_file = b.path("src/main.zig"),
@@ -161,7 +175,7 @@ pub fn build(b: *std.Build) void {
161 // absence here was a live hazard recorded in decisions.md — muxd's 175 // absence here was a live hazard recorded in decisions.md — muxd's
162 // entrypoint could grow tests that silently never ran, exactly as 176 // entrypoint could grow tests that silently never ran, exactly as
163 // mux_main.zig's five did before it was added. 177 // mux_main.zig's five did before it was added.
164 for ([_]*std.Build.Module{ protocol_mod, engine_mod, pty_mod, server_mod, client_mod, proxy_mod, mux_mod, quic_mod, exe_mod }) |mod| { 178 for ([_]*std.Build.Module{ protocol_mod, engine_mod, pty_mod, server_mod, client_mod, proxy_mod, mux_mod, quic_mod, exe_mod, testtmp_mod }) |mod| {
165 const t = b.addTest(.{ .root_module = mod }); 179 const t = b.addTest(.{ .root_module = mod });
166 t.use_llvm = true; 180 t.use_llvm = true;
167 t.use_lld = true; 181 t.use_lld = true;
src/client.zig
Old New
@@ -8,6 +8,7 @@
8 const std = @import("std"); 8 const std = @import("std");
9 const Engine = @import("engine").Engine; 9 const Engine = @import("engine").Engine;
10 const proto = @import("protocol"); 10 const proto = @import("protocol");
11 const TmpDir = @import("testtmp").TmpDir;
11 12
12 var winch_flag = std.atomic.Value(bool).init(false); 13 var winch_flag = std.atomic.Value(bool).init(false);
13 14
@@ -690,10 +691,9 @@ fn renderScrollback(
690 test "Transport.close is idempotent: the abort path closes what reconnect already closed" { 691 test "Transport.close is idempotent: the abort path closes what reconnect already closed" {
691 const alloc = std.testing.allocator; 692 const alloc = std.testing.allocator;
692 693
693 var tmp = std.testing.tmpDir(.{}); 694 var tmp = try TmpDir.make();
694 defer tmp.cleanup(); 695 defer tmp.cleanup();
695 var path_buf: [256]u8 = undefined; 696 const dir_path = tmp.path();
696 const dir_path = try tmp.dir.realpath(".", &path_buf);
697 const sock_path = try std.fmt.allocPrint(alloc, "{s}/t.sock", .{dir_path}); 697 const sock_path = try std.fmt.allocPrint(alloc, "{s}/t.sock", .{dir_path});
698 defer alloc.free(sock_path); 698 defer alloc.free(sock_path);
699 699
src/proxy.zig
Old New
@@ -4,6 +4,7 @@
4 //! pipe suffices to carry the protocol over SSH, transport is a swap, 4 //! pipe suffices to carry the protocol over SSH, transport is a swap,
5 //! not a redesign. Keep this file's import list empty of `protocol`. 5 //! not a redesign. Keep this file's import list empty of `protocol`.
6 const std = @import("std"); 6 const std = @import("std");
7 const TmpDir = @import("testtmp").TmpDir;
7 8
8 /// `muxd proxy` proper: pump between this process's stdio and `sock_path`. 9 /// `muxd proxy` proper: pump between this process's stdio and `sock_path`.
9 pub fn run(sock_path: []const u8) !u8 { 10 pub fn run(sock_path: []const u8) !u8 {
@@ -197,10 +198,9 @@ fn readExactly(fd: std.posix.fd_t, buf: []u8) !void {
197 test "pump carries bytes both ways verbatim" { 198 test "pump carries bytes both ways verbatim" {
198 const alloc = std.testing.allocator; 199 const alloc = std.testing.allocator;
199 200
200 var tmp = std.testing.tmpDir(.{}); 201 var tmp = try TmpDir.make();
201 defer tmp.cleanup(); 202 defer tmp.cleanup();
202 var path_buf: [256]u8 = undefined; 203 const dir_path = tmp.path();
203 const dir_path = try tmp.dir.realpath(".", &path_buf);
204 var sock_buf: [280]u8 = undefined; 204 var sock_buf: [280]u8 = undefined;
205 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/proxy.sock", .{dir_path}); 205 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/proxy.sock", .{dir_path});
206 206
@@ -253,10 +253,9 @@ test "pump carries bytes both ways verbatim" {
253 test "pump carries a large transfer verbatim under backpressure" { 253 test "pump carries a large transfer verbatim under backpressure" {
254 const alloc = std.testing.allocator; 254 const alloc = std.testing.allocator;
255 255
256 var tmp = std.testing.tmpDir(.{}); 256 var tmp = try TmpDir.make();
257 defer tmp.cleanup(); 257 defer tmp.cleanup();
258 var path_buf: [256]u8 = undefined; 258 const dir_path = tmp.path();
259 const dir_path = try tmp.dir.realpath(".", &path_buf);
260 var sock_buf: [280]u8 = undefined; 259 var sock_buf: [280]u8 = undefined;
261 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/big.sock", .{dir_path}); 260 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/big.sock", .{dir_path});
262 261
@@ -311,10 +310,9 @@ test "pump carries a large transfer verbatim under backpressure" {
311 test "pump exits when the far side of its output pipe is gone" { 310 test "pump exits when the far side of its output pipe is gone" {
312 const alloc = std.testing.allocator; 311 const alloc = std.testing.allocator;
313 312
314 var tmp = std.testing.tmpDir(.{}); 313 var tmp = try TmpDir.make();
315 defer tmp.cleanup(); 314 defer tmp.cleanup();
316 var path_buf: [256]u8 = undefined; 315 const dir_path = tmp.path();
317 const dir_path = try tmp.dir.realpath(".", &path_buf);
318 var sock_buf: [280]u8 = undefined; 316 var sock_buf: [280]u8 = undefined;
319 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/gone.sock", .{dir_path}); 317 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/gone.sock", .{dir_path});
320 318
@@ -347,10 +345,9 @@ test "pump exits when the far side of its output pipe is gone" {
347 } 345 }
348 346
349 test "pump reports a missing daemon socket instead of hanging" { 347 test "pump reports a missing daemon socket instead of hanging" {
350 var tmp = std.testing.tmpDir(.{}); 348 var tmp = try TmpDir.make();
351 defer tmp.cleanup(); 349 defer tmp.cleanup();
352 var path_buf: [256]u8 = undefined; 350 const dir_path = tmp.path();
353 const dir_path = try tmp.dir.realpath(".", &path_buf);
354 var sock_buf: [280]u8 = undefined; 351 var sock_buf: [280]u8 = undefined;
355 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/absent.sock", .{dir_path}); 352 const sock_path = try std.fmt.bufPrint(&sock_buf, "{s}/absent.sock", .{dir_path});
356 353
src/quic_server.zig
Old New
@@ -1517,7 +1517,14 @@ test "Listener: PSK handshake, Retry, and a payload larger than the initial wind
1517 // production too, which is why it is worth a test rather than a comment. 1517 // production too, which is why it is worth a test rather than a comment.
1518 const payload = try alloc.alloc(u8, 3 * 1024 * 1024); 1518 const payload = try alloc.alloc(u8, 3 * 1024 * 1024);
1519 defer alloc.free(payload); 1519 defer alloc.free(payload);
1520 // A repeating byte pattern, checked as it arrives rather than counted
1521 // at the end. Counting alone cannot tell a working transport from one
1522 // that delivers the right NUMBER of the wrong bytes — reordered,
1523 // duplicated, or read back out of a buffer that had been recycled
1524 // underneath it, which is precisely the failure this file has already
1525 // had once.
1520 for (payload, 0..) |*b, i| b.* = @truncate(i); 1526 for (payload, 0..) |*b, i| b.* = @truncate(i);
1527 cl.verify = payload;
1521 cl.out = payload; 1528 cl.out = payload;
1522 cl.drain(); 1529 cl.drain();
1523 1530
@@ -1534,6 +1541,7 @@ test "Listener: PSK handshake, Retry, and a payload larger than the initial wind
1534 } 1541 }
1535 try std.testing.expect(ok); 1542 try std.testing.expect(ok);
1536 try std.testing.expectEqual(@as(usize, 3 * 1024 * 1024), owner.received); 1543 try std.testing.expectEqual(@as(usize, 3 * 1024 * 1024), owner.received);
1544 try std.testing.expect(!cl.mismatch);
1537 } 1545 }
1538 1546
1539 test "Listener: a client holding the wrong key never completes a handshake" { 1547 test "Listener: a client holding the wrong key never completes a handshake" {
@@ -1562,6 +1570,16 @@ test "Listener: a client holding the wrong key never completes a handshake" {
1562 try std.testing.expect(!cl.handshake_done); 1570 try std.testing.expect(!cl.handshake_done);
1563 try std.testing.expectEqual(@as(usize, 0), owner.opened); 1571 try std.testing.expectEqual(@as(usize, 0), owner.opened);
1564 try std.testing.expectEqual(@as(usize, 0), owner.received); 1572 try std.testing.expectEqual(@as(usize, 0), owner.received);
1573
1574 // ...and it failed on the KEY, which is a stronger claim than "it
1575 // failed". A client refused earlier — a Retry token that never
1576 // validated, an Initial the server would not parse — would satisfy
1577 // every assertion above while proving nothing about authentication.
1578 // The listener only allocates a connection id once it has accepted a
1579 // token-bearing Initial, so an id having been handed out is the witness
1580 // that this client got all the way to the point where the PSK is the
1581 // only thing left to disagree about.
1582 try std.testing.expect(setup.l.next_id > 1);
1565 } 1583 }
1566 1584
1567 test "Listener: keepalive carries an idle connection past its idle timeout" { 1585 test "Listener: keepalive carries an idle connection past its idle timeout" {
src/server.zig
Old New
@@ -10,6 +10,7 @@ const Engine = @import("engine").Engine;
10 const Pty = @import("pty").Pty; 10 const Pty = @import("pty").Pty;
11 const proto = @import("protocol"); 11 const proto = @import("protocol");
12 const quic = @import("quic"); 12 const quic = @import("quic");
13 const TmpDir = @import("testtmp").TmpDir;
13 14
14 const max_clients = 8; 15 const max_clients = 8;
15 const max_observers = 4; 16 const max_observers = 4;
@@ -1382,10 +1383,9 @@ fn applyFrame(alloc: std.mem.Allocator, replica: *Engine, frame: proto.Frame) !v
1382 test "Server: survives a client that dies without detaching; next attach works" { 1383 test "Server: survives a client that dies without detaching; next attach works" {
1383 const alloc = std.testing.allocator; 1384 const alloc = std.testing.allocator;
1384 1385
1385 var tmp = std.testing.tmpDir(.{}); 1386 var tmp = try TmpDir.make();
1386 defer tmp.cleanup(); 1387 defer tmp.cleanup();
1387 var path_buf: [256]u8 = undefined; 1388 const dir_path = tmp.path();
1388 const dir_path = try tmp.dir.realpath(".", &path_buf);
1389 const sock_path = try std.fmt.allocPrint(alloc, "{s}/kill.sock", .{dir_path}); 1389 const sock_path = try std.fmt.allocPrint(alloc, "{s}/kill.sock", .{dir_path});
1390 defer alloc.free(sock_path); 1390 defer alloc.free(sock_path);
1391 1391
@@ -1431,10 +1431,9 @@ test "Server: survives a client that dies without detaching; next attach works"
1431 test "Server: serves scrollback chunks on request" { 1431 test "Server: serves scrollback chunks on request" {
1432 const alloc = std.testing.allocator; 1432 const alloc = std.testing.allocator;
1433 1433
1434 var tmp = std.testing.tmpDir(.{}); 1434 var tmp = try TmpDir.make();
1435 defer tmp.cleanup(); 1435 defer tmp.cleanup();
1436 var path_buf: [256]u8 = undefined; 1436 const dir_path = tmp.path();
1437 const dir_path = try tmp.dir.realpath(".", &path_buf);
1438 const sock_path = try std.fmt.allocPrint(alloc, "{s}/sb.sock", .{dir_path}); 1437 const sock_path = try std.fmt.allocPrint(alloc, "{s}/sb.sock", .{dir_path});
1439 defer alloc.free(sock_path); 1438 defer alloc.free(sock_path);
1440 1439
@@ -1500,10 +1499,9 @@ test "Server: serves scrollback chunks on request" {
1500 test "Server: a full session refuses the next attach instead of displacing anyone" { 1499 test "Server: a full session refuses the next attach instead of displacing anyone" {
1501 const alloc = std.testing.allocator; 1500 const alloc = std.testing.allocator;
1502 1501
1503 var tmp = std.testing.tmpDir(.{}); 1502 var tmp = try TmpDir.make();
1504 defer tmp.cleanup(); 1503 defer tmp.cleanup();
1505 var path_buf: [256]u8 = undefined; 1504 const dir_path = tmp.path();
1506 const dir_path = try tmp.dir.realpath(".", &path_buf);
1507 const sock_path = try std.fmt.allocPrint(alloc, "{s}/full.sock", .{dir_path}); 1505 const sock_path = try std.fmt.allocPrint(alloc, "{s}/full.sock", .{dir_path});
1508 defer alloc.free(sock_path); 1506 defer alloc.free(sock_path);
1509 1507
@@ -1578,10 +1576,9 @@ test "Server: a full session refuses the next attach instead of displacing anyon
1578 test "Server: two clients converge on one session" { 1576 test "Server: two clients converge on one session" {
1579 const alloc = std.testing.allocator; 1577 const alloc = std.testing.allocator;
1580 1578
1581 var tmp = std.testing.tmpDir(.{}); 1579 var tmp = try TmpDir.make();
1582 defer tmp.cleanup(); 1580 defer tmp.cleanup();
1583 var path_buf: [256]u8 = undefined; 1581 const dir_path = tmp.path();
1584 const dir_path = try tmp.dir.realpath(".", &path_buf);
1585 const sock_path = try std.fmt.allocPrint(alloc, "{s}/two.sock", .{dir_path}); 1582 const sock_path = try std.fmt.allocPrint(alloc, "{s}/two.sock", .{dir_path});
1586 defer alloc.free(sock_path); 1583 defer alloc.free(sock_path);
1587 1584
@@ -1685,10 +1682,9 @@ test "Server: two clients converge on one session" {
1685 test "Server: a same-size join snapshots the joiner only" { 1682 test "Server: a same-size join snapshots the joiner only" {
1686 const alloc = std.testing.allocator; 1683 const alloc = std.testing.allocator;
1687 1684
1688 var tmp = std.testing.tmpDir(.{}); 1685 var tmp = try TmpDir.make();
1689 defer tmp.cleanup(); 1686 defer tmp.cleanup();
1690 var path_buf: [256]u8 = undefined; 1687 const dir_path = tmp.path();
1691 const dir_path = try tmp.dir.realpath(".", &path_buf);
1692 const sock_path = try std.fmt.allocPrint(alloc, "{s}/join.sock", .{dir_path}); 1688 const sock_path = try std.fmt.allocPrint(alloc, "{s}/join.sock", .{dir_path});
1693 defer alloc.free(sock_path); 1689 defer alloc.free(sock_path);
1694 1690
@@ -1839,10 +1835,9 @@ fn shrinkSendBuf(fd: std.posix.fd_t) !void {
1839 test "Server: a stalled client does not block delivery to others" { 1835 test "Server: a stalled client does not block delivery to others" {
1840 const alloc = std.testing.allocator; 1836 const alloc = std.testing.allocator;
1841 1837
1842 var tmp = std.testing.tmpDir(.{}); 1838 var tmp = try TmpDir.make();
1843 defer tmp.cleanup(); 1839 defer tmp.cleanup();
1844 var path_buf: [256]u8 = undefined; 1840 const dir_path = tmp.path();
1845 const dir_path = try tmp.dir.realpath(".", &path_buf);
1846 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stall.sock", .{dir_path}); 1841 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stall.sock", .{dir_path});
1847 defer alloc.free(sock_path); 1842 defer alloc.free(sock_path);
1848 1843
@@ -1897,10 +1892,9 @@ test "Server: a stalled client does not block delivery to others" {
1897 test "Server: a client exceeding the pending cap is dropped" { 1892 test "Server: a client exceeding the pending cap is dropped" {
1898 const alloc = std.testing.allocator; 1893 const alloc = std.testing.allocator;
1899 1894
1900 var tmp = std.testing.tmpDir(.{}); 1895 var tmp = try TmpDir.make();
1901 defer tmp.cleanup(); 1896 defer tmp.cleanup();
1902 var path_buf: [256]u8 = undefined; 1897 const dir_path = tmp.path();
1903 const dir_path = try tmp.dir.realpath(".", &path_buf);
1904 const sock_path = try std.fmt.allocPrint(alloc, "{s}/cap.sock", .{dir_path}); 1898 const sock_path = try std.fmt.allocPrint(alloc, "{s}/cap.sock", .{dir_path});
1905 defer alloc.free(sock_path); 1899 defer alloc.free(sock_path);
1906 1900
@@ -1955,10 +1949,9 @@ test "Server: a client exceeding the pending cap is dropped" {
1955 test "Server: a writable backlog is flushed by poll, not mistaken for input" { 1949 test "Server: a writable backlog is flushed by poll, not mistaken for input" {
1956 const alloc = std.testing.allocator; 1950 const alloc = std.testing.allocator;
1957 1951
1958 var tmp = std.testing.tmpDir(.{}); 1952 var tmp = try TmpDir.make();
1959 defer tmp.cleanup(); 1953 defer tmp.cleanup();
1960 var path_buf: [256]u8 = undefined; 1954 const dir_path = tmp.path();
1961 const dir_path = try tmp.dir.realpath(".", &path_buf);
1962 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pollout.sock", .{dir_path}); 1955 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pollout.sock", .{dir_path});
1963 defer alloc.free(sock_path); 1956 defer alloc.free(sock_path);
1964 1957
@@ -2007,10 +2000,9 @@ test "Server: a writable backlog is flushed by poll, not mistaken for input" {
2007 2000
2008 test "Server: a partially flushed queue delivers every byte exactly once" { 2001 test "Server: a partially flushed queue delivers every byte exactly once" {
2009 const alloc = std.testing.allocator; 2002 const alloc = std.testing.allocator;
2010 var tmp = std.testing.tmpDir(.{}); 2003 var tmp = try TmpDir.make();
2011 defer tmp.cleanup(); 2004 defer tmp.cleanup();
2012 var path_buf: [256]u8 = undefined; 2005 const dir_path = tmp.path();
2013 const dir_path = try tmp.dir.realpath(".", &path_buf);
2014 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pf.sock", .{dir_path}); 2006 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pf.sock", .{dir_path});
2015 defer alloc.free(sock_path); 2007 defer alloc.free(sock_path);
2016 2008
@@ -2071,10 +2063,9 @@ test "Server: a partially flushed queue delivers every byte exactly once" {
2071 test "Server: the shell's exit status reaches an attached client" { 2063 test "Server: the shell's exit status reaches an attached client" {
2072 const alloc = std.testing.allocator; 2064 const alloc = std.testing.allocator;
2073 2065
2074 var tmp = std.testing.tmpDir(.{}); 2066 var tmp = try TmpDir.make();
2075 defer tmp.cleanup(); 2067 defer tmp.cleanup();
2076 var path_buf: [256]u8 = undefined; 2068 const dir_path = tmp.path();
2077 const dir_path = try tmp.dir.realpath(".", &path_buf);
2078 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exit.sock", .{dir_path}); 2069 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exit.sock", .{dir_path});
2079 defer alloc.free(sock_path); 2070 defer alloc.free(sock_path);
2080 2071
@@ -2164,10 +2155,9 @@ const PeerDrainer = struct {
2164 test "Server: the exit drain delivers a backlog once the peer resumes reading" { 2155 test "Server: the exit drain delivers a backlog once the peer resumes reading" {
2165 const alloc = std.testing.allocator; 2156 const alloc = std.testing.allocator;
2166 2157
2167 var tmp = std.testing.tmpDir(.{}); 2158 var tmp = try TmpDir.make();
2168 defer tmp.cleanup(); 2159 defer tmp.cleanup();
2169 var path_buf: [256]u8 = undefined; 2160 const dir_path = tmp.path();
2170 const dir_path = try tmp.dir.realpath(".", &path_buf);
2171 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exitdrain.sock", .{dir_path}); 2161 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exitdrain.sock", .{dir_path});
2172 defer alloc.free(sock_path); 2162 defer alloc.free(sock_path);
2173 2163
@@ -2216,10 +2206,9 @@ test "Server: the exit drain delivers a backlog once the peer resumes reading" {
2216 test "Server: the exit drain gives up on a peer that never reads" { 2206 test "Server: the exit drain gives up on a peer that never reads" {
2217 const alloc = std.testing.allocator; 2207 const alloc = std.testing.allocator;
2218 2208
2219 var tmp = std.testing.tmpDir(.{}); 2209 var tmp = try TmpDir.make();
2220 defer tmp.cleanup(); 2210 defer tmp.cleanup();
2221 var path_buf: [256]u8 = undefined; 2211 const dir_path = tmp.path();
2222 const dir_path = try tmp.dir.realpath(".", &path_buf);
2223 const sock_path = try std.fmt.allocPrint(alloc, "{s}/drain.sock", .{dir_path}); 2212 const sock_path = try std.fmt.allocPrint(alloc, "{s}/drain.sock", .{dir_path});
2224 defer alloc.free(sock_path); 2213 defer alloc.free(sock_path);
2225 2214
@@ -2256,10 +2245,9 @@ test "Server: the exit drain gives up on a peer that never reads" {
2256 test "Server: broadcast stats count every send but the counterfactual once" { 2245 test "Server: broadcast stats count every send but the counterfactual once" {
2257 const alloc = std.testing.allocator; 2246 const alloc = std.testing.allocator;
2258 2247
2259 var tmp = std.testing.tmpDir(.{}); 2248 var tmp = try TmpDir.make();
2260 defer tmp.cleanup(); 2249 defer tmp.cleanup();
2261 var path_buf: [256]u8 = undefined; 2250 const dir_path = tmp.path();
2262 const dir_path = try tmp.dir.realpath(".", &path_buf);
2263 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stats.sock", .{dir_path}); 2251 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stats.sock", .{dir_path});
2264 defer alloc.free(sock_path); 2252 defer alloc.free(sock_path);
2265 2253
@@ -2305,10 +2293,9 @@ test "Server: broadcast stats count every send but the counterfactual once" {
2305 test "Server: latest attacher's size wins; earlier client is resnapshotted at the new size" { 2293 test "Server: latest attacher's size wins; earlier client is resnapshotted at the new size" {
2306 const alloc = std.testing.allocator; 2294 const alloc = std.testing.allocator;
2307 2295
2308 var tmp = std.testing.tmpDir(.{}); 2296 var tmp = try TmpDir.make();
2309 defer tmp.cleanup(); 2297 defer tmp.cleanup();
2310 var path_buf: [256]u8 = undefined; 2298 const dir_path = tmp.path();
2311 const dir_path = try tmp.dir.realpath(".", &path_buf);
2312 const sock_path = try std.fmt.allocPrint(alloc, "{s}/latest.sock", .{dir_path}); 2299 const sock_path = try std.fmt.allocPrint(alloc, "{s}/latest.sock", .{dir_path});
2313 defer alloc.free(sock_path); 2300 defer alloc.free(sock_path);
2314 2301
@@ -2410,10 +2397,9 @@ fn awaitSnapshotSize(
2410 test "Server: typing claims the grid for the typist (latest-wins on input)" { 2397 test "Server: typing claims the grid for the typist (latest-wins on input)" {
2411 const alloc = std.testing.allocator; 2398 const alloc = std.testing.allocator;
2412 2399
2413 var tmp = std.testing.tmpDir(.{}); 2400 var tmp = try TmpDir.make();
2414 defer tmp.cleanup(); 2401 defer tmp.cleanup();
2415 var path_buf: [256]u8 = undefined; 2402 const dir_path = tmp.path();
2416 const dir_path = try tmp.dir.realpath(".", &path_buf);
2417 const sock_path = try std.fmt.allocPrint(alloc, "{s}/typing.sock", .{dir_path}); 2403 const sock_path = try std.fmt.allocPrint(alloc, "{s}/typing.sock", .{dir_path});
2418 defer alloc.free(sock_path); 2404 defer alloc.free(sock_path);
2419 2405
@@ -2508,10 +2494,9 @@ fn awaitMarkerWithoutSnapshot(
2508 test "Server: a size the grid refuses never becomes a claim" { 2494 test "Server: a size the grid refuses never becomes a claim" {
2509 const alloc = std.testing.allocator; 2495 const alloc = std.testing.allocator;
2510 2496
2511 var tmp = std.testing.tmpDir(.{}); 2497 var tmp = try TmpDir.make();
2512 defer tmp.cleanup(); 2498 defer tmp.cleanup();
2513 var path_buf: [256]u8 = undefined; 2499 const dir_path = tmp.path();
2514 const dir_path = try tmp.dir.realpath(".", &path_buf);
2515 const sock_path = try std.fmt.allocPrint(alloc, "{s}/refused.sock", .{dir_path}); 2500 const sock_path = try std.fmt.allocPrint(alloc, "{s}/refused.sock", .{dir_path});
2516 defer alloc.free(sock_path); 2501 defer alloc.free(sock_path);
2517 2502
@@ -2587,10 +2572,9 @@ test "Server: a size the grid refuses never becomes a claim" {
2587 test "Server: scrollback fetch is per-client and independent" { 2572 test "Server: scrollback fetch is per-client and independent" {
2588 const alloc = std.testing.allocator; 2573 const alloc = std.testing.allocator;
2589 2574
2590 var tmp = std.testing.tmpDir(.{}); 2575 var tmp = try TmpDir.make();
2591 defer tmp.cleanup(); 2576 defer tmp.cleanup();
2592 var path_buf: [256]u8 = undefined; 2577 const dir_path = tmp.path();
2593 const dir_path = try tmp.dir.realpath(".", &path_buf);
2594 const sock_path = try std.fmt.allocPrint(alloc, "{s}/percli.sock", .{dir_path}); 2578 const sock_path = try std.fmt.allocPrint(alloc, "{s}/percli.sock", .{dir_path});
2595 defer alloc.free(sock_path); 2579 defer alloc.free(sock_path);
2596 2580
@@ -2684,10 +2668,9 @@ test "Server: scrollback fetch is per-client and independent" {
2684 test "Server: replica rebuilt from snapshots matches the authoritative grid" { 2668 test "Server: replica rebuilt from snapshots matches the authoritative grid" {
2685 const alloc = std.testing.allocator; 2669 const alloc = std.testing.allocator;
2686 2670
2687 var tmp = std.testing.tmpDir(.{}); 2671 var tmp = try TmpDir.make();
2688 defer tmp.cleanup(); 2672 defer tmp.cleanup();
2689 var path_buf: [256]u8 = undefined; 2673 const dir_path = tmp.path();
2690 const dir_path = try tmp.dir.realpath(".", &path_buf);
2691 const sock_path = try std.fmt.allocPrint(alloc, "{s}/m2.sock", .{dir_path}); 2674 const sock_path = try std.fmt.allocPrint(alloc, "{s}/m2.sock", .{dir_path});
2692 defer alloc.free(sock_path); 2675 defer alloc.free(sock_path);
2693 2676
@@ -2761,10 +2744,9 @@ test "Server: replica rebuilt from snapshots matches the authoritative grid" {
2761 test "Server: typing produces deltas, not snapshots; stats track both" { 2744 test "Server: typing produces deltas, not snapshots; stats track both" {
2762 const alloc = std.testing.allocator; 2745 const alloc = std.testing.allocator;
2763 2746
2764 var tmp = std.testing.tmpDir(.{}); 2747 var tmp = try TmpDir.make();
2765 defer tmp.cleanup(); 2748 defer tmp.cleanup();
2766 var path_buf: [256]u8 = undefined; 2749 const dir_path = tmp.path();
2767 const dir_path = try tmp.dir.realpath(".", &path_buf);
2768 const sock_path = try std.fmt.allocPrint(alloc, "{s}/delta.sock", .{dir_path}); 2750 const sock_path = try std.fmt.allocPrint(alloc, "{s}/delta.sock", .{dir_path});
2769 defer alloc.free(sock_path); 2751 defer alloc.free(sock_path);
2770 2752
@@ -2857,10 +2839,9 @@ test "Server: typing produces deltas, not snapshots; stats track both" {
2857 test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a delta" { 2839 test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a delta" {
2858 const alloc = std.testing.allocator; 2840 const alloc = std.testing.allocator;
2859 2841
2860 var tmp = std.testing.tmpDir(.{}); 2842 var tmp = try TmpDir.make();
2861 defer tmp.cleanup(); 2843 defer tmp.cleanup();
2862 var path_buf: [256]u8 = undefined; 2844 const dir_path = tmp.path();
2863 const dir_path = try tmp.dir.realpath(".", &path_buf);
2864 const sock_path = try std.fmt.allocPrint(alloc, "{s}/resync.sock", .{dir_path}); 2845 const sock_path = try std.fmt.allocPrint(alloc, "{s}/resync.sock", .{dir_path});
2865 defer alloc.free(sock_path); 2846 defer alloc.free(sock_path);
2866 2847
@@ -2953,10 +2934,9 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a
2953 test "Server: a daemon restart invalidates have_seq even with the old epoch presented" { 2934 test "Server: a daemon restart invalidates have_seq even with the old epoch presented" {
2954 const alloc = std.testing.allocator; 2935 const alloc = std.testing.allocator;
2955 2936
2956 var tmp = std.testing.tmpDir(.{}); 2937 var tmp = try TmpDir.make();
2957 defer tmp.cleanup(); 2938 defer tmp.cleanup();
2958 var path_buf: [256]u8 = undefined; 2939 const dir_path = tmp.path();
2959 const dir_path = try tmp.dir.realpath(".", &path_buf);
2960 const sock_path = try std.fmt.allocPrint(alloc, "{s}/restart.sock", .{dir_path}); 2940 const sock_path = try std.fmt.allocPrint(alloc, "{s}/restart.sock", .{dir_path});
2961 defer alloc.free(sock_path); 2941 defer alloc.free(sock_path);
2962 2942
@@ -3058,10 +3038,9 @@ fn expectInitRefused(alloc: std.mem.Allocator, path: []const u8, want: anyerror)
3058 test "Server: injected bytes reach frame handling, split anywhere" { 3038 test "Server: injected bytes reach frame handling, split anywhere" {
3059 const alloc = std.testing.allocator; 3039 const alloc = std.testing.allocator;
3060 3040
3061 var tmp = std.testing.tmpDir(.{}); 3041 var tmp = try TmpDir.make();
3062 defer tmp.cleanup(); 3042 defer tmp.cleanup();
3063 var path_buf: [256]u8 = undefined; 3043 const dir_path = tmp.path();
3064 const dir_path = try tmp.dir.realpath(".", &path_buf);
3065 const sock_path = try std.fmt.allocPrint(alloc, "{s}/inject.sock", .{dir_path}); 3044 const sock_path = try std.fmt.allocPrint(alloc, "{s}/inject.sock", .{dir_path});
3066 defer alloc.free(sock_path); 3045 defer alloc.free(sock_path);
3067 3046
@@ -3107,10 +3086,9 @@ test "Server: injected bytes reach frame handling, split anywhere" {
3107 test "Server: a second daemon refuses a live socket instead of stealing it" { 3086 test "Server: a second daemon refuses a live socket instead of stealing it" {
3108 const alloc = std.testing.allocator; 3087 const alloc = std.testing.allocator;
3109 3088
3110 var tmp = std.testing.tmpDir(.{}); 3089 var tmp = try TmpDir.make();
3111 defer tmp.cleanup(); 3090 defer tmp.cleanup();
3112 var path_buf: [256]u8 = undefined; 3091 const dir_path = tmp.path();
3113 const dir_path = try tmp.dir.realpath(".", &path_buf);
3114 const sock_path = try std.fmt.allocPrint(alloc, "{s}/live.sock", .{dir_path}); 3092 const sock_path = try std.fmt.allocPrint(alloc, "{s}/live.sock", .{dir_path});
3115 defer alloc.free(sock_path); 3093 defer alloc.free(sock_path);
3116 3094
@@ -3145,10 +3123,9 @@ test "Server: a second daemon refuses a live socket instead of stealing it" {
3145 test "Server: a dead daemon's leftover socket file is cleared and rebound" { 3123 test "Server: a dead daemon's leftover socket file is cleared and rebound" {
3146 const alloc = std.testing.allocator; 3124 const alloc = std.testing.allocator;
3147 3125
3148 var tmp = std.testing.tmpDir(.{}); 3126 var tmp = try TmpDir.make();
3149 defer tmp.cleanup(); 3127 defer tmp.cleanup();
3150 var path_buf: [256]u8 = undefined; 3128 const dir_path = tmp.path();
3151 const dir_path = try tmp.dir.realpath(".", &path_buf);
3152 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stale.sock", .{dir_path}); 3129 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stale.sock", .{dir_path});
3153 defer alloc.free(sock_path); 3130 defer alloc.free(sock_path);
3154 3131
@@ -3183,10 +3160,9 @@ test "Server: a dead daemon's leftover socket file is cleared and rebound" {
3183 test "Server: a path that cannot be bound fails as AddressInUse" { 3160 test "Server: a path that cannot be bound fails as AddressInUse" {
3184 const alloc = std.testing.allocator; 3161 const alloc = std.testing.allocator;
3185 3162
3186 var tmp = std.testing.tmpDir(.{}); 3163 var tmp = try TmpDir.make();
3187 defer tmp.cleanup(); 3164 defer tmp.cleanup();
3188 var path_buf: [256]u8 = undefined; 3165 const dir_path = tmp.path();
3189 const dir_path = try tmp.dir.realpath(".", &path_buf);
3190 const sock_path = try std.fmt.allocPrint(alloc, "{s}/dangling.sock", .{dir_path}); 3166 const sock_path = try std.fmt.allocPrint(alloc, "{s}/dangling.sock", .{dir_path});
3191 defer alloc.free(sock_path); 3167 defer alloc.free(sock_path);
3192 3168
@@ -3213,10 +3189,9 @@ test "Server: a path that cannot be bound fails as AddressInUse" {
3213 test "Server: a non-socket at the path is refused, not deleted" { 3189 test "Server: a non-socket at the path is refused, not deleted" {
3214 const alloc = std.testing.allocator; 3190 const alloc = std.testing.allocator;
3215 3191
3216 var tmp = std.testing.tmpDir(.{}); 3192 var tmp = try TmpDir.make();
3217 defer tmp.cleanup(); 3193 defer tmp.cleanup();
3218 var path_buf: [256]u8 = undefined; 3194 const dir_path = tmp.path();
3219 const dir_path = try tmp.dir.realpath(".", &path_buf);
3220 const file_path = try std.fmt.allocPrint(alloc, "{s}/notes.txt", .{dir_path}); 3195 const file_path = try std.fmt.allocPrint(alloc, "{s}/notes.txt", .{dir_path});
3221 defer alloc.free(file_path); 3196 defer alloc.free(file_path);
3222 3197
@@ -3430,10 +3405,9 @@ fn attachOver(cl: *quic.TestClient, buf: *std.ArrayList(u8), alloc: std.mem.Allo
3430 3405
3431 test "Server: a QUIC client still receives the shell's exit status" { 3406 test "Server: a QUIC client still receives the shell's exit status" {
3432 const alloc = std.testing.allocator; 3407 const alloc = std.testing.allocator;
3433 var tmp = std.testing.tmpDir(.{}); 3408 var tmp = try TmpDir.make();
3434 defer tmp.cleanup(); 3409 defer tmp.cleanup();
3435 var path_buf: [256]u8 = undefined; 3410 const dir_path = tmp.path();
3436 const dir_path = try tmp.dir.realpath(".", &path_buf);
3437 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qexit.sock", .{dir_path}); 3411 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qexit.sock", .{dir_path});
3438 defer alloc.free(sock_path); 3412 defer alloc.free(sock_path);
3439 3413
@@ -3484,10 +3458,9 @@ test "Server: a QUIC client still receives the shell's exit status" {
3484 3458
3485 test "Server: one QUIC client leaving does not disturb the other" { 3459 test "Server: one QUIC client leaving does not disturb the other" {
3486 const alloc = std.testing.allocator; 3460 const alloc = std.testing.allocator;
3487 var tmp = std.testing.tmpDir(.{}); 3461 var tmp = try TmpDir.make();
3488 defer tmp.cleanup(); 3462 defer tmp.cleanup();
3489 var path_buf: [256]u8 = undefined; 3463 const dir_path = tmp.path();
3490 const dir_path = try tmp.dir.realpath(".", &path_buf);
3491 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qtwo.sock", .{dir_path}); 3464 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qtwo.sock", .{dir_path});
3492 defer alloc.free(sock_path); 3465 defer alloc.free(sock_path);
3493 3466
@@ -3555,10 +3528,9 @@ test "Server: one QUIC client leaving does not disturb the other" {
3555 3528
3556 test "Server: a QUIC client that stops reading is dropped by the cap, not tolerated" { 3529 test "Server: a QUIC client that stops reading is dropped by the cap, not tolerated" {
3557 const alloc = std.testing.allocator; 3530 const alloc = std.testing.allocator;
3558 var tmp = std.testing.tmpDir(.{}); 3531 var tmp = try TmpDir.make();
3559 defer tmp.cleanup(); 3532 defer tmp.cleanup();
3560 var path_buf: [256]u8 = undefined; 3533 const dir_path = tmp.path();
3561 const dir_path = try tmp.dir.realpath(".", &path_buf);
3562 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qcap.sock", .{dir_path}); 3534 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qcap.sock", .{dir_path});
3563 defer alloc.free(sock_path); 3535 defer alloc.free(sock_path);
3564 3536
@@ -3612,10 +3584,9 @@ test "Server: a QUIC client that stops reading is dropped by the cap, not tolera
3612 3584
3613 test "Server: drainPending waits for a QUIC client's acks, not just its queue" { 3585 test "Server: drainPending waits for a QUIC client's acks, not just its queue" {
3614 const alloc = std.testing.allocator; 3586 const alloc = std.testing.allocator;
3615 var tmp = std.testing.tmpDir(.{}); 3587 var tmp = try TmpDir.make();
3616 defer tmp.cleanup(); 3588 defer tmp.cleanup();
3617 var path_buf: [256]u8 = undefined; 3589 const dir_path = tmp.path();
3618 const dir_path = try tmp.dir.realpath(".", &path_buf);
3619 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qdrain.sock", .{dir_path}); 3590 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qdrain.sock", .{dir_path});
3620 defer alloc.free(sock_path); 3591 defer alloc.free(sock_path);
3621 3592
src/testtmp.zig
Old New
@@ -0,0 +1,86 @@
1 //! Temp directories with SHORT paths, for tests that bind unix sockets.
2 //!
3 //! `std.testing.tmpDir` puts its directory under `.zig-cache/tmp`, which
4 //! makes the path as long as wherever the repository happens to be checked
5 //! out. A unix socket address caps its path at 108 bytes (`sun_path`), so a
6 //! checkout a couple of directories deeper than usual turns every
7 //! socket-binding test in this project into a `NameTooLong` failure — and
8 //! that failure surfaces from inside `std.net`, so it reads like a bug in
9 //! the code under test rather than a fact about the checkout. It has now
10 //! cost three people a gate cycle each, which is three more than a hazard
11 //! that is this cheap to remove should cost anyone.
12 //!
13 //! Same shape as `std.testing.tmpDir` — `.dir` and `.cleanup()` — plus
14 //! `path()`, which is what socket paths get built from. A directory here is
15 //! ~21 characters regardless of where the repository lives.
16 const std = @import("std");
17
18 pub const TmpDir = struct {
19 dir: std.fs.Dir,
20 buf: [32]u8 = undefined,
21 len: usize = 0,
22
23 pub fn make() !TmpDir {
24 var self: TmpDir = .{ .dir = undefined };
25 while (true) {
26 const p = try std.fmt.bufPrint(
27 &self.buf,
28 "/tmp/mux-t{x:0>12}",
29 .{std.crypto.random.int(u48)},
30 );
31 self.len = p.len;
32 std.fs.cwd().makeDir(p) catch |err| switch (err) {
33 // Two tests in the same millisecond is ordinary; two that
34 // drew the same 48 bits is not, but it costs one retry.
35 error.PathAlreadyExists => continue,
36 else => return err,
37 };
38 self.dir = try std.fs.cwd().openDir(p, .{ .iterate = true });
39 return self;
40 }
41 }
42
43 pub fn path(self: *const TmpDir) []const u8 {
44 return self.buf[0..self.len];
45 }
46
47 /// Idempotent, because the natural way to use this is a `defer` plus an
48 /// occasional early cleanup, and closing an already-closed directory
49 /// aborts rather than complaining.
50 pub fn cleanup(self: *TmpDir) void {
51 if (self.len == 0) return;
52 const p = self.path();
53 self.dir.close();
54 std.fs.cwd().deleteTree(p) catch {};
55 self.len = 0;
56 }
57 };
58
59 test "TmpDir: a path short enough to bind a socket in" {
60 var tmp = try TmpDir.make();
61 defer tmp.cleanup();
62
63 // The number that matters. sun_path is 108 bytes including the
64 // terminator, and this leaves room for a filename inside it.
65 try std.testing.expect(tmp.path().len < 32);
66 try std.testing.expect(std.mem.startsWith(u8, tmp.path(), "/tmp/"));
67
68 // It really is a directory, and it really is writable.
69 try tmp.dir.writeFile(.{ .sub_path = "probe", .data = "x" });
70
71 // ...and a socket binds in it, which is the whole point. Built the way
72 // the tests build theirs, so the length being tested is the real one.
73 var buf: [128]u8 = undefined;
74 const sock_path = try std.fmt.bufPrint(&buf, "{s}/probe.sock", .{tmp.path()});
75 const addr = try std.net.Address.initUnix(sock_path);
76 var server = try addr.listen(.{});
77 server.deinit();
78
79 const saved = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{tmp.path()});
80 defer std.testing.allocator.free(saved);
81 tmp.cleanup();
82 try std.testing.expectError(error.FileNotFound, std.fs.cwd().access(saved, .{}));
83 // The `defer` above will call cleanup a second time. That has to be
84 // harmless, or every test using this would have to choose between an
85 // early cleanup and a defer — and closing a closed directory aborts.
86 }