test harness: add -Dname to scope the corpus to specific examples

`zig build test -Dname=examples/0625-foo.sx[,examples/0626-bar.sx]` runs ONLY the
named example(s) — full repo-relative .sx paths, comma-separated (a leading `./`
is tolerated). Empty = run everything (unchanged default).

Why: a full `-Dupdate-goldens` re-runs and rewrites all ~690 snapshots, so one
flaky/host-divergent example (AOT links, cross-arch `target` examples) can clobber
a good snapshot. `-Dname` regenerates only the named example(s) and touches
nothing else. It also busts the cached test-run result — the corpus enumerates
.sx/expected files at runtime, so a bare snapshot edit alone is otherwise served
from cache.

- build.zig: new `name` option threaded onto corpus_paths.
- corpus_run.test.zig: `nameMatchesFilter` + a per-example skip in the run loop.
- CLAUDE.md: document the targeted-regen workflow under Snapshot integrity.
This commit is contained in:
agra
2026-06-17 14:55:06 +03:00
parent 0b4c50b187
commit 88c4cbcfa5
3 changed files with 54 additions and 0 deletions

View File

@@ -492,6 +492,23 @@ Safe workflow:
2. Only run `zig build test -Dupdate-goldens` when you've intentionally changed output (new feature, new test, changed formatting).
3. After regenerating, review the diff (`git diff examples/expected/ issues/expected/`) to confirm no error messages or empty output were captured.
**Scope a regen to specific examples with `-Dname`.** A *full* `-Dupdate-goldens`
re-runs and rewrites all ~690 snapshots, so a single flaky/host-divergent example
(AOT links, cross-arch `target` examples, anything that intermittently fails) can
silently clobber a good snapshot. To capture just the example(s) you added, pass
their full repo-relative `.sx` path(s), comma-separated — this rewrites ONLY those
and touches nothing else:
```sh
zig build test -Dname=examples/0625-comptime-weld-struct-field.sx -Dupdate-goldens
zig build test -Dname=examples/0625-foo.sx,examples/0626-bar.sx # verify just these
```
`-Dname` also busts the test-run cache (the corpus enumerates `.sx`/`expected/`
files at RUNTIME, so editing a snapshot alone does NOT force a re-run — a plain
`zig build test` may be served a cached result). Changing `-Dname` — or any
compiler source — forces a fresh run.
### Adding a new language feature
There is no monolithic smoke file — each feature is its own focused example.

View File

@@ -218,6 +218,20 @@ pub fn build(b: *std.Build) void {
"Regenerate example/issue snapshots instead of verifying them (use with `zig build test`)",
) orelse false;
corpus_opts.addOption(bool, "update_goldens", update_goldens);
// `zig build test -Dname=examples/0213-foo.sx[,examples/0214-bar.sx]` restricts
// the corpus runner to ONLY the named example(s) — full repo-relative `.sx`
// paths, comma-separated. Empty = run every example. Use it to verify or
// regenerate (-Dupdate-goldens) a specific example without re-running (or
// clobbering the snapshots of) the rest of the corpus. Because the value is
// baked into the corpus options module, changing it also busts the cached
// test-run result (the runner enumerates .sx/expected files at RUNTIME, so a
// bare snapshot edit alone would otherwise be served from cache).
const name_filter = b.option(
[]const u8,
"name",
"Run only the named example(s): comma-separated repo-relative .sx paths (e.g. examples/0213-foo.sx)",
) orelse "";
corpus_opts.addOption([]const u8, "name", name_filter);
mod.addOptions("corpus_paths", corpus_opts);
const mod_tests = b.addTest(.{

View File

@@ -160,6 +160,21 @@ fn readOptional(io: std.Io, gpa: std.mem.Allocator, abs_path: []const u8) ?[]u8
return std.Io.Dir.readFileAlloc(.cwd(), io, abs_path, gpa, .limited(MAX_OUTPUT)) catch null;
}
/// True when `rel_path` (a repo-relative `.sx` path like `examples/0213-foo.sx`)
/// matches the `-Dname` filter — a comma-separated list of full `.sx` paths.
/// Each entry is whitespace-trimmed and a leading `./` is ignored, so both
/// `examples/0213-foo.sx` and `./examples/0213-foo.sx` match.
fn nameMatchesFilter(filter: []const u8, rel_path: []const u8) bool {
var it = std.mem.splitScalar(u8, filter, ',');
while (it.next()) |raw| {
var entry = std.mem.trim(u8, raw, " \t\r\n");
if (std.mem.startsWith(u8, entry, "./")) entry = entry[2..];
if (entry.len == 0) continue;
if (std.mem.eql(u8, entry, rel_path)) return true;
}
return false;
}
/// Per-example build/run directives, parsed from an optional `<name>.build`
/// JSON sidecar (replaces the old standalone `.aot` marker). Output snapshots
/// (.exit/.stdout/.stderr/.ir) stay separate — they are regenerated data, not
@@ -281,6 +296,14 @@ fn sweepRoot(
_ = work_state.reset(.retain_capacity);
const a = work_state.allocator();
// `-Dname=<paths>` filter: when set, run ONLY the named example(s) (full
// repo-relative `.sx` paths, comma-separated). A non-matching example is
// dropped silently — not counted as ran or skipped.
if (corpus_paths.name.len > 0) {
const this_rel = try std.fmt.allocPrint(a, "{s}/{s}.sx", .{ root_base, name });
if (!nameMatchesFilter(corpus_paths.name, this_rel)) continue;
}
const sx_abs = try std.fs.path.join(a, &.{ root_dir, try std.fmt.allocPrint(a, "{s}.sx", .{name}) });
std.Io.Dir.access(.cwd(), io, sx_abs, .{}) catch { // marker without source
skipped += 1;