ce7fb793
build: every wall_test_* sibling is reached, or the build says which is not
a73x 2026-08-28 20:58
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -522,11 +522,11 @@ fn checkGrantsUsed(b: *std.Build) void { | |||
| 522 | } | 522 | } |
| 523 | } | 523 | } |
| 524 | 524 | ||
| 525 | /// server.zig's tests live beside it in `src/server/`, and analysis is what | 525 | /// A split file's tests live beside it, and analysis is what registers a |
| 526 | /// registers a test: the `_ = @import(…)` lines in server.zig's test block | 526 | /// test: the `_ = @import(…)` lines in the root's test block are the only |
| 527 | /// are the only thing that reaches them. Drop one line and that whole domain | 527 | /// thing that reaches them. Drop one line and that whole domain stops |
| 528 | /// stops running — against a green tree, because a suite that never ran | 528 | /// running — against a green tree, because a suite that never ran fails |
| 529 | /// fails nothing. Nothing in the toolchain notices; only a test count someone | 529 | /// nothing. Nothing in the toolchain notices; only a test count someone |
| 530 | /// wrote down by hand would, and the count is not written down anywhere. | 530 | /// wrote down by hand would, and the count is not written down anywhere. |
| 531 | /// | 531 | /// |
| 532 | /// Both directions are gated, because either half alone is silent. A file | 532 | /// Both directions are gated, because either half alone is silent. A file |
| @@ -536,54 +536,62 @@ fn checkGrantsUsed(b: *std.Build) void { | |||
| 536 | /// where the rule lives. | 536 | /// where the rule lives. |
| 537 | /// | 537 | /// |
| 538 | /// Textual, for checkGrantsUsed's reason and soundly for the same one: the | 538 | /// Textual, for checkGrantsUsed's reason and soundly for the same one: the |
| 539 | /// import is a relative path, so server.zig is the only file that can spell | 539 | /// import is a relative path, so the root is the only file that can spell |
| 540 | /// it. Globbed rather than listed for zigFilesIn's: a domain file added | 540 | /// it. Globbed rather than listed for zigFilesIn's: a domain file added |
| 541 | /// tomorrow is covered without anybody remembering to add it here. | 541 | /// tomorrow is covered without anybody remembering to add it here. |
| 542 | fn checkServerTestsReached(b: *std.Build) void { | 542 | const sibling_tests = [_]struct { root: []const u8, dir: []const u8, prefix: []const u8 }{ |
| 543 | const root = "src/server/server.zig"; | 543 | .{ .root = "src/server/server.zig", .dir = "src/server", .prefix = "server_test_" }, |
| 544 | .{ .root = "src/tui/wallview.zig", .dir = "src/tui", .prefix = "wall_test_" }, | ||
| 545 | }; | ||
| 546 | |||
| 547 | fn checkSiblingTestsReached(b: *std.Build) void { | ||
| 548 | for (sibling_tests) |spec| checkOneRootReaches(b, spec.root, spec.dir, spec.prefix); | ||
| 549 | } | ||
| 550 | |||
| 551 | fn checkOneRootReaches(b: *std.Build, root: []const u8, subdir: []const u8, prefix: []const u8) void { | ||
| 544 | const src = b.build_root.handle.readFileAlloc(b.allocator, root, 4 << 20) catch |err| | 552 | const src = b.build_root.handle.readFileAlloc(b.allocator, root, 4 << 20) catch |err| |
| 545 | fatal("server tests: cannot read {s} ({s})", .{ root, @errorName(err) }); | 553 | fatal("sibling tests: cannot read {s} ({s})", .{ root, @errorName(err) }); |
| 546 | 554 | ||
| 547 | var dir = b.build_root.handle.openDir("src/server", .{ .iterate = true }) catch |e| | 555 | var dir = b.build_root.handle.openDir(subdir, .{ .iterate = true }) catch |e| |
| 548 | fatal("server tests: cannot open src/server/ ({s})", .{@errorName(e)}); | 556 | fatal("sibling tests: cannot open {s}/ ({s})", .{ subdir, @errorName(e) }); |
| 549 | defer dir.close(); | 557 | defer dir.close(); |
| 550 | 558 | ||
| 551 | var found: usize = 0; | 559 | var found: usize = 0; |
| 552 | var it = dir.iterate(); | 560 | var it = dir.iterate(); |
| 553 | while (it.next() catch |e| | 561 | while (it.next() catch |e| |
| 554 | fatal("server tests: cannot list src/server/ ({s})", .{@errorName(e)})) |ent| | 562 | fatal("sibling tests: cannot list {s}/ ({s})", .{ subdir, @errorName(e) })) |ent| |
| 555 | { | 563 | { |
| 556 | if (ent.kind != .file) continue; | 564 | if (ent.kind != .file) continue; |
| 557 | if (!std.mem.startsWith(u8, ent.name, "server_test_")) continue; | 565 | if (!std.mem.startsWith(u8, ent.name, prefix)) continue; |
| 558 | if (!std.mem.endsWith(u8, ent.name, ".zig")) continue; | 566 | if (!std.mem.endsWith(u8, ent.name, ".zig")) continue; |
| 559 | found += 1; | 567 | found += 1; |
| 560 | if (std.mem.indexOf(u8, src, b.fmt("@import(\"{s}\")", .{ent.name})) == null) fatal( | 568 | if (std.mem.indexOf(u8, src, b.fmt("@import(\"{s}\")", .{ent.name})) == null) fatal( |
| 561 | "server tests: src/server/{s} exists, but {s} never writes " ++ | 569 | "sibling tests: {s}/{s} exists, but {s} never writes " ++ |
| 562 | "@import(\"{s}\") — add the line to its test block; until then " ++ | 570 | "@import(\"{s}\") — add the line to its test block; until then " ++ |
| 563 | "every test in that file is unreached and passes by not running", | 571 | "every test in that file is unreached and passes by not running", |
| 564 | .{ ent.name, root, ent.name }, | 572 | .{ subdir, ent.name, root, ent.name }, |
| 565 | ); | 573 | ); |
| 566 | } | 574 | } |
| 567 | // An empty glob would make this gate green forever, the doc gate's hazard. | 575 | // An empty glob would make this gate green forever, the doc gate's hazard. |
| 568 | if (found == 0) fatal( | 576 | if (found == 0) fatal( |
| 569 | "server tests: no src/server/server_test_*.zig at all — if the tests moved " ++ | 577 | "sibling tests: no {s}/{s}*.zig at all — if the tests moved " ++ |
| 570 | "back into {s}, delete this check rather than leave it passing on " ++ | 578 | "back into {s}, delete this row rather than leave it passing on " ++ |
| 571 | "an empty set", | 579 | "an empty set", |
| 572 | .{root}, | 580 | .{ subdir, prefix, root }, |
| 573 | ); | 581 | ); |
| 574 | 582 | ||
| 575 | const needle = "@import(\"server_test_"; | 583 | const needle = b.fmt("@import(\"{s}", .{prefix}); |
| 576 | var rest = src; | 584 | var rest = src; |
| 577 | while (std.mem.indexOf(u8, rest, needle)) |at| { | 585 | while (std.mem.indexOf(u8, rest, needle)) |at| { |
| 578 | const open = at + "@import(\"".len; | 586 | const open = at + "@import(\"".len; |
| 579 | const close = std.mem.indexOfScalarPos(u8, rest, open, '"') orelse | 587 | const close = std.mem.indexOfScalarPos(u8, rest, open, '"') orelse |
| 580 | fatal("server tests: unterminated @import in {s}", .{root}); | 588 | fatal("sibling tests: unterminated @import in {s}", .{root}); |
| 581 | const name = rest[open..close]; | 589 | const name = rest[open..close]; |
| 582 | dir.access(name, .{}) catch fatal( | 590 | dir.access(name, .{}) catch fatal( |
| 583 | "server tests: {s} writes @import(\"{s}\"), but src/server/{s} does not " ++ | 591 | "sibling tests: {s} writes @import(\"{s}\"), but {s}/{s} does not " ++ |
| 584 | "exist — point the line at the file the tests live in now; a " ++ | 592 | "exist — point the line at the file the tests live in now; a " ++ |
| 585 | "line naming nothing is a domain of tests nobody runs", | 593 | "line naming nothing is a domain of tests nobody runs", |
| 586 | .{ root, name, name }, | 594 | .{ root, name, subdir, name }, |
| 587 | ); | 595 | ); |
| 588 | rest = rest[close..]; | 596 | rest = rest[close..]; |
| 589 | } | 597 | } |
| @@ -806,8 +814,8 @@ pub fn build(b: *std.Build) void { | |||
| 806 | }; | 814 | }; |
| 807 | // No grant outlives its @import (and the message names the row). | 815 | // No grant outlives its @import (and the message names the row). |
| 808 | checkGrantsUsed(b); | 816 | checkGrantsUsed(b); |
| 809 | // ...and no domain of server.zig's tests goes unreached (nor the reverse). | 817 | // ...and no domain of a split file's tests goes unreached (nor the reverse). |
| 810 | checkServerTestsReached(b); | 818 | checkSiblingTestsReached(b); |
| 811 | // ...and every module still sits in the folder of the thing that owns it. | 819 | // ...and every module still sits in the folder of the thing that owns it. |
| 812 | checkFolderRules(b); | 820 | checkFolderRules(b); |
| 813 | 821 | ||