use std::cell::{Cell, RefCell};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Rc;
use adaptite::{EffectHandle, Reactor, on_cleanup, ownership_stats, signal_in};
#[test]
fn an_effect_re_entered_from_its_own_cleanup_defers_the_run() {
let reactor = Reactor::new();
let tick = signal_in(&reactor, 0u32);
let gate = signal_in(&reactor, 0u32);
let child_dep = signal_in(&reactor, 0u32);
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let before = ownership_stats();
let handle = reactor.effect({
let reactor = reactor.clone();
let tick = tick.clone();
let gate = gate.clone();
let child_dep = child_dep.clone();
let log = Rc::clone(&log);
move || {
log.borrow_mut()
.push(format!("outer t{} g{}", tick.get(), gate.get()));
let child = reactor.effect({
let child_dep = child_dep.clone();
let log = Rc::clone(&log);
move || log.borrow_mut().push(format!("child {}", child_dep.get()))
});
drop(child);
on_cleanup({
let reactor = reactor.clone();
let gate = gate.clone();
let log = Rc::clone(&log);
move || {
log.borrow_mut().push("cleanup".into());
gate.set(10);
reactor.flush_now();
}
});
}
});
reactor.flush_now();
let settled = ownership_stats();
assert_eq!(
settled.owned_children - before.owned_children,
1,
"one nested child after the first run"
);
tick.set(1);
reactor.flush_now();
assert_eq!(
*log.borrow(),
[
"outer t0 g0",
"child 0",
"cleanup",
"outer t1 g10",
"child 0",
"cleanup",
"outer t1 g10",
"child 0",
],
"every body run must be preceded by its own teardown"
);
let after = ownership_stats();
assert_eq!(
after.owned_children - before.owned_children,
1,
"the effect must hold one generation of children, not two; log: {:?}",
log.borrow()
);
assert_eq!(
after.cleanup_registrations - before.cleanup_registrations,
1,
"the effect must hold one generation of cleanups, not two"
);
log.borrow_mut().clear();
child_dep.set(99);
reactor.flush_now();
assert_eq!(
log.borrow().len(),
1,
"exactly one child should observe the write; log: {:?}",
log.borrow()
);
handle.dispose();
}
#[test]
fn the_run_deferred_from_a_cleanup_is_still_re_queued() {
let reactor = Reactor::new();
let tick = signal_in(&reactor, 0u32);
let gate = signal_in(&reactor, 0u32);
let seen = Rc::new(RefCell::new(Vec::<(u32, u32)>::new()));
let handle = reactor.effect({
let reactor = reactor.clone();
let tick = tick.clone();
let gate = gate.clone();
let seen = Rc::clone(&seen);
move || {
seen.borrow_mut().push((tick.get(), gate.get()));
on_cleanup({
let reactor = reactor.clone();
let gate = gate.clone();
move || {
gate.set(10);
reactor.flush_now();
}
});
}
});
reactor.flush_now();
tick.set(1);
reactor.flush_now();
assert_eq!(
*seen.borrow(),
[(0, 0), (1, 10), (1, 10)],
"the deferred run must still happen and observe the cleanup's write"
);
handle.dispose();
}
#[test]
fn an_effect_disposed_from_its_own_cleanup_does_not_run_its_body() {
let reactor = Reactor::new();
let tick = signal_in(&reactor, 0u32);
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let slot: Rc<RefCell<Option<EffectHandle>>> = Rc::new(RefCell::new(None));
let handle = reactor.effect({
let tick = tick.clone();
let log = Rc::clone(&log);
let slot = Rc::clone(&slot);
move || {
let disposed = slot
.borrow()
.as_ref()
.map(EffectHandle::is_disposed)
.unwrap_or(false);
log.borrow_mut()
.push(format!("body t{} disposed={disposed}", tick.get()));
on_cleanup({
let log = Rc::clone(&log);
let slot = Rc::clone(&slot);
move || {
log.borrow_mut().push("cleanup".into());
if let Some(handle) = slot.borrow().as_ref() {
handle.dispose();
log.borrow_mut().push("self-disposed".into());
}
}
});
}
});
*slot.borrow_mut() = Some(handle.clone());
reactor.flush_now();
tick.set(1);
reactor.flush_now();
assert_eq!(
*log.borrow(),
["body t0 disposed=false", "cleanup", "self-disposed"],
"the body must not run after the cleanup disposed the effect"
);
assert!(handle.is_disposed());
}
#[test]
fn the_divergence_guard_counts_runs_per_drain_not_per_flush_epoch() {
let reactor = Reactor::new();
let value = signal_in(&reactor, 0u32);
let runs = Rc::new(Cell::new(0u32));
const CONVERGES_AT: u32 = 150;
let handle = reactor.effect({
let reactor = reactor.clone();
let value = value.clone();
let runs = Rc::clone(&runs);
move || {
runs.set(runs.get() + 1);
let seen = value.get();
if seen < CONVERGES_AT {
value.set(seen + 1);
reactor.flush_now();
}
}
});
let result = catch_unwind(AssertUnwindSafe(|| reactor.flush_now()));
let payload = match result {
Ok(()) => panic!(
"the divergence guard did not fire: the effect ran {} times inside one drain, each \
run opening a nested flush epoch",
runs.get()
),
Err(payload) => payload,
};
let message = payload
.downcast_ref::<String>()
.cloned()
.expect("panic payload should be a formatted string");
assert!(
message.contains("divergent reactive feedback loop"),
"got: {message}"
);
assert!(
runs.get() < CONVERGES_AT,
"the guard should fire before the loop converges, ran {} times",
runs.get()
);
handle.dispose();
}