use std::sync::{Condvar, Mutex};
use std::time::Duration;
use behavior_contracts::{
run_plan, run_plan_parallel, ExecOutcome, ExecutionPlanSpec, OpSpec, Value,
};
fn op(id: &str, parent: Option<usize>) -> OpSpec {
OpSpec {
id: id.into(),
parent,
bind_field: None,
relation_kind: None,
policy: None,
}
}
#[test]
fn siblings_overlap_with_barrier() {
let plan = ExecutionPlanSpec {
groups: vec![vec![0], vec![1, 2]],
concurrency: 2,
};
let ops = vec![op("root", None), op("sibA", Some(0)), op("sibB", Some(0))];
let arrived = Mutex::new(0usize);
let cv = Condvar::new();
let exec = |o: &OpSpec, _b: Option<&Value>| -> ExecOutcome {
if o.parent.is_none() {
return ExecOutcome::Ok(Value::Obj(vec![]));
}
let mut n = arrived.lock().unwrap();
*n += 1;
cv.notify_all();
while *n < 2 {
let (guard, timeout) = cv.wait_timeout(n, Duration::from_secs(2)).unwrap();
n = guard;
if timeout.timed_out() && *n < 2 {
return ExecOutcome::Error("no-overlap".into());
}
}
ExecOutcome::Ok(Value::Str(format!("{}-done", o.id)))
};
let res = run_plan_parallel(Some(&plan), &ops, exec)
.expect("siblings must overlap (sequential execution would fail here)");
assert_eq!(res.executed, vec!["root", "sibA", "sibB"]);
}
#[test]
fn inflight_bounded_by_concurrency() {
let mut ops = vec![op("root", None)];
for id in ["sib1", "sib2", "sib3", "sib4"] {
ops.push(op(id, Some(0)));
}
let plan = ExecutionPlanSpec {
groups: vec![vec![0], vec![1, 2, 3, 4]],
concurrency: 2,
};
let state = Mutex::new((0usize, 0usize)); let exec = |o: &OpSpec, _b: Option<&Value>| -> ExecOutcome {
if o.parent.is_none() {
return ExecOutcome::Ok(Value::Obj(vec![]));
}
{
let mut s = state.lock().unwrap();
s.0 += 1;
s.1 = s.1.max(s.0);
}
std::thread::sleep(Duration::from_millis(20));
state.lock().unwrap().0 -= 1;
ExecOutcome::Ok(Value::Str(o.id.clone()))
};
let res = run_plan_parallel(Some(&plan), &ops, exec).expect("run");
assert_eq!(res.executed.len(), 5);
let peak = state.lock().unwrap().1;
assert_eq!(peak, 2, "peak in-flight {peak} != concurrency bound 2");
*state.lock().unwrap() = (0, 0);
let seq_plan = ExecutionPlanSpec {
groups: plan.groups.clone(),
concurrency: 1,
};
run_plan_parallel(Some(&seq_plan), &ops, exec).expect("run");
let peak = state.lock().unwrap().1;
assert_eq!(peak, 1, "concurrency=1 must not overlap (peak {peak})");
}
#[test]
fn failure_identity_matches_declaration_order() {
let ops = vec![
op("root", None),
op("sibA", Some(0)),
op("sibB", Some(0)),
op("sibC", Some(0)),
];
let groups = vec![vec![0], vec![1, 2, 3]];
let mk = |slow: bool| {
move |o: &OpSpec, _b: Option<&Value>| -> ExecOutcome {
match o.id.as_str() {
"root" => ExecOutcome::Ok(Value::Obj(vec![])),
"sibA" => {
if slow {
std::thread::sleep(Duration::from_millis(5));
}
ExecOutcome::Ok(Value::Str("a".into()))
}
"sibB" => {
if slow {
std::thread::sleep(Duration::from_millis(30));
}
ExecOutcome::Error("b-broke".into())
}
_ => ExecOutcome::Error("c-broke-first".into()),
}
}
};
let seq_err = run_plan(
Some(&ExecutionPlanSpec {
groups: groups.clone(),
concurrency: 1,
}),
&ops,
mk(false),
)
.expect_err("sequential reference must fail");
let par_err = run_plan_parallel(
Some(&ExecutionPlanSpec {
groups,
concurrency: 3,
}),
&ops,
mk(true),
)
.expect_err("parallel run must fail");
assert_eq!(seq_err.code, par_err.code);
assert_eq!(seq_err.message, par_err.message); }
#[test]
fn intra_stage_dependency_falls_back_to_sequential() {
let ops = vec![
op("root", None),
OpSpec {
id: "child".into(),
parent: Some(0),
bind_field: Some("k".into()),
relation_kind: None,
policy: None,
},
];
let plan = ExecutionPlanSpec {
groups: vec![vec![0, 1]],
concurrency: 8,
};
let state = Mutex::new((0usize, 0usize));
let exec = |o: &OpSpec, _b: Option<&Value>| -> ExecOutcome {
{
let mut s = state.lock().unwrap();
s.0 += 1;
s.1 = s.1.max(s.0);
}
std::thread::sleep(Duration::from_millis(10));
state.lock().unwrap().0 -= 1;
if o.parent.is_none() {
ExecOutcome::Ok(Value::Obj(vec![("k".into(), Value::Str("v".into()))]))
} else {
ExecOutcome::Ok(Value::Str("child-done".into()))
}
};
let res = run_plan_parallel(Some(&plan), &ops, exec).expect("run");
let peak = state.lock().unwrap().1;
assert_eq!(
peak, 1,
"intra-stage dependency must fall back to sequential"
);
assert_eq!(res.executed, vec!["root", "child"]);
}