test(lsp): permanent corpus-sweep over the editor analyzer [dist B]
Adds `src/lsp/corpus_sweep.test.zig`: a permanent test that drives the editor analyzer (`DocumentStore.analyzeDocument` — the exact path the server's `textDocument/didOpen` uses) over EVERY `.sx` file in the example + issue corpora, in process. The contract: analysis must complete without panic/abort for any file. A panic aborts the test binary — the loud CI signal that some new AST node shape crashes the analyzer (the bug class issue 0099 fixed at sema.zig:397). - Corpus dirs are injected as absolute paths at configure time (build.zig `corpus_paths` options module) 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 test edit. - Imports resolve against the shipped `library/` (root_path + stdlib path set), so the analyzer runs over real, fully-resolved code — maximum crash surface, mirroring an editor session opened on the repo. - Wired into `zig build test` via the `src/root.zig` lsp barrel, same mechanism document.test.zig uses (refAllDecls reaches one struct deep, so the file is referenced directly). - `SX_LSP_SWEEP_VERBOSE` prints each file before analysis; on a crash the last printed line names the offending file. Coverage: 470 examples + 1 issue repro analyze with zero crashes. Regression-guard proven: temporarily reverting A's sema.zig:397 fix (`@intCast(ate.length.data.int_literal.value)`) makes the sweep abort with `access of union field 'int_literal' while field 'identifier' is active`; restoring it turns the sweep green.
This commit is contained in:
10
build.zig
10
build.zig
@@ -193,6 +193,16 @@ pub fn build(b: *std.Build) void {
|
|||||||
run_cmd.addArgs(args);
|
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(.{
|
const mod_tests = b.addTest(.{
|
||||||
.root_module = mod,
|
.root_module = mod,
|
||||||
});
|
});
|
||||||
|
|||||||
73
src/lsp/corpus_sweep.test.zig
Normal file
73
src/lsp/corpus_sweep.test.zig
Normal file
@@ -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});
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub const lsp = struct {
|
|||||||
pub const types = @import("lsp/types.zig");
|
pub const types = @import("lsp/types.zig");
|
||||||
pub const document = @import("lsp/document.zig");
|
pub const document = @import("lsp/document.zig");
|
||||||
pub const document_tests = @import("lsp/document.test.zig");
|
pub const document_tests = @import("lsp/document.test.zig");
|
||||||
|
pub const corpus_sweep_tests = @import("lsp/corpus_sweep.test.zig");
|
||||||
};
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@@ -37,6 +38,7 @@ test {
|
|||||||
_ = lsp.server;
|
_ = lsp.server;
|
||||||
_ = lsp.document;
|
_ = lsp.document;
|
||||||
_ = lsp.document_tests;
|
_ = lsp.document_tests;
|
||||||
|
_ = lsp.corpus_sweep_tests;
|
||||||
_ = lsp.types;
|
_ = lsp.types;
|
||||||
_ = lsp.transport;
|
_ = lsp.transport;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user