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"); } }