a73x

7c83f8de

build: a server_test_ file nobody imports fails the build

a73x   2026-08-26 17:15

Commit message
build: a server_test_ file nobody imports fails the build

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

build.zig
Old New
@@ -381,6 +381,73 @@ fn checkGrantsUsed(b: *std.Build) void {
381 } 381 }
382 } 382 }
383 383
384 /// server.zig's tests live in `src/server_test_*.zig`, and analysis is what
385 /// registers a test: the `_ = @import(…)` lines in server.zig's test block
386 /// are the only thing that reaches them. Drop one line and that whole domain
387 /// stops running — against a green tree, because a suite that never ran
388 /// fails nothing. Nothing in the toolchain notices; only a test count someone
389 /// wrote down by hand would, and the count is not written down anywhere.
390 ///
391 /// Both directions are gated, because either half alone is silent. A file
392 /// with no import is tests nobody runs. An import with no file is a rename
393 /// that took a domain with it — that one is at least a compile error today,
394 /// but the message names a missing file rather than the rule, and this is
395 /// where the rule lives.
396 ///
397 /// Textual, for checkGrantsUsed's reason and soundly for the same one: the
398 /// import is a relative path, so server.zig is the only file that can spell
399 /// it. Globbed rather than listed for zigFilesIn's: a domain file added
400 /// tomorrow is covered without anybody remembering to add it here.
401 fn checkServerTestsReached(b: *std.Build) void {
402 const root = "src/server.zig";
403 const src = b.build_root.handle.readFileAlloc(b.allocator, root, 4 << 20) catch |err|
404 fatal("server tests: cannot read {s} ({s})", .{ root, @errorName(err) });
405
406 var dir = b.build_root.handle.openDir("src", .{ .iterate = true }) catch |e|
407 fatal("server tests: cannot open src/ ({s})", .{@errorName(e)});
408 defer dir.close();
409
410 var found: usize = 0;
411 var it = dir.iterate();
412 while (it.next() catch |e|
413 fatal("server tests: cannot list src/ ({s})", .{@errorName(e)})) |ent|
414 {
415 if (ent.kind != .file) continue;
416 if (!std.mem.startsWith(u8, ent.name, "server_test_")) continue;
417 if (!std.mem.endsWith(u8, ent.name, ".zig")) continue;
418 found += 1;
419 if (std.mem.indexOf(u8, src, b.fmt("@import(\"{s}\")", .{ent.name})) == null) fatal(
420 "server tests: src/{s} exists, but {s} never writes " ++
421 "@import(\"{s}\") — add the line to its test block; until then " ++
422 "every test in that file is unreached and passes by not running",
423 .{ ent.name, root, ent.name },
424 );
425 }
426 // An empty glob would make this gate green forever, the doc gate's hazard.
427 if (found == 0) fatal(
428 "server tests: no src/server_test_*.zig at all — if the tests moved " ++
429 "back into {s}, delete this check rather than leave it passing on " ++
430 "an empty set",
431 .{root},
432 );
433
434 const needle = "@import(\"server_test_";
435 var rest = src;
436 while (std.mem.indexOf(u8, rest, needle)) |at| {
437 const open = at + "@import(\"".len;
438 const close = std.mem.indexOfScalarPos(u8, rest, open, '"') orelse
439 fatal("server tests: unterminated @import in {s}", .{root});
440 const name = rest[open..close];
441 dir.access(name, .{}) catch fatal(
442 "server tests: {s} writes @import(\"{s}\"), but src/{s} does not " ++
443 "exist — point the line at the file the tests live in now; a " ++
444 "line naming nothing is a domain of tests nobody runs",
445 .{ root, name, name },
446 );
447 rest = rest[close..];
448 }
449 }
450
384 /// The gates that are shell get a gate of their own. test/*.sh and tools/*.sh 451 /// The gates that are shell get a gate of their own. test/*.sh and tools/*.sh
385 /// ARE the e2e, agent, soak and valgrind checks; nothing type-checks them, so 452 /// ARE the e2e, agent, soak and valgrind checks; nothing type-checks them, so
386 /// an unbalanced quote turns a gate into a script that dies on line 3 — and a 453 /// an unbalanced quote turns a gate into a script that dies on line 3 — and a
@@ -598,6 +665,8 @@ pub fn build(b: *std.Build) void {
598 }; 665 };
599 // No grant outlives its @import (and the message names the row). 666 // No grant outlives its @import (and the message names the row).
600 checkGrantsUsed(b); 667 checkGrantsUsed(b);
668 // ...and no domain of server.zig's tests goes unreached (nor the reverse).
669 checkServerTestsReached(b);
601 670
602 // Two instances per row that has test grants: `mods[i]` is production — 671 // Two instances per row that has test grants: `mods[i]` is production —
603 // what the exes are built from and what every importer sees — and 672 // what the exes are built from and what every importer sees — and
src/server.zig
Old New
@@ -3984,13 +3984,13 @@ pub const Server = struct {
3984 test { 3984 test {
3985 std.testing.refAllDeclsRecursive(@This()); 3985 std.testing.refAllDeclsRecursive(@This());
3986 3986
3987 // The tests moved out by domain; this is what makes the runner find 3987 // Reaching a file is what registers its tests; build.zig gates the list.
3988 // them, and a file dropped from the list stops running silently.
3989 _ = @import("server_test_agent.zig"); 3988 _ = @import("server_test_agent.zig");
3990 _ = @import("server_test_attach.zig"); 3989 _ = @import("server_test_attach.zig");
3991 _ = @import("server_test_await.zig"); 3990 _ = @import("server_test_await.zig");
3992 _ = @import("server_test_clipboard.zig"); 3991 _ = @import("server_test_clipboard.zig");
3993 _ = @import("server_test_deliver.zig"); 3992 _ = @import("server_test_deliver.zig");
3993 _ = @import("server_test_harness.zig");
3994 _ = @import("server_test_modes.zig"); 3994 _ = @import("server_test_modes.zig");
3995 _ = @import("server_test_quic.zig"); 3995 _ = @import("server_test_quic.zig");
3996 _ = @import("server_test_session.zig"); 3996 _ = @import("server_test_session.zig");