test: group examples into per-category folders

Move examples/*.sx and their expected/ snapshots into per-category
subfolders (examples/<category>/...). Folder = leading filename token,
with ffi-objc/ffi-jni kept whole; filenames are unchanged. The corpus
runner and LSP sweep now discover each category's expected/ dir, while
issues/ stays flat. Example 1058's repo-root-relative companion import
is made file-relative. Path strings embedded in 164 snapshots were
regenerated (path-only changes). Test-layout docs in CLAUDE.md updated.
This commit is contained in:
agra
2026-06-21 14:41:34 +03:00
parent 6d1409bc1f
commit 66bdc70bf1
3357 changed files with 456 additions and 363 deletions

View File

@@ -333,6 +333,15 @@ fn cleanupApk(a: std.mem.Allocator, io: std.Io, out_abs: []const u8, so_abs: []c
/// Run every `<root>/expected/*.exit` test. Appends a formatted diagnostic to
/// `failures` (owned by `fail_gpa`) for each mismatch. Returns the number of
/// tests actually run (markers whose `.sx` is missing are skipped).
/// Sweep a corpus root, discovering every `expected/` directory under it and
/// running the markers in each. Two layouts are supported simultaneously:
/// * flat: `<root>/expected/<name>.exit` with `<root>/<name>.sx`
/// (used by `issues/`)
/// * by-category: `<root>/<cat>/expected/<name>.exit` with
/// `<root>/<cat>/<name>.sx` (used by `examples/`)
/// `rel_prefix` (the source dir relative to repo root, e.g. `examples/basic`)
/// is what gets handed to `sx` as the repo-relative `.sx` path, so diagnostics
/// and snapshots stay normalized to the on-disk location.
fn sweepRoot(
fail_gpa: std.mem.Allocator,
io: std.Io,
@@ -345,11 +354,52 @@ fn sweepRoot(
const repo_root = std.fs.path.dirname(root_dir) orelse ".";
const root_base = std.fs.path.basename(root_dir); // "examples" | "issues"
var arena_state = std.heap.ArenaAllocator.init(fail_gpa);
defer arena_state.deinit();
const a = arena_state.allocator();
var total: usize = 0;
// A direct `<root>/expected/` (flat layout, e.g. issues/).
if (std.Io.Dir.access(.cwd(), io, try std.fs.path.join(a, &.{ root_dir, "expected" }), .{})) |_| {
total += try sweepExpectedDir(fail_gpa, io, repo_root, root_dir, root_base, failures);
} else |_| {}
// Each immediate child dir holding an `expected/` (by-category layout,
// e.g. examples/<cat>/). Collect child names first — spawning subprocesses
// while iterating the dir handle is asking for trouble.
var root = std.Io.Dir.openDirAbsolute(io, root_dir, .{ .iterate = true }) catch return total;
defer root.close(io);
var child_names: std.ArrayList([]const u8) = .empty;
var rit = root.iterate();
while (try rit.next(io)) |entry| {
if (entry.kind != .directory) continue;
if (std.mem.eql(u8, entry.name, "expected")) continue;
try child_names.append(a, try a.dupe(u8, entry.name));
}
for (child_names.items) |child| {
const child_dir = try std.fs.path.join(a, &.{ root_dir, child });
const child_expected = try std.fs.path.join(a, &.{ child_dir, "expected" });
std.Io.Dir.access(.cwd(), io, child_expected, .{}) catch continue;
const rel_prefix = try std.fmt.allocPrint(a, "{s}/{s}", .{ root_base, child });
total += try sweepExpectedDir(fail_gpa, io, repo_root, child_dir, rel_prefix, failures);
}
return total;
}
fn sweepExpectedDir(
fail_gpa: std.mem.Allocator,
io: std.Io,
repo_root: []const u8,
source_dir: []const u8,
rel_prefix: []const u8,
failures: *std.ArrayList([]const u8),
) !usize {
var name_arena_state = std.heap.ArenaAllocator.init(fail_gpa);
defer name_arena_state.deinit();
const name_arena = name_arena_state.allocator();
const expected_dir_path = try std.fs.path.join(name_arena, &.{ root_dir, "expected" });
const expected_dir_path = try std.fs.path.join(name_arena, &.{ source_dir, "expected" });
var dir = std.Io.Dir.openDirAbsolute(io, expected_dir_path, .{ .iterate = true }) catch return 0;
defer dir.close(io);
@@ -378,11 +428,11 @@ fn sweepRoot(
// 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 });
const this_rel = try std.fmt.allocPrint(a, "{s}/{s}.sx", .{ rel_prefix, 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}) });
const sx_abs = try std.fs.path.join(a, &.{ source_dir, try std.fmt.allocPrint(a, "{s}.sx", .{name}) });
std.Io.Dir.access(.cwd(), io, sx_abs, .{}) catch { // marker without source
skipped += 1;
std.debug.print("[corpus-run] skip {s} (no {s}.sx)\n", .{ name, name });
@@ -390,7 +440,7 @@ fn sweepRoot(
};
ran += 1;
const rel_path = try std.fmt.allocPrint(a, "{s}/{s}.sx", .{ root_base, name });
const rel_path = try std.fmt.allocPrint(a, "{s}/{s}.sx", .{ rel_prefix, name });
const exp_dir = expected_dir_path;
const exit_raw = readOptional(io, a, try std.fmt.allocPrint(a, "{s}/{s}.exit", .{ exp_dir, name })) orelse "";
const out_raw = readOptional(io, a, try std.fmt.allocPrint(a, "{s}/{s}.stdout", .{ exp_dir, name })) orelse "";
@@ -646,9 +696,9 @@ fn sweepRoot(
try recordIfFailed(fail_gpa, failures, name, diag.items);
}
if (skipped > 0)
std.debug.print("[corpus-run] {s}: {d} marker(s) skipped (no matching .sx)\n", .{ root_base, skipped });
std.debug.print("[corpus-run] {s}: {d} marker(s) skipped (no matching .sx)\n", .{ rel_prefix, skipped });
if (corpus_paths.update_goldens)
std.debug.print("[corpus-run] {s}: {d} snapshot(s) regenerated\n", .{ root_base, updated });
std.debug.print("[corpus-run] {s}: {d} snapshot(s) regenerated\n", .{ rel_prefix, updated });
return ran;
}