mod common;
use std::sync::Arc;
use common::{TestRuntime, TestValue};
use graphrefly_core::{
BindingBoundary, Core, EqualsMode, NodeOpts, NodeRegistration, RegisterError,
SchedulingGroupId, SetDepsError, SetGroupError,
};
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SchedulingGroupId>();
trait AmbiguousIfSend<A> {
fn probe() {}
}
impl<T: ?Sized> AmbiguousIfSend<()> for T {}
impl<T: ?Sized + Send> AmbiguousIfSend<u16> for T {}
let _ = <graphrefly_core::Core as AmbiguousIfSend<_>>::probe;
};
const G1: SchedulingGroupId = SchedulingGroupId::new(1);
const G2: SchedulingGroupId = SchedulingGroupId::new(2);
#[test]
fn default_none_graph_has_no_groups() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let d = rt.derived(&[s.id], |_| Some(TestValue::Int(1)));
assert_eq!(rt.core().partition_of(s.id), None);
assert_eq!(rt.core().partition_of(d), None);
assert_eq!(
rt.core().partition_count(),
0,
"all-None graph touches no group lock — the floor"
);
}
#[test]
fn partition_of_unregistered_is_none() {
let rt = TestRuntime::new();
assert_eq!(
rt.core().partition_of(graphrefly_core::NodeId::new(9999)),
None
);
}
#[test]
fn register_with_group_then_partition_of_reports_it() {
let rt = TestRuntime::new();
let init = rt.binding.intern(TestValue::Int(0));
let s = rt
.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init,
scheduling_group: Some(G1),
..Default::default()
},
})
.expect("register grouped state");
assert_eq!(rt.core().partition_of(s), Some(G1));
}
#[test]
fn set_scheduling_group_on_isolated_node_ok() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
rt.core()
.set_scheduling_group(s.id, Some(G1))
.expect("isolated node group assignment");
assert_eq!(rt.core().partition_of(s.id), Some(G1));
rt.core()
.set_scheduling_group(s.id, None)
.expect("clear group");
assert_eq!(rt.core().partition_of(s.id), None);
}
#[test]
fn set_scheduling_group_unknown_node() {
let rt = TestRuntime::new();
let err = rt
.core()
.set_scheduling_group(graphrefly_core::NodeId::new(42), Some(G1))
.unwrap_err();
assert!(matches!(err, SetGroupError::UnknownNode(_)));
}
#[test]
fn register_all_none_chain_ok() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let _d = rt.derived(&[s.id], |_| Some(TestValue::Int(1)));
}
#[test]
fn register_all_some_chain_ok_even_with_distinct_groups() {
let rt = TestRuntime::new();
let init = rt.binding.intern(TestValue::Int(0));
let s = rt
.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init,
scheduling_group: Some(G1),
..Default::default()
},
})
.unwrap();
let f = rt
.binding
.register_fn(|_: &[TestValue]| Some(TestValue::Int(0)));
let d = rt
.core()
.register(NodeRegistration {
deps: vec![s],
fn_or_op: Some(graphrefly_core::NodeFnOrOp::Fn(f)),
opts: NodeOpts {
scheduling_group: Some(G2),
..Default::default()
},
})
.expect("all-Some component with distinct groups is consistent");
assert_eq!(rt.core().partition_of(s), Some(G1));
assert_eq!(rt.core().partition_of(d), Some(G2));
}
#[test]
fn register_mixed_component_rejected() {
let rt = TestRuntime::new();
let init = rt.binding.intern(TestValue::Int(0));
let s = rt
.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init,
scheduling_group: Some(G1),
..Default::default()
},
})
.unwrap();
let f = rt
.binding
.register_fn(|_: &[TestValue]| Some(TestValue::Int(0)));
let err = rt
.core()
.register(NodeRegistration {
deps: vec![s],
fn_or_op: Some(graphrefly_core::NodeFnOrOp::Fn(f)),
opts: NodeOpts::default(), })
.unwrap_err();
assert!(
matches!(err, RegisterError::GroupInconsistent),
"mixing a grouped dep with an ungrouped consumer must be rejected; got {err:?}"
);
}
#[test]
fn set_deps_into_mixed_component_rejected() {
let rt = TestRuntime::new();
let s1 = rt.state(Some(TestValue::Int(1)));
let d = rt.dynamic(&[s1.id], |_| (Some(TestValue::Int(0)), None));
let init2 = rt.binding.intern(TestValue::Int(2));
let s2 = rt
.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init2,
scheduling_group: Some(G1),
..Default::default()
},
})
.unwrap();
let err = rt.core().set_deps(d, &[s1.id, s2]).unwrap_err();
assert!(
matches!(err, SetDepsError::GroupInconsistent { n } if n == d),
"set_deps creating a mixed component must be rejected; got {err:?}"
);
assert_eq!(
rt.core().deps_of(d),
vec![s1.id],
"rejected — deps unchanged"
);
}
#[test]
fn set_deps_consistent_rewire_ok() {
let rt = TestRuntime::new();
let s1 = rt.state(Some(TestValue::Int(1)));
let s2 = rt.state(Some(TestValue::Int(2)));
let d = rt.dynamic(&[s1.id], |_| (Some(TestValue::Int(0)), None));
rt.core()
.set_deps(d, &[s1.id, s2.id])
.expect("all-None rewire");
assert_eq!(rt.core().deps_of(d), vec![s1.id, s2.id]);
}
#[test]
fn set_scheduling_group_is_component_wide_retroactive_regroup() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let d = rt.derived(&[s.id], |_| Some(TestValue::Int(1)));
rt.core()
.set_scheduling_group(s.id, Some(G1))
.expect("component-wide assign succeeds (consistent by construction)");
assert_eq!(rt.core().partition_of(s.id), Some(G1));
assert_eq!(
rt.core().partition_of(d),
Some(G1),
"the connected derived node was regrouped too (component-wide)"
);
rt.core()
.set_scheduling_group(d, None)
.expect("component-wide clear succeeds");
assert_eq!(rt.core().partition_of(s.id), None);
assert_eq!(rt.core().partition_of(d), None);
}
fn grouped_state(rt: &TestRuntime, g: Option<SchedulingGroupId>) -> graphrefly_core::NodeId {
let init = rt.binding.intern(TestValue::Int(0));
rt.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init,
scheduling_group: g,
..Default::default()
},
})
.expect("register isolated state")
}
#[test]
fn add_meta_companion_mixed_component_panics() {
let rt = TestRuntime::new();
let parent = grouped_state(&rt, Some(G1)); let companion = grouped_state(&rt, None); let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
rt.core().add_meta_companion(parent, companion);
}));
assert!(
res.is_err(),
"add_meta_companion joining grouped+ungrouped must panic (mixed component)"
);
assert!(
rt.core().meta_companions_of(parent).is_empty(),
"meta edge rolled back on the panic path"
);
}
#[test]
fn add_meta_companion_consistent_and_walk_includes_meta() {
let rt = TestRuntime::new();
let parent = grouped_state(&rt, Some(G1));
let companion = grouped_state(&rt, Some(G1));
rt.core().add_meta_companion(parent, companion);
rt.core()
.set_scheduling_group(parent, None)
.expect("component-wide clear");
assert_eq!(rt.core().partition_of(parent), None);
assert_eq!(
rt.core().partition_of(companion),
None,
"meta companion regrouped via the unified deps+children+meta walk"
);
}
#[test]
fn grouped_emit_drives_wave_and_touches_group_lock() {
let rt = TestRuntime::new();
let init = rt.binding.intern(TestValue::Int(1));
let s = rt
.core()
.register(NodeRegistration {
deps: vec![],
fn_or_op: None,
opts: NodeOpts {
initial: init,
scheduling_group: Some(G1),
..Default::default()
},
})
.unwrap();
let f = rt
.binding
.register_fn(|deps: &[TestValue]| match deps.first() {
Some(TestValue::Int(v)) => Some(TestValue::Int(v + 1)),
_ => None,
});
let d = rt
.core()
.register(NodeRegistration {
deps: vec![s],
fn_or_op: Some(graphrefly_core::NodeFnOrOp::Fn(f)),
opts: NodeOpts {
scheduling_group: Some(G1),
..Default::default()
},
})
.unwrap();
let _sub = rt
.core()
.subscribe(d, Arc::new(|_msgs: &[graphrefly_core::Message]| {}));
let v = rt.binding.intern(TestValue::Int(41));
rt.core().emit(s, v);
assert_eq!(
rt.core().partition_count(),
1,
"the wave touched scheduling group G1 → one group lock resolved"
);
}
#[test]
fn single_owner_core_is_functional() {
let binding = common::TestBinding::new();
let core: Core = Core::new(binding.clone() as Arc<dyn BindingBoundary>);
let init = binding.intern(TestValue::Int(10));
let s = core.register_state(init, false).unwrap();
let f = binding.register_fn(|deps: &[TestValue]| match deps.first() {
Some(TestValue::Int(v)) => Some(TestValue::Int(v * 2)),
_ => None,
});
let d = core
.register_derived(&[s], f, EqualsMode::Identity, false)
.unwrap();
let seen = Arc::new(std::sync::Mutex::new(Vec::<TestValue>::new()));
let seen2 = seen.clone();
let bclone = binding.clone();
let _sub = core.subscribe(
d,
Arc::new(move |msgs: &[graphrefly_core::Message]| {
for m in msgs {
if let graphrefly_core::Message::Data(h) = m {
seen2.lock().unwrap().push(bclone.deref(*h));
}
}
}),
);
let v = binding.intern(TestValue::Int(21));
core.emit(s, v);
assert_eq!(
*seen.lock().unwrap(),
vec![TestValue::Int(20), TestValue::Int(42)],
"single-owner lock-free Core dispatches activation + emit correctly"
);
assert_eq!(core.partition_of(d), None);
}