refactor(test): hoist duplicated test helpers into ailang-test-support

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.
This commit is contained in:
2026-06-02 01:54:37 +02:00
parent ac9171ebf0
commit 7ae92d3e60
40 changed files with 139 additions and 316 deletions
Generated
+9
View File
@@ -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"
+2
View File
@@ -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"
+1
View File
@@ -21,3 +21,4 @@ anyhow.workspace = true
[dev-dependencies]
tempfile.workspace = true
blake3.workspace = true
ailang-test-support.workspace = true
+1 -9
View File
@@ -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
+1 -9
View File
@@ -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
+2 -4
View File
@@ -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()));
@@ -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()
+2 -6
View File
@@ -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");
@@ -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())
+2 -4
View File
@@ -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-{}-{}",
+3 -7
View File
@@ -2,18 +2,14 @@
//! produces `lib<entry>.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()));
+2 -5
View File
@@ -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",
+2 -4
View File
@@ -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-{}-{}",
+4 -8
View File
@@ -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");
+1 -8
View File
@@ -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();
+1 -9
View File
@@ -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();
+1 -8
View File
@@ -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
+1 -7
View File
@@ -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()])
+1 -46
View File
@@ -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<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = 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> | ail render | ail parse` is byte-identical
+1 -11
View File
@@ -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
+1 -8
View File
@@ -6,16 +6,9 @@
//! the fixtures live in `<repo>/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
+3
View File
@@ -12,3 +12,6 @@ thiserror.workspace = true
indexmap.workspace = true
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
ailang-test-support.workspace = true
@@ -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<String> {
let path = examples_dir().join(file);
@@ -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
@@ -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).
+1 -10
View File
@@ -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};
/// `<repo>/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 {
@@ -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`
+1 -5
View File
@@ -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() {
+1
View File
@@ -14,3 +14,4 @@ indexmap.workspace = true
[dev-dependencies]
tempfile.workspace = true
ailang-surface = { path = "../ailang-surface" }
ailang-test-support.workspace = true
@@ -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();
@@ -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() {
+1 -5
View File
@@ -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 {
+2 -34
View File
@@ -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<VariantTag>) {
}
}
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<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = 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();
+1 -4
View File
@@ -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<ailang_core::ast::Module, WorkspaceLo
}
}
fn examples_dir() -> 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() {
+1
View File
@@ -9,3 +9,4 @@ ailang-core.workspace = true
[dev-dependencies]
ailang-surface = { path = "../ailang-surface" }
ailang-test-support.workspace = true
+1 -10
View File
@@ -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();
+1
View File
@@ -12,3 +12,4 @@ thiserror.workspace = true
[dev-dependencies]
tempfile.workspace = true
ailang-test-support.workspace = true
+1 -36
View File
@@ -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<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = 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`
+11
View File
@@ -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]
+68
View File
@@ -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<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = 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
}