use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Rc;
use adaptite::{
ComputeOutcome, DiagnosticEvent, FlushStats, InvalidationLevel, Reactor, memo_in, signal_in,
thunk_in,
};
#[derive(Default)]
struct Capture {
events: Vec<DiagnosticEvent>,
flushes: Vec<FlushStats>,
}
impl Capture {
fn install(reactor: &Reactor) -> (Rc<RefCell<Self>>, adaptite::DiagnosticSubscription) {
let capture = Rc::new(RefCell::new(Self::default()));
let subscription = reactor.subscribe_diagnostics({
let capture = Rc::clone(&capture);
move |event| {
let mut capture = capture.borrow_mut();
if let DiagnosticEvent::FlushFinished { stats, .. } = event {
capture.flushes.push(stats);
}
capture.events.push(event);
}
});
(capture, subscription)
}
fn totals(&self) -> FlushStats {
let mut total = FlushStats::default();
for stats in &self.flushes {
total.root_writes += stats.root_writes;
total.nodes_marked_check += stats.nodes_marked_check;
total.nodes_marked_dirty += stats.nodes_marked_dirty;
total.effects_queued += stats.effects_queued;
total.effects_coalesced += stats.effects_coalesced;
total.effects_run += stats.effects_run;
total.effects_skipped += stats.effects_skipped;
total.effects_disposed += stats.effects_disposed;
total.computed_verified += stats.computed_verified;
total.computed_recomputed += stats.computed_recomputed;
total.computed_changed += stats.computed_changed;
total.computed_suppressed += stats.computed_suppressed;
total.edges_added += stats.edges_added;
total.edges_removed += stats.edges_removed;
}
total
}
fn tallied(&self) -> FlushStats {
let mut tally = FlushStats::default();
for event in &self.events {
match event {
DiagnosticEvent::ReactiveWrite { .. } => tally.root_writes += 1,
DiagnosticEvent::ComputedInvalidated { level, .. }
| DiagnosticEvent::EffectInvalidated { level, .. } => match level {
InvalidationLevel::Check => tally.nodes_marked_check += 1,
InvalidationLevel::Dirty => tally.nodes_marked_dirty += 1,
_ => {}
},
DiagnosticEvent::EffectScheduled { queued, .. } => {
if *queued {
tally.effects_queued += 1;
} else {
tally.effects_coalesced += 1;
}
}
DiagnosticEvent::EffectRunStarted { .. } => tally.effects_run += 1,
DiagnosticEvent::EffectRunSkipped { .. } => tally.effects_skipped += 1,
DiagnosticEvent::EffectDisposed { .. } => tally.effects_disposed += 1,
DiagnosticEvent::ComputedVerified { .. } => tally.computed_verified += 1,
DiagnosticEvent::ComputedRecomputeStarted { .. } => tally.computed_recomputed += 1,
DiagnosticEvent::ComputedRecomputeFinished {
changed, outcome, ..
} if *outcome == ComputeOutcome::Completed => {
if *changed {
tally.computed_changed += 1;
} else {
tally.computed_suppressed += 1;
}
}
_ => {}
}
}
tally
}
}
#[track_caller]
fn assert_consistent(capture: &Rc<RefCell<Capture>>) {
let capture = capture.borrow();
let totals = capture.totals();
let tallied = capture.tallied();
assert_eq!(
totals.root_writes, tallied.root_writes,
"root writes disagree"
);
assert_eq!(
(totals.nodes_marked_check, totals.nodes_marked_dirty),
(tallied.nodes_marked_check, tallied.nodes_marked_dirty),
"marks disagree"
);
assert_eq!(
(
totals.effects_queued,
totals.effects_coalesced,
totals.effects_run,
totals.effects_skipped,
totals.effects_disposed
),
(
tallied.effects_queued,
tallied.effects_coalesced,
tallied.effects_run,
tallied.effects_skipped,
tallied.effects_disposed
),
"effect totals disagree"
);
assert_eq!(
(
totals.computed_verified,
totals.computed_recomputed,
totals.computed_changed,
totals.computed_suppressed
),
(
tallied.computed_verified,
tallied.computed_recomputed,
tallied.computed_changed,
tallied.computed_suppressed
),
"computed totals disagree"
);
}
#[test]
fn totals_agree_with_the_stream_through_suppression_and_coalescing() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 1_u32);
let other = signal_in(&reactor, 0_u32);
let parity = memo_in(&reactor, {
let source = source.clone();
move || source.get() % 2
});
let effect = reactor.effect({
let parity = parity.clone();
let other = other.clone();
move || {
let _ = (parity.get(), other.get());
}
});
reactor.flush_now();
source.set(3);
other.set(1);
reactor.flush_now();
source.set(5);
reactor.flush_now();
effect.dispose();
let keeper = reactor.effect(|| {});
reactor.flush_now();
assert_consistent(&capture);
keeper.dispose();
}
#[test]
fn totals_agree_with_the_stream_across_a_panicking_job() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 1_u32);
let boom = thunk_in(&reactor, {
let source = source.clone();
move || {
if source.get() > 1 {
panic!("compute failed");
}
source.get()
}
});
let effect = reactor.effect({
let boom = boom.clone();
move || {
let _ = boom.get();
}
});
reactor.flush_now();
source.set(2);
let result = catch_unwind(AssertUnwindSafe(|| reactor.flush_now()));
assert!(result.is_err(), "the panic propagates out of the flush");
assert_consistent(&capture);
assert!(
capture.borrow().flushes.len() >= 2,
"the unwinding flush still reported"
);
effect.dispose();
}
#[test]
fn a_nested_flush_keeps_its_work_out_of_the_enclosing_one() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let inner_source = signal_in(&reactor, 0_u32);
let seen = Rc::new(RefCell::new(Vec::new()));
let inner_effect = reactor.effect({
let inner_source = inner_source.clone();
let seen = Rc::clone(&seen);
move || seen.borrow_mut().push(inner_source.get())
});
reactor.flush_now();
capture.borrow_mut().flushes.clear();
capture.borrow_mut().events.clear();
reactor.schedule({
let reactor = reactor.clone();
let inner_source = inner_source.clone();
move || {
inner_source.set(1);
reactor.flush_now();
}
});
reactor.flush_now();
assert_eq!(*seen.borrow(), [0, 1]);
let capture_ref = capture.borrow();
assert_eq!(capture_ref.flushes.len(), 2, "an inner and an outer flush");
let inner = capture_ref.flushes[0];
let outer = capture_ref.flushes[1];
assert_eq!(inner.effects_run, 1, "the inner flush ran the effect");
assert_eq!(
outer.effects_run, 0,
"and the outer flush must not count it again"
);
drop(capture_ref);
assert_consistent(&capture);
inner_effect.dispose();
}
#[test]
fn the_writes_that_scheduled_a_flush_are_counted_in_it() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 0_u32);
let effect = reactor.effect({
let source = source.clone();
move || {
let _ = source.get();
}
});
reactor.flush_now();
capture.borrow_mut().flushes.clear();
source.set(1);
reactor.flush_now();
let flushes = capture.borrow().flushes.clone();
assert_eq!(flushes.len(), 1);
assert_eq!(flushes[0].root_writes, 1);
assert_eq!(flushes[0].effects_run, 1);
effect.dispose();
}
#[test]
fn a_settled_graph_reports_an_empty_flush() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 0_u32);
let effect = reactor.effect({
let source = source.clone();
move || {
let _ = source.get();
}
});
reactor.flush_now();
capture.borrow_mut().flushes.clear();
reactor.flush_now();
assert!(
capture.borrow().flushes.is_empty(),
"a settled graph must not flush at all, got {:?}",
capture.borrow().flushes
);
reactor.external_flush(|| {});
let flushes = capture.borrow().flushes.clone();
assert_eq!(flushes.len(), 1, "the declared boundary is still reported");
assert!(
flushes[0].is_empty(),
"and it did no work, got {:?}",
flushes[0]
);
effect.dispose();
}
#[test]
fn a_producer_that_runs_more_often_than_it_publishes_is_visible() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let sampled = signal_in(&reactor, 99_u32);
let effect = reactor.effect({
let sampled = sampled.clone();
move || {
let _ = sampled.get();
}
});
reactor.flush_now();
capture.borrow_mut().flushes.clear();
capture.borrow_mut().events.clear();
let write_line = line!() + 2;
for step in 0..20 {
sampled.set(step / 5);
reactor.flush_now();
}
let tally = |capture: &std::cell::RefMut<'_, Capture>| {
(
capture.flushes.iter().map(|s| s.root_writes).sum::<u32>(),
capture
.flushes
.iter()
.map(|s| s.writes_suppressed)
.sum::<u32>(),
)
};
let (published, discarded) = tally(&capture.borrow_mut());
assert_eq!(published, 4, "only four attempts moved the value");
assert_eq!(
discarded, 12,
"twelve of the sixteen discarded writes have been carried by a flush; the last four \
happened after the final flush and are still pending, because a suppressed write \
schedules nothing and a drain with an empty queue is not a flush"
);
sampled.set(1_000);
reactor.flush_now();
let (published, discarded) = tally(&capture.borrow_mut());
assert_eq!(published, 5);
assert_eq!(discarded, 16, "and now all sixteen are accounted for");
let capture = capture.borrow();
let sites = capture
.events
.iter()
.filter_map(|event| match event {
DiagnosticEvent::WriteSuppressed {
node, write_origin, ..
} if *node == sampled.id() => Some(write_origin.line()),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(sites.len(), 16);
assert!(
sites.iter().all(|line| *line == write_line),
"every discarded write must name the line that made it, not merely agree with itself: \
got {sites:?}, expected all {write_line}"
);
let propagated = capture
.events
.iter()
.filter(|event| matches!(event, DiagnosticEvent::ReactiveWrite { .. }))
.count();
assert_eq!(propagated, 5);
drop(capture);
effect.dispose();
}
#[test]
fn a_nested_external_flush_joins_rather_than_opening_a_second() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let effect = reactor.effect(|| {});
reactor.flush_now();
capture.borrow_mut().events.clear();
reactor.external_flush(|| {
reactor.external_flush(|| {
reactor.external_flush(|| {});
});
});
let boundaries = capture
.borrow()
.events
.iter()
.filter(|event| {
matches!(
event,
DiagnosticEvent::FlushStarted { .. } | DiagnosticEvent::FlushFinished { .. }
)
})
.count();
assert_eq!(
boundaries, 2,
"three nested declared boundaries are one flush, so one start and one finish"
);
effect.dispose();
}
#[test]
fn the_queue_depth_fields_report_the_queue() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
for _ in 0..3 {
reactor.schedule(|| {});
}
reactor.flush_now();
let flushes = capture.borrow().flushes.clone();
assert_eq!(flushes.len(), 1);
assert_eq!(
flushes[0].jobs_at_start, 3,
"the flush opened with three jobs waiting"
);
assert_eq!(flushes[0].jobs_at_finish, 0, "and drained all of them");
assert_eq!(flushes[0].effects_pending, 0);
let stats = reactor.graph_stats();
assert_eq!(stats.pending_jobs, 0);
assert!(
stats.peak_pending_jobs >= 3,
"the peak must survive the drain, got {}",
stats.peak_pending_jobs
);
}
#[test]
fn an_effect_left_unrun_in_a_lane_is_reported_as_pending() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let lane: Rc<RefCell<Vec<adaptite::EffectRun>>> = Rc::new(RefCell::new(Vec::new()));
let effect = reactor.effect_with(
{
let lane = Rc::clone(&lane);
move |ready| lane.borrow_mut().push(ready)
},
|| {},
);
assert_eq!(lane.borrow().len(), 1, "the initial run went to the lane");
reactor.external_flush(|| {});
capture.borrow_mut().flushes.clear();
reactor.external_flush(|| {});
let flushes = capture.borrow().flushes.clone();
let last = flushes.last().expect("the declared boundary is reported");
assert!(last.is_empty(), "this flush did no work");
assert_eq!(
last.effects_pending, 1,
"and yet an effect is still holding a run — which is why `is_empty` ignores this field"
);
drop(lane);
effect.dispose();
}
#[test]
fn a_re_entrant_flush_inside_external_flush_closes_the_right_epochs() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 0_u32);
let effect = reactor.effect({
let source = source.clone();
let reactor = reactor.clone();
move || {
if source.get() == 1 {
reactor.schedule(|| {});
reactor.flush_now();
}
}
});
reactor.flush_now();
capture.borrow_mut().events.clear();
reactor.external_flush(|| {
source.set(1);
reactor.flush_now();
});
let boundaries = capture
.borrow()
.events
.iter()
.filter_map(|event| match event {
DiagnosticEvent::FlushStarted { flush_epoch, .. } => Some(("start", *flush_epoch)),
DiagnosticEvent::FlushFinished { flush_epoch, .. } => Some(("finish", *flush_epoch)),
_ => None,
})
.collect::<Vec<_>>();
let mut open: Vec<u64> = Vec::new();
for (kind, epoch) in &boundaries {
match *kind {
"start" => open.push(*epoch),
_ => assert_eq!(
open.pop(),
Some(*epoch),
"flush boundaries do not nest: {boundaries:?}"
),
}
}
assert!(open.is_empty(), "a flush was never closed: {boundaries:?}");
assert!(
boundaries.len() >= 4,
"expected a nested flush, got {boundaries:?}"
);
effect.dispose();
}
#[test]
fn propagation_depth_counts_the_chain() {
let reactor = Reactor::new();
let (capture, _subscription) = Capture::install(&reactor);
let source = signal_in(&reactor, 1_u32);
let a = memo_in(&reactor, {
let source = source.clone();
move || source.get() + 1
});
let b = memo_in(&reactor, {
let a = a.clone();
move || a.get() + 1
});
let effect = reactor.effect({
let b = b.clone();
move || {
let _ = b.get();
}
});
reactor.flush_now();
capture.borrow_mut().flushes.clear();
source.set(2);
reactor.flush_now();
let depth = capture
.borrow()
.flushes
.iter()
.map(|stats| stats.max_propagation_depth)
.max()
.expect("a flush closed");
assert_eq!(depth, 3, "three links from the write to the effect");
effect.dispose();
}