use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Rc;
use adaptite::{
DiagnosticEvent, NodeKind, NodeState, Reactor, RecordedDependency, memo_in, ownership_stats,
signal_in,
};
#[test]
fn a_divergent_loop_panics_in_this_build_profile() {
let reactor = Reactor::new();
let counter = signal_in(&reactor, 0_u64);
let effect = reactor.effect({
let counter = counter.clone();
move || {
let next = counter.get() + 1;
counter.set(next);
}
});
let panic = catch_unwind(AssertUnwindSafe(|| reactor.flush_now()))
.expect_err("a divergent feedback loop must panic rather than hang, in every profile");
let message = panic
.downcast_ref::<String>()
.cloned()
.expect("the divergence panic payload is a formatted String");
assert!(
message.contains("divergent reactive feedback loop"),
"the panic must diagnose the divergence, got: {message}"
);
assert!(
message.contains("in a single drain"),
"the guard counts per drain, not per flush, and the message says so: {message}"
);
effect.dispose();
}
#[test]
fn the_reference_section_names_items_that_exist_and_answer() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 1_u32);
let doubled = memo_in(&reactor, {
let source = source.clone();
move || source.get() * 2
});
let dead = {
let short_lived = signal_in(&reactor, 0_u8);
short_lived.id()
};
assert_eq!(reactor.node_kind(dead), None, "the id is genuinely dead");
let seen = Rc::new(Cell::new(0_u32));
let effect = reactor.effect({
let doubled = doubled.clone();
let seen = Rc::clone(&seen);
move || seen.set(doubled.get())
});
reactor.flush_now();
assert_eq!(
reactor.node_state(doubled.id()),
Some(NodeState::Clean),
"a memo that has just been read is clean"
);
source.set(2);
assert_ne!(
reactor.node_state(doubled.id()),
Some(NodeState::Clean),
"a write upstream must show up as staleness on the node itself"
);
assert_eq!(
reactor.node_state(dead),
None,
"a node that is not live has no state to report"
);
reactor.flush_now();
let snapshot = reactor.graph_snapshot();
let node = snapshot
.node(doubled.id())
.expect("a live memo appears in a snapshot of its own reactor");
assert_eq!(node.id, doubled.id());
assert_eq!(node.kind, NodeKind::Memo);
assert_eq!(
snapshot.node(dead),
None,
"looking up an id that is not in the snapshot answers None, not a wrong node"
);
let dependencies: Vec<RecordedDependency> = reactor.dependencies_of(doubled.id());
assert_eq!(dependencies.len(), 1);
assert_eq!(dependencies[0].node, source.id());
assert_eq!(
Some(dependencies[0].version),
reactor.node_version(source.id()),
"the recorded version matches the dependency's current version once it is verified"
);
let kinds: Vec<NodeKind> = NodeKind::all().collect();
assert_eq!(kinds.len(), 6, "the documented kind list is the whole set");
let stats = reactor.graph_stats();
let per_kind: usize = kinds
.iter()
.map(|kind| stats.live_nodes_of_kind(*kind))
.sum();
assert_eq!(
per_kind, stats.live_nodes,
"the per-kind counts must account for every live node, or the breakdown lies"
);
assert!(
!ownership_stats().is_empty(),
"a live effect holds an owner frame"
);
assert_eq!(
format!("{}", reactor.id()),
format!("{}", reactor.id().get())
);
effect.dispose();
}
#[test]
fn the_generic_accessors_cover_the_whole_event_stream() {
let reactor = Reactor::new();
let events = Rc::new(std::cell::RefCell::new(Vec::new()));
let subscription = reactor.subscribe_diagnostics({
let events = Rc::clone(&events);
move |event| events.borrow_mut().push(event)
});
let source = signal_in(&reactor, 1_u32);
let doubled = memo_in(&reactor, {
let source = source.clone();
move || source.get() * 2
});
let effect = reactor.effect({
let doubled = doubled.clone();
move || {
let _ = doubled.get();
}
});
reactor.flush_now();
source.set(2);
reactor.flush_now();
effect.dispose();
reactor.flush_now();
drop(subscription);
let events = events.borrow();
assert!(
events.len() > 8,
"the workload must produce a real stream, got {}",
events.len()
);
let reactor_id = reactor.id();
let mut with_node = 0_usize;
for event in events.iter() {
assert_eq!(
event.reactor(),
reactor_id,
"every event is scoped to the graph it came from: {event:?}"
);
assert!(
event.node_origin().is_none() || event.node().is_some(),
"an origin without a node would name nothing: {event:?}"
);
if event.node().is_some() {
with_node += 1;
}
let is_boundary = matches!(
event,
DiagnosticEvent::FlushStarted { .. } | DiagnosticEvent::FlushFinished { .. }
);
assert_eq!(
is_boundary,
event.node().is_none(),
"only the flush boundaries have no node: {event:?}"
);
if let Some(epoch) = event.flush_epoch() {
assert!(
epoch <= reactor.graph_stats().flush_epoch,
"an event cannot belong to a flush that has not happened: {event:?}"
);
}
}
assert!(
with_node > 0,
"the workload must include node-scoped events for the check above to mean anything"
);
}