mod common;
use std::sync::Arc;
use common::{InnerBinding, OpRuntime};
use graphrefly_operators::{
concat, exhaust_map, higher_order::merge_map_with_concurrency, race, switch_map, take_until,
zip,
};
fn assert_binding_drops_after<F>(register: F)
where
F: FnOnce(&OpRuntime),
{
let weak = {
let rt = OpRuntime::new();
let weak: std::sync::Weak<InnerBinding> = Arc::downgrade(&rt.binding);
assert!(
weak.upgrade().is_some(),
"binding should be alive while runtime is held"
);
register(&rt);
weak
};
assert!(
weak.upgrade().is_none(),
"binding strong count > 0 after runtime drop — Arc cycle leak (producer-build closure path)"
);
}
#[test]
fn zip_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s1 = rt.state_int(None);
let s2 = rt.state_int(None);
let pack_fn = rt.register_tuple_packer();
let _ = zip(rt.core(), &rt.producer_binding, vec![s1, s2], pack_fn).unwrap();
});
}
#[test]
fn concat_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s1 = rt.state_int(None);
let s2 = rt.state_int(None);
let _ = concat(rt.core(), &rt.producer_binding, s1, s2);
});
}
#[test]
fn race_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s1 = rt.state_int(None);
let s2 = rt.state_int(None);
let _ = race(rt.core(), &rt.producer_binding, vec![s1, s2]).unwrap();
});
}
#[test]
fn take_until_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s = rt.state_int(None);
let n = rt.state_int(None);
let _ = take_until(rt.core(), &rt.producer_binding, s, n);
});
}
#[test]
fn switch_map_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s = rt.state_int(None);
let project: graphrefly_operators::higher_order::ProjectFn = Box::new(|_h| {
graphrefly_core::NodeId::new(0)
});
let _ = switch_map(rt.core(), &rt.ho_binding, s, project);
});
}
#[test]
fn exhaust_map_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s = rt.state_int(None);
let project: graphrefly_operators::higher_order::ProjectFn =
Box::new(|_h| graphrefly_core::NodeId::new(0));
let _ = exhaust_map(rt.core(), &rt.ho_binding, s, project);
});
}
#[test]
fn merge_map_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let s = rt.state_int(None);
let project: graphrefly_operators::higher_order::ProjectFn =
Box::new(|_h| graphrefly_core::NodeId::new(0));
let _ = merge_map_with_concurrency(rt.core(), &rt.ho_binding, s, project, None);
});
}
#[test]
fn many_producers_no_leak_after_runtime_drop() {
assert_binding_drops_after(|rt| {
let a = rt.state_int(None);
let b = rt.state_int(None);
let pack_fn = rt.register_tuple_packer();
let _ = zip(rt.core(), &rt.producer_binding, vec![a, b], pack_fn).unwrap();
let _ = concat(rt.core(), &rt.producer_binding, a, b);
let _ = race(rt.core(), &rt.producer_binding, vec![a, b]).unwrap();
let _ = take_until(rt.core(), &rt.producer_binding, a, b);
let _ = switch_map(
rt.core(),
&rt.ho_binding,
a,
Box::new(|_| graphrefly_core::NodeId::new(0)),
);
let _ = exhaust_map(
rt.core(),
&rt.ho_binding,
a,
Box::new(|_| graphrefly_core::NodeId::new(0)),
);
let _ = merge_map_with_concurrency(
rt.core(),
&rt.ho_binding,
a,
Box::new(|_| graphrefly_core::NodeId::new(0)),
None,
);
});
}