use std::sync::Arc;
mod common;
use common::{TestRuntime, TestValue};
use graphrefly_core::{NodeId, SetDepsError};
#[test]
fn straight_rewire_a_to_b() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let calls = Arc::new(std::sync::Mutex::new(0u32));
let calls_inner = calls.clone();
let c = rt.derived(&[a.id], move |deps| {
*calls_inner.lock().unwrap() += 1;
match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
}
});
let _rec = rt.subscribe_recorder(c);
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
let calls_before = *calls.lock().unwrap();
rt.core.set_deps(c, &[b.id]).expect("rewire ok");
assert_eq!(rt.cache_value(c), Some(TestValue::Int(20)));
assert!(*calls.lock().unwrap() > calls_before);
let calls_after_rewire = *calls.lock().unwrap();
a.set(TestValue::Int(99));
assert_eq!(*calls.lock().unwrap(), calls_after_rewire);
assert_eq!(rt.cache_value(c), Some(TestValue::Int(20)));
b.set(TestValue::Int(50));
assert_eq!(rt.cache_value(c), Some(TestValue::Int(50)));
}
#[test]
fn additive_rewire_a_to_a_b() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
rt.core.set_deps(c, &[a.id, b.id]).expect("rewire ok");
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
b.set(TestValue::Int(30));
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
}
#[test]
fn full_removal_to_empty_deps() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
rt.core.set_deps(c, &[]).expect("rewire to empty ok");
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
a.set(TestValue::Int(99));
assert_eq!(rt.cache_value(c), Some(TestValue::Int(10)));
}
#[test]
fn idempotent_no_change() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let calls = Arc::new(std::sync::Mutex::new(0u32));
let calls_inner = calls.clone();
let c = rt.derived(&[a.id], move |deps| {
*calls_inner.lock().unwrap() += 1;
match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
}
});
let _rec = rt.subscribe_recorder(c);
let calls_before = *calls.lock().unwrap();
rt.core.set_deps(c, &[a.id]).expect("idempotent ok");
assert_eq!(*calls.lock().unwrap(), calls_before);
}
#[test]
fn self_rewire_rejected() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let result = rt.core.set_deps(c, &[c]);
assert!(matches!(result, Err(SetDepsError::SelfDependency { .. })));
}
#[test]
fn cycle_rejected() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(1)));
let b = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n + 1)),
_ => panic!("type"),
});
let c = rt.derived(&[b], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n + 1)),
_ => panic!("type"),
});
let d = rt.derived(&[c], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n + 1)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(d);
let result = rt.core.set_deps(b, &[d]);
match result {
Err(SetDepsError::WouldCreateCycle {
n: cycle_n,
added_dep,
path,
}) => {
assert_eq!(cycle_n, b);
assert_eq!(added_dep, d);
assert_eq!(path.first(), Some(&b));
assert_eq!(path.last(), Some(&d));
assert!(
path.len() >= 2,
"path should include at least endpoints, got {path:?}"
);
}
other => panic!("expected WouldCreateCycle, got {other:?}"),
}
let unrelated = rt.state(Some(TestValue::Int(7)));
rt.core
.set_deps(b, &[unrelated.id])
.expect("unrelated rewire ok");
}
#[test]
fn unknown_node_rejected() {
let rt = TestRuntime::new();
let bogus = NodeId::new(99999);
let result = rt.core.set_deps(bogus, &[]);
assert!(matches!(result, Err(SetDepsError::UnknownNode(_))));
}
#[test]
fn rewire_state_node_rejected() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(1)));
let result = rt.core.set_deps(s.id, &[]);
assert!(matches!(result, Err(SetDepsError::NotComputeNode(_))));
}
#[test]
fn cache_preserved_across_rewire() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(100)));
let b = rt.state(None);
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
let cache_before = rt.cache_value(c);
assert_eq!(cache_before, Some(TestValue::Int(100)));
rt.core.set_deps(c, &[b.id]).expect("rewire ok");
assert_eq!(rt.cache_value(c), cache_before);
}
#[test]
fn dynamic_rewire_refires_fn_on_new_deps() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let calls = Arc::new(std::sync::Mutex::new(0u32));
let calls_inner = calls.clone();
let n = rt.dynamic(&[a.id], move |deps| {
*calls_inner.lock().unwrap() += 1;
let value = match &deps[0] {
TestValue::Int(v) => Some(TestValue::Int(*v)),
_ => None,
};
let tracked: Vec<usize> = (0..deps.len()).collect();
(value, Some(tracked))
});
let _rec = rt.subscribe_recorder(n);
assert_eq!(*calls.lock().unwrap(), 1);
assert_eq!(rt.cache_value(n), Some(TestValue::Int(10)));
rt.core.set_deps(n, &[b.id]).expect("rewire ok");
assert_eq!(
rt.cache_value(n),
Some(TestValue::Int(20)),
"dynamic must re-fire on rewire to a dep with cached DATA"
);
assert!(
*calls.lock().unwrap() >= 2,
"fn should have fired at least once post-rewire"
);
let calls_after_rewire = *calls.lock().unwrap();
b.set(TestValue::Int(99));
assert_eq!(rt.cache_value(n), Some(TestValue::Int(99)));
assert!(*calls.lock().unwrap() > calls_after_rewire);
let calls_before_old = *calls.lock().unwrap();
a.set(TestValue::Int(7));
assert_eq!(*calls.lock().unwrap(), calls_before_old);
}
#[test]
fn rewire_terminal_node_rejected() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
rt.core.complete(c);
let result = rt.core.set_deps(c, &[b.id]);
assert!(matches!(result, Err(SetDepsError::TerminalNode { .. })));
}
#[test]
fn rewire_to_terminal_non_resubscribable_dep_rejected() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
rt.core.complete(b.id);
let result = rt.core.set_deps(c, &[b.id]);
match result {
Err(SetDepsError::TerminalDep { dep, .. }) => assert_eq!(dep, b.id),
other => panic!("expected TerminalDep, got {other:?}"),
}
}
#[test]
fn rewire_to_terminal_resubscribable_dep_accepted() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
rt.core.set_resubscribable(b.id, true);
let c = rt.derived(&[a.id], |deps| match &deps[0] {
TestValue::Int(n) => Some(TestValue::Int(*n)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
rt.core.complete(b.id);
let result = rt.core.set_deps(c, &[b.id]);
assert!(
result.is_ok(),
"resubscribable terminal dep should be accepted"
);
}
#[test]
fn rewire_with_kept_terminal_dep_does_not_re_check() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(Some(TestValue::Int(20)));
let c = rt.derived(&[a.id, b.id], |deps| match (&deps[0], &deps[1]) {
(TestValue::Int(av), TestValue::Int(bv)) => Some(TestValue::Int(av + bv)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(c);
rt.core.complete(a.id);
let result = rt.core.set_deps(c, &[a.id, b.id]);
assert!(result.is_ok(), "kept terminal dep is not re-validated");
}
#[test]
fn rewire_releases_error_handles_in_removed_dep_slots() {
let rt = TestRuntime::new();
let p = rt.state(Some(TestValue::Int(1)));
let q = rt.state(Some(TestValue::Int(2)));
let consumer = rt.derived(&[p.id, q.id], |deps| match (&deps[0], &deps[1]) {
(TestValue::Int(pv), TestValue::Int(qv)) => Some(TestValue::Int(pv + qv)),
_ => panic!("type"),
});
let _rec = rt.subscribe_recorder(consumer);
let err = rt.binding.intern(TestValue::Str("p-err".into()));
rt.core.error(p.id, err);
let count_after_error = rt.binding.refcount_of(err);
assert_eq!(
count_after_error, 2,
"err refcount = p.terminal(1) + consumer.dep_terminals(1) \
(intern share released by Core::error)"
);
rt.core.set_deps(consumer, &[q.id]).expect("rewire ok");
let count_after_rewire = rt.binding.refcount_of(err);
assert_eq!(
count_after_rewire, 1,
"set_deps must release the per-slot Error retain when removing the dep \
(refcount 2 → 1: p.terminal(1) preserved)"
);
}
#[test]
fn dynamic_rewire_to_sentinel_dep_holds_until_dep_emits() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(10)));
let b = rt.state(None); let calls = Arc::new(std::sync::Mutex::new(0u32));
let calls_inner = calls.clone();
let n = rt.dynamic(&[a.id], move |deps| {
*calls_inner.lock().unwrap() += 1;
let value = match &deps[0] {
TestValue::Int(v) => Some(TestValue::Int(*v)),
_ => None,
};
(value, Some((0..deps.len()).collect()))
});
let _rec = rt.subscribe_recorder(n);
let calls_at_setup = *calls.lock().unwrap();
rt.core.set_deps(n, &[b.id]).expect("rewire ok");
assert_eq!(
*calls.lock().unwrap(),
calls_at_setup,
"no fire while new dep is sentinel"
);
b.set(TestValue::Int(50));
assert!(*calls.lock().unwrap() > calls_at_setup);
assert_eq!(rt.cache_value(n), Some(TestValue::Int(50)));
}