From 2542d3adfecf6e9704843c50b6e45770132141eb Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 29 Jul 2026 02:20:54 -0500 Subject: [PATCH] build: fail the test step when a file's tests are unreachable Zig only runs tests in files the test root actually resolves. A re-export like `pub const Foo = @import("f.zig").Foo` never references the decl in a test build, so f.zig is never resolved: its tests do not run, and its function bodies are never analyzed, so even hard compile errors stay hidden. root.zig carried a hand-written list of files to pull in, and it rotted -- identity, oauth, jetstream, xrpc/transport and xrpc/xrpc all drifted out of it. That is how da77f87 was possible: oauth/client.zig had two compile errors sitting on main while CI reported green, and its 13 tests had never run. Completing the list takes the suite from 486 to 551. unreachedTestFiles() in build.zig now walks src/ and fails the test step with the exact import lines to paste, so the list cannot silently rot again. Co-Authored-By: Claude Opus 5 (1M context) --- build.zig | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index ec63e03..828cddf 100644 --- a/build.zig +++ b/build.zig @@ -70,6 +70,9 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "run unit tests"); test_step.dependOn(&run_tests.step); + if (unreachedTestFiles(b)) |missing| { + test_step.dependOn(&b.addFail(missing).step); + } // jetstream smoke test const jetstream_smoke = b.addExecutable(.{ @@ -264,3 +267,52 @@ pub fn build(b: *std.Build) void { }); b.installArtifact(publish_docs); } + +/// Zig only runs tests in files the test root actually resolves, so every file +/// carrying a `test` block has to be listed in root.zig's `is_test` import +/// block. That list is hand-written and silently rotted once already: five +/// subsystems drifted out of it, taking 66 tests and two compile errors with +/// them. Returns an error message naming any file the list has missed. +fn unreachedTestFiles(b: *std.Build) ?[]const u8 { + const io = b.graph.io; + const gpa = b.allocator; + const limit: std.Io.Limit = .limited(4 << 20); + + const root_dir = b.build_root.handle; + const root = root_dir.readFileAlloc(io, "src/root.zig", gpa, limit) catch return null; + + var src = root_dir.openDir(io, "src", .{ .iterate = true }) catch return null; + defer src.close(io); + var walker = src.walk(gpa) catch return null; + defer walker.deinit(); + + var missing: std.ArrayList(u8) = .empty; + while (walker.next(io) catch return null) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".zig")) continue; + if (std.mem.eql(u8, entry.path, "root.zig")) continue; + + const path = b.pathJoin(&.{ "src", entry.path }); + const source = root_dir.readFileAlloc(io, path, gpa, limit) catch continue; + if (!declaresTest(source)) continue; + + const posix = std.mem.replaceOwned(u8, gpa, entry.path, "\\", "/") catch return null; + const needle = b.fmt("@import(\"{s}\")", .{posix}); + if (std.mem.indexOf(u8, root, needle) == null) { + missing.print(gpa, "\n _ = @import(\"{s}\");", .{posix}) catch return null; + } + } + + if (missing.items.len == 0) return null; + return b.fmt( + "these files declare tests that will never run -- add them to the `is_test` block in src/root.zig:{s}", + .{missing.items}, + ); +} + +fn declaresTest(source: []const u8) bool { + var lines = std.mem.splitScalar(u8, source, '\n'); + while (lines.next()) |line| { + if (std.mem.startsWith(u8, line, "test \"") or std.mem.startsWith(u8, line, "test {")) return true; + } + return false; +} diff --git a/src/root.zig b/src/root.zig index 094bfdd..c60f863 100644 --- a/src/root.zig +++ b/src/root.zig @@ -75,16 +75,46 @@ pub const FirehoseEvent = firehose.Event; // - *_test.zig files are supplemental black-box, stress, and compliance suites // that must be imported explicitly because Zig does not auto-discover tests // - interop fixtures are resolved by build.zig's lazy dependency wiring +// +// A re-export like `pub const Foo = @import("f.zig").Foo` does NOT reach f.zig +// in a test build: nothing references the decl, so the file is never resolved +// and its tests never run — nor are its function bodies ever analyzed, so even +// outright compile errors stay hidden. Every file carrying a `test` block must +// be listed here. `zig build test` fails if one is missing; see build.zig. comptime { if (@import("builtin").is_test) { - _ = @import("internal/testing/interop_tests.zig"); - _ = @import("internal/repo/repo.zig"); - _ = @import("internal/repo/cbor_test.zig"); - _ = @import("internal/repo/cbor_read_test.zig"); - _ = @import("internal/repo/cbor_write_test.zig"); + _ = @import("internal/crypto/jwt.zig"); + _ = @import("internal/crypto/keypair.zig"); + _ = @import("internal/crypto/multibase.zig"); + _ = @import("internal/crypto/multicodec.zig"); + _ = @import("internal/identity/did_document.zig"); + _ = @import("internal/identity/did_resolver.zig"); + _ = @import("internal/identity/handle_resolver.zig"); + _ = @import("internal/identity/network_safety.zig"); + _ = @import("internal/oauth/client.zig"); + _ = @import("internal/oauth/primitives.zig"); + _ = @import("internal/repo/car.zig"); _ = @import("internal/repo/car_test.zig"); + _ = @import("internal/repo/cbor.zig"); + _ = @import("internal/repo/cbor_read_test.zig"); _ = @import("internal/repo/cbor_rfc8949_test.zig"); + _ = @import("internal/repo/cbor_test.zig"); + _ = @import("internal/repo/cbor_write_test.zig"); + _ = @import("internal/repo/mst.zig"); _ = @import("internal/repo/mst_test.zig"); + _ = @import("internal/repo/repo.zig"); _ = @import("internal/streaming/firehose.zig"); + _ = @import("internal/streaming/jetstream.zig"); + _ = @import("internal/streaming/sync.zig"); + _ = @import("internal/syntax/at_uri.zig"); + _ = @import("internal/syntax/did.zig"); + _ = @import("internal/syntax/handle.zig"); + _ = @import("internal/syntax/nsid.zig"); + _ = @import("internal/syntax/rkey.zig"); + _ = @import("internal/syntax/tid.zig"); + _ = @import("internal/testing/interop_tests.zig"); + _ = @import("internal/xrpc/json.zig"); + _ = @import("internal/xrpc/transport.zig"); + _ = @import("internal/xrpc/xrpc.zig"); } } -- 2.51.2