#![cfg(feature = "builtin-kernels")]
use std::ffi::c_void;
use std::sync::atomic::{AtomicIsize, Ordering};
use lmflow::packet::{dtype, BufferData, Builtin};
use lmflow::{Graph, Packet, Timestamp};
static ALIVE: AtomicIsize = AtomicIsize::new(0);
static ACCOUNTING: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn accounting() -> (std::sync::MutexGuard<'static, ()>, isize) {
let g = ACCOUNTING.lock().unwrap_or_else(|e| e.into_inner());
let base = ALIVE.load(Ordering::SeqCst);
(g, base)
}
unsafe extern "C" fn tracked_drop(p: *mut c_void) {
ALIVE.fetch_sub(1, Ordering::SeqCst);
drop(Box::from_raw(p as *mut i32));
}
fn tracked_packet(v: i32, ts: i64) -> Packet {
ALIVE.fetch_add(1, Ordering::SeqCst);
let ptr = Box::into_raw(Box::new(v)) as *mut c_void;
unsafe { Packet::from_foreign(ptr, 0, Some(tracked_drop)) }.at(Timestamp(ts))
}
fn linear_graph(kernel: &str) -> Graph {
lmflow::register_builtin_kernels();
Graph::from_yaml(&format!(
r#"
nodes:
- {{ name: "a", kernel: "{kernel}", input_ports: ["in"], output_ports: ["mid"] }}
- {{ name: "b", kernel: "{kernel}", input_ports: ["mid"], output_ports: ["out"] }}
input_ports: ["in"]
output_ports: ["out"]
"#
))
.unwrap()
}
#[test]
fn foreign_payloads_are_released_exactly_once() {
let (_lock, base) = accounting();
{
let graph = linear_graph("PassThroughKernel");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..20i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
let p = poller.next().expect("should have output");
drop(p); }
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"no residue after taking and releasing"
);
graph.close_all_inputs();
graph.wait_done().unwrap();
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"all must be returned after the graph is destroyed"
);
}
#[test]
fn packets_left_in_queues_are_released_on_graph_drop() {
let (_lock, base) = accounting();
{
let graph = linear_graph("PassThroughKernel");
graph.start().unwrap(); let input = graph.input("in").unwrap();
for i in 0..10i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
}
assert!(
ALIVE.load(Ordering::SeqCst) > base,
"there should be in-flight packets now (otherwise this test checks nothing)"
);
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"destroying the graph without draining must still release in-flight packets"
);
}
#[test]
fn packets_are_released_when_kernel_fails() {
let (_lock, base) = accounting();
{
lmflow::register_builtin_kernels();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "s", kernel: "ScaleKernel", input_ports: ["in"], output_ports: ["out"], options: { factor: 2 } }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
graph.start().unwrap();
graph
.input("in")
.unwrap()
.send(tracked_packet(1, 0))
.unwrap();
graph.close_all_inputs();
assert!(graph.wait_done().is_err(), "type mismatch should error");
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"the failure path must not leak either (packets must be released when staging is discarded)"
);
}
#[test]
fn cancel_path_releases_everything() {
let (_lock, base) = accounting();
{
let graph = linear_graph("PassThroughKernel");
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..5i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
}
graph.cancel();
let _ = graph.wait_done();
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"all must be returned after cancellation as well"
);
}
#[test]
fn rejected_send_releases_the_packet() {
let (_lock, base) = accounting();
{
let graph = linear_graph("PassThroughKernel");
graph.start().unwrap();
let input = graph.input("in").unwrap();
input.send(tracked_packet(0, 5)).unwrap();
let after_accept = ALIVE.load(Ordering::SeqCst);
assert!(after_accept > base, "the first packet should be in flight");
let r = input.send(tracked_packet(1, 3));
assert!(r.is_err(), "a backward timestamp must be rejected");
assert_eq!(
ALIVE.load(Ordering::SeqCst),
after_accept,
"a rejected packet must be released immediately, not added to the in-flight count"
);
input.close();
let r = input.send(tracked_packet(2, 6));
assert!(r.is_err(), "sending to a closed port must be rejected");
let _ = graph.wait_done();
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"all packets (including rejected ones) must ultimately be returned"
);
}
fn pool_graph_mif(kernel: &str, mif: usize) -> Graph {
lmflow::register_builtin_kernels();
Graph::from_yaml(&format!(
r#"
executors:
- {{ name: "cpu", type: "ThreadPoolExecutor", num_threads: 4 }}
nodes:
- {{ name: "a", kernel: "{kernel}", executor: "cpu", max_in_flight: {mif},
input_ports: ["in"], output_ports: ["out"] }}
input_ports: ["in"]
output_ports: ["out"]
"#
))
.unwrap()
}
#[test]
fn cancel_with_pool_max_in_flight_releases_all() {
let (_lock, base) = accounting();
{
let graph = pool_graph_mif("PassThroughKernel", 4);
graph.start().unwrap(); let input = graph.input("in").unwrap();
for i in 0..64i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
}
graph.cancel();
let _ = graph.wait_done();
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"on cancel with parallel in-flight, every slot's input must be returned"
);
}
#[test]
fn drop_undrained_pool_max_in_flight_releases_all() {
let (_lock, base) = accounting();
{
let graph = pool_graph_mif("PassThroughKernel", 4);
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..64i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
}
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"on destroying the graph with parallel in-flight, every slot's input must be returned"
);
}
#[test]
fn steady_state_pool_max_in_flight_no_accumulation() {
let (_lock, base) = accounting();
let graph = pool_graph_mif("PassThroughKernel", 4);
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..300i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
let mut n = 0;
while poller.try_next().is_some() {
n += 1;
}
assert_eq!(n, 300, "still no loss and no duplication under parallelism");
assert_eq!(
graph.inner().shared.total_queued(),
0,
"no residual in-flight packets in steady state"
);
drop(graph);
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"no payload should remain after 300 parallel frames"
);
}
#[test]
fn side_packet_payload_released_exactly_once() {
let (_lock, base) = accounting();
{
let graph = linear_graph("PassThroughKernel");
graph
.set_side_packet("model", tracked_packet(42, 0))
.unwrap();
assert!(
ALIVE.load(Ordering::SeqCst) > base,
"the side packet should be in flight after injection"
);
graph.start().unwrap(); graph.close_all_inputs();
graph.wait_done().unwrap();
}
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"the side packet payload must be released exactly once (no leak, no double-free)"
);
}
fn buffer_packet(len: usize, ts: i64) -> (Packet, usize) {
let b = BufferData::new(&[2, (len / 2) as i64], dtype::U8).unwrap();
let addr = b.bytes.as_ptr() as usize;
(
Packet::from_builtin(Builtin::Buffer(b)).at(Timestamp(ts)),
addr,
)
}
fn buffer_addr(p: &Packet) -> usize {
match p.as_builtin() {
Some(Builtin::Buffer(b)) => b.bytes.as_ptr() as usize,
_ => panic!("not a buffer packet"),
}
}
#[test]
fn cow_is_zero_copy_on_linear_pipeline() {
lmflow::register_builtin_kernels();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "p1", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["m1"] }
- { name: "p2", kernel: "PassThroughKernel", input_ports: ["m1"], output_ports: ["m2"] }
- { name: "inv", kernel: "InvertKernel", input_ports: ["m2"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (pkt, addr_in) = buffer_packet(8, 0);
graph.input("in").unwrap().send(pkt).unwrap();
let out = poller.next().expect("should have output");
assert_eq!(
buffer_addr(&out),
addr_in,
"in-place mutation by InvertKernel on a linear pipeline should incur no copy -- \
if this assertion fails, it usually means the engine kept an extra reference after dispatch (see docs/design.md §3.4)"
);
match out.as_builtin() {
Some(Builtin::Buffer(b)) => assert_eq!(b.bytes[0], 0xFF, "should be inverted in place"),
_ => panic!("not a buffer packet"),
}
graph.close_all_inputs();
graph.wait_done().unwrap();
}
#[test]
fn cow_copies_on_fanout_without_polluting_siblings() {
lmflow::register_builtin_kernels();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "sp", kernel: "SplitKernel", input_ports: ["in"], output_ports: ["a", "b"] }
- { name: "inv", kernel: "InvertKernel", input_ports: ["a"], output_ports: ["oa"] }
- { name: "pas", kernel: "PassThroughKernel", input_ports: ["b"], output_ports: ["ob"] }
input_ports: ["in"]
output_ports: ["oa", "ob"]
"#,
)
.unwrap();
let pa = graph.add_poller("oa").unwrap();
let pb = graph.add_poller("ob").unwrap();
graph.start().unwrap();
let (pkt, addr_in) = buffer_packet(8, 0);
graph.input("in").unwrap().send(pkt).unwrap();
graph.close_all_inputs();
graph.wait_done().unwrap();
let inverted = pa.try_next().expect("branch a");
let untouched = pb.try_next().expect("branch b");
assert_ne!(
buffer_addr(&inverted),
addr_in,
"in-place mutation while shared must copy first"
);
assert_eq!(
buffer_addr(&untouched),
addr_in,
"the unmodified branch should still point to the original buffer (zero-copy)"
);
match (inverted.as_builtin(), untouched.as_builtin()) {
(Some(Builtin::Buffer(x)), Some(Builtin::Buffer(y))) => {
assert_eq!(x.bytes[0], 0xFF, "branch a should be inverted");
assert_eq!(y.bytes[0], 0x00, "branch b must not be polluted");
}
_ => panic!("not a buffer packet"),
}
}
#[test]
fn steady_state_has_no_accumulation() {
let (_lock, base) = accounting();
lmflow::register_builtin_kernels();
let graph = linear_graph("PassThroughKernel");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..200i32 {
input.send(tracked_packet(i, i as i64)).unwrap();
drop(poller.next().expect("should have output"));
}
assert_eq!(
graph.inner().shared.total_queued(),
0,
"no residual in-flight packets in steady state"
);
graph.close_all_inputs();
graph.wait_done().unwrap();
drop(graph);
assert_eq!(
ALIVE.load(Ordering::SeqCst),
base,
"no payload should remain after 200 rounds"
);
}