mod common;
use std::sync::{Arc, Mutex};
use common::{TestRuntime, TestValue};
use graphrefly_core::{DepBatch, EqualsMode, FnResult, HandleId};
type Captured = Arc<Mutex<Vec<Vec<CapturedDep>>>>;
#[derive(Clone, Debug)]
struct CapturedDep {
data: Vec<HandleId>,
prev_data: HandleId,
involved: bool,
}
fn snapshot(deps: &[DepBatch]) -> Vec<CapturedDep> {
deps.iter()
.map(|d| CapturedDep {
data: d.data.iter().copied().collect(),
prev_data: d.prev_data,
involved: d.involved,
})
.collect()
}
#[test]
fn single_emit_outside_batch_yields_data_len_1() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[s.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
s.set(TestValue::Int(42));
let snaps = captured.lock().unwrap().clone();
assert_eq!(snaps.len(), 1, "fn fired once for the single emit");
assert_eq!(snaps[0].len(), 1, "1 dep");
assert_eq!(snaps[0][0].data.len(), 1, "single-emit fast path");
assert!(snaps[0][0].involved);
}
#[test]
fn k_emit_in_batch_coalesces_into_one_fire_with_k_data_entries() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[s.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
let h1 = rt.binding.intern(TestValue::Int(1));
let h2 = rt.binding.intern(TestValue::Int(2));
let h3 = rt.binding.intern(TestValue::Int(3));
rt.core().batch(|| {
rt.core().emit(s.id, h1);
rt.core().emit(s.id, h2);
rt.core().emit(s.id, h3);
});
let snaps = captured.lock().unwrap().clone();
assert_eq!(snaps.len(), 1, "fn fires once per wave regardless of K");
let dep = &snaps[0][0];
assert_eq!(dep.data, vec![h1, h2, h3], "K-emit coalesces in emit order",);
assert!(dep.involved);
}
#[test]
fn prev_data_rotates_to_last_batch_entry_across_waves() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[s.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
let h_a = rt.binding.intern(TestValue::Int(10));
let h_b = rt.binding.intern(TestValue::Int(20));
rt.core().batch(|| {
rt.core().emit(s.id, h_a);
rt.core().emit(s.id, h_b);
});
let h_c = rt.binding.intern(TestValue::Int(30));
rt.core().emit(s.id, h_c);
let snaps = captured.lock().unwrap().clone();
assert_eq!(snaps.len(), 2);
assert_ne!(
snaps[0][0].prev_data,
graphrefly_core::NO_HANDLE,
"wave-1 prev_data reflects activation-fire rotation, not sentinel",
);
assert_eq!(snaps[1][0].prev_data, h_b);
assert_eq!(snaps[1][0].data, vec![h_c]);
}
#[test]
fn uninvolved_dep_shows_data_empty_and_involved_false() {
let rt = TestRuntime::new();
let s1 = rt.state(Some(TestValue::Int(1)));
let s2 = rt.state(Some(TestValue::Int(2)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[s1.id, s2.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
s1.set(TestValue::Int(99));
let snaps = captured.lock().unwrap().clone();
assert_eq!(
snaps.len(),
1,
"exactly one fire from s1.set; activation drained pre-set",
);
let s = &snaps[0];
assert!(s[0].involved);
assert_eq!(s[0].data.len(), 1);
assert!(!s[1].involved);
assert!(s[1].data.is_empty());
}
#[test]
fn resolved_in_wave_yields_involved_true_with_empty_data() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(7)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[s.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
s.set(TestValue::Int(7));
let snaps = captured.lock().unwrap().clone();
if snaps.is_empty() {
} else {
for s in &snaps {
assert!(
!s[0].data.is_empty() || s[0].involved,
"RESOLVED-in-wave must surface as involved=true with empty data, not silent uninvolved",
);
}
}
}
#[test]
fn k_emit_batch_refcounts_balance_after_wave_end() {
let rt = TestRuntime::new();
let s = rt.state(Some(TestValue::Int(0)));
let fn_id = rt
.binding
.register_raw_fn(|_deps| FnResult::Noop { tracked: None });
let d = rt
.core()
.register_derived(&[s.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
let h_a = rt.binding.intern(TestValue::Int(101));
let h_b = rt.binding.intern(TestValue::Int(102));
let h_c = rt.binding.intern(TestValue::Int(103));
let rc_a_before = rt.binding.refcount_of(h_a);
let rc_b_before = rt.binding.refcount_of(h_b);
let rc_c_before = rt.binding.refcount_of(h_c);
rt.core().batch(|| {
rt.core().emit(s.id, h_a);
rt.core().emit(s.id, h_b);
rt.core().emit(s.id, h_c);
});
let rc_a_after = rt.binding.refcount_of(h_a);
let rc_b_after = rt.binding.refcount_of(h_b);
let rc_c_after = rt.binding.refcount_of(h_c);
assert_eq!(
rc_a_after, 0,
"h_a was consumed by emit + replaced by h_b's commit + rotated out of data_batch — must be fully released (was {rc_a_before}, now {rc_a_after})",
);
assert_eq!(
rc_b_after, 0,
"h_b same discipline as h_a — fully released after rotation (was {rc_b_before}, now {rc_b_after})",
);
assert!(
rc_c_after >= 2,
"h_c (rotation winner) held by source cache + derived prev_data — rc must be >= 2 (was {rc_c_before}, now {rc_c_after})",
);
}
#[test]
fn multi_dep_each_dep_rotates_prev_data_independently() {
let rt = TestRuntime::new();
let a = rt.state(Some(TestValue::Int(1)));
let b = rt.state(Some(TestValue::Int(2)));
let captured: Captured = Arc::new(Mutex::new(Vec::new()));
let captured_c = captured.clone();
let fn_id = rt.binding.register_raw_fn(move |deps| {
captured_c.lock().unwrap().push(snapshot(deps));
FnResult::Noop { tracked: None }
});
let d = rt
.core()
.register_derived(&[a.id, b.id], fn_id, EqualsMode::Identity, false)
.unwrap();
let _rec = rt.subscribe_recorder(d);
captured.lock().unwrap().clear();
let h_a_w1 = rt.binding.intern(TestValue::Int(10));
a.set(TestValue::Int(10));
b.set(TestValue::Int(20));
let snaps = captured.lock().unwrap().clone();
assert_eq!(
snaps.len(),
2,
"exactly one fire per single-dep emit; activation drained pre-wave-1",
);
let last = &snaps[snaps.len() - 1];
assert!(!last[0].involved, "dep a uninvolved in wave 2");
assert!(last[1].involved, "dep b involved in wave 2");
assert_eq!(last[1].data.len(), 1);
assert_eq!(
last[0].prev_data, h_a_w1,
"dep a's prev_data is exactly the handle emitted in wave 1",
);
}