Cut the E2E suite wall-clock: 247s -> 46s (cli_run 211s -> 25s) #255
Generated
+2
@@ -138,6 +138,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -181,6 +182,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"data-server",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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:"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user