7c83f8de
build: a server_test_ file nobody imports fails the build
a73x 2026-08-26 17:15
Commit message
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"); |