mod common;
use graphrefly_core::{EqualsMode, FnId, HandleId, Message, NO_HANDLE};
use graphrefly_graph::{Graph, GraphRemoveAudit, RemoveError, SignalKind};
use std::sync::{Arc, Mutex};
fn h(n: u64) -> HandleId {
HandleId::new(n)
}
#[test]
fn set_and_get_by_name() {
let g = Graph::new("test", common::binding());
let s = g.state("count", Some(h(10))).unwrap();
assert_eq!(g.get("count"), h(10));
g.set("count", h(20));
assert_eq!(g.get("count"), h(20));
assert_eq!(g.cache_of(s), h(20));
}
#[test]
#[should_panic(expected = "no node at path")]
fn get_panics_on_missing_name() {
let g = Graph::new("test", common::binding());
let _ = g.get("nonexistent");
}
#[test]
#[should_panic(expected = "no node at path")]
fn set_panics_on_missing_name() {
let g = Graph::new("test", common::binding());
g.set("nonexistent", h(1));
}
#[test]
fn invalidate_by_name_clears_cache() {
let g = Graph::new("test", common::binding());
let s = g.state("x", Some(h(5))).unwrap();
assert_eq!(g.cache_of(s), h(5));
g.invalidate_by_name("x");
assert_eq!(g.cache_of(s), NO_HANDLE);
}
#[test]
fn complete_by_name_terminates() {
let g = Graph::new("test", common::binding());
let s = g.state("x", Some(h(1))).unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
let _sub = g.subscribe(
s,
Arc::new(move |msgs: &[Message]| {
events_clone.lock().unwrap().extend_from_slice(msgs);
}),
);
g.complete_by_name("x");
let evts = events.lock().unwrap();
assert!(evts.iter().any(|m| matches!(m, Message::Complete)));
}
#[test]
fn error_by_name_terminates_with_handle() {
let g = Graph::new("test", common::binding());
let s = g.state("x", Some(h(1))).unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
let _sub = g.subscribe(
s,
Arc::new(move |msgs: &[Message]| {
events_clone.lock().unwrap().extend_from_slice(msgs);
}),
);
g.error_by_name("x", h(99));
let evts = events.lock().unwrap();
assert!(evts.iter().any(|m| matches!(m, Message::Error(_))));
}
#[test]
fn remove_node_sends_teardown_and_clears_namespace() {
let g = Graph::new("test", common::binding());
let s = g.state("temp", Some(h(1))).unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
let _sub = g.subscribe(
s,
Arc::new(move |msgs: &[Message]| {
events_clone.lock().unwrap().extend_from_slice(msgs);
}),
);
let audit = g.remove("temp").unwrap();
assert_eq!(
audit,
GraphRemoveAudit {
node_count: 1,
mount_count: 0
}
);
assert!(g.try_resolve("temp").is_none());
let evts = events.lock().unwrap();
assert!(evts.iter().any(|m| matches!(m, Message::Teardown)));
}
#[test]
fn remove_mounted_subgraph_delegates_to_unmount() {
let g = Graph::new("root", common::binding());
let child = g.mount_new("child").unwrap();
child.state("a", Some(h(1))).unwrap();
child.state("b", Some(h(2))).unwrap();
let audit = g.remove("child").unwrap();
assert_eq!(audit.node_count, 2);
assert_eq!(audit.mount_count, 0); assert!(g.try_resolve("child::a").is_none());
}
#[test]
fn remove_not_found_returns_error() {
let g = Graph::new("test", common::binding());
let err = g.remove("ghost").unwrap_err();
assert!(matches!(err, RemoveError::NotFound(_)));
}
#[test]
fn remove_preserves_namespace_during_teardown_cascade() {
let g = Graph::new("test", common::binding());
let s = g.state("temp", Some(h(1))).unwrap();
let g_clone = g.clone();
let observed_name = Arc::new(Mutex::new(None::<String>));
let observed_clone = observed_name.clone();
let _sub = g.subscribe(
s,
Arc::new(move |msgs: &[Message]| {
for m in msgs {
if matches!(m, Message::Teardown) {
*observed_clone.lock().unwrap() = g_clone.name_of(s);
}
}
}),
);
g.remove("temp").unwrap();
assert_eq!(
observed_name.lock().unwrap().as_deref(),
Some("temp"),
"sink observing TEARDOWN must see namespace name"
);
assert_eq!(g.name_of(s), None);
}
#[test]
fn remove_on_destroyed_graph() {
let g = Graph::new("test", common::binding());
g.state("x", None).unwrap();
g.destroy();
let err = g.remove("x").unwrap_err();
assert!(matches!(err, RemoveError::Destroyed));
}
#[test]
fn signal_invalidate_clears_all_named_caches() {
let g = Graph::new("test", common::binding());
let a = g.state("a", Some(h(1))).unwrap();
let b = g.state("b", Some(h(2))).unwrap();
g.signal(SignalKind::Invalidate);
assert_eq!(g.cache_of(a), NO_HANDLE);
assert_eq!(g.cache_of(b), NO_HANDLE);
}
#[test]
fn signal_pause_and_resume_round_trip() {
let g = Graph::new("test", common::binding());
let s = g.state("x", Some(h(1))).unwrap();
let lock = g.alloc_lock_id();
g.signal(SignalKind::Pause(lock));
assert!(g.core().is_paused(s));
g.signal(SignalKind::Resume(lock));
assert!(!g.core().is_paused(s));
}
#[test]
fn signal_invalidate_recurses_into_mounts() {
let g = Graph::new("root", common::binding());
let child = g.mount_new("child").unwrap();
let c = child.state("x", Some(h(5))).unwrap();
g.signal(SignalKind::Invalidate);
assert_eq!(g.cache_of(c), NO_HANDLE);
}
#[test]
fn signal_on_destroyed_graph_is_noop() {
let g = Graph::new("test", common::binding());
g.state("x", Some(h(1))).unwrap();
g.destroy();
g.signal(SignalKind::Invalidate);
}
#[test]
fn edges_local_only() {
let g = Graph::new("test", common::binding());
let a = g.state("a", None).unwrap();
g.derived("b", &[a], FnId::new(1), EqualsMode::Identity)
.unwrap();
let edges = g.edges(false);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0], ("a".to_string(), "b".to_string()));
}
#[test]
fn edges_recursive_into_mounts() {
let g = Graph::new("root", common::binding());
let a = g.state("a", None).unwrap();
g.derived("b", &[a], FnId::new(1), EqualsMode::Identity)
.unwrap();
let child = g.mount_new("child").unwrap();
let c = child.state("c", None).unwrap();
child
.derived("d", &[c], FnId::new(2), EqualsMode::Identity)
.unwrap();
let edges = g.edges(true);
assert_eq!(edges.len(), 2);
assert!(edges.contains(&("a".to_string(), "b".to_string())));
assert!(edges.contains(&("child::c".to_string(), "child::d".to_string())));
}
#[test]
fn edges_cross_graph_dep_shows_as_anon() {
let g = Graph::new("test", common::binding());
let anon = g.core().register_state(h(1), false).unwrap();
g.derived("d", &[anon], FnId::new(1), EqualsMode::Identity)
.unwrap();
let edges = g.edges(false);
assert_eq!(edges.len(), 1);
assert!(edges[0].0.starts_with("_anon_"));
assert_eq!(edges[0].1, "d");
}
#[test]
fn edges_empty_graph() {
let g = Graph::new("empty", common::binding());
assert!(g.edges(false).is_empty());
assert!(g.edges(true).is_empty());
}