9ffd1952d2
Task 1-2 of the bound-override cycle: PrimitiveBuilder::bind/try_bind stop capturing the bound value in a wrapped build closure and store it only as BoundParam data; build() merges bound values back into the full-arity vector at call time (ascending original position — behaviour-identical to the retired closure chain). This makes the reversal possible: unbind(slot) returns the param to the declared surface at its original order. aura-engine gains the path-addressed Composite::reopen (mirroring collect_params' prefix rules, lockstep with expansion_map) plus the read-only bound_param_space() enumeration (path-qualified BoundSpec with values) and the ReopenError/BoundSpec exports. An e2e proves a reopened param rebuilds from the freshly supplied value through compile_with_params, not the stale default. refs #246
104 lines
4.5 KiB
Rust
104 lines
4.5 KiB
Rust
//! End-to-end coverage for #246 tasks 1-2: `PrimitiveBuilder::bind`/`unbind`
|
|
//! storing the bound value as data merged back at build time (aura-core), and
|
|
//! `Composite::reopen` re-opening a bound param at its `param_space()` path
|
|
//! (aura-engine). The in-crate unit tests (`node.rs`/`blueprint.rs` `mod
|
|
//! tests`) already pin the mechanism directly against `PrimitiveBuilder`; this
|
|
//! file instead drives the **full public compile pipeline**
|
|
//! (`Composite::reopen` -> `compile_with_params` -> `FlatGraph`) through only
|
|
//! `aura_engine`'s exported surface — the seam the future CLI axis-override
|
|
//! path (the rest of #246) will call. A regression that broke the merge only
|
|
//! when routed through `compile_with_params` (as opposed to calling
|
|
//! `PrimitiveBuilder::build` directly) would pass every existing unit test
|
|
//! while breaking this.
|
|
|
|
use aura_core::{Scalar, ScalarKind};
|
|
use aura_engine::{Composite, Role, Target};
|
|
use aura_std::{Bias, Sma};
|
|
|
|
/// A single bound `Sma`, wired from a root role so it compiles standalone.
|
|
fn one_bound_sma() -> Composite {
|
|
Composite::new(
|
|
"sig",
|
|
vec![Sma::builder().named("sma").bind("length", Scalar::i64(2)).into()],
|
|
vec![],
|
|
vec![Role {
|
|
name: "series".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }],
|
|
source: Some(ScalarKind::F64),
|
|
}],
|
|
vec![],
|
|
)
|
|
}
|
|
|
|
/// Property: a bound param, once reopened, is genuinely re-bindable through
|
|
/// the full `compile_with_params` pipeline — the compiled node reflects the
|
|
/// value supplied AT COMPILE TIME, not the stale bound default. This is the
|
|
/// build-time merge (`PrimitiveBuilder::build`, aura-core #246 task 1) proven
|
|
/// under the compile boundary it must survive (aura-engine #246 task 2), not
|
|
/// just called directly.
|
|
#[test]
|
|
fn reopened_param_rebuilds_with_the_freshly_supplied_value_not_the_stale_default() {
|
|
// Before reopen: fully bound, nothing to supply, and the default (2) is
|
|
// what gets built. (A fresh `Composite` — `reopen`/`compile_with_params`
|
|
// both consume `self`, so the "before" and "after" instances are built
|
|
// separately from the same constructor rather than cloned.)
|
|
let flat = one_bound_sma().compile_with_params(&[]).expect("fully-bound composite compiles with no params");
|
|
assert_eq!(flat.nodes[0].label(), "SMA(2)", "the bound default builds unchanged");
|
|
|
|
let reopened = one_bound_sma().reopen("sma.length").expect("bound knob reopens");
|
|
let space = reopened.param_space();
|
|
assert_eq!(space.len(), 1, "reopen returns exactly the one knob to the open surface");
|
|
assert_eq!(space[0].name, "sma.length");
|
|
|
|
let flat2 = reopened
|
|
.compile_with_params(&[Scalar::i64(9)])
|
|
.expect("reopened composite compiles once the freed slot is supplied");
|
|
assert_eq!(
|
|
flat2.nodes[0].label(),
|
|
"SMA(9)",
|
|
"the compiled node must be built from the freshly supplied value (9), not the stale bound default (2)"
|
|
);
|
|
}
|
|
|
|
/// Property: reopening one node's bound param does not disturb a SIBLING
|
|
/// node's own still-bound default. `BoundParam`'s per-node bookkeeping
|
|
/// (`pos`, restored on `unbind`) must stay node-local through the compile
|
|
/// pipeline — a bug that let one node's reopen leak into another's merge
|
|
/// would build the sibling from the wrong cell.
|
|
#[test]
|
|
fn reopening_one_node_leaves_a_sibling_bound_default_intact_through_compile() {
|
|
let signal = Composite::new(
|
|
"sig",
|
|
vec![
|
|
Sma::builder().named("fast").bind("length", Scalar::i64(2)).into(),
|
|
Bias::builder().named("b").bind("scale", Scalar::f64(0.5)).into(),
|
|
],
|
|
vec![],
|
|
vec![
|
|
Role {
|
|
name: "series".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }],
|
|
source: Some(ScalarKind::F64),
|
|
},
|
|
Role {
|
|
name: "signal".into(),
|
|
targets: vec![Target { node: 1, slot: 0 }],
|
|
source: Some(ScalarKind::F64),
|
|
},
|
|
],
|
|
vec![],
|
|
);
|
|
|
|
let reopened = signal.reopen("fast.length").expect("fast.length reopens");
|
|
let flat = reopened
|
|
.compile_with_params(&[Scalar::i64(7)])
|
|
.expect("compiles once the one freed slot is supplied");
|
|
|
|
assert_eq!(flat.nodes[0].label(), "SMA(7)", "the reopened node builds from the freshly supplied value");
|
|
assert_eq!(
|
|
flat.nodes[1].label(),
|
|
"Bias(0.5)",
|
|
"the sibling's still-bound default must build unchanged — reopen must not perturb it"
|
|
);
|
|
}
|