Cut the E2E suite wall-clock: 247s -> 46s (cli_run 211s -> 25s) #255

Merged
claude merged 8 commits from worktree-250-test-wallclock into main 2026-07-13 16:49:45 +02:00
12 changed files with 1055 additions and 420 deletions
Generated
+2
View File
@@ -138,6 +138,7 @@ dependencies = [
"serde_json",
"sha2",
"toml",
"zip",
]
[[package]]
@@ -181,6 +182,7 @@ dependencies = [
"chrono",
"chrono-tz",
"data-server",
"zip",
]
[[package]]
+9
View File
@@ -36,3 +36,12 @@ serde = { version = "1", features = ["derive"] }
# parsed back. Exercised (with a 1-ULP canary value) by aura-cli's
# `f64_blueprint_param_survives_store_round_trip_bit_identically`.
serde_json = { version = "1", features = ["float_roundtrip"] }
# Dependencies at opt-level 2, workspace crates at the dev default (0): the E2E
# suite spawns the debug `aura` binary whose wall-clock is dominated by dependency
# code (zip inflate, sha2 dylib hashing, serde) — optimizing deps is multiplicative
# there, while workspace crates stay fast-to-rebuild and debuggable. Float pins are
# unaffected: every pinned float is computed by workspace code (IEEE semantics are
# opt-level-independent in Rust regardless).
[profile.dev.package."*"]
opt-level = 2
+7
View File
@@ -53,3 +53,10 @@ toml = "0.8"
# long-option abbreviation. Research-side CLI only (invariant 8): a dev-loop
# compile tax, never a frozen-artifact tax.
clap = { version = "4", features = ["derive"] }
[dev-dependencies]
# zip: hand-packs a tiny synthetic M1 archive (tests/common/mod.rs) in the
# exact on-disk format data-server reads — already transitive via the
# data-server dependency above; declared directly here so test code may
# `use zip::...` (#250, no new dependency actually enters the build graph).
zip = "2"
+51 -19
View File
@@ -639,17 +639,8 @@ fn probe_window(
to_ms: Option<i64>,
env: &project::Env,
) -> (Timestamp, Timestamp) {
let mut probe = aura_ingest::open_columns(server, symbol, from_ms, to_ms, &[aura_ingest::M1Field::Close])
aura_ingest::archive_extent(server, std::path::Path::new(&env.data_path()), symbol, from_ms, to_ms)
.unwrap_or_else(|| no_data_in_window(symbol, from_ms, to_ms, env))
.pop()
.expect("open_columns yields one source per requested field");
let first =
aura_engine::Source::peek(probe.as_ref()).unwrap_or_else(|| no_data_in_window(symbol, from_ms, to_ms, env));
let mut last = first;
while let Some((t, _)) = aura_engine::Source::next(&mut *probe) {
last = t;
}
(first, last)
}
/// Open the real M1 sources for a recorded symbol over an optional window — one
@@ -2098,6 +2089,52 @@ fn run_oos_blueprint(
(Vec::new(), report)
}
/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
/// run BEFORE this closure is invoked per grid point, so its body never influences the
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
/// satisfy the terminal, at O(1) cost per point instead of a full member run.
fn axis_grid_probe_report() -> RunReport {
RunReport {
manifest: RunManifest {
commit: String::new(),
params: Vec::new(),
defaults: Vec::new(),
window: (Timestamp(0), Timestamp(0)),
seed: 0,
broker: "wf-axis-preflight-placeholder".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None },
}
}
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
/// member (#253). Reuses the SAME strict, erroring axis-name check
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
/// paths cannot drift to differently-worded rejections) to derive the #246 override
/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks
/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for
/// the run closure — no data access, no sim engine tick. Axis resolution is
/// window-agnostic, so the caller derives or passes no window at all.
fn validate_axis_grid(
doc: &str, axes: &[(String, Vec<Scalar>)], raw_space: &[ParamSpec], probe_signal: &Composite,
env: &project::Env,
) -> Result<(), BindError> {
let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
let mut binder = probe.axis(first_name, first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(n, vals.clone());
}
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
}
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
@@ -2147,15 +2184,10 @@ fn blueprint_walkforward_family(
// (which resolves its axes a single time before any member runs). `walk_forward` fans
// the per-window closure out across the windows in parallel, so a `BindError` raised
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic,
// so pre-flighting the first IS window here surfaces the error exactly once (it fails in
// `resolve_axes`, before any member runs); a per-window resolve then cannot re-raise.
let first_is = WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling)
.expect("roller config validated just above")
.next()
.expect("roller yields >= 1 window (validated above)")
.is;
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env, &binding) {
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic
// (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without
// running a single member — no IS window (or a second roller) is needed here at all.
if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) {
eprintln!("aura: {e:?}");
std::process::exit(2);
}
+29 -3
View File
@@ -182,12 +182,23 @@ impl Env {
TraceStore::open(self.runs_root())
}
/// The data archive root: `paths.data` inside a project when set,
/// the data-server default otherwise.
/// The data archive root: `paths.data` inside a project when set
/// (relative values resolved against the project root, same as
/// `runs_root()` and the `[nodes]` pointers), the data-server default
/// otherwise.
pub fn data_path(&self) -> String {
self.project
.as_ref()
.and_then(|p| p.toml.paths.data.clone())
.and_then(|p| {
p.toml.paths.data.as_ref().map(|data| {
let path = Path::new(data);
if path.is_absolute() {
data.clone()
} else {
p.root.join(path).to_string_lossy().to_string()
}
})
})
.unwrap_or_else(|| data_server::DEFAULT_DATA_PATH.to_string())
}
@@ -615,6 +626,21 @@ mod tests {
std::fs::remove_dir_all(&tmp).ok();
}
/// #254: a relative `paths.data` must resolve against the project root
/// (`p.root`), matching `runs_root()`'s and the `[nodes]` pointers'
/// resolution — not against the invoking process's cwd, which silently
/// changes the data location depending on the caller's working directory.
#[test]
fn data_path_resolves_a_relative_paths_data_against_the_project_root() {
let tmp = std::env::temp_dir().join(format!("aura-datarel-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("Aura.toml"), "[paths]\ndata = \"data\"\n").unwrap();
let p = load(&tmp, false).expect("data-only load");
let env = Env::with_project(p);
assert_eq!(env.data_path(), tmp.join("data").to_string_lossy().to_string());
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn two_nodes_pointers_refuse_with_multi_crate() {
let tmp = std::env::temp_dir().join(format!("aura-multi-{}", std::process::id()));
+237 -173
View File
@@ -1,12 +1,11 @@
//! Integration test: drive the built `aura` binary as a downstream user would,
//! asserting the `run` subcommand's stdout/exit contract and the bad-args path.
use std::path::{Path, PathBuf};
use std::path::Path;
use std::process::Command;
use std::sync::MutexGuard;
mod common;
use common::{built_project, project_lock};
use common::{fresh_project, fresh_project_with_data};
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
@@ -22,29 +21,6 @@ fn temp_cwd(name: &str) -> std::path::PathBuf {
dir
}
/// Removes `<fixture>/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);
impl Drop for RunsCleanup {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
/// Enter the shared built project fixture with a FRESH `runs/` store, serialized.
/// Returns the fixture dir (has an Aura.toml, so `provenance()` is `Some`) and the
/// guards; bind the guards for the whole test body: `let (dir, _g) = fresh_project();`.
/// The guard tuple orders `RunsCleanup` before the `MutexGuard` deliberately: tuple
/// fields drop in index order, so the `runs/` removal (index 0) runs while the lock
/// (index 1) is still held — the serialization the lock exists to enforce.
fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>)) {
let lock = project_lock();
let dir = built_project();
let runs = dir.join("runs");
std::fs::remove_dir_all(&runs).ok();
(dir, (RunsCleanup(runs), lock))
}
/// #218: a well-formed dissolved verb run outside a project refuses up front
/// (exit 1, "needs a project") and leaves no store — parity with `campaign run`.
/// The refusal names the INVOKED verb, not a copy-pasted generic string — a
@@ -1215,7 +1191,7 @@ fn sweep_real_no_geometry_symbol_refuses_with_exit_1() {
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args(["sweep", &fixture, "--axis", "sma_signal.fast.length=2,4", "--real", "NONEXISTENT"])
.current_dir(dir).output().unwrap();
.current_dir(&dir).output().unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry"));
}
@@ -1301,7 +1277,7 @@ fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
"--axis", "sma_signal.slow.length=8",
"--name", "brp_nostop",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -1348,7 +1324,7 @@ fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
"--axis", "sma_signal.slow.length=8",
"--name", "brp_provenance",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -1434,7 +1410,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() {
let fixture = format!("{}/tests/fixtures/r_channel_open.json", env!("CARGO_MANIFEST_DIR"));
let axes = std::process::Command::new(BIN)
.args(["sweep", &fixture, "--list-axes"])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert_eq!(
@@ -1455,7 +1431,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() {
"--axis", "hl_channel.channel_length=3,5",
"--name", "chan",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -1476,7 +1452,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() {
.to_string();
let rep = std::process::Command::new(BIN)
.args(["reproduce", &family_id])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(
@@ -1523,7 +1499,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -1571,7 +1547,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
// exactly one new Sweep family persisted, with the grid's four members.
let fams = std::process::Command::new(BIN)
.args(["runs", "families"])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
@@ -1674,7 +1650,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out2.status.success(), "second run stderr: {}", String::from_utf8_lossy(&out2.stderr));
@@ -1761,7 +1737,7 @@ fn sweep_real_with_trace_writes_tap_series_to_disk() {
"--axis", "sma_signal.slow.length=8",
"--trace", "brp",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
// (1) the #168 refusal is lifted: the advertised flag runs instead of exit 2.
@@ -1829,13 +1805,13 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() {
"--axis", "sma_signal.slow.length=8",
"--name", "repro_sweep",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(mint.status.success(), "sweep mint stderr: {}", String::from_utf8_lossy(&mint.stderr));
let id = family_id_of_kind(dir, "Sweep");
let out = Command::new(BIN).args(["reproduce", &id]).current_dir(dir).output().unwrap();
let id = family_id_of_kind(&dir, "Sweep");
let out = Command::new(BIN).args(["reproduce", &id]).current_dir(&dir).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
@@ -1984,13 +1960,13 @@ fn reproduce_real_walkforward_family_does_not_panic() {
"--axis", "sma_signal.slow.length=8",
"--name", "repro_wf",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(mint.status.success(), "walkforward mint stderr: {}", String::from_utf8_lossy(&mint.stderr));
let id = family_id_of_kind(dir, "WalkForward");
let out = Command::new(BIN).args(["reproduce", &id]).current_dir(dir).output().unwrap();
let id = family_id_of_kind(&dir, "WalkForward");
let out = Command::new(BIN).args(["reproduce", &id]).current_dir(&dir).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_ne!(
@@ -2147,7 +2123,7 @@ fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() {
"--axis", "hl_signal.fast.length=2,4",
"--axis", "hl_signal.slow.length=8",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -2232,7 +2208,7 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
"--axis", "sma_signal.fast.length=2,4",
"--name", "c0110-subset",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.expect("spawn aura sweep (subset axes)");
assert_ne!(
@@ -2288,7 +2264,7 @@ fn sweep_dissolved_accepts_an_axis_over_a_bound_param() {
"--axis", "sma_signal.fast.length=2,3",
"--name", "bound-override",
])
.current_dir(dir)
.current_dir(&dir)
.output()
.expect("spawn aura sweep over a bound-param axis");
assert_eq!(
@@ -2298,7 +2274,7 @@ fn sweep_dissolved_accepts_an_axis_over_a_bound_param() {
path too, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(dir).output().unwrap();
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&dir).output().unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
@@ -2731,7 +2707,7 @@ fn generalize_grades_a_candidate_across_two_instruments() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2777,7 +2753,7 @@ fn generalize_real_e2e_pins_the_exact_current_grade() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2834,7 +2810,7 @@ fn generalize_grades_a_non_r_sma_blueprint() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2882,7 +2858,7 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2944,7 +2920,7 @@ fn walkforward_dissolves_a_non_r_sma_blueprint() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2992,8 +2968,11 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() {
/// #215: --select plateau is no longer refused on the dissolved path — it now
/// threads through to the wf stage exactly like the built-in synthetic path.
/// Gated on the shared GER40 archive; skips cleanly on a data refusal (exit 1),
/// same tolerance as the sibling stop-knob-defaulting test.
/// The property is flag acceptance, not window semantics, so the window is
/// bounded (~135 days, one IS(90d)+OOS(30d) roll) rather than streaming the
/// full multi-year archive. Gated on the shared GER40 archive; skips cleanly
/// on a data refusal (exit 1), same tolerance as the sibling
/// stop-knob-defaulting test.
#[test]
fn walkforward_dissolved_accepts_select_plateau() {
let (cwd, _g) = fresh_project();
@@ -3004,8 +2983,9 @@ fn walkforward_dissolved_accepts_select_plateau() {
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--select", "plateau:worst",
"--from", "1735689600000", "--to", "1747353600000",
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3050,7 +3030,7 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() {
"--from", FROM_MS, "--to", TO_MS,
"--select", "plateau:worst",
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3090,8 +3070,10 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() {
/// explicit still succeeds, proving the defaulting is per-flag, not
/// all-or-nothing (the sibling
/// `walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2`
/// pins the both-omitted case). Gated on the shared GER40 archive; skips
/// cleanly on a data refusal.
/// pins the both-omitted case). The property is per-flag defaulting, not
/// window semantics, so the window is bounded (~135 days) rather than
/// streaming the full multi-year archive. Gated on the shared GER40 archive;
/// skips cleanly on a data refusal.
#[test]
fn walkforward_dissolved_defaults_a_single_omitted_knob() {
let (cwd, _g) = fresh_project();
@@ -3101,8 +3083,9 @@ fn walkforward_dissolved_defaults_a_single_omitted_knob() {
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "3",
"--from", "1735689600000", "--to", "1747353600000",
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3250,7 +3233,7 @@ fn walkforward_dissolves_through_the_campaign_path() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3296,7 +3279,7 @@ fn walkforward_dissolves_through_the_campaign_path() {
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -3344,7 +3327,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if stopless.status.code() == Some(1) {
@@ -3373,7 +3356,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(
@@ -3427,7 +3410,7 @@ fn generalize_dissolves_through_the_campaign_path() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3478,7 +3461,7 @@ fn generalize_dissolves_through_the_campaign_path() {
// per-instrument Sweep family per cell (persisted by the executor).
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -3519,7 +3502,7 @@ fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
@@ -3575,7 +3558,7 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
@@ -3611,30 +3594,24 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
/// independent full window). A wrong intersection (or a panic on the
/// per-symbol probe) in that fallback would only surface when `--from`/`--to`
/// are absent — every other generalize e2e pins the explicit-window path,
/// leaving this one unexercised. Gated on local GER40/USDJPY data.
/// leaving this one unexercised. The property is the no-window RESOLUTION
/// mechanics, not any particular archive's span, so it runs against
/// `fresh_project_with_data`'s tiny synthetic two-symbol archive (#250) —
/// hostless, no data-refusal skip arm needed.
#[test]
fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
let (cwd, _g) = fresh_project();
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"generalize", &fixture, "--real", "SYMA,SYMB",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
assert_eq!(out.status.code(), Some(0), "exit: {:?}, stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
@@ -3650,24 +3627,21 @@ fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
/// instrument conflates the instrument axis with the period axis, so the common
/// window is the only one that isolates what the verb measures. Today's fallback
/// picks `symbols[0]`'s full window alone (the superseded shape test above):
/// listing the WIDER symbol first (`AAPL.US`, archive from 2006) keeps AAPL's
/// early start, whereas the intersection with the later-starting `GER40` (archive
/// from 2014) must start at GER40's first bar and end at whichever archive ends
/// first — so on the current tree the resolved window differs from the
/// listing the WIDER synthetic symbol first (`SYMA`, spanning 2024-01..08) keeps
/// SYMA's early start, whereas the intersection with `SYMB` (spanning 2024-03..06,
/// strictly inside SYMA's span) must start at SYMB's first bar and end at SYMB's
/// last — so on the current tree the resolved window differs from the
/// intersection on both bounds. Each symbol's full window is resolved
/// independently here: a single-symbol `sweep` with no window records it into its
/// generated campaign document, in the same Unix-ms currency and via the same
/// `campaign_window_ms(full_window)` probe generalize itself resolves against —
/// so the expected intersection is derived from the live archive, never a
/// hard-coded span that shifts as the archive grows. Archive-gated; skips
/// cleanly on a data-less host.
/// hard-coded span. The archive's SPAN SHAPE is authored (#250,
/// `fresh_project_with_data`), not its size, so this runs hostless in
/// milliseconds instead of streaming two full host archives.
#[test]
fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (cwd, _g) = fresh_project();
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
// The resolved shared window persists verbatim in the generated campaign
@@ -3700,7 +3674,7 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols()
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", name,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura sweep");
assert_eq!(
@@ -3711,28 +3685,28 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols()
window_of(name)
};
let aapl = probe("AAPL.US", "probe-aapl");
let ger40 = probe("GER40", "probe-ger40");
let syma = probe("SYMA", "probe-syma");
let symb = probe("SYMB", "probe-symb");
// Precondition: the archives genuinely differ, so "the intersection" is a
// distinct claim from "symbols[0]'s full window" (else the test is vacuous).
assert_ne!(
aapl, ger40,
"AAPL.US {aapl:?} and GER40 {ger40:?} must span different windows for the test to bite"
syma, symb,
"SYMA {syma:?} and SYMB {symb:?} must span different windows for the test to bite"
);
// The contract: the shared window is the intersection — latest start, earliest end.
let expected = (aapl.0.max(ger40.0), aapl.1.min(ger40.1));
let expected = (syma.0.max(symb.0), syma.1.min(symb.1));
// AAPL.US listed FIRST: today's `symbols[0]` fallback keeps AAPL's (wider)
// SYMA listed FIRST: today's `symbols[0]` fallback keeps SYMA's (wider)
// window, which differs from the intersection on both bounds — so a pass here
// can only mean the intersection semantics landed.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "AAPL.US,GER40",
"generalize", &fixture, "--real", "SYMA,SYMB",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", "gen-both",
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura generalize");
assert_eq!(
@@ -3744,8 +3718,8 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols()
assert_eq!(
resolved, expected,
"no-window generalize must resolve the INTERSECTION {expected:?} of AAPL.US {aapl:?} \
and GER40 {ger40:?} not symbols[0]'s ({aapl:?}) full window (resolved {resolved:?})"
"no-window generalize must resolve the INTERSECTION {expected:?} of SYMA {syma:?} \
and SYMB {symb:?} not symbols[0]'s ({syma:?}) full window (resolved {resolved:?})"
);
}
@@ -3912,7 +3886,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3929,7 +3903,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
// the run persisted the per-instrument members as a discoverable CrossInstrument family.
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
@@ -3974,7 +3948,7 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3990,7 +3964,7 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() {
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
@@ -4041,7 +4015,7 @@ fn generalize_family_members_stamp_project_provenance() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -4057,7 +4031,7 @@ fn generalize_family_members_stamp_project_provenance() {
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
@@ -5076,7 +5050,7 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5133,7 +5107,7 @@ fn mc_dissolves_a_non_r_sma_blueprint() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5188,7 +5162,7 @@ fn mc_dissolves_through_the_campaign_path() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5242,7 +5216,7 @@ fn mc_dissolves_through_the_campaign_path() {
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -5268,11 +5242,12 @@ fn mc_dissolves_through_the_campaign_path() {
/// to completion by fitting the injected walk-forward roller down to the campaign
/// window, instead of refusing late at the executor with "walk_forward windows do not
/// fit the campaign window" (exit 1, no remedy reachable from the mc surface). The
/// short window is DERIVED from the live archive — probe GER40's full span via a
/// no-window single-cell sweep, then take the last ~30 days — the same live-archive
/// derivation `generalize_without_explicit_window_resolves_the_intersection_of_all_symbols`
/// uses, so the window stays short as the archive grows and always holds recent bars.
/// The leading sweep stage runs first over the short window; the refusal today fires at
/// short window is a FIXED ~30 days (2025-04-16..2025-05-16, the tail of the
/// ~135-day span the walkforward e2e tests use): what the property needs is "a
/// data-carrying window far shorter than the roller", which a fixed recent window
/// satisfies without the full-archive probe sweep a live-span derivation costs —
/// the archive only grows forward, so the window never falls off its end. The
/// leading sweep stage runs first over the short window; the refusal today fires at
/// stage 1 (the walk_forward roller), so this bites only when the window genuinely has
/// data. Gated on the GER40 archive; skips cleanly on a data-less host.
#[test]
@@ -5284,56 +5259,10 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() {
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
// Probe GER40's full archive window: a single-cell, no-window sweep records
// `campaign_window_ms(full_window)` into its generated campaign document, in the
// same Unix-ms currency `--from`/`--to` speak (the generalize no-window test's
// derivation).
let full_window = {
let name = "probe-ger40-span";
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"sweep", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", name,
])
.current_dir(cwd)
.output()
.expect("spawn aura sweep probe");
assert_eq!(
out.status.code(), Some(0),
"the full-archive window-probe sweep must run to exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
let dir = cwd.join("runs").join("campaigns");
let mut resolved = None;
for entry in std::fs::read_dir(&dir).expect("campaigns dir exists after a run") {
let path = entry.expect("readable dir entry").path();
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).expect("read campaign doc"))
.expect("campaign doc parses as JSON");
if doc["name"].as_str() == Some(name) {
let w = &doc["data"]["windows"][0];
resolved = Some((
w["from_ms"].as_i64().expect("from_ms is an integer"),
w["to_ms"].as_i64().expect("to_ms is an integer"),
));
break;
}
}
resolved.expect("the probe campaign document records GER40's full window")
};
// The last ~30 days of the archive: far shorter than the fixed 90+30-day roller,
// so the injected walk_forward cannot fit unless it scales down to the window.
const THIRTY_DAYS_MS: i64 = 30 * 86_400_000;
let sub_from = full_window.1 - THIRTY_DAYS_MS;
let sub_to = full_window.1;
assert!(
sub_from > full_window.0,
"the derived ~30-day sub-window must sit inside the archive span {full_window:?}"
);
let sub_from_s = sub_from.to_string();
let sub_to_s = sub_to.to_string();
// ~30 days, far shorter than the fixed 90+30-day roller, so the injected
// walk_forward cannot fit unless it scales down to the window.
const SUB_FROM_MS: &str = "1744761600000";
const SUB_TO_MS: &str = "1747353600000";
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -5341,9 +5270,9 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() {
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "100", "--seed", "42",
"--from", &sub_from_s, "--to", &sub_to_s,
"--from", SUB_FROM_MS, "--to", SUB_TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura mc");
@@ -5452,7 +5381,7 @@ fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
"--block-len", "5", "--resamples", "1000", "--seed", seed,
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
@@ -5513,7 +5442,7 @@ fn walkforward_campaign_runs_an_arbitrary_blueprint() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5565,7 +5494,7 @@ fn mc_campaign_runs_an_arbitrary_blueprint() {
"--block-len", "5", "--resamples", "100", "--seed", "7",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5614,7 +5543,7 @@ fn generalize_campaign_runs_an_arbitrary_blueprint() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS,
])
.current_dir(cwd)
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -5765,6 +5694,141 @@ fn shipped_r_sma_example_reproduces_the_builtin_grade() {
assert!(stdout.contains("\"n_trades\":3"), "{stdout}");
}
/// Characterization pin (#252, byte-identity anchor before `probe_window`'s
/// internals swap from a full-window drain to `aura_ingest::archive_extent`'s
/// boundary-month derivation). Captured against the PRE-#252 drain-based code
/// over `fresh_project_with_data`'s hermetic two-symbol synthetic archive
/// (SYMA spans 2024-01..08, SYMB 2024-03..06, weekday 08:00-16:00 UTC bars):
/// the resolved `campaign_window_ms(full_window)` span persists verbatim into
/// the generated campaign document's `data.windows[0]`, exactly like the
/// sibling `generalize_without_explicit_window_resolves_the_intersection_of_all_symbols`
/// probe. Three cases: SYMA's and SYMB's full (no-window) spans, and an
/// explicit `--from`/`--to` sub-window straddling three month files
/// (2024-03-15..2024-05-15, so `filter_files` alone would load 3 files under
/// the old drain). The first bar of each captured span carries a known -1ms
/// float round-trip artifact (the synthetic fixture's Delphi-`TDateTime`
/// pack/unpack in `data-server`'s own record format, not a `probe_window` bug)
/// — this test does not explain it, only pins it exactly, since the
/// post-swap code must reproduce the identical bytes (same underlying decoder,
/// only fewer files touched to reach them).
#[test]
fn probe_window_resolves_the_pinned_span_before_the_252_swap() {
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let window_of = |name: &str| -> (i64, i64) {
let dir = cwd.join("runs").join("campaigns");
let path = std::fs::read_dir(&dir)
.expect("campaigns dir exists after a run")
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(p).expect("read campaign doc"))
.expect("campaign doc parses as JSON");
doc["name"].as_str() == Some(name)
})
.unwrap_or_else(|| panic!("no campaign document named {name:?} in {}", dir.display()));
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).expect("read campaign doc"))
.expect("campaign doc parses as JSON");
let w = &doc["data"]["windows"][0];
(
w["from_ms"].as_i64().expect("from_ms is an integer"),
w["to_ms"].as_i64().expect("to_ms is an integer"),
)
};
let sweep = |symbol: &str, name: &str, from: Option<&str>, to: Option<&str>| {
let mut args = vec![
"sweep", fixture.as_str(), "--real", symbol,
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", name,
];
if let Some(f) = from {
args.push("--from");
args.push(f);
}
if let Some(t) = to {
args.push("--to");
args.push(t);
}
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(&args)
.current_dir(&cwd)
.output()
.expect("spawn aura sweep");
assert_eq!(
out.status.code(), Some(0),
"window-probe sweep {name:?} must run to exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
};
sweep("SYMA", "probe-252-no-window-syma", None, None);
assert_eq!(
window_of("probe-252-no-window-syma"),
(1_704_095_999_999, 1_725_033_540_000),
"SYMA's full (no-window) resolved span"
);
sweep("SYMB", "probe-252-no-window-symb", None, None);
assert_eq!(
window_of("probe-252-no-window-symb"),
(1_709_279_999_999, 1_719_590_340_000),
"SYMB's full (no-window) resolved span"
);
// 2024-03-15 00:00 UTC .. 2024-05-15 00:00 UTC: straddles the March, April,
// and May files (three boundary months under the old file-range drain).
sweep("SYMA", "probe-252-straddle", Some("1710460800000"), Some("1715731200000"));
assert_eq!(
window_of("probe-252-straddle"),
(1_710_489_599_999, 1_715_702_340_000),
"explicit sub-window straddling month boundaries, clipped to the archive's actual bars"
);
}
/// Characterization pin (#252, hostless zero-bar case): a window that falls
/// entirely on a weekend inside a COVERED month (2024-03-02/03, both inside
/// SYMA's Jan-Aug 2024 span, weekday-only bars) still refuses with the SAME
/// exit-1 "no data ... in the requested window" message `probe_window`'s
/// drain-based emptiness check raises today — `archive_extent` must reach the
/// identical conclusion by finding zero matching months in range, not by
/// draining a stream, and the refusal wording is part of what must not move
/// (mirrors `run_real_empty_window_refusal_names_the_window_not_the_symbol`,
/// hostless here since the property (an empty resolved sub-window still
/// refuses) needs no full archive, only a covered-but-empty range).
#[test]
fn probe_window_still_refuses_a_hostless_zero_bar_window() {
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"sweep", &fixture, "--real", "SYMA",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", "probe-252-zero-bars",
"--from", "1709337600000", // 2024-03-02 00:00 UTC (Saturday)
"--to", "1709424000000", // 2024-03-03 00:00 UTC (Sunday)
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep");
assert_eq!(
out.status.code(), Some(1),
"a zero-bar window inside covered archive months still refuses with exit 1: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no data") && stderr.contains("window"),
"must name the empty window, not a missing symbol: {stderr}"
);
assert!(
!cwd.join("runs").join("campaigns").exists()
|| std::fs::read_dir(cwd.join("runs").join("campaigns")).unwrap().count() == 0,
"a refused zero-bar window must not register a generated campaign document"
);
}
/// `aura run` on the shipped closed r-meanrev example (#159 cut 3) actually loads
/// and grades through the real, separately-linked `aura` binary — not merely
/// in-process, as `r_meanrev_example_loaded_runs_identically_to_the_carved_signal`
+222 -16
View File
@@ -3,14 +3,11 @@
//! 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.
//! **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
@@ -18,7 +15,8 @@
//! rather than deleting a helper just because one binary doesn't need it.
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard, OnceLock};
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
@@ -42,13 +40,82 @@ pub fn built_project() -> &'static PathBuf {
})
}
/// 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 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);
}
}
/// Mints a unique, empty tempdir under `std::env::temp_dir()`, named
/// `aura-e2e-<pid>-<counter>` so concurrent test threads (same process) and
/// concurrent `cargo test` processes never collide.
fn mint_tempdir() -> PathBuf {
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");
root
}
/// Writes the `Aura.toml` + `demo_signal.json` common to every fresh project:
/// an absolute `[nodes]` pointer at the shared, once-built fixture crate, and
/// — when `data_dir` is `Some` — a `[paths] data` override pointing at it
/// (relative, resolved against the project root — `root` itself — since
/// `Env::data_path` joins a relative `paths.data` onto the project root,
/// same as `runs_root()` and the `[nodes]` pointers, #254).
fn write_project_files(root: &Path, data_dir: Option<&str>) {
let fixture = built_project();
let data_line = data_dir.map(|d| format!("data = \"{d}\"\n")).unwrap_or_default();
std::fs::write(
root.join("Aura.toml"),
format!(
"[paths]\nruns = \"runs\"\n{data_line}\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");
}
/// 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) {
let root = mint_tempdir();
write_project_files(&root, None);
(root.clone(), FreshProject { root })
}
/// Like [`fresh_project`], but additionally generates a tiny synthetic M1
/// archive under `<root>/data/` (two symbols, `SYMA` spanning 2024-01..08 and
/// `SYMB` spanning 2024-03..06 — `SYMB` lies strictly inside `SYMA`) and
/// points `[paths] data` at it. For tests whose property is archive-window
/// (span) semantics, not archive size — no host tick-data mount needed, and
/// no `--real`-symbol data refusal is ever reachable, so gates on data
/// presence are dead code once a test switches to this helper (#250).
#[allow(dead_code)]
pub fn fresh_project_with_data() -> (PathBuf, FreshProject) {
let root = mint_tempdir();
write_project_files(&root, Some("data"));
let data_dir = root.join("data");
std::fs::create_dir_all(&data_dir).expect("create synthetic archive dir");
synthetic_data::write_symbol_archive(&data_dir, "SYMA", (2024, 1), (2024, 8));
synthetic_data::write_symbol_archive(&data_dir, "SYMB", (2024, 3), (2024, 6));
(root.clone(), FreshProject { root })
}
/// A scratch filesystem entry a test writes under the git-tracked
@@ -78,3 +145,142 @@ impl Drop for ScratchGuard {
}
}
}
/// A tiny, deterministic synthetic M1 archive generator: hand-packs the exact
/// on-disk format `data-server` reads (its `loader.rs` and `records.rs`) —
/// one zip per `SYMBOL_YYYY_MM.m1` holding a single `.bin` entry of packed
/// 48-byte `RawM1Record`s, plus a `SYMBOL.meta.json` geometry sidecar — so
/// the two no-window `generalize` E2E tests get a real, tiny, hostless
/// archive instead of streaming the 6.6 GB host archive (#250).
#[allow(dead_code)]
mod synthetic_data {
use std::io::Write;
use std::path::Path;
/// Days since the Unix epoch for a proleptic-Gregorian civil date.
/// Howard Hinnant's `days_from_civil` — dependency-free calendar math so
/// this module needs no `chrono` (data-server's own dependency, not
/// aura-cli's).
fn days_from_civil(y: i32, m: u32, d: u32) -> i64 {
let y = i64::from(if m <= 2 { y - 1 } else { y });
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400; // [0, 399]
let mp = (i64::from(m) + 9) % 12; // [0, 11], Mar=0 .. Feb=11
let doy = (153 * mp + 2) / 5 + i64::from(d) - 1; // [0, 365]
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
era * 146_097 + doe - 719_468
}
/// Unix milliseconds at `y-m-d hh:mm:00` UTC.
fn unix_ms(y: i32, m: u32, d: u32, hh: u32, mm: u32) -> i64 {
days_from_civil(y, m, d) * 86_400_000 + (i64::from(hh) * 3600 + i64::from(mm) * 60) * 1000
}
/// 0 = Sunday .. 6 = Saturday (1970-01-01 was a Thursday, day 0).
fn weekday(y: i32, m: u32, d: u32) -> i64 {
(days_from_civil(y, m, d) % 7 + 4 + 7) % 7
}
fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29,
2 => 28,
_ => unreachable!("month out of range: {m}"),
}
}
/// A deterministic price at continuous minute offset `t` (minutes since
/// 2024-01-01T00:00 UTC, shared across both symbols so their overlapping
/// span carries the same underlying curve): a gentle upward trend plus a
/// 180-minute sine — short enough against a day's 480 tradeable minutes to
/// produce several SMA(3)/SMA(12) crossovers per day.
fn price_at(t_minutes: f64) -> f64 {
let hours = t_minutes / 60.0;
100.0 + 0.01 * hours + 5.0 * (2.0 * std::f64::consts::PI * t_minutes / 180.0).sin()
}
/// Packs one `RawM1Record`-shaped 48-byte little-endian record: `time`
/// (Delphi `TDateTime`, days since 1899-12-30 — the inverse of
/// `data_server::records::delphi_to_unix_ms`), `open/high/low/close`,
/// `spread` (f32), `volume` (i32).
fn pack_record(unix_ms_time: i64, open: f64, high: f64, low: f64, close: f64) -> [u8; 48] {
const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0;
const MS_PER_DAY: f64 = 86_400_000.0;
let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS;
let mut rec = [0u8; 48];
rec[0..8].copy_from_slice(&dt.to_le_bytes());
rec[8..16].copy_from_slice(&open.to_le_bytes());
rec[16..24].copy_from_slice(&high.to_le_bytes());
rec[24..32].copy_from_slice(&low.to_le_bytes());
rec[32..40].copy_from_slice(&close.to_le_bytes());
rec[40..44].copy_from_slice(&0.5_f32.to_le_bytes());
rec[44..48].copy_from_slice(&100_i32.to_le_bytes());
rec
}
/// One month's worth of weekday, 08:00-16:00 UTC, 1-minute bars (~10k).
fn month_records(year: i32, month: u32) -> Vec<[u8; 48]> {
let epoch_ms = unix_ms(2024, 1, 1, 0, 0);
let mut records = Vec::new();
for day in 1..=days_in_month(year, month) {
if !(1..=5).contains(&weekday(year, month, day)) {
continue; // weekend
}
for hh in 8..16 {
for mm in 0..60 {
let ms = unix_ms(year, month, day, hh, mm);
let t = (ms - epoch_ms) as f64 / 60_000.0;
let (open, close) = (price_at(t), price_at(t + 1.0));
let high = open.max(close) + 0.2;
let low = open.min(close) - 0.2;
records.push(pack_record(ms, open, high, low, close));
}
}
}
records
}
/// Writes one `SYMBOL_YYYY_MM.m1` zip (a single `<SYMBOL>.bin` entry of
/// packed records — mirrors `data-server`'s own `create_test_m1_zip` test
/// helper) under `data_dir`.
fn write_month_zip(data_dir: &Path, symbol: &str, year: i32, month: u32) {
let path = data_dir.join(format!("{symbol}_{year:04}_{month:02}.m1"));
let file = std::fs::File::create(&path).expect("create synthetic m1 zip");
let mut zip = zip::ZipWriter::new(file);
zip.start_file::<String, ()>(format!("{symbol}.bin"), Default::default())
.expect("start synthetic m1 zip entry");
for rec in month_records(year, month) {
zip.write_all(&rec).expect("write synthetic m1 record");
}
zip.finish().expect("finish synthetic m1 zip");
}
/// Writes every month in `[from, to]` (inclusive, `(year, month)` pairs)
/// for `symbol`, plus its `<SYMBOL>.meta.json` geometry sidecar —
/// GER40-shaped (digits 1, pip/tick/lot sized like an index CFD) so the
/// R/stop machinery never hits "no recorded geometry".
pub fn write_symbol_archive(data_dir: &Path, symbol: &str, from: (i32, u32), to: (i32, u32)) {
let (mut y, mut m) = from;
loop {
write_month_zip(data_dir, symbol, y, m);
if (y, m) == to {
break;
}
m += 1;
if m > 12 {
m = 1;
y += 1;
}
}
std::fs::write(
data_dir.join(format!("{symbol}.meta.json")),
format!(
"{{\"schemaVersion\":2,\"symbol\":\"{symbol}\",\"digits\":1,\"pipSize\":1,\
\"tickSize\":0.1,\"lotSize\":1,\"baseAsset\":\"{symbol}\",\"quoteAsset\":\"EUR\"}}"
),
)
.expect("write synthetic geometry sidecar");
}
}
+1 -2
View File
@@ -9,7 +9,7 @@ use std::process::{Command, Output};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
mod common;
use common::{built_project, project_lock};
use common::built_project;
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project")
@@ -98,7 +98,6 @@ 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 _fixture = project_lock();
let dir = built_project();
let sub = dir.join("src");
let runs_dir = dir.join("runs");
@@ -63,11 +63,15 @@ fn aura_in(dir: &Path, args: &[&str]) -> Output {
/// `blueprints/scaled_open.json`. Returns the project dir. `OnceLock` so the
/// expensive scaffold+build happens once per test binary; the node crate's
/// path-dep resolves into this checkout (the baked engine root), like
/// `project_new.rs`.
/// `project_new.rs`. The base is a FIXED name under `CARGO_TARGET_TMPDIR`
/// (real disk, reclaimed by `cargo clean`), not a pid-suffixed tempdir: the
/// wipe below must hit the PREVIOUS run's copy, else every test-binary run
/// leaks the ~66 MB built scaffold — 159 of those filled the 16 GB /tmp
/// tmpfs to 100 % (#257).
fn built_scale_project() -> &'static PathBuf {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let base = std::env::temp_dir().join(format!("aura-235-{}", std::process::id()));
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("aura-235-knob-lab");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).expect("create scaffold base");
+172 -205
View File
@@ -5,7 +5,7 @@
use std::path::{Path, PathBuf};
mod common;
use common::{ScratchGuard, ScratchPath, built_project, project_lock};
use common::{ScratchGuard, ScratchPath, fresh_project};
/// A fresh, unique working directory for a process test that persists
/// content-addressed documents under `./runs/` (so `aura process register`
@@ -304,8 +304,7 @@ fn process_register_stores_content_addressed_under_runs_root() {
/// them together end to end.
#[test]
fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -321,7 +320,7 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
// test).
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (sweep_out, sweep_code) = run_code_in(
dir,
&dir,
&[
"sweep",
&closed_bp,
@@ -360,8 +359,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
// overfit_probability (not per-member) flip to a per-member scalar.
.replacen("\"overfit_probability\", \"cmp\": \"lt\", \"value\": 0.1", "\"win_rate\", \"cmp\": \"lt\", \"value\": 1.1", 1);
assert_ne!(exec_process, PROCESS_DOC, "replacen must match the sweep stage");
write_doc(dir, "seed.process.json", &exec_process);
let (proc_out, proc_code) = run_code_in(dir, &["process", "register", "seed.process.json"]);
write_doc(&dir, "seed.process.json", &exec_process);
let (proc_out, proc_code) = run_code_in(&dir, &["process", "register", "seed.process.json"]);
assert_eq!(proc_code, Some(0), "seed process register failed: {proc_out}");
let proc_id = proc_out
.lines()
@@ -394,8 +393,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
bp = bp_id,
proc = proc_id
);
write_doc(dir, "ref-ok.campaign.json", &campaign);
let (out, code) = run_code_in(dir, &["campaign", "validate", "ref-ok.campaign.json"]);
write_doc(&dir, "ref-ok.campaign.json", &campaign);
let (out, code) = run_code_in(&dir, &["campaign", "validate", "ref-ok.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
assert!(out.contains(
"campaign document valid (referential): all references resolve, axes are in the param space"
@@ -404,8 +403,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
// An unresolved process reference: the fault block + `ref_fault_prose`
// mapping, prose exit 1, at the same CLI seam.
let bad = campaign.replace(&proc_id, "0000000000000000000000000000000000000000000000000000000000000000");
write_doc(dir, "ref-bad.campaign.json", &bad);
let (out2, code2) = run_code_in(dir, &["campaign", "validate", "ref-bad.campaign.json"]);
write_doc(&dir, "ref-bad.campaign.json", &bad);
let (out2, code2) = run_code_in(&dir, &["campaign", "validate", "ref-bad.campaign.json"]);
assert_eq!(code2, Some(1));
assert!(out2.contains("campaign references do not resolve:"), "stdout/stderr: {out2}");
assert!(out2.contains("not found in the project store"), "stdout/stderr: {out2}");
@@ -1129,9 +1128,8 @@ fn campaign_run_outside_project_refuses() {
/// both readings (inside the project, past the project gate).
#[test]
fn campaign_run_bogus_target_refuses() {
let _fixture = project_lock();
let dir = built_project();
let (out, code) = run_code_in(dir, &["campaign", "run", "no-such-target"]);
let (dir, _fixture) = fresh_project();
let (out, code) = run_code_in(&dir, &["campaign", "run", "no-such-target"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("'no-such-target' is neither a readable .json file nor a 64-hex content id"),
@@ -1142,10 +1140,9 @@ fn campaign_run_bogus_target_refuses() {
/// A well-formed but unknown content id refuses with the store-miss prose.
#[test]
fn campaign_run_unknown_id_refuses() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let id = "0".repeat(64);
let (out, code) = run_code_in(dir, &["campaign", "run", &id]);
let (out, code) = run_code_in(&dir, &["campaign", "run", &id]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains(&format!("no campaign {id} in the project store")),
@@ -1169,8 +1166,7 @@ fn campaign_run_unknown_id_refuses() {
/// bare-store-line contract exactly.
#[test]
fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
std::fs::create_dir_all(&runs_dir).expect("create runs dir");
@@ -1185,7 +1181,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
// LIST mode: one scannable line per stored record, carrying the campaign
// id (bare 64-hex or a prefix), exit 0.
let (list_out, list_code) = run_code_in(dir, &["campaign", "runs"]);
let (list_out, list_code) = run_code_in(&dir, &["campaign", "runs"]);
assert_eq!(list_code, Some(0), "list exits 0: {list_out}");
assert!(
list_out.contains(&campaign_id[..8]),
@@ -1195,7 +1191,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
// DUMP mode: addressed by the campaign content id, prints the BARE stored
// record line — the store form, NOT the `{{"campaign_run": …}}` run-emit
// wrapper (F10) — byte-identical to the seeded line, exit 0.
let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]);
let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]);
assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}");
assert!(
!dump_out.contains("\"campaign_run\""),
@@ -1221,8 +1217,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
/// depends on.
#[test]
fn campaign_runs_dump_round_trips_a_populated_trace_name() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
std::fs::create_dir_all(&runs_dir).expect("create runs dir");
@@ -1235,7 +1230,7 @@ fn campaign_runs_dump_round_trips_a_populated_trace_name() {
std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n"))
.expect("seed the campaign-run store");
let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]);
let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]);
assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}");
assert_eq!(
dump_out.trim(),
@@ -1254,8 +1249,7 @@ fn campaign_runs_dump_round_trips_a_populated_trace_name() {
/// runs, with Debug-free PipelineShape prose.
#[test]
fn campaign_run_refuses_mc_before_walk_forward() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1263,10 +1257,10 @@ fn campaign_run_refuses_mc_before_walk_forward() {
ScratchPath::File(dir.join("mcwf.process.json")),
ScratchPath::File(dir.join("mcwf.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-mcwf-seed");
let proc_id = register_process_doc(dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
write_doc(dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "mcwf.campaign.json"]);
let bp_id = seed_blueprint(&dir, "campaign-run-mcwf-seed");
let proc_id = register_process_doc(&dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
write_doc(&dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "mcwf.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
assert!(
@@ -1304,8 +1298,7 @@ const SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC: &str = r#"{
/// nothing for a following stage to consume).
#[test]
fn campaign_run_refuses_a_non_terminal_selection_free_sweep() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1313,11 +1306,11 @@ fn campaign_run_refuses_a_non_terminal_selection_free_sweep() {
ScratchPath::File(dir.join("selfree.process.json")),
ScratchPath::File(dir.join("selfree.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-selfree-seed");
let bp_id = seed_blueprint(&dir, "campaign-run-selfree-seed");
let proc_id =
register_process_doc(dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC);
write_doc(dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "selfree.campaign.json"]);
register_process_doc(&dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC);
write_doc(&dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "selfree.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains(
@@ -1344,8 +1337,7 @@ const SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC: &str = r#"{
#[test]
fn campaign_validate_accepts_a_terminal_selection_free_sweep() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1353,15 +1345,15 @@ fn campaign_validate_accepts_a_terminal_selection_free_sweep() {
ScratchPath::File(dir.join("selfreeonly.process.json")),
ScratchPath::File(dir.join("selfreeonly.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-validate-selfreeonly-seed");
let bp_id = seed_blueprint(&dir, "campaign-validate-selfreeonly-seed");
let proc_id =
register_process_doc(dir, "selfreeonly.process.json", SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC);
register_process_doc(&dir, "selfreeonly.process.json", SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC);
write_doc(
dir,
&dir,
"selfreeonly.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""),
);
let (out, code) = run_code_in(dir, &["campaign", "validate", "selfreeonly.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "validate", "selfreeonly.campaign.json"]);
assert_eq!(code, Some(0), "a terminal selection-free sweep validates as executable: {out}");
assert!(
out.contains("campaign document valid (executable): pipeline shape and static guards pass"),
@@ -1378,8 +1370,7 @@ fn campaign_validate_accepts_a_terminal_selection_free_sweep() {
/// single-instrument, and this refusal is data-free by construction).
#[test]
fn campaign_run_refuses_single_instrument_generalize() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1387,10 +1378,10 @@ fn campaign_run_refuses_single_instrument_generalize() {
ScratchPath::File(dir.join("gen1.process.json")),
ScratchPath::File(dir.join("gen1.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-gen1-seed");
let proc_id = register_process_doc(dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
write_doc(dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "gen1.campaign.json"]);
let bp_id = seed_blueprint(&dir, "campaign-run-gen1-seed");
let proc_id = register_process_doc(&dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
write_doc(&dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "gen1.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("std::generalize needs at least 2"),
@@ -1423,8 +1414,7 @@ const GENERALIZE_NON_R_PROCESS_DOC: &str = r#"{
/// would silently accept a pip-denominated `std::generalize`.
#[test]
fn campaign_run_refuses_generalize_non_r_metric() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1432,8 +1422,8 @@ fn campaign_run_refuses_generalize_non_r_metric() {
ScratchPath::File(dir.join("genpip.process.json")),
ScratchPath::File(dir.join("genpip.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-genpip-seed");
let proc_id = register_process_doc(dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-genpip-seed");
let proc_id = register_process_doc(&dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC);
// Two instruments (arity passes), so the metric guard is the one that
// actually fires. The [1, 2] 1970 window never matters: this refusal is
// data-free, before any member runs.
@@ -1451,8 +1441,8 @@ fn campaign_run_refuses_generalize_non_r_metric() {
"presentation": {{ "persist_taps": [], "emit": [] }}
}}"#
);
write_doc(dir, "genpip.campaign.json", &campaign_doc);
let (out, code) = run_code_in(dir, &["campaign", "run", "genpip.campaign.json"]);
write_doc(&dir, "genpip.campaign.json", &campaign_doc);
let (out, code) = run_code_in(&dir, &["campaign", "run", "genpip.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
// #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable
// R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is
@@ -1505,8 +1495,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
);
// executor tier: campaign run's v2 preflight refuses the same shape.
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1514,10 +1503,10 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
ScratchPath::File(dir.join("wfmc.process.json")),
ScratchPath::File(dir.join("wfmc.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-wfmc-seed");
let proc_id = register_process_doc(dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
write_doc(dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "wfmc.campaign.json"]);
let bp_id = seed_blueprint(&dir, "campaign-run-wfmc-seed");
let proc_id = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "wfmc.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("std::walk_forward cannot follow std::monte_carlo"),
@@ -1545,8 +1534,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
/// shape validate clean (exit 0), the F6 friction.
#[test]
fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1556,15 +1544,15 @@ fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() {
ScratchPath::File(dir.join("wfmc.process.json")),
ScratchPath::File(dir.join("wfmc.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-validate-3tier-seed");
let bp_id = seed_blueprint(&dir, "campaign-validate-3tier-seed");
// Face 1 — the happy path: an executable pipeline shape (a bare
// std::sweep). Intrinsic + referential pass today (exit 0), but the
// executor tier is invisible; after #205 validate gains the third
// "valid (executable)" line while staying exit 0.
let ok_proc = register_process_doc(dir, "runnable.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(dir, "runnable.campaign.json", &campaign_doc_json(&bp_id, &ok_proc, (1, 2), "", ""));
let (ok_out, ok_code) = run_code_in(dir, &["campaign", "validate", "runnable.campaign.json"]);
let ok_proc = register_process_doc(&dir, "runnable.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(&dir, "runnable.campaign.json", &campaign_doc_json(&bp_id, &ok_proc, (1, 2), "", ""));
let (ok_out, ok_code) = run_code_in(&dir, &["campaign", "validate", "runnable.campaign.json"]);
assert_eq!(ok_code, Some(0), "an executable campaign validates clean: {ok_out}");
assert!(
ok_out.contains("campaign document valid (referential): all references resolve"),
@@ -1583,9 +1571,9 @@ fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() {
// reaches the third tier — which refuses the non-executable shape at
// validate time (data-free: the [1, 2] 1970-epoch-ms window is never
// reached), exit 1, with the executor fault header + the shape prose.
let bad_proc = register_process_doc(dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
write_doc(dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &bad_proc, (1, 2), "", ""));
let (bad_out, bad_code) = run_code_in(dir, &["campaign", "validate", "wfmc.campaign.json"]);
let bad_proc = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &bad_proc, (1, 2), "", ""));
let (bad_out, bad_code) = run_code_in(&dir, &["campaign", "validate", "wfmc.campaign.json"]);
assert_eq!(bad_code, Some(1), "an executor-unrunnable shape refuses at validate: {bad_out}");
assert!(
bad_out.contains("campaign is not executable:"),
@@ -1639,8 +1627,7 @@ const WF_PLATEAU_GATED_PROCESS_DOC: &str = r#"{
/// exit 0 — either direction goes red here.
#[test]
fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1650,19 +1637,19 @@ fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() {
ScratchPath::File(dir.join("wf-plateau-gated.process.json")),
ScratchPath::File(dir.join("wf-plateau-gated.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-validate-wf-plateau-seed");
let bp_id = seed_blueprint(&dir, "campaign-validate-wf-plateau-seed");
// Face 1 — gate-free: sweep -> walk_forward(plateau:worst). The lattice
// is intact, so the executor tier accepts it (exit 0).
let free_id =
register_process_doc(dir, "wf-plateau-free.process.json", WF_PLATEAU_GATE_FREE_PROCESS_DOC);
register_process_doc(&dir, "wf-plateau-free.process.json", WF_PLATEAU_GATE_FREE_PROCESS_DOC);
write_doc(
dir,
&dir,
"wf-plateau-free.campaign.json",
&campaign_doc_json(&bp_id, &free_id, (1, 2), "", ""),
);
let (free_out, free_code) =
run_code_in(dir, &["campaign", "validate", "wf-plateau-free.campaign.json"]);
run_code_in(&dir, &["campaign", "validate", "wf-plateau-free.campaign.json"]);
assert_eq!(free_code, Some(0), "gate-free plateau walk_forward validates clean: {free_out}");
assert!(
free_out.contains(
@@ -1675,14 +1662,14 @@ fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() {
// The gate can break the lattice, so the executor tier still refuses it
// (exit 1), naming the offending stage.
let gated_id =
register_process_doc(dir, "wf-plateau-gated.process.json", WF_PLATEAU_GATED_PROCESS_DOC);
register_process_doc(&dir, "wf-plateau-gated.process.json", WF_PLATEAU_GATED_PROCESS_DOC);
write_doc(
dir,
&dir,
"wf-plateau-gated.campaign.json",
&campaign_doc_json(&bp_id, &gated_id, (1, 2), "", ""),
);
let (gated_out, gated_code) =
run_code_in(dir, &["campaign", "validate", "wf-plateau-gated.campaign.json"]);
run_code_in(&dir, &["campaign", "validate", "wf-plateau-gated.campaign.json"]);
assert_eq!(gated_code, Some(1), "gate-preceded plateau walk_forward still refuses: {gated_out}");
assert!(
gated_out.contains("campaign is not executable:"),
@@ -1717,8 +1704,7 @@ const ZERO_RESAMPLES_MC_PROCESS_DOC: &str = r#"{
/// would let this refusal disappear or drift to a different message.
#[test]
fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1726,10 +1712,10 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
ScratchPath::File(dir.join("zeromc.process.json")),
ScratchPath::File(dir.join("zeromc.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-zeromc-seed");
let proc_id = register_process_doc(dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC);
write_doc(dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "zeromc.campaign.json"]);
let bp_id = seed_blueprint(&dir, "campaign-run-zeromc-seed");
let proc_id = register_process_doc(&dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC);
write_doc(&dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "zeromc.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("process stage 1: monte_carlo resamples must be > 0"),
@@ -1749,8 +1735,7 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
/// pinned here.
#[test]
fn campaign_run_refuses_unknown_tap_at_validate() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1758,14 +1743,14 @@ fn campaign_run_refuses_unknown_tap_at_validate() {
ScratchPath::File(dir.join("badtap.process.json")),
ScratchPath::File(dir.join("badtap.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-badtap-seed");
let proc_id = register_process_doc(dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-badtap-seed");
let proc_id = register_process_doc(&dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(
dir,
&dir,
"badtap.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""),
);
let (out, code) = run_code_in(dir, &["campaign", "run", "badtap.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "badtap.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}");
assert!(
@@ -1794,8 +1779,7 @@ fn campaign_run_refuses_unknown_tap_at_validate() {
/// tightens this test to pin its absence.
#[test]
fn campaign_run_valid_tap_reaches_the_member_data_seam() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1803,14 +1787,14 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() {
ScratchPath::File(dir.join("taps.process.json")),
ScratchPath::File(dir.join("taps.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-taps-seed");
let proc_id = register_process_doc(dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-taps-seed");
let proc_id = register_process_doc(&dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(
dir,
&dir,
"taps.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""),
);
let (out, code) = run_code_in(dir, &["campaign", "run", "taps.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
!out.contains("unknown tap"),
@@ -1849,8 +1833,7 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() {
/// stays green on a data-less machine.
#[test]
fn campaign_run_real_e2e_sweep_gate_walkforward() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -1858,12 +1841,12 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
ScratchPath::File(dir.join("wf.process.json")),
ScratchPath::File(dir.join("wf.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-e2e-seed");
let proc_id = register_process_doc(dir, "wf.process.json", WF_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-e2e-seed");
let proc_id = register_process_doc(&dir, "wf.process.json", WF_PROCESS_DOC);
// The GER40 Sept-2024 window (inclusive Unix-ms) — the same gated window
// cli_run.rs drives; ~30 days, so the (14d, 7d, 7d) roller tiles it.
write_doc(
dir,
&dir,
"wf.campaign.json",
&campaign_doc_json(
&bp_id,
@@ -1873,7 +1856,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
"\"family_table\", \"selection_report\"",
),
);
let (out, code) = run_code_in(dir, &["campaign", "run", "wf.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "wf.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -1999,7 +1982,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
// The shipped viewer reads the member back (the cli_run.rs chart
// idiom): exit 0 and an HTML page carrying both persisted tap series.
let (chart_out, chart_code) =
run_code_in(dir, &["chart", &format!("{trace_name}/{cell_key}")]);
run_code_in(&dir, &["chart", &format!("{trace_name}/{cell_key}")]);
assert_eq!(chart_code, Some(0), "aura chart reads the persisted member: {chart_out}");
assert!(
chart_out.contains("\"equity\"") && chart_out.contains("\"r_equity\""),
@@ -2015,8 +1998,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
/// Gated on the local GER40 archive; skips cleanly when absent.
#[test]
fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2025,8 +2007,8 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
ScratchPath::File(dir.join("close.campaign.json")),
ScratchPath::File(dir.join("open.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "binding-override-seed");
let proc_id = register_process_doc(dir, "sweep.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "binding-override-seed");
let proc_id = register_process_doc(&dir, "sweep.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2039,10 +2021,10 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
r#""data": { "bindings": { "price": "open" }, "instruments": ["GER40"],"#,
);
assert_ne!(rebound, base, "the override splice must hit the data section");
write_doc(dir, "close.campaign.json", &base);
write_doc(dir, "open.campaign.json", &rebound);
write_doc(&dir, "close.campaign.json", &base);
write_doc(&dir, "open.campaign.json", &rebound);
let (out_close, code_close) = run_code_in(dir, &["campaign", "run", "close.campaign.json"]);
let (out_close, code_close) = run_code_in(&dir, &["campaign", "run", "close.campaign.json"]);
if code_close == Some(1)
&& (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument"))
{
@@ -2050,7 +2032,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
return;
}
assert_eq!(code_close, Some(0), "stdout/stderr: {out_close}");
let (out_open, code_open) = run_code_in(dir, &["campaign", "run", "open.campaign.json"]);
let (out_open, code_open) = run_code_in(&dir, &["campaign", "run", "open.campaign.json"]);
assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}");
let first_member = |out: &str| -> serde_json::Value {
@@ -2081,8 +2063,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
/// above; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2090,8 +2071,8 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
ScratchPath::File(dir.join("regime.process.json")),
ScratchPath::File(dir.join("regime.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-regime-seed");
let proc_id = register_process_doc(dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-regime-seed");
let proc_id = register_process_doc(&dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2105,8 +2086,8 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
1,
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "regime.campaign.json", &with_regime);
let (out, code) = run_code_in(dir, &["campaign", "run", "regime.campaign.json"]);
write_doc(&dir, "regime.campaign.json", &with_regime);
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime.campaign.json"]);
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
@@ -2151,8 +2132,7 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
/// on the local GER40 archive like its siblings; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2160,8 +2140,8 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
ScratchPath::File(dir.join("tworegimes.process.json")),
ScratchPath::File(dir.join("tworegimes.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-two-regimes-seed");
let proc_id = register_process_doc(dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-two-regimes-seed");
let proc_id = register_process_doc(&dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2176,8 +2156,8 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
1,
);
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "tworegimes.campaign.json", &with_regimes);
let (out, code) = run_code_in(dir, &["campaign", "run", "tworegimes.campaign.json"]);
write_doc(&dir, "tworegimes.campaign.json", &with_regimes);
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes.campaign.json"]);
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
@@ -2236,8 +2216,7 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
/// archive like its siblings; skips on a data-less host.
#[test]
fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2245,9 +2224,9 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
ScratchPath::File(dir.join("regime-persist.process.json")),
ScratchPath::File(dir.join("regime-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-regime-seed");
let bp_id = seed_blueprint(&dir, "campaign-persist-regime-seed");
let proc_id =
register_process_doc(dir, "regime-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
register_process_doc(&dir, "regime-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2264,8 +2243,8 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
1,
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "regime-persist.campaign.json", &with_regime);
let (out, code) = run_code_in(dir, &["campaign", "run", "regime-persist.campaign.json"]);
write_doc(&dir, "regime-persist.campaign.json", &with_regime);
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2321,8 +2300,7 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
/// the local GER40 archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2330,8 +2308,8 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
ScratchPath::File(dir.join("cost-persist.process.json")),
ScratchPath::File(dir.join("cost-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-cost-seed");
let proc_id = register_process_doc(dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-persist-cost-seed");
let proc_id = register_process_doc(&dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2345,8 +2323,8 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "cost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "cost-persist.campaign.json"]);
write_doc(&dir, "cost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "cost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2417,8 +2395,7 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
/// archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2426,8 +2403,8 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
ScratchPath::File(dir.join("net-persist.process.json")),
ScratchPath::File(dir.join("net-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-net-seed");
let proc_id = register_process_doc(dir, "net-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-persist-net-seed");
let proc_id = register_process_doc(&dir, "net-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2441,8 +2418,8 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "net-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "net-persist.campaign.json"]);
write_doc(&dir, "net-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "net-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2531,8 +2508,7 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
/// wired). Gated on the local GER40 archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2540,9 +2516,9 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
ScratchPath::File(dir.join("volcost-persist.process.json")),
ScratchPath::File(dir.join("volcost-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-volcost-seed");
let bp_id = seed_blueprint(&dir, "campaign-persist-volcost-seed");
let proc_id =
register_process_doc(dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
register_process_doc(&dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2556,8 +2532,8 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "volcost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "volcost-persist.campaign.json"]);
write_doc(&dir, "volcost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "volcost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2617,8 +2593,7 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
/// into one dir. Gated on the local GER40 archive; skips on a data-less host.
#[test]
fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2626,9 +2601,9 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
ScratchPath::File(dir.join("tworegimes-persist.process.json")),
ScratchPath::File(dir.join("tworegimes-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-two-regimes-seed");
let bp_id = seed_blueprint(&dir, "campaign-persist-two-regimes-seed");
let proc_id = register_process_doc(
dir,
&dir,
"tworegimes-persist.process.json",
SWEEP_ONLY_PROCESS_DOC,
);
@@ -2646,8 +2621,8 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
1,
);
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "tworegimes-persist.campaign.json", &with_regimes);
let (out, code) = run_code_in(dir, &["campaign", "run", "tworegimes-persist.campaign.json"]);
write_doc(&dir, "tworegimes-persist.campaign.json", &with_regimes);
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2704,8 +2679,7 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
/// host like its siblings.
#[test]
fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2713,10 +2687,10 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
ScratchPath::File(dir.join("mixedtap.process.json")),
ScratchPath::File(dir.join("mixedtap.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-mixedtap-seed");
let proc_id = register_process_doc(dir, "mixedtap.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-mixedtap-seed");
let proc_id = register_process_doc(&dir, "mixedtap.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(
dir,
&dir,
"mixedtap.campaign.json",
&campaign_doc_json(
&bp_id,
@@ -2726,7 +2700,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
"",
),
);
let (out, code) = run_code_in(dir, &["campaign", "run", "mixedtap.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -2781,8 +2755,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
/// manifests). Skips on a data-less host like its siblings.
#[test]
fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2791,8 +2764,8 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
ScratchPath::File(dir.join("costless.campaign.json")),
ScratchPath::File(dir.join("costnet.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-costnet-seed");
let proc_id = register_process_doc(dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-costnet-seed");
let proc_id = register_process_doc(&dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2809,8 +2782,8 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "costless.campaign.json", &base);
write_doc(dir, "costnet.campaign.json", &with_cost);
write_doc(&dir, "costless.campaign.json", &base);
write_doc(&dir, "costnet.campaign.json", &with_cost);
// Per-member (axis-params key -> (expectancy_r, net_expectancy_r)),
// parsed from the emitted family_table member lines. Cost stamps are
@@ -2843,7 +2816,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
// (a) the cost-less baseline: net == gross, and net_r_equity skips with
// the remedy notice while equity still persists.
let (out_gross, code_gross) = run_code_in(dir, &["campaign", "run", "costless.campaign.json"]);
let (out_gross, code_gross) = run_code_in(&dir, &["campaign", "run", "costless.campaign.json"]);
if code_gross == Some(1)
&& (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument"))
{
@@ -2865,7 +2838,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
// (b) the costed run: gross identical member-for-member, net strictly
// below gross, no skip notice, net_r_equity persisted on disk.
let (out_net, code_net) = run_code_in(dir, &["campaign", "run", "costnet.campaign.json"]);
let (out_net, code_net) = run_code_in(&dir, &["campaign", "run", "costnet.campaign.json"]);
assert_eq!(code_net, Some(0), "costed run: {out_net}");
assert!(
!out_net.contains("skipped"),
@@ -2916,7 +2889,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
.to_string()
})
.expect("a costed member line carries the family id");
let (rep_out, rep_code) = run_code_in(dir, &["reproduce", &family_id]);
let (rep_out, rep_code) = run_code_in(&dir, &["reproduce", &family_id]);
assert_eq!(rep_code, Some(0), "reproduce: {rep_out}");
assert!(
rep_out.contains("reproduced 4/4 members bit-identically"),
@@ -2939,8 +2912,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
/// local GER40 archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -2948,9 +2920,9 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
ScratchPath::File(dir.join("carrycost-persist.process.json")),
ScratchPath::File(dir.join("carrycost-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-carrycost-seed");
let bp_id = seed_blueprint(&dir, "campaign-persist-carrycost-seed");
let proc_id =
register_process_doc(dir, "carrycost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
register_process_doc(&dir, "carrycost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
@@ -2964,8 +2936,8 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "carrycost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "carrycost-persist.campaign.json"]);
write_doc(&dir, "carrycost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "carrycost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -3021,8 +2993,7 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
/// data-free refusal fixture).
#[test]
fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -3030,15 +3001,15 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
ScratchPath::File(dir.join("mc.process.json")),
ScratchPath::File(dir.join("mc.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-mc-persurvivor-seed");
let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC);
let bp_id = seed_blueprint(&dir, "campaign-run-mc-persurvivor-seed");
let proc_id = register_process_doc(&dir, "mc.process.json", MC_PROCESS_DOC);
// Same GER40 Sept-2024 window as the gated wf e2e above.
write_doc(
dir,
&dir,
"mc.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""),
);
let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
@@ -3095,8 +3066,7 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
/// prose either way (the message depends only on env/instrument/window).
#[test]
fn campaign_run_by_content_id_matches_file_sugar_refusal() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -3104,14 +3074,14 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
ScratchPath::File(dir.join("mc2.process.json")),
ScratchPath::File(dir.join("mc2.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-id-seed");
let proc_id = register_process_doc(dir, "mc2.process.json", MC_PROCESS_DOC);
write_doc(dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let bp_id = seed_blueprint(&dir, "campaign-run-id-seed");
let proc_id = register_process_doc(&dir, "mc2.process.json", MC_PROCESS_DOC);
write_doc(&dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (file_out, file_code) = run_code_in(dir, &["campaign", "run", "mc2.campaign.json"]);
let (file_out, file_code) = run_code_in(&dir, &["campaign", "run", "mc2.campaign.json"]);
assert_eq!(file_code, Some(1), "stdout/stderr: {file_out}");
let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "mc2.campaign.json"]);
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]);
assert_eq!(reg_code, Some(0), "register failed: {reg_out}");
let id = reg_out
.lines()
@@ -3123,7 +3093,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
.expect("id")
.trim_start_matches("content:")
.to_string();
let (id_out, id_code) = run_code_in(dir, &["campaign", "run", &id]);
let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]);
assert_eq!(id_code, Some(1), "stdout/stderr: {id_out}");
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
@@ -3146,8 +3116,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
/// needed.
#[test]
fn campaign_run_tolerates_content_prefix_on_target() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -3155,11 +3124,11 @@ fn campaign_run_tolerates_content_prefix_on_target() {
ScratchPath::File(dir.join("prefix.process.json")),
ScratchPath::File(dir.join("prefix.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-prefix-seed");
let proc_id = register_process_doc(dir, "prefix.process.json", MC_PROCESS_DOC);
write_doc(dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let bp_id = seed_blueprint(&dir, "campaign-run-prefix-seed");
let proc_id = register_process_doc(&dir, "prefix.process.json", MC_PROCESS_DOC);
write_doc(&dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "prefix.campaign.json"]);
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "prefix.campaign.json"]);
assert_eq!(reg_code, Some(0), "register failed: {reg_out}");
let id = reg_out
.lines()
@@ -3172,8 +3141,8 @@ fn campaign_run_tolerates_content_prefix_on_target() {
.trim_start_matches("content:")
.to_string();
let (bare_out, _) = run_code_in(dir, &["campaign", "run", &id]);
let (pfx_out, _) = run_code_in(dir, &["campaign", "run", &format!("content:{id}")]);
let (bare_out, _) = run_code_in(&dir, &["campaign", "run", &id]);
let (pfx_out, _) = run_code_in(&dir, &["campaign", "run", &format!("content:{id}")]);
let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line");
assert!(
@@ -3201,8 +3170,7 @@ fn campaign_run_tolerates_content_prefix_on_target() {
/// data-free refusal) rather than failing earlier at referencing or binding.
#[test]
fn campaign_run_accepts_a_graph_register_seeded_strategy() {
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
@@ -3211,7 +3179,7 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
ScratchPath::File(dir.join("onramp.campaign.json")),
]);
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (reg_out, reg_code) = run_code_in(dir, &["graph", "register", &closed_bp]);
let (reg_out, reg_code) = run_code_in(&dir, &["graph", "register", &closed_bp]);
assert_eq!(reg_code, Some(0), "graph register failed: {reg_out}");
let bp_id = reg_out
.lines()
@@ -3224,9 +3192,9 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
.trim_start_matches("content:")
.to_string();
let proc_id = register_process_doc(dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(dir, &["campaign", "run", "onramp.campaign.json"]);
let proc_id = register_process_doc(&dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(&dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "onramp.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("no recorded geometry") || out.contains("no data for instrument"),
@@ -3248,15 +3216,14 @@ fn campaign_run_invalid_file_refuses_before_touching_store() {
// Run inside the built project (campaign run needs one, per the project
// gate); the document fails intrinsic validation before any store or
// referential check is reached, so the project's own store is untouched.
let _fixture = project_lock();
let dir = built_project();
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let bad = CAMPAIGN_DOC.replacen(r#""values": [8]"#, r#""values": []"#, 1);
let bad_path = write_doc(dir, "bad.campaign.json", &bad);
let bad_path = write_doc(&dir, "bad.campaign.json", &bad);
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone()), ScratchPath::File(bad_path)]);
let (out, code) = run_code_in(dir, &["campaign", "run", "bad.campaign.json"]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "bad.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("aura: campaign document invalid:"),
+7
View File
@@ -16,6 +16,13 @@ data-server = { git = "http://192.168.178.103:3000/Brummel/data-server.git", bra
aura-engine = { path = "../aura-engine" }
[dev-dependencies]
# zip: hand-packs a tiny synthetic M1 archive (lib.rs's own unit tests, #252's
# archive_extent gap-walk coverage) in the exact on-disk format data-server
# reads — already transitive via the data-server dependency above (a normal,
# non-dev dependency there); declared directly here so test code may `use
# zip::...` (mirrors aura-cli's tests/common/mod.rs, no new dependency
# actually enters the build graph).
zip = "2"
# the integration tests bootstrap a sample harness + fold a RunReport
aura-std = { path = "../aura-std" }
# chrono / chrono-tz build the Berlin-local-wall-clock window bounds + the
+312
View File
@@ -23,6 +23,7 @@
use aura_core::{Scalar, Timestamp};
use data_server::records::M1Parsed;
use data_server::SymbolChunkIter;
use std::path::Path;
/// Re-export of the data-server archive entry points (#81): a real-data source
/// builds from `aura-ingest` alone — a consumer never names the external
@@ -411,10 +412,168 @@ pub fn instrument_geometry(server: &Arc<DataServer>, symbol: &str) -> Option<Ins
server.symbol_meta(symbol)
}
/// Days since 1970-01-01 for the first of a proleptic-Gregorian civil
/// `(year, month, day)` — Howard Hinnant's closed-form `days_from_civil`, so
/// [`month_start_ms`] needs no calendar library. Already precedented in this
/// codebase (`aura-cli`'s `tests/common::synthetic_data` carries the identical
/// formula for its own synthetic archive generator); duplicated here rather
/// than shared because that copy lives in a test-only module of a different
/// crate.
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64; // [0, 399]
let mp = (u64::from(m) + 9) % 12; // [0, 11]: Mar=0 .. Feb=11
let doy = (153 * mp + 2) / 5 + u64::from(d) - 1; // [0, 365]
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
era * 146_097 + doe as i64 - 719_468
}
/// UTC epoch-ms of `(year, month)`'s first instant — matches
/// `data_server::records::unix_ms_to_year_month`'s own UTC calendar (chrono's
/// `DateTime<Utc>`), the reference [`archive_extent`] derives a
/// single-candidate-month probe window against.
fn month_start_ms(year: u16, month: u8) -> i64 {
days_from_civil(i64::from(year), u32::from(month), 1) * 86_400_000
}
/// UTC epoch-ms of the instant immediately after `(year, month)`'s last
/// millisecond (the exclusive upper bound [`archive_extent`] clamps a
/// forward-search probe's `to_ms` against, so a single query loads at most
/// one month's file).
fn month_end_ms_exclusive(year: u16, month: u8) -> i64 {
let (ny, nm) = if month == 12 { (year + 1, 1) } else { (year, month + 1) };
month_start_ms(ny, nm)
}
/// The sorted `(year, month)` pairs that exist as `<symbol>_YYYY_MM.m1` files
/// directly under `data_path` — mirrors data-server's own file-naming
/// convention (`SymbolIndex::scan`, already load-bearing in its loader) via a
/// plain directory listing scoped to the one already-known `symbol`, so this
/// needs no general symbol-boundary regex and reads no private index (#252:
/// no new data-server surface). Empty when `data_path` doesn't exist or holds
/// no matching file for `symbol` — the same "no archive for this symbol"
/// condition an absent `DataServer` symbol represents.
fn list_m1_months(data_path: &Path, symbol: &str) -> Vec<(u16, u8)> {
let prefix = format!("{symbol}_");
let mut months = Vec::new();
let Ok(entries) = std::fs::read_dir(data_path) else {
return months;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(name) = file_name.to_str() else { continue };
let Some(rest) = name.strip_prefix(prefix.as_str()) else { continue };
let Some(year_month) = rest.strip_suffix(".m1") else { continue };
let Some((year_str, month_str)) = year_month.split_once('_') else { continue };
if year_str.len() != 4
|| month_str.len() != 2
|| !year_str.bytes().all(|b| b.is_ascii_digit())
|| !month_str.bytes().all(|b| b.is_ascii_digit())
{
continue;
}
let (Ok(year), Ok(month)) = (year_str.parse::<u16>(), month_str.parse::<u8>()) else {
continue;
};
if (1..=12).contains(&month) {
months.push((year, month));
}
}
months.sort_unstable();
months
}
/// Derive the archive's `(first, last)` bar timestamp inside `[from_ms, to_ms]`
/// (inclusive Unix-ms, `None` = open-ended) at O(1) file loads per boundary
/// instead of draining every bar in the window (#252). Lists the symbol's
/// monthly files directly off `data_path` ([`list_m1_months`]), then loads
/// ONLY the boundary month through the existing `stream_m1_windowed`/
/// [`M1FieldSource`] seam — walking forward (for the first bar) or backward
/// (for the last) across gap months whose candidate file yields nothing in
/// range, exactly like the cross-file skip-ahead a full-window drain already
/// did internally, just narrowed to one file per candidate instead of every
/// file in the window. `server` is the caller's own `Arc<DataServer>` (shares
/// its file cache — the two boundary files loaded here are exactly the ones a
/// subsequent real run reads first/last, so this is not wasted work, only
/// reordered). Returns `None` exactly when no month in `[from_ms, to_ms]` has
/// a bar — no archive for the symbol, or a window with zero matching bars —
/// the same refusal condition a drain-based emptiness check detects.
pub fn archive_extent(
server: &Arc<DataServer>,
data_path: &Path,
symbol: &str,
from_ms: Option<i64>,
to_ms: Option<i64>,
) -> Option<(Timestamp, Timestamp)> {
let months = list_m1_months(data_path, symbol);
if months.is_empty() {
return None;
}
let from_ym = from_ms.map(data_server::records::unix_ms_to_year_month);
let to_ym = to_ms.map(data_server::records::unix_ms_to_year_month);
let in_range = |&(y, m): &(u16, u8)| {
from_ym.is_none_or(|fym| (y, m) >= fym) && to_ym.is_none_or(|tym| (y, m) <= tym)
};
// Forward walk: the first candidate month (ascending) whose file, probed
// over just that month's span, yields a bar — a gap or an empty boundary
// file simply advances to the next one.
let first = months.iter().filter(|ym| in_range(ym)).find_map(|&(y, m)| {
let month_cap = month_end_ms_exclusive(y, m) - 1;
let probe_to = to_ms.map_or(month_cap, |t| t.min(month_cap));
let src = M1FieldSource::open(server, symbol, from_ms, Some(probe_to), M1Field::Close)?;
aura_engine::Source::peek(&src)
})?;
// Backward walk: the last candidate month (descending) whose file,
// probed over just that month's span, yields a bar — fully drained (a
// single file) to find its own last matching record.
let last = months.iter().rev().filter(|ym| in_range(ym)).find_map(|&(y, m)| {
let month_floor = month_start_ms(y, m);
let probe_from = from_ms.map_or(month_floor, |f| f.max(month_floor));
let mut src = M1FieldSource::open(server, symbol, Some(probe_from), to_ms, M1Field::Close)?;
let mut last_ts = aura_engine::Source::peek(&src)?;
while let Some((t, _)) = aura_engine::Source::next(&mut src) {
last_ts = t;
}
Some(last_ts)
})?;
Some((first, last))
}
#[cfg(test)]
mod tests {
use super::*;
/// A unique scratch directory under the OS temp root, removed on drop —
/// mirrors `aura-cli`'s own `tests/common::mint_tempdir` pattern (unique
/// name via pid + an atomic counter) so this crate's tests need no
/// `tempfile` dev-dependency for a handful of throwaway fixture dirs.
struct ScratchDir(std::path::PathBuf);
impl ScratchDir {
fn new(tag: &str) -> Self {
static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir()
.join(format!("aura-ingest-test-{tag}-{}-{n}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create scratch dir");
Self(dir)
}
fn path(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn instrument_geometry_none_for_unknown_symbol() {
// A symbol with no sidecar yields None — the accessor surfaces the reader's
@@ -447,6 +606,159 @@ mod tests {
}
}
/// [`month_start_ms`] anchored against the same 2017-03-01 UTC reference
/// [`unix_ms_to_epoch_ns_scales_ms_to_ns`] pins, plus the Feb/Dec month-end
/// edges [`month_end_ms_exclusive`] must get right (leap-year length, and
/// the December -> next-January year rollover) — the two properties
/// [`archive_extent`]'s per-candidate-month probe bound depends on.
#[test]
fn month_start_and_end_ms_bracket_known_months() {
assert_eq!(month_start_ms(2017, 3), 1_488_326_400_000);
// 2024 is a leap year: February runs through the 29th, so March 1
// 00:00 UTC is exactly 29 days after February 1 00:00 UTC.
assert_eq!(
month_end_ms_exclusive(2024, 2) - month_start_ms(2024, 2),
29 * 86_400_000
);
// 2023 is not a leap year: 28 days.
assert_eq!(
month_end_ms_exclusive(2023, 2) - month_start_ms(2023, 2),
28 * 86_400_000
);
// December rolls over into the next year's January.
assert_eq!(month_end_ms_exclusive(2024, 12), month_start_ms(2025, 1));
// A month's end is exclusive: the next month starts immediately after.
assert_eq!(month_end_ms_exclusive(2024, 3), month_start_ms(2024, 4));
}
/// [`list_m1_months`] scans `data_path` for `<symbol>_YYYY_MM.m1` files —
/// mirroring data-server's own `SymbolIndex::scan` regex (`test_symbol_index_scan`
/// in its `lib.rs`) but scoped to one already-known symbol, so it needs no
/// general-purpose parser. Sorted ascending, and never confused by another
/// symbol's file or a non-M1 extension sharing the same directory.
#[test]
fn list_m1_months_lists_and_sorts_the_one_symbols_files() {
let dir = ScratchDir::new("list-months");
for name in [
"EURUSD_2020_06.m1",
"EURUSD_2019_12.m1",
"EURUSD_2020_01.m1",
"EURUSD_2020_01.tick", // different format, same symbol+month: ignored
"GBPUSD_2020_01.m1", // different symbol: ignored
"EURUSD_2020_1.m1", // malformed month (one digit): ignored
"not_a_data_file.txt",
] {
std::fs::write(dir.path().join(name), b"").expect("write stub file");
}
assert_eq!(
list_m1_months(dir.path(), "EURUSD"),
vec![(2019, 12), (2020, 1), (2020, 6)]
);
}
#[test]
fn list_m1_months_empty_for_unknown_symbol_or_missing_dir() {
let dir = ScratchDir::new("list-months-empty");
std::fs::write(dir.path().join("EURUSD_2020_01.m1"), b"").expect("write stub file");
assert!(list_m1_months(dir.path(), "NOSYM").is_empty());
assert!(list_m1_months(Path::new("/nonexistent-archive-path"), "EURUSD").is_empty());
}
/// Writes a minimal one-record `<symbol>_YYYY_MM.m1` zip at `unix_ms_time`
/// (mirrors `data-server`'s own `write_m1_file` test helper — duplicated
/// here, not imported, since that helper lives in its private `#[cfg(test)]`
/// module). Deliberately zip/binary-shaped, not a stub, since
/// [`archive_extent`] must actually load this file through the real
/// `stream_m1_windowed`/`M1FieldSource` seam, not just see its filename.
fn write_one_bar_month(dir: &Path, symbol: &str, year: u16, month: u8, unix_ms_time: i64) {
use std::io::Write;
const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0;
const MS_PER_DAY: f64 = 86_400_000.0;
let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS;
let mut rec = [0u8; 48];
rec[0..8].copy_from_slice(&dt.to_le_bytes());
rec[8..16].copy_from_slice(&1.0_f64.to_le_bytes()); // open
rec[16..24].copy_from_slice(&1.0_f64.to_le_bytes()); // high
rec[24..32].copy_from_slice(&1.0_f64.to_le_bytes()); // low
rec[32..40].copy_from_slice(&1.0_f64.to_le_bytes()); // close
rec[40..44].copy_from_slice(&0.0_f32.to_le_bytes()); // spread
rec[44..48].copy_from_slice(&0_i32.to_le_bytes()); // volume
let path = dir.join(format!("{symbol}_{year:04}_{month:02}.m1"));
let file = std::fs::File::create(&path).expect("create synthetic m1 zip");
let mut zip = zip::ZipWriter::new(file);
zip.start_file::<String, ()>(format!("{symbol}.bin"), Default::default())
.expect("start synthetic m1 zip entry");
zip.write_all(&rec).expect("write synthetic m1 record");
zip.finish().expect("finish synthetic m1 zip");
}
/// Property (#252): a requested window whose starting month has NO archive
/// file (a gap) still resolves the first bar, by walking forward to the
/// next available month that actually has one — not refusing outright.
/// Three files (Jan, MISSING Feb, Mar 2021), each holding one bar; a
/// window opening in the missing February must land on March's bar.
#[test]
fn archive_extent_walks_forward_across_a_gap_month() {
let dir = ScratchDir::new("extent-forward-gap");
let jan_ms = month_start_ms(2021, 1) + 3_600_000; // 2021-01-01 01:00 UTC
let mar_ms = month_start_ms(2021, 3) + 3_600_000; // 2021-03-01 01:00 UTC
write_one_bar_month(dir.path(), "GAPSYM", 2021, 1, jan_ms);
write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, mar_ms);
let server = Arc::new(DataServer::new(dir.path()));
// From = the start of the missing February; no upper bound.
let from_ms = month_start_ms(2021, 2);
let (first, last) = archive_extent(&server, dir.path(), "GAPSYM", Some(from_ms), None)
.expect("March's bar is reachable by walking past the empty February");
assert_eq!(first, unix_ms_to_epoch_ns(mar_ms));
assert_eq!(last, unix_ms_to_epoch_ns(mar_ms));
}
/// Property (#252): symmetric to the forward walk, an upper bound landing
/// in a gap month walks BACKWARD to find the last bar of the nearest
/// earlier available month.
#[test]
fn archive_extent_walks_backward_across_a_gap_month() {
let dir = ScratchDir::new("extent-backward-gap");
let jan_ms = month_start_ms(2021, 1) + 3_600_000;
let mar_ms = month_start_ms(2021, 3) + 3_600_000;
write_one_bar_month(dir.path(), "GAPSYM", 2021, 1, jan_ms);
write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, mar_ms);
let server = Arc::new(DataServer::new(dir.path()));
// To = the end of the missing February; no lower bound.
let to_ms = month_end_ms_exclusive(2021, 2) - 1;
let (first, last) = archive_extent(&server, dir.path(), "GAPSYM", None, Some(to_ms))
.expect("January's bar is reachable by walking back past the empty February");
assert_eq!(first, unix_ms_to_epoch_ns(jan_ms));
assert_eq!(last, unix_ms_to_epoch_ns(jan_ms));
}
/// Property (#252): an unknown symbol (no monthly files at all) yields
/// `None` — the same file-level-absence contract [`open_columns`] and
/// [`instrument_geometry`] already share.
#[test]
fn archive_extent_none_for_unknown_symbol() {
let dir = ScratchDir::new("extent-unknown-symbol");
let server = Arc::new(DataServer::new(dir.path()));
assert!(archive_extent(&server, dir.path(), "NOSYM", None, None).is_none());
}
/// Property (#252): a window entirely before the archive's first month (or
/// entirely after its last) yields `None` rather than snapping to the
/// nearest available month — [`archive_extent`]'s `in_range` bound must
/// reject every candidate, not just prefer the closest one.
#[test]
fn archive_extent_none_when_window_misses_every_month() {
let dir = ScratchDir::new("extent-misses-every-month");
write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, month_start_ms(2021, 3) + 3_600_000);
let server = Arc::new(DataServer::new(dir.path()));
// Entirely before the one archived month.
assert!(archive_extent(&server, dir.path(), "GAPSYM", None, Some(month_start_ms(2021, 1))).is_none());
// Entirely after it.
assert!(archive_extent(&server, dir.path(), "GAPSYM", Some(month_start_ms(2021, 5)), None).is_none());
}
#[test]
fn open_columns_propagates_file_level_absence_as_none() {
// The open_ohlc contract, generalized: file-level absence (unknown