From 7ae92d3e6099187703f6a22a5e8c72e251facbd2 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 2 Jun 2026 01:54:37 +0200 Subject: [PATCH] refactor(test): hoist duplicated test helpers into ailang-test-support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #60 The fixture-corpus filter (NON_PARSEABLE_FIXTURES + list_ail_fixtures) was copy-pasted across three test crates and drifted out of sync as new reject fixtures landed; the examples_dir / workspace_root path walk was reimplemented ~30 more times across the suite, under two spellings (workspace_root / ws_root). Introduce crates/ailang-test-support — a zero-dependency, dev-only leaf crate — as the single home for these helpers: - NON_PARSEABLE_FIXTURES, list_ail_fixtures - workspace_root, examples_dir - canonical_workspace_root (symlink-resolved variant, for the tsan/ race tests that feed the root into native build steps) All integration-test copies across ailang-core, ailang-surface, ailang-prose, ailang-check, and ail now import from the shared crate; the local definitions and their now-orphaned path imports are removed. Path helpers resolve via the support crate's own CARGO_MANIFEST_DIR, so they return the correct absolute paths from any caller. ail_bin helpers are intentionally left in place: they depend on env!("CARGO_BIN_EXE_ail"), which Cargo only defines when compiling the ail crate's own integration tests, so they cannot move to a shared crate. Behaviour-identical: same paths, same fixture lists; full workspace test suite green. Net -177 LOC. --- Cargo.lock | 9 +++ Cargo.toml | 2 + crates/ail/Cargo.toml | 1 + crates/ail/tests/ct1_check_cli.rs | 10 +-- crates/ail/tests/e2e.rs | 10 +-- crates/ail/tests/embed_e2e.rs | 6 +- .../ail/tests/embed_missing_main_baseline.rs | 7 +- crates/ail/tests/embed_rc_accounting_tsan.rs | 8 +-- .../ail/tests/embed_rc_global_stats_race.rs | 6 +- crates/ail/tests/embed_record_e2e.rs | 6 +- crates/ail/tests/embed_staticlib_cli.rs | 10 +-- crates/ail/tests/embed_swarm_tsan.rs | 7 +- crates/ail/tests/embed_tick_e2e.rs | 6 +- crates/ail/tests/emit_ir_staticlib_cli.rs | 12 ++-- crates/ail/tests/floats_e2e.rs | 9 +-- crates/ail/tests/ir_snapshot.rs | 10 +-- crates/ail/tests/mono_xmod_ctor_pattern.rs | 9 +-- crates/ail/tests/mq3_multi_class_e2e.rs | 8 +-- crates/ail/tests/roundtrip_cli.rs | 47 +------------ crates/ail/tests/typeclass_22b2.rs | 12 +--- crates/ail/tests/typeclass_22b3.rs | 9 +-- crates/ailang-check/Cargo.toml | 3 + .../ailang-check/tests/embed_export_gate.rs | 8 +-- .../tests/env_construction_pin.rs | 10 +-- .../tests/loop_recur_typecheck_pin.rs | 10 +-- crates/ailang-check/tests/lower_to_mir_ty.rs | 11 +-- .../tests/method_collision_pin.rs | 10 +-- crates/ailang-check/tests/workspace.rs | 6 +- crates/ailang-core/Cargo.toml | 1 + .../ailang-core/tests/carve_out_inventory.rs | 6 +- .../ailang-core/tests/ctt2_registry_rekey.rs | 10 +-- crates/ailang-core/tests/hash_pin.rs | 6 +- crates/ailang-core/tests/schema_coverage.rs | 36 +--------- crates/ailang-core/tests/workspace_pin.rs | 5 +- crates/ailang-prose/Cargo.toml | 1 + crates/ailang-prose/tests/snapshot.rs | 11 +-- crates/ailang-surface/Cargo.toml | 1 + crates/ailang-surface/tests/round_trip.rs | 37 +--------- crates/ailang-test-support/Cargo.toml | 11 +++ crates/ailang-test-support/src/lib.rs | 68 +++++++++++++++++++ 40 files changed, 139 insertions(+), 316 deletions(-) create mode 100644 crates/ailang-test-support/Cargo.toml create mode 100644 crates/ailang-test-support/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 26dfd38..58fd9d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "ailang-core", "ailang-prose", "ailang-surface", + "ailang-test-support", "anyhow", "blake3", "clap", @@ -25,6 +26,7 @@ dependencies = [ "ailang-core", "ailang-mir", "ailang-surface", + "ailang-test-support", "indexmap", "serde", "serde_json", @@ -48,6 +50,7 @@ name = "ailang-core" version = "0.0.1" dependencies = [ "ailang-surface", + "ailang-test-support", "blake3", "indexmap", "serde", @@ -73,6 +76,7 @@ version = "0.0.1" dependencies = [ "ailang-core", "ailang-surface", + "ailang-test-support", ] [[package]] @@ -81,11 +85,16 @@ version = "0.0.1" dependencies = [ "ailang-core", "ailang-kernel", + "ailang-test-support", "serde_json", "tempfile", "thiserror", ] +[[package]] +name = "ailang-test-support" +version = "0.0.1" + [[package]] name = "anstream" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index dab9976..787a23e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/ailang-kernel", "crates/ailang-surface", "crates/ailang-prose", + "crates/ailang-test-support", "crates/ail", ] # `ail-embed/` is intentionally NOT a member: it depends on the @@ -39,6 +40,7 @@ ailang-codegen = { path = "crates/ailang-codegen" } ailang-kernel = { path = "crates/ailang-kernel" } ailang-surface = { path = "crates/ailang-surface" } ailang-prose = { path = "crates/ailang-prose" } +ailang-test-support = { path = "crates/ailang-test-support" } [profile.release] lto = "thin" diff --git a/crates/ail/Cargo.toml b/crates/ail/Cargo.toml index b624f77..3888c20 100644 --- a/crates/ail/Cargo.toml +++ b/crates/ail/Cargo.toml @@ -21,3 +21,4 @@ anyhow.workspace = true [dev-dependencies] tempfile.workspace = true blake3.workspace = true +ailang-test-support.workspace = true diff --git a/crates/ail/tests/ct1_check_cli.rs b/crates/ail/tests/ct1_check_cli.rs index 22b8bff..8d86e3d 100644 --- a/crates/ail/tests/ct1_check_cli.rs +++ b/crates/ail/tests/ct1_check_cli.rs @@ -11,6 +11,7 @@ //! someone reverts the migration (or breaks the canonical-form //! acceptance path in the registry), that test goes red. +use ailang_test_support::examples_dir; use std::path::PathBuf; use std::process::Command; @@ -19,15 +20,6 @@ fn ail_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_ail")) } -fn examples_dir() -> PathBuf { - // CARGO_MANIFEST_DIR points at `crates/ail`; examples/ sits at the - // workspace root, two levels up. - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("examples") -} - /// Property: a workspace whose entry module contains a bare /// cross-module Type::Con / Term::Ctor reference — here `Mystery_Type`, /// a name no module in the workspace declares (post-prep.1: in-scope diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 11e8ae5..f37a407 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -3,6 +3,7 @@ //! This test guards the most important property of the entire pipeline: //! AST → typecheck → LLVM IR → clang → binary → correct stdout. +use ailang_test_support::workspace_root; use std::path::Path; use std::process::Command; @@ -2801,15 +2802,6 @@ fn merge_prose_prints_framed_prompt() { // ---------- ext-cli.1: CLI accepts `.ail` (Form A) source files ---------- -fn workspace_root() -> std::path::PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} - /// Property: `ail check` accepts a Form-A `.ail` source file (not just /// the Form-B `.ail.json` canonical form) and exits zero on a /// well-formed program. Guards against the post-rename misleading-JSON diff --git a/crates/ail/tests/embed_e2e.rs b/crates/ail/tests/embed_e2e.rs index 634c191..eee07d2 100644 --- a/crates/ail/tests/embed_e2e.rs +++ b/crates/ail/tests/embed_e2e.rs @@ -4,18 +4,16 @@ //! + `libailang_rt.a`, run it, assert the `s == 25` assertion holds //! (exit 0). This is the whole-milestone existence proof. +use ailang_test_support::workspace_root; use std::path::PathBuf; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn manifest() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } -fn ws_root() -> PathBuf { - manifest().parent().unwrap().parent().unwrap().to_path_buf() -} #[test] fn c_host_calls_exported_scalar_kernel() { - let fixture = ws_root().join("examples").join("embed_backtest_step.ail"); + let fixture = workspace_root().join("examples").join("embed_backtest_step.ail"); let host_c = manifest().join("tests").join("embed").join("host.c"); let outdir = std::env::temp_dir() .join(format!("ail-embed-e2e-{}", std::process::id())); diff --git a/crates/ail/tests/embed_missing_main_baseline.rs b/crates/ail/tests/embed_missing_main_baseline.rs index 884648e..75264e2 100644 --- a/crates/ail/tests/embed_missing_main_baseline.rs +++ b/crates/ail/tests/embed_missing_main_baseline.rs @@ -9,18 +9,13 @@ //! //! Pure reader: writes nothing to the repo. -use std::path::{Path, PathBuf}; +use ailang_test_support::workspace_root; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } -fn workspace_root() -> PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir).parent().unwrap().parent().unwrap().to_path_buf() -} - #[test] fn ail_build_on_main_free_module_is_rejected_at_cli() { let fixture = workspace_root() diff --git a/crates/ail/tests/embed_rc_accounting_tsan.rs b/crates/ail/tests/embed_rc_accounting_tsan.rs index c06a1d5..fe3aee4 100644 --- a/crates/ail/tests/embed_rc_accounting_tsan.rs +++ b/crates/ail/tests/embed_rc_accounting_tsan.rs @@ -4,11 +4,7 @@ //! Per-thread-ctx build: tsan-clean + exit 0. RED until rc.c gains //! ailang_ctx_* + __ail_tls_ctx (link error: undefined symbols). use std::process::Command; -use std::path::PathBuf; -fn ws_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() -} fn cc(args: &[&str], ctx: &str) { let st = Command::new("clang").args(args).status().expect("spawn clang"); @@ -17,7 +13,7 @@ fn cc(args: &[&str], ctx: &str) { #[test] fn rc_accounting_per_ctx_is_tsan_clean() { - let root = ws_root(); + let root = ailang_test_support::canonical_workspace_root(); let tmp = std::env::temp_dir().join("ail_m2_rcacct"); std::fs::create_dir_all(&tmp).unwrap(); let rc_o = tmp.join("rc.o"); @@ -48,7 +44,7 @@ fn rc_accounting_shared_ctx_is_flagged_by_tsan() { // calling ailang_rc_alloc/ailang_rc_dec → concurrent // ctx->alloc_count++ is a genuine data race tsan MUST report. // Proves the de-globalisation property is not vacuous. - let root = ws_root(); + let root = ailang_test_support::canonical_workspace_root(); let tmp = std::env::temp_dir().join("ail_m2_rcacct_neg"); std::fs::create_dir_all(&tmp).unwrap(); let rc_o = tmp.join("rc.o"); diff --git a/crates/ail/tests/embed_rc_global_stats_race.rs b/crates/ail/tests/embed_rc_global_stats_race.rs index 838d2da..bfee5ba 100644 --- a/crates/ail/tests/embed_rc_global_stats_race.rs +++ b/crates/ail/tests/embed_rc_global_stats_race.rs @@ -28,12 +28,8 @@ //! build of any kernel emits `libailang_rt.a`; the C host links the //! runtime archive directly and never calls a kernel. -use std::path::PathBuf; use std::process::Command; -fn ws_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() -} fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } /// 8 threads * 2_000_000 cycles — must match the `#define`s in @@ -47,7 +43,7 @@ fn global_rc_stats_counters_are_exact_under_thread_contention() { let outdir = std::env::temp_dir().join(format!( "ail-rc-global-stats-race-{}", std::process::id())); std::fs::create_dir_all(&outdir).unwrap(); - let root = ws_root(); + let root = ailang_test_support::canonical_workspace_root(); // Any kernel staticlib build emits libailang_rt.a alongside it. let st = Command::new(ail_bin()) diff --git a/crates/ail/tests/embed_record_e2e.rs b/crates/ail/tests/embed_record_e2e.rs index 6b7b364..5447935 100644 --- a/crates/ail/tests/embed_record_e2e.rs +++ b/crates/ail/tests/embed_record_e2e.rs @@ -23,14 +23,12 @@ //! `e2e.rs::build_and_run_with_rc_stats` (the runtime's fixed //! `ailang_rc_stats: allocs=N frees=M live=K` shape). +use ailang_test_support::workspace_root; use std::path::PathBuf; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn manifest() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } -fn ws_root() -> PathBuf { - manifest().parent().unwrap().parent().unwrap().to_path_buf() -} #[derive(Debug)] struct RcStats { @@ -44,7 +42,7 @@ struct RcStats { /// `AILANG_RC_STATS=1`, and return the `ailang_ctx_free` readback. fn build_link_run_embed(fixture: &str, host_rel: &str, extra_cc: &[&str]) -> RcStats { let module = fixture.strip_suffix(".ail").expect("fixture is a .ail file"); - let fixture_path = ws_root().join("examples").join(fixture); + let fixture_path = workspace_root().join("examples").join(fixture); let host_c = manifest().join("tests").join(host_rel); let outdir = std::env::temp_dir().join(format!( "ail-embed-record-e2e-{}-{}", diff --git a/crates/ail/tests/embed_staticlib_cli.rs b/crates/ail/tests/embed_staticlib_cli.rs index d2f679c..0bb0f37 100644 --- a/crates/ail/tests/embed_staticlib_cli.rs +++ b/crates/ail/tests/embed_staticlib_cli.rs @@ -2,18 +2,14 @@ //! produces `lib.a` (program-only) + a separate //! `libailang_rt.a`, and emits NO executable / NO `@main`. -use std::path::{Path, PathBuf}; +use ailang_test_support::workspace_root; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } -fn ws_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap().parent().unwrap().to_path_buf() -} #[test] fn staticlib_emit_produces_two_archives() { - let fixture = ws_root().join("examples").join("embed_backtest_step.ail"); + let fixture = workspace_root().join("examples").join("embed_backtest_step.ail"); let outdir = std::env::temp_dir() .join(format!("ail-embed-cli-{}", std::process::id())); std::fs::create_dir_all(&outdir).unwrap(); @@ -39,7 +35,7 @@ fn staticlib_emit_produces_two_archives() { #[test] fn staticlib_emit_requires_an_export() { // embed_noentry_baseline has no `(export …)` fn. - let fixture = ws_root().join("examples") + let fixture = workspace_root().join("examples") .join("embed_noentry_baseline.ail"); let outdir = std::env::temp_dir() .join(format!("ail-embed-noexp-{}", std::process::id())); diff --git a/crates/ail/tests/embed_swarm_tsan.rs b/crates/ail/tests/embed_swarm_tsan.rs index 62cdaaa..915ae94 100644 --- a/crates/ail/tests/embed_swarm_tsan.rs +++ b/crates/ail/tests/embed_swarm_tsan.rs @@ -6,9 +6,6 @@ use std::process::Command; use std::path::PathBuf; -fn ws_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() -} fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn build_staticlib(outdir: &PathBuf) { @@ -16,7 +13,7 @@ fn build_staticlib(outdir: &PathBuf) { let st = Command::new(ail_bin()) .args(["build", "examples/embed_backtest_step.ail", "--emit=staticlib", "--alloc=rc", "-o", outdir.to_str().unwrap()]) - .current_dir(ws_root()).status().expect("ail build"); + .current_dir(ailang_test_support::canonical_workspace_root()).status().expect("ail build"); assert!(st.success(), "ail build --emit=staticlib failed"); } @@ -24,7 +21,7 @@ fn build_staticlib(outdir: &PathBuf) { fn scalar_swarm_per_ctx_is_tsan_clean_and_matches_oracle() { let outdir = std::env::temp_dir().join("ail_m2_swarm_ok"); build_staticlib(&outdir); - let root = ws_root(); + let root = ailang_test_support::canonical_workspace_root(); let bin = outdir.join("swarm_ok"); let st = Command::new("clang").args([ "-O1", "-g", "-fsanitize=thread", "-pthread", diff --git a/crates/ail/tests/embed_tick_e2e.rs b/crates/ail/tests/embed_tick_e2e.rs index b9c699a..105f597 100644 --- a/crates/ail/tests/embed_tick_e2e.rs +++ b/crates/ail/tests/embed_tick_e2e.rs @@ -28,14 +28,12 @@ //! `g_rc_*`, not ctx; the invariant is GLOBAL Σallocs == Σfrees //! across *all* stat lines, not per-ctx-line balance). +use ailang_test_support::workspace_root; use std::path::PathBuf; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn manifest() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } -fn ws_root() -> PathBuf { - manifest().parent().unwrap().parent().unwrap().to_path_buf() -} #[derive(Debug)] struct RcStats { @@ -49,7 +47,7 @@ struct RcStats { /// `AILANG_RC_STATS=1`, and return the summed RC-stats readback. fn build_link_run_embed(fixture: &str, host_rel: &str, extra_cc: &[&str]) -> RcStats { let module = fixture.strip_suffix(".ail").expect("fixture is a .ail file"); - let fixture_path = ws_root().join("examples").join(fixture); + let fixture_path = workspace_root().join("examples").join(fixture); let host_c = manifest().join("tests").join(host_rel); let outdir = std::env::temp_dir().join(format!( "ail-embed-tick-e2e-{}-{}", diff --git a/crates/ail/tests/emit_ir_staticlib_cli.rs b/crates/ail/tests/emit_ir_staticlib_cli.rs index 579842f..9b99fa5 100644 --- a/crates/ail/tests/emit_ir_staticlib_cli.rs +++ b/crates/ail/tests/emit_ir_staticlib_cli.rs @@ -4,18 +4,14 @@ //! executable-path `MissingEntryMain` rejection — the Decision-5 //! IR-readability affordance for the artefact M1 introduced. -use std::path::{Path, PathBuf}; +use ailang_test_support::workspace_root; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } -fn ws_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap().parent().unwrap().to_path_buf() -} #[test] fn emit_ir_staticlib_prints_kernel_forwarder_no_main() { - let fixture = ws_root().join("examples").join("embed_backtest_step.ail"); + let fixture = workspace_root().join("examples").join("embed_backtest_step.ail"); let out = Command::new(ail_bin()) .args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"]) .output().expect("ail emit-ir --emit=staticlib"); @@ -37,7 +33,7 @@ fn emit_ir_staticlib_prints_kernel_forwarder_no_main() { fn emit_ir_staticlib_requires_an_export() { // embed_noentry_baseline has no `(export …)` fn — symmetric with // build's zero-export guard (embed_staticlib_cli.rs). - let fixture = ws_root().join("examples").join("embed_noentry_baseline.ail"); + let fixture = workspace_root().join("examples").join("embed_noentry_baseline.ail"); let out = Command::new(ail_bin()) .args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"]) .output().expect("ail emit-ir --emit=staticlib"); @@ -54,7 +50,7 @@ fn emit_ir_default_exe_still_requires_main() { // Regression: --emit defaults to "exe"; emit-ir on a main-free // module WITHOUT --emit=staticlib must still hit MissingEntryMain // (the new staticlib branch must not alter the default path). - let fixture = ws_root().join("examples").join("embed_backtest_step.ail"); + let fixture = workspace_root().join("examples").join("embed_backtest_step.ail"); let out = Command::new(ail_bin()) .args(["emit-ir", fixture.to_str().unwrap()]) .output().expect("ail emit-ir"); diff --git a/crates/ail/tests/floats_e2e.rs b/crates/ail/tests/floats_e2e.rs index c9e02b5..9651de4 100644 --- a/crates/ail/tests/floats_e2e.rs +++ b/crates/ail/tests/floats_e2e.rs @@ -9,16 +9,9 @@ //! `float_to_str`, libc `%g` plus the runtime `.0`-fallback for //! whole-valued Float results, per Gitea #7). -use std::path::PathBuf; +use ailang_test_support::workspace_root; use std::process::Command; -fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .to_path_buf() -} - #[test] fn floats_example_prints_expected_stdout() { let root = workspace_root(); diff --git a/crates/ail/tests/ir_snapshot.rs b/crates/ail/tests/ir_snapshot.rs index 7fdfe3f..f6a225c 100644 --- a/crates/ail/tests/ir_snapshot.rs +++ b/crates/ail/tests/ir_snapshot.rs @@ -2,6 +2,7 @@ //! changes to the LLVM IR. Snapshots in tests/snapshots/. Update with //! UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. +use ailang_test_support::examples_dir; use std::path::{Path, PathBuf}; use std::process::Command; @@ -44,15 +45,6 @@ fn snapshots_dir() -> PathBuf { Path::new(manifest_dir).join("tests").join("snapshots") } -fn examples_dir() -> PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir) - .parent() - .unwrap() - .parent() - .unwrap() - .join("examples") -} fn first_diff_line(a: &str, b: &str) -> Option<(usize, String, String)> { let mut la = a.lines(); diff --git a/crates/ail/tests/mono_xmod_ctor_pattern.rs b/crates/ail/tests/mono_xmod_ctor_pattern.rs index e80c4b8..e9fe772 100644 --- a/crates/ail/tests/mono_xmod_ctor_pattern.rs +++ b/crates/ail/tests/mono_xmod_ctor_pattern.rs @@ -28,14 +28,7 @@ //! `workspace_has_typeclasses` gate so every workspace exercises the //! mono pass. -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - d.pop(); - d.pop(); - d.join("examples") -} +use ailang_test_support::examples_dir; /// Property: `monomorphise_workspace` must succeed on a workspace that /// (a) flips the typeclass gate (any `Def::Class` + `Def::Instance` is diff --git a/crates/ail/tests/mq3_multi_class_e2e.rs b/crates/ail/tests/mq3_multi_class_e2e.rs index f5aaa1e..66837aa 100644 --- a/crates/ail/tests/mq3_multi_class_e2e.rs +++ b/crates/ail/tests/mq3_multi_class_e2e.rs @@ -17,6 +17,7 @@ //! diagnostic stream's `code` field. Stdout / runtime behaviour is //! out of scope here — the dispatch path is a check-time concern. +use ailang_test_support::examples_dir; use std::path::PathBuf; use std::process::Command; @@ -24,13 +25,6 @@ fn ail_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_ail")) } -fn examples_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("examples") -} - fn run_ail_check_json(entry: &str) -> std::process::Output { Command::new(ail_bin()) .args(["check", "--json", examples_dir().join(entry).to_str().unwrap()]) diff --git a/crates/ail/tests/roundtrip_cli.rs b/crates/ail/tests/roundtrip_cli.rs index 4a7b597..14451aa 100644 --- a/crates/ail/tests/roundtrip_cli.rs +++ b/crates/ail/tests/roundtrip_cli.rs @@ -20,58 +20,13 @@ //! were the only consumers of `.ail.json` walks and are retired //! alongside. -use std::path::{Path, PathBuf}; +use ailang_test_support::{examples_dir, list_ail_fixtures}; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } -fn workspace_root() -> PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir).parent().unwrap().parent().unwrap().to_path_buf() -} - -fn examples_dir() -> PathBuf { - workspace_root().join("examples") -} - -/// Fixtures that intentionally do NOT parse — the `#55` cutover -/// reject corpus. These are negative fixtures whose whole point is -/// that the parser rejects them, so `parse∘render∘parse` is not a -/// meaningful property for them and the harness must skip them. -/// -/// Scope is deliberately minimal: only fixtures that fail at the -/// PARSE stage belong here. The other `*reject*.ail` cutover -/// fixtures (`borrow_return_reject`, `borrow_value_reject`, -/// `own_return_provenance_reject`, the `embed_export_*_rejected` -/// set, …) parse cleanly and are rejected only later at CHECK time, -/// so they round-trip fine and are NOT excluded. -/// -/// - `bare_slot_reject.ail`: a bare fn-type slot with no mode. The -/// binary-ParamMode parser rejects it ("fn-type slot requires a -/// mode: write (own T) or (borrow T)"). -const NON_PARSEABLE_FIXTURES: &[&str] = &["bare_slot_reject.ail"]; - -fn list_ail_fixtures() -> Vec { - let dir = examples_dir(); - let mut paths: Vec = std::fs::read_dir(&dir) - .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display())) - .filter_map(|entry| entry.ok()) - .map(|e| e.path()) - .filter(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .map(|n| { - n.ends_with(".ail") && !NON_PARSEABLE_FIXTURES.contains(&n) - }) - .unwrap_or(false) - }) - .collect(); - paths.sort(); - paths -} - /// CLI-pipeline idempotency (post-form-a-default-authoring §C3): for /// every `.ail` fixture, the user-facing pipeline /// `ail parse | ail render | ail parse` is byte-identical diff --git a/crates/ail/tests/typeclass_22b2.rs b/crates/ail/tests/typeclass_22b2.rs index 6be70d0..6c95e57 100644 --- a/crates/ail/tests/typeclass_22b2.rs +++ b/crates/ail/tests/typeclass_22b2.rs @@ -6,17 +6,7 @@ //! Tasks 9 (`missing-constraint`) and 10 (`no-instance`). use ailang_core::ast::Type; -use std::path::{Path, PathBuf}; - -fn examples_dir() -> PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir) - .parent() - .unwrap() - .parent() - .unwrap() - .join("examples") -} +use ailang_test_support::examples_dir; /// Property protected: a class method declared in a `Def::Class` must be /// reachable as a name in the module's `ModuleGlobals` so that pass-2 diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index 26539ac..6826113 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -6,16 +6,9 @@ //! the fixtures live in `/examples/`. use ailang_core::ast::Type; +use ailang_test_support::examples_dir; use std::path::PathBuf; -fn examples_dir() -> PathBuf { - // crates/ail/ → repo root → examples/ - let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - d.pop(); - d.pop(); - d.join("examples") -} - #[test] fn monomorphise_workspace_is_identity_on_class_free_workspace() { // Pick any class-free fixture; `examples/hello.ail.json` is diff --git a/crates/ailang-check/Cargo.toml b/crates/ailang-check/Cargo.toml index def29dd..15802ef 100644 --- a/crates/ailang-check/Cargo.toml +++ b/crates/ailang-check/Cargo.toml @@ -12,3 +12,6 @@ thiserror.workspace = true indexmap.workspace = true serde.workspace = true serde_json.workspace = true + +[dev-dependencies] +ailang-test-support.workspace = true diff --git a/crates/ailang-check/tests/embed_export_gate.rs b/crates/ailang-check/tests/embed_export_gate.rs index 00ec7cc..2be426e 100644 --- a/crates/ailang-check/tests/embed_export_gate.rs +++ b/crates/ailang-check/tests/embed_export_gate.rs @@ -3,13 +3,7 @@ //! `Int`/`Float`, or whose effect set is non-empty, MUST fail //! `ail check` with the new diagnostic. A scalar+pure export passes. -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap().parent().unwrap() - .join("examples") -} +use ailang_test_support::examples_dir; fn check_codes(file: &str) -> Vec { let path = examples_dir().join(file); diff --git a/crates/ailang-check/tests/env_construction_pin.rs b/crates/ailang-check/tests/env_construction_pin.rs index c2dc79d..ed49fba 100644 --- a/crates/ailang-check/tests/env_construction_pin.rs +++ b/crates/ailang-check/tests/env_construction_pin.rs @@ -12,16 +12,8 @@ use ailang_check::{build_check_env, build_module_globals, Env}; use ailang_core::ast::Def; use ailang_surface::load_workspace; +use ailang_test_support::examples_dir; use std::collections::BTreeMap; -use std::path::Path; - -fn examples_dir() -> std::path::PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest) - .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)") - .parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)") - .join("examples") -} /// Independent reproduction of every workspace-flat env seeding /// step. Pre-refactor, workspace-flat seeding was split between diff --git a/crates/ailang-check/tests/loop_recur_typecheck_pin.rs b/crates/ailang-check/tests/loop_recur_typecheck_pin.rs index c193d26..31d676f 100644 --- a/crates/ailang-check/tests/loop_recur_typecheck_pin.rs +++ b/crates/ailang-check/tests/loop_recur_typecheck_pin.rs @@ -6,15 +6,7 @@ use ailang_check::check_workspace; use ailang_surface::load_workspace; -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - PathBuf::from(manifest) - .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)") - .parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)") - .join("examples") -} +use ailang_test_support::examples_dir; /// Load the named fixture under `examples/`, run check_workspace, and /// return the diagnostic code list (one entry per Diagnostic). diff --git a/crates/ailang-check/tests/lower_to_mir_ty.rs b/crates/ailang-check/tests/lower_to_mir_ty.rs index 037e26f..0f4a0b5 100644 --- a/crates/ailang-check/tests/lower_to_mir_ty.rs +++ b/crates/ailang-check/tests/lower_to_mir_ty.rs @@ -8,16 +8,7 @@ use ailang_check::elaborate_workspace; use ailang_mir::{Callee, MTerm, MirWorkspace, Mode, StrRep}; use ailang_surface::load_workspace; -use std::path::{Path, PathBuf}; - -/// `/examples` — resolved from this crate's manifest dir, -/// cwd-independent (same pattern as `method_collision_pin.rs`). -fn examples_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().expect("crates/ailang-check has parent crates/") - .parent().expect("crates/ has parent repo root") - .join("examples") -} +use ailang_test_support::examples_dir; /// Load a witness fixture (prelude injected) and elaborate it to MIR. fn elaborate_fixture(module: &str) -> MirWorkspace { diff --git a/crates/ailang-check/tests/method_collision_pin.rs b/crates/ailang-check/tests/method_collision_pin.rs index da0c1d5..d336325 100644 --- a/crates/ailang-check/tests/method_collision_pin.rs +++ b/crates/ailang-check/tests/method_collision_pin.rs @@ -14,15 +14,7 @@ use ailang_check::build_check_env; use ailang_surface::load_workspace; -use std::path::Path; - -fn examples_dir() -> std::path::PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest) - .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)") - .parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)") - .join("examples") -} +use ailang_test_support::examples_dir; /// Class-class repurpose: the fixture /// `examples/test_22b2_method_name_collision_class_class.ail.json` diff --git a/crates/ailang-check/tests/workspace.rs b/crates/ailang-check/tests/workspace.rs index 27942f6..49a570f 100644 --- a/crates/ailang-check/tests/workspace.rs +++ b/crates/ailang-check/tests/workspace.rs @@ -6,7 +6,7 @@ use ailang_check::{check_workspace, Severity}; use ailang_surface::load_workspace; -use std::path::Path; +use ailang_test_support::examples_dir; /// assert that the linearity check's `suggested_rewrites` /// payload is non-empty AND each `replacement` parses as a form-A AILang @@ -30,10 +30,6 @@ fn assert_suggested_rewrites_well_formed(d: &ailang_check::Diagnostic) { } } -fn examples_dir() -> std::path::PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest).parent().unwrap().parent().unwrap().join("examples") -} #[test] fn happy_path_resolves_qualified_import() { diff --git a/crates/ailang-core/Cargo.toml b/crates/ailang-core/Cargo.toml index a8e1371..115c338 100644 --- a/crates/ailang-core/Cargo.toml +++ b/crates/ailang-core/Cargo.toml @@ -14,3 +14,4 @@ indexmap.workspace = true [dev-dependencies] tempfile.workspace = true ailang-surface = { path = "../ailang-surface" } +ailang-test-support.workspace = true diff --git a/crates/ailang-core/tests/carve_out_inventory.rs b/crates/ailang-core/tests/carve_out_inventory.rs index 1940dc3..f457511 100644 --- a/crates/ailang-core/tests/carve_out_inventory.rs +++ b/crates/ailang-core/tests/carve_out_inventory.rs @@ -20,6 +20,7 @@ //! for the lambda-capture-of-loop-binder rejection //! (loop-binder-captured-by-lambda). +use ailang_test_support::examples_dir; use std::collections::BTreeSet; const EXPECTED: &[&str] = &[ @@ -40,11 +41,6 @@ const EXPECTED: &[&str] = &[ "test_recur_type_mismatch.ail.json", ]; -fn examples_dir() -> std::path::PathBuf { - let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - crate_dir.parent().unwrap().parent().unwrap().join("examples") -} - #[test] fn examples_ail_json_inventory_matches_carve_outs() { let expected: BTreeSet<&str> = EXPECTED.iter().copied().collect(); diff --git a/crates/ailang-core/tests/ctt2_registry_rekey.rs b/crates/ailang-core/tests/ctt2_registry_rekey.rs index befc584..b090461 100644 --- a/crates/ailang-core/tests/ctt2_registry_rekey.rs +++ b/crates/ailang-core/tests/ctt2_registry_rekey.rs @@ -15,15 +15,7 @@ use ailang_core::canonical; use ailang_core::ast::Type; use ailang_surface::load_workspace; -use std::path::Path; - -fn examples_dir() -> std::path::PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest) - .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/)") - .parent().expect("crates has a parent (workspace root)") - .join("examples") -} +use ailang_test_support::examples_dir; #[test] fn two_modules_with_same_bare_foo_both_register() { diff --git a/crates/ailang-core/tests/hash_pin.rs b/crates/ailang-core/tests/hash_pin.rs index e609c14..7583c5d 100644 --- a/crates/ailang-core/tests/hash_pin.rs +++ b/crates/ailang-core/tests/hash_pin.rs @@ -18,11 +18,7 @@ use ailang_core::ast::*; use ailang_core::canonical; use ailang_core::hash::def_hash; - -fn examples_dir() -> std::path::PathBuf { - let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir.parent().unwrap().parent().unwrap().join("examples") -} +use ailang_test_support::examples_dir; fn sample_fn() -> Def { Def::Fn(FnDef { diff --git a/crates/ailang-core/tests/schema_coverage.rs b/crates/ailang-core/tests/schema_coverage.rs index 8d45266..92ccd05 100644 --- a/crates/ailang-core/tests/schema_coverage.rs +++ b/crates/ailang-core/tests/schema_coverage.rs @@ -19,7 +19,8 @@ //! a separate iteration); the test must not be relaxed. use std::collections::HashSet; -use std::path::PathBuf; + +use ailang_test_support::{examples_dir, list_ail_fixtures}; use ailang_core::ast::{ Def, InstanceMethod, Literal, Module, NewArg, ParamMode, Pattern, Term, Type, @@ -354,39 +355,6 @@ fn visit_module(m: &Module, observed: &mut HashSet) { } } -fn examples_dir() -> PathBuf { - let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - crate_dir.parent().unwrap().parent().unwrap().join("examples") -} - -/// Fixtures that intentionally do NOT parse — the `#55` cutover -/// reject corpus. Negative fixtures whose whole point is that the -/// parser rejects them, so they have no AST to scan for variant -/// coverage and the scan must skip them. Mirrors the same list in -/// `crates/ail/tests/roundtrip_cli.rs`. -/// -/// - `bare_slot_reject.ail`: a bare fn-type slot with no mode. The -/// binary-ParamMode parser rejects it ("fn-type slot requires a -/// mode: write (own T) or (borrow T)"). -const NON_PARSEABLE_FIXTURES: &[&str] = &["bare_slot_reject.ail"]; - -fn list_ail_fixtures() -> Vec { - let dir = examples_dir(); - let mut paths: Vec = std::fs::read_dir(&dir) - .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display())) - .filter_map(|entry| entry.ok()) - .map(|e| e.path()) - .filter(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .map(|n| n.ends_with(".ail") && !NON_PARSEABLE_FIXTURES.contains(&n)) - .unwrap_or(false) - }) - .collect(); - paths.sort(); - paths -} - #[test] fn every_ast_variant_is_observed_in_the_fixture_corpus() { let fixtures = list_ail_fixtures(); diff --git a/crates/ailang-core/tests/workspace_pin.rs b/crates/ailang-core/tests/workspace_pin.rs index e818eb2..c396266 100644 --- a/crates/ailang-core/tests/workspace_pin.rs +++ b/crates/ailang-core/tests/workspace_pin.rs @@ -19,6 +19,7 @@ use ailang_core::workspace::{Registry, RegistryEntry, WorkspaceLoadError}; use ailang_surface::load_workspace; +use ailang_test_support::examples_dir; use std::path::{Path, PathBuf}; // Adapter so this integration crate can call `load_modules_with` with @@ -37,10 +38,6 @@ fn load_one_adapter(path: &Path) -> Result PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir).parent().unwrap().parent().unwrap().join("examples") -} #[test] fn loads_example_workspace_happy_path() { diff --git a/crates/ailang-prose/Cargo.toml b/crates/ailang-prose/Cargo.toml index 0a31a22..e52d740 100644 --- a/crates/ailang-prose/Cargo.toml +++ b/crates/ailang-prose/Cargo.toml @@ -9,3 +9,4 @@ ailang-core.workspace = true [dev-dependencies] ailang-surface = { path = "../ailang-surface" } +ailang-test-support.workspace = true diff --git a/crates/ailang-prose/tests/snapshot.rs b/crates/ailang-prose/tests/snapshot.rs index f451972..1c540da 100644 --- a/crates/ailang-prose/tests/snapshot.rs +++ b/crates/ailang-prose/tests/snapshot.rs @@ -12,16 +12,7 @@ //! purpose — Iter 20a is the seeding round, and we want every change //! after this to be visible in a diff. -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - // tests/ runs with CARGO_MANIFEST_DIR == crates/ailang-prose. - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.pop(); - p.pop(); - p.push("examples"); - p -} +use ailang_test_support::examples_dir; fn check_snapshot(stem: &str) { let dir = examples_dir(); diff --git a/crates/ailang-surface/Cargo.toml b/crates/ailang-surface/Cargo.toml index a6299ae..fa691dd 100644 --- a/crates/ailang-surface/Cargo.toml +++ b/crates/ailang-surface/Cargo.toml @@ -12,3 +12,4 @@ thiserror.workspace = true [dev-dependencies] tempfile.workspace = true +ailang-test-support.workspace = true diff --git a/crates/ailang-surface/tests/round_trip.rs b/crates/ailang-surface/tests/round_trip.rs index 9e71a2d..a1ba8ea 100644 --- a/crates/ailang-surface/tests/round_trip.rs +++ b/crates/ailang-surface/tests/round_trip.rs @@ -32,14 +32,7 @@ //! `every_ail_fixture_matches_its_json_counterpart` (no longer //! expressible: only one form is hand-authored post-iter). -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - // `CARGO_MANIFEST_DIR` is the surface crate root; examples live two - // levels up. - let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - crate_dir.parent().unwrap().parent().unwrap().join("examples") -} +use ailang_test_support::{examples_dir, list_ail_fixtures}; // Retired iter form-a.1 T9: // - `list_json_fixtures` helper (walked `.ail.json` for the two retired tests below). @@ -51,34 +44,6 @@ fn examples_dir() -> PathBuf { // only one form is hand-authored post-iter; counterparts are derived in-process // via `ail parse`. -/// Fixtures that intentionally do NOT parse — the `#55` cutover -/// reject corpus. Negative fixtures whose whole point is that the -/// parser rejects them, so the parse/round-trip properties below are -/// not meaningful for them and the corpus scan must skip them. -/// Mirrors the same list in `crates/ail/tests/roundtrip_cli.rs`. -/// -/// - `bare_slot_reject.ail`: a bare fn-type slot with no mode. The -/// binary-ParamMode parser rejects it ("fn-type slot requires a -/// mode: write (own T) or (borrow T)"). -const NON_PARSEABLE_FIXTURES: &[&str] = &["bare_slot_reject.ail"]; - -fn list_ail_fixtures() -> Vec { - let dir = examples_dir(); - let mut paths: Vec = std::fs::read_dir(&dir) - .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display())) - .filter_map(|entry| entry.ok()) - .map(|e| e.path()) - .filter(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .map(|n| n.ends_with(".ail") && !NON_PARSEABLE_FIXTURES.contains(&n)) - .unwrap_or(false) - }) - .collect(); - paths.sort(); - paths -} - /// Pins **idempotency-under-print** (property 2 of the post-form-a /// Roundtrip Invariant, design/contracts/0009-roundtrip-invariant.md): for every /// well-formed `.ail` text `t`, the composition `parse → print → parse` diff --git a/crates/ailang-test-support/Cargo.toml b/crates/ailang-test-support/Cargo.toml new file mode 100644 index 0000000..09b16b0 --- /dev/null +++ b/crates/ailang-test-support/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ailang-test-support" +version.workspace = true +edition.workspace = true +license.workspace = true + +# Zero-dependency leaf crate. Holds test helpers shared across the +# workspace's integration-test suites (path navigation, the fixture +# corpus filter). Depended on only as a `dev-dependency`, never by +# production code. +[dependencies] diff --git a/crates/ailang-test-support/src/lib.rs b/crates/ailang-test-support/src/lib.rs new file mode 100644 index 0000000..4c5830b --- /dev/null +++ b/crates/ailang-test-support/src/lib.rs @@ -0,0 +1,68 @@ +//! Shared helpers for the workspace's integration-test suites. +//! +//! Several crates' `tests/*.rs` files need the same handful of path +//! computations and the same fixture-corpus filter. Before this crate +//! they were copy-pasted per file and drifted out of sync. This crate +//! is the single source; integration tests depend on it as a +//! `dev-dependency` and `use ailang_test_support::*`. +//! +//! Path helpers resolve relative to *this* crate's `CARGO_MANIFEST_DIR` +//! (`crates/ailang-test-support`), so they are independent of which +//! crate's test binary calls them. + +use std::path::PathBuf; + +/// Absolute path to the workspace root (the directory containing +/// `crates/` and `examples/`). +pub fn workspace_root() -> PathBuf { + let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + crate_dir + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() +} + +/// [`workspace_root`] with symlinks resolved and the path normalized +/// (`std::fs::canonicalize`). Used by the tsan/race tests that feed the +/// root into native build/link steps where a symlinked checkout would +/// otherwise leak a non-canonical path. +pub fn canonical_workspace_root() -> PathBuf { + std::fs::canonicalize(workspace_root()).unwrap() +} + +/// Absolute path to the `examples/` fixture directory at the workspace +/// root. +pub fn examples_dir() -> PathBuf { + workspace_root().join("examples") +} + +/// Fixtures under `examples/` that intentionally do NOT parse — the +/// `#55` cutover reject corpus. Negative fixtures whose whole point is +/// that the parser rejects them, so they have no AST and any corpus +/// scan must skip them. +/// +/// - `bare_slot_reject.ail`: a bare fn-type slot with no mode. The +/// binary-`ParamMode` parser rejects it ("fn-type slot requires a +/// mode: write (own T) or (borrow T)"). +pub const NON_PARSEABLE_FIXTURES: &[&str] = &["bare_slot_reject.ail"]; + +/// All parseable `.ail` fixtures under `examples/`, sorted, with the +/// [`NON_PARSEABLE_FIXTURES`] reject corpus filtered out. +pub fn list_ail_fixtures() -> Vec { + let dir = examples_dir(); + let mut paths: Vec = std::fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display())) + .filter_map(|entry| entry.ok()) + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .map(|n| n.ends_with(".ail") && !NON_PARSEABLE_FIXTURES.contains(&n)) + .unwrap_or(false) + }) + .collect(); + paths.sort(); + paths +}