test(cli): extract the shared demo-project fixture helpers into tests/common (#223)
built_project/project_lock/ScratchPath/ScratchGuard move from their three per-binary copies (cli_run.rs, research_docs.rs, project_load.rs's built_fixture near-copy) into tests/common/mod.rs; project_load's one shared-store-mutating test now takes the lock it previously lacked. The module doc states the boundary plainly: the Mutex serializes threads within one process only — process-parallel runners still race on the shared fixture store (documented, dormant under cargo test's sequential binaries; see the issue thread for the observed process-parallel repro). closes #223
This commit is contained in:
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
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
|
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
|
||||||
/// crate; the binary is named `aura` in `Cargo.toml`).
|
/// crate; the binary is named `aura` in `Cargo.toml`).
|
||||||
@@ -19,35 +22,6 @@ fn temp_cwd(name: &str) -> std::path::PathBuf {
|
|||||||
dir
|
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<PathBuf> = 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
|
|
||||||
/// `<fixture>/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 `<fixture>/runs` on drop (including mid-panic) so a migrated test never
|
/// Removes `<fixture>/runs` on drop (including mid-panic) so a migrated test never
|
||||||
/// leaks its content-addressed store into the git-tracked fixture tree.
|
/// leaks its content-addressed store into the git-tracked fixture tree.
|
||||||
struct RunsCleanup(PathBuf);
|
struct RunsCleanup(PathBuf);
|
||||||
|
|||||||
@@ -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<PathBuf> = 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 `<fixture>/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<ScratchPath>);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::{built_project, project_lock};
|
||||||
|
|
||||||
fn fixture_dir() -> PathBuf {
|
fn fixture_dir() -> PathBuf {
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project")
|
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")
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/badcharter-project")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn built_fixture() -> &'static PathBuf {
|
|
||||||
static BUILT: OnceLock<PathBuf> = 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 {
|
fn aura(args: &[&str], cwd: &Path) -> Output {
|
||||||
Command::new(env!("CARGO_BIN_EXE_aura"))
|
Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
.args(args)
|
.args(args)
|
||||||
@@ -45,7 +29,7 @@ fn aura(args: &[&str], cwd: &Path) -> Output {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project_run_resolves_demo_node_and_is_bit_identical() {
|
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);
|
let a = aura(&["run", "demo_signal.json"], dir);
|
||||||
assert!(
|
assert!(
|
||||||
a.status.success(),
|
a.status.success(),
|
||||||
@@ -59,7 +43,7 @@ fn project_run_resolves_demo_node_and_is_bit_identical() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project_run_manifest_carries_provenance() {
|
fn project_run_manifest_carries_provenance() {
|
||||||
let dir = built_fixture();
|
let dir = built_project();
|
||||||
let out = aura(&["run", "demo_signal.json"], dir);
|
let out = aura(&["run", "demo_signal.json"], dir);
|
||||||
assert!(out.status.success());
|
assert!(out.status.success());
|
||||||
let v: serde_json::Value =
|
let v: serde_json::Value =
|
||||||
@@ -72,7 +56,7 @@ fn project_run_manifest_carries_provenance() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn introspect_vocabulary_lists_project_types() {
|
fn introspect_vocabulary_lists_project_types() {
|
||||||
let dir = built_fixture();
|
let dir = built_project();
|
||||||
let out = aura(&["graph", "introspect", "--vocabulary"], dir);
|
let out = aura(&["graph", "introspect", "--vocabulary"], dir);
|
||||||
assert!(out.status.success());
|
assert!(out.status.success());
|
||||||
let text = String::from_utf8_lossy(&out.stdout);
|
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).
|
/// rather than shell-relative (C17).
|
||||||
#[test]
|
#[test]
|
||||||
fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
|
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 sub = dir.join("src");
|
||||||
let runs_dir = dir.join("runs");
|
let runs_dir = dir.join("runs");
|
||||||
std::fs::remove_dir_all(&runs_dir).ok();
|
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.
|
/// visible instead of silent.
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_dylib_warns_naming_both_timestamps_but_still_runs() {
|
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 src = dir.join("src/lib.rs");
|
||||||
let dylib = dir.join("target/debug").join(format!(
|
let dylib = dir.join("target/debug").join(format!(
|
||||||
"{}demo_project{}",
|
"{}demo_project{}",
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
//! author uses.
|
//! author uses.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
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
|
/// A fresh, unique working directory for a process test that persists
|
||||||
/// content-addressed documents under `./runs/` (so `aura process register`
|
/// 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());
|
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<PathBuf> = 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 `<fixture>/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<ScratchPath>);
|
|
||||||
|
|
||||||
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
|
/// The `aura campaign validate` referential tier (the campaign family's
|
||||||
/// headline addition over the intrinsic-only process family), exercised at
|
/// headline addition over the intrinsic-only process family), exercised at
|
||||||
/// the CLI seam inside a real project: a fully-resolved document prints the
|
/// the CLI seam inside a real project: a fully-resolved document prints the
|
||||||
|
|||||||
Reference in New Issue
Block a user