diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 1394da9..a127023 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -3,7 +3,10 @@ use std::path::{Path, PathBuf}; use std::process::Command; -use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::sync::MutexGuard; + +mod common; +use common::{built_project, project_lock}; /// Path to the freshly-built `aura` binary (Cargo sets this env var for the test /// crate; the binary is named `aura` in `Cargo.toml`). @@ -19,35 +22,6 @@ fn temp_cwd(name: &str) -> std::path::PathBuf { dir } -/// The demo-project fixture (Aura.toml + a built cdylib present), built once — -/// mirrors research_docs.rs's helper (a separate test binary, so it needs its -/// own `OnceLock`); `cargo build` is idempotent, so a second binary building -/// the same fixture is a no-op. -fn built_project() -> &'static PathBuf { - static BUILT: OnceLock = OnceLock::new(); - BUILT.get_or_init(|| { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project"); - let out = std::process::Command::new("cargo") - .arg("build") - .current_dir(&dir) - .output() - .expect("spawn cargo build for the fixture project"); - assert!( - out.status.success(), - "fixture build failed:\n{}", - String::from_utf8_lossy(&out.stderr) - ); - dir - }) -} - -/// Serializes every test that runs a verb inside the shared fixture (they reset -/// `/runs`, so parallel threads would race). Poison-tolerant. -fn project_lock() -> MutexGuard<'static, ()> { - static LOCK: Mutex<()> = Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - /// Removes `/runs` on drop (including mid-panic) so a migrated test never /// leaks its content-addressed store into the git-tracked fixture tree. struct RunsCleanup(PathBuf); diff --git a/crates/aura-cli/tests/common/mod.rs b/crates/aura-cli/tests/common/mod.rs new file mode 100644 index 0000000..c3ba3f9 --- /dev/null +++ b/crates/aura-cli/tests/common/mod.rs @@ -0,0 +1,80 @@ +//! Shared fixture helpers for the `aura-cli` end-to-end test binaries +//! (`research_docs.rs`, `cli_run.rs`, `project_load.rs`), all of which build +//! and drive the `tests/fixtures/demo-project` fixture through the real +//! `aura` binary (#223). +//! +//! **Process-local serialization only.** [`project_lock`]'s `Mutex` is a +//! per-process `static`: it serializes test THREADS within one test-binary +//! process, but grants no cross-process exclusion. A process-parallel test +//! runner (e.g. `cargo-nextest`, which forks one process per test binary) — +//! or simply two concurrent `cargo test` invocations over the same working +//! tree — still race on the shared `tests/fixtures/demo-project/runs` store. +//! That boundary is a known, documented limitation (#223), not something +//! this module claims to close. +//! +//! Each of the three test binaries compiles this file as its own `mod +//! common`, so an item unused by one binary trips that binary's `-D +//! warnings` `dead_code` lint; see the `#[allow(dead_code)]` markers below +//! rather than deleting a helper just because one binary doesn't need it. + +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, MutexGuard, OnceLock}; + +/// The demo-project fixture (`Aura.toml` present), built once per test-binary +/// process. Each test binary has its own `OnceLock` (this module is compiled +/// separately per binary), but `cargo build` is idempotent, so a second +/// binary building the same fixture is a no-op. +pub fn built_project() -> &'static PathBuf { + static BUILT: OnceLock = OnceLock::new(); + BUILT.get_or_init(|| { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project"); + let out = std::process::Command::new("cargo") + .arg("build") + .current_dir(&dir) + .output() + .expect("spawn cargo build for the fixture project"); + assert!( + out.status.success(), + "fixture build failed:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + dir + }) +} + +/// Serializes every test that touches the shared demo-project fixture store +/// (they remove/re-seed `/runs`, so parallel test threads would race +/// on it). A poisoned lock is taken over: one failed test must not cascade +/// into unrelated lock panics. +pub fn project_lock() -> MutexGuard<'static, ()> { + static LOCK: Mutex<()> = Mutex::new(()); + LOCK.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// A scratch filesystem entry a test writes under the git-tracked +/// demo-project fixture root (only `runs/` there is fixture-gitignored), +/// removed on drop — including during a mid-test panic — so a failed +/// assertion never leaks scratch docs into tracked working-tree state. +#[allow(dead_code)] +pub enum ScratchPath { + File(PathBuf), + Dir(PathBuf), +} + +#[allow(dead_code)] +pub struct ScratchGuard(pub Vec); + +impl Drop for ScratchGuard { + fn drop(&mut self) { + for p in &self.0 { + match p { + ScratchPath::File(p) => { + let _ = std::fs::remove_file(p); + } + ScratchPath::Dir(p) => { + let _ = std::fs::remove_dir_all(p); + } + } + } + } +} diff --git a/crates/aura-cli/tests/project_load.rs b/crates/aura-cli/tests/project_load.rs index 96c9268..e7a764e 100644 --- a/crates/aura-cli/tests/project_load.rs +++ b/crates/aura-cli/tests/project_load.rs @@ -6,9 +6,11 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Output}; -use std::sync::OnceLock; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +mod common; +use common::{built_project, project_lock}; + fn fixture_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project") } @@ -17,24 +19,6 @@ fn badcharter_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/badcharter-project") } -fn built_fixture() -> &'static PathBuf { - static BUILT: OnceLock = OnceLock::new(); - BUILT.get_or_init(|| { - let dir = fixture_dir(); - let out = Command::new("cargo") - .arg("build") - .current_dir(&dir) - .output() - .expect("spawn cargo build for the fixture project"); - assert!( - out.status.success(), - "fixture build failed:\n{}", - String::from_utf8_lossy(&out.stderr) - ); - dir - }) -} - fn aura(args: &[&str], cwd: &Path) -> Output { Command::new(env!("CARGO_BIN_EXE_aura")) .args(args) @@ -45,7 +29,7 @@ fn aura(args: &[&str], cwd: &Path) -> Output { #[test] fn project_run_resolves_demo_node_and_is_bit_identical() { - let dir = built_fixture(); + let dir = built_project(); let a = aura(&["run", "demo_signal.json"], dir); assert!( a.status.success(), @@ -59,7 +43,7 @@ fn project_run_resolves_demo_node_and_is_bit_identical() { #[test] fn project_run_manifest_carries_provenance() { - let dir = built_fixture(); + let dir = built_project(); let out = aura(&["run", "demo_signal.json"], dir); assert!(out.status.success()); let v: serde_json::Value = @@ -72,7 +56,7 @@ fn project_run_manifest_carries_provenance() { #[test] fn introspect_vocabulary_lists_project_types() { - let dir = built_fixture(); + let dir = built_project(); let out = aura(&["graph", "introspect", "--vocabulary"], dir); assert!(out.status.success()); let text = String::from_utf8_lossy(&out.stdout); @@ -114,7 +98,8 @@ fn outside_a_project_the_demo_blueprint_is_unknown() { /// rather than shell-relative (C17). #[test] fn project_registry_anchors_at_discovered_root_not_invocation_cwd() { - let dir = built_fixture(); + let _fixture = project_lock(); + let dir = built_project(); let sub = dir.join("src"); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); @@ -202,7 +187,7 @@ fn set_mtime(path: &Path, secs_since_epoch: u64) { /// visible instead of silent. #[test] fn stale_dylib_warns_naming_both_timestamps_but_still_runs() { - let dir = built_fixture(); + let dir = built_project(); let src = dir.join("src/lib.rs"); let dylib = dir.join("target/debug").join(format!( "{}demo_project{}", diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index 81b7420..3d32c4c 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -3,7 +3,9 @@ //! author uses. use std::path::{Path, PathBuf}; -use std::sync::{Mutex, MutexGuard, OnceLock}; + +mod common; +use common::{ScratchGuard, ScratchPath, built_project, project_lock}; /// A fresh, unique working directory for a process test that persists /// content-addressed documents under `./runs/` (so `aura process register` @@ -292,62 +294,6 @@ fn process_register_stores_content_addressed_under_runs_root() { assert!(dir.join("runs").join("processes").join(format!("{id}.json")).is_file()); } -/// The demo-project fixture (Aura.toml present), built once — mirrors -/// `project_load.rs`'s `built_fixture` (a separate test binary, so it needs -/// its own `OnceLock`, but `cargo build` is idempotent). -fn built_project() -> &'static PathBuf { - static BUILT: OnceLock = OnceLock::new(); - BUILT.get_or_init(|| { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project"); - let out = std::process::Command::new("cargo") - .arg("build") - .current_dir(&dir) - .output() - .expect("spawn cargo build for the fixture project"); - assert!( - out.status.success(), - "fixture build failed:\n{}", - String::from_utf8_lossy(&out.stderr) - ); - dir - }) -} - -/// Serializes every test that touches the shared demo-project fixture store -/// (they remove/re-seed `/runs`, so parallel test threads would race -/// on it). A poisoned lock is taken over: one failed test must not cascade -/// into unrelated lock panics. -fn project_lock() -> MutexGuard<'static, ()> { - static LOCK: Mutex<()> = Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) -} - -/// A scratch filesystem entry this test writes under the git-tracked -/// demo-project fixture root (only `runs/` there is fixture-gitignored), -/// removed on drop — including during a mid-test panic — so a failed -/// assertion never leaks scratch docs into tracked working-tree state. -enum ScratchPath { - File(PathBuf), - Dir(PathBuf), -} - -struct ScratchGuard(Vec); - -impl Drop for ScratchGuard { - fn drop(&mut self) { - for p in &self.0 { - match p { - ScratchPath::File(p) => { - let _ = std::fs::remove_file(p); - } - ScratchPath::Dir(p) => { - let _ = std::fs::remove_dir_all(p); - } - } - } - } -} - /// The `aura campaign validate` referential tier (the campaign family's /// headline addition over the intrinsic-only process family), exercised at /// the CLI seam inside a real project: a fully-resolved document prints the