diff --git a/build.zig b/build.zig index 3663992..dd37cb6 100644 --- a/build.zig +++ b/build.zig @@ -193,6 +193,16 @@ pub fn build(b: *std.Build) void { run_cmd.addArgs(args); } + // Corpus paths for the LSP corpus-sweep test (src/lsp/corpus_sweep.test.zig). + // Inject absolute corpus dirs at configure time so the in-process analyzer + // sweep is CWD-independent; the test still ENUMERATES the directory + // contents at runtime (new examples are covered with no test edit). + const corpus_opts = b.addOptions(); + corpus_opts.addOption([]const u8, "examples_dir", b.path("examples").getPath(b)); + corpus_opts.addOption([]const u8, "issues_dir", b.path("issues").getPath(b)); + corpus_opts.addOption([]const u8, "library_dir", b.path("library").getPath(b)); + mod.addOptions("corpus_paths", corpus_opts); + const mod_tests = b.addTest(.{ .root_module = mod, }); diff --git a/src/lsp/corpus_sweep.test.zig b/src/lsp/corpus_sweep.test.zig new file mode 100644 index 0000000..22dd784 --- /dev/null +++ b/src/lsp/corpus_sweep.test.zig @@ -0,0 +1,73 @@ +const std = @import("std"); +const corpus_paths = @import("corpus_paths"); +const doc_mod = @import("document.zig"); + +// Permanent LSP corpus-sweep test (distribution step B). Drives the editor +// analyzer (`DocumentStore.analyzeDocument` — the exact path `server.zig`'s +// `textDocument/didOpen` handler uses) over EVERY `.sx` file in the example + +// issue corpora, in process. The contract is simply: analysis must complete +// without a panic/abort for any file. A panic aborts the whole test binary — +// that is the loud CI signal that some new AST node shape crashes the analyzer +// (the bug class issue 0099 fixed at `sema.zig`'s `resolveTypeNode`). Files +// that merely fail to parse or sema cleanly are fine: `analyzeDocument` records +// a null index and returns, which counts as a clean (non-crashing) outcome. +// +// The corpus directories are injected as absolute paths at configure time (see +// build.zig `corpus_paths`) so the sweep is CWD-independent. The FILE LIST is +// still read from disk at test time, so new examples are covered automatically +// with no edit to this file. + +var g_test_threaded: ?std.Io.Threaded = null; +fn test_io() std.Io { + if (g_test_threaded == null) { + g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{}); + } + return g_test_threaded.?.io(); +} + +/// Analyze every `.sx` file directly under `dir` through the didOpen pipeline. +/// Returns the number of files swept. Imports resolve against the shipped +/// `library/` so the analyzer runs over real, fully-resolved code (maximum +/// crash surface), exactly like an editor session opened on the repo. Set +/// `SX_LSP_SWEEP_VERBOSE` to print each file before it is analyzed — on a crash +/// the last printed line names the offending file. +fn sweepDirectory(alloc: std.mem.Allocator, io: std.Io, dir: []const u8) !usize { + const verbose = std.c.getenv("SX_LSP_SWEEP_VERBOSE") != null; + + const lib_paths = [_][]const u8{corpus_paths.library_dir}; + var store = doc_mod.DocumentStore.init(alloc, io, &lib_paths); + store.root_path = std.fs.path.dirname(corpus_paths.examples_dir) orelse ""; + + const files = store.listDirectoryFiles(dir) orelse return error.CorpusDirNotFound; + for (files) |path| { + if (verbose) std.debug.print("[lsp-sweep] {s}\n", .{path}); + const bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, path, alloc, .limited(10 * 1024 * 1024)); + const source = try alloc.dupeZ(u8, bytes); + const doc = try store.openOrUpdate(path, source, 1); + // didOpen swallows analyze errors (clean failures); a genuine crash + // panics and aborts here — exactly the regression signal we want. + store.analyzeDocument(doc) catch {}; + } + return files.len; +} + +test "lsp corpus sweep: every examples/*.sx analyzes without panicking" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = test_io(); + + const n = try sweepDirectory(alloc, io, corpus_paths.examples_dir); + std.debug.print("[lsp-sweep] examples: analyzed {d} files without a crash\n", .{n}); + try std.testing.expect(n > 0); +} + +test "lsp corpus sweep: every issues/*.sx repro analyzes without panicking" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = test_io(); + + const n = try sweepDirectory(alloc, io, corpus_paths.issues_dir); + std.debug.print("[lsp-sweep] issues: analyzed {d} files without a crash\n", .{n}); +} diff --git a/src/root.zig b/src/root.zig index 97b1137..faf2280 100644 --- a/src/root.zig +++ b/src/root.zig @@ -23,6 +23,7 @@ pub const lsp = struct { pub const types = @import("lsp/types.zig"); pub const document = @import("lsp/document.zig"); pub const document_tests = @import("lsp/document.test.zig"); + pub const corpus_sweep_tests = @import("lsp/corpus_sweep.test.zig"); }; test { @@ -37,6 +38,7 @@ test { _ = lsp.server; _ = lsp.document; _ = lsp.document_tests; + _ = lsp.corpus_sweep_tests; _ = lsp.types; _ = lsp.transport; }