//! 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). //! //! **Per-test project directories, no shared store.** [`fresh_project`] mints //! a unique tempdir per test, wired to the shared, once-built fixture crate //! via an absolute `[nodes]` pointer (`project.rs`'s pointer-arm routing), so //! no two tests ever race on the same `runs/` store — libtest's default //! thread parallelism applies without a serializing lock (#250). //! //! 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::OnceLock; use std::sync::atomic::{AtomicUsize, Ordering}; /// 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 }) } /// A per-test project directory, removed on drop (including mid-test panic). #[allow(dead_code)] pub struct FreshProject { root: PathBuf, } impl Drop for FreshProject { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.root); } } /// Mint a unique tempdir holding a 2-line data-level project (`Aura.toml` /// with an absolute `[nodes]` pointer at the shared, once-built fixture /// crate) plus a copy of `demo_signal.json`. Every test gets its own `runs/` /// store under this root, so no two tests ever contend for the same store — /// this is what lets `project_lock` disappear (#250). Bind the guard for the /// whole test body: `let (dir, _g) = fresh_project();`. #[allow(dead_code)] pub fn fresh_project() -> (PathBuf, FreshProject) { static COUNTER: AtomicUsize = AtomicUsize::new(0); let root = std::env::temp_dir().join(format!( "aura-e2e-{}-{}", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed) )); std::fs::create_dir_all(&root).expect("create fresh project tempdir"); let fixture = built_project(); std::fs::write( root.join("Aura.toml"), format!( "[paths]\nruns = \"runs\"\n\n[nodes]\ncrates = [\"{}\"]\n", fixture.display() ), ) .expect("write fresh project Aura.toml"); std::fs::copy(fixture.join("demo_signal.json"), root.join("demo_signal.json")) .expect("copy demo_signal.json into the fresh project"); (root.clone(), FreshProject { root }) } /// 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); } } } } }