#![cfg(feature = "builtin-kernels")]
use std::time::Duration;
use lmflow::{Graph, Packet, Timestamp};
fn init() {
lmflow::register_builtin_kernels();
}
fn zip_graph(policy: &str) -> Graph {
init();
let extra = if policy.is_empty() {
String::new()
} else {
format!(" input_policy: {{ type: \"{policy}\" }}\n")
};
Graph::from_yaml(&format!(
r#"
nodes:
- name: "z"
kernel: "ZipKernel"
input_ports: ["A:x", "B:y"]
output_ports: ["out"]
{extra}input_ports: ["x", "y"]
output_ports: ["out"]
"#
))
.unwrap()
}
fn read_int(p: &Packet) -> i64 {
p.as_i64().expect("should be an integer packet")
}
#[test]
fn pairs_by_timestamp_not_by_arrival_order() {
let graph = zip_graph("");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let a = graph.input("x").unwrap();
let b = graph.input("y").unwrap();
for i in 0..3i32 {
a.send(Packet::from_i64((i * 10) as i64).at(Timestamp(i as i64)))
.unwrap();
}
graph.wait_until_idle().unwrap();
assert!(
poller.try_next().is_none(),
"B never arrived, nothing should be produced at any timestamp -- otherwise it paired by arrival order"
);
b.send(Packet::from_i64(1_i64).at(Timestamp(1))).unwrap();
graph.wait_until_idle().unwrap();
let mut got = Vec::new();
while let Some(p) = poller.try_next() {
got.push((p.timestamp().0, read_int(&p)));
}
assert_eq!(
got,
vec![(1, 11)],
"only timestamps where both streams have data should produce, actual {got:?}"
);
graph.close_all_inputs();
let _ = graph.wait_done();
}
#[test]
fn aligned_streams_pair_correctly() {
let graph = zip_graph("");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let a = graph.input("x").unwrap();
let b = graph.input("y").unwrap();
for i in 0..5i32 {
a.send(Packet::from_i64(i as i64).at(Timestamp(i as i64)))
.unwrap();
}
for i in 0..5i32 {
b.send(Packet::from_i64((i * 100) as i64).at(Timestamp(i as i64)))
.unwrap();
}
graph.close_all_inputs();
graph.wait_done_timeout(Duration::from_secs(10)).unwrap();
let mut got = Vec::new();
while let Some(p) = poller.try_next() {
got.push((p.timestamp().0, read_int(&p)));
}
assert_eq!(
got,
(0..5).map(|i| (i, i + i * 100)).collect::<Vec<_>>(),
"the two streams at the same timestamp must be paired together"
);
}
#[test]
fn closing_one_input_unblocks_alignment() {
let graph = zip_graph("");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let a = graph.input("x").unwrap();
for i in 0..3i32 {
a.send(Packet::from_i64(i as i64).at(Timestamp(i as i64)))
.unwrap();
}
graph.wait_until_idle().unwrap();
assert!(
poller.try_next().is_none(),
"B not closed and no data, should keep waiting"
);
graph.close_input("y").unwrap();
graph.wait_until_idle().unwrap();
let st = graph.node_stats(0).unwrap();
assert_eq!(
st.processed, 3,
"after B closes, all three of A's timestamps should be immediately processable (actual {} times)",
st.processed
);
graph.close_all_inputs();
let _ = graph.wait_done();
}
#[test]
fn immediate_policy_skips_alignment() {
let graph = zip_graph("immediate");
graph.add_poller("out").unwrap();
graph.start().unwrap();
graph
.input("x")
.unwrap()
.send(Packet::from_i64(1_i64).at(Timestamp(0)))
.unwrap();
graph.wait_until_idle().unwrap();
assert_eq!(
graph.node_stats(0).unwrap().processed,
1,
"under immediate a single input triggers (sync would wait)"
);
graph.close_all_inputs();
let _ = graph.wait_done();
}
#[test]
fn dropping_kernel_does_not_stall_downstream() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "f"
kernel: "FilterKernel"
input_ports: ["in"]
output_ports: ["mid"]
options: { threshold: 5 }
- name: "z"
kernel: "ZipKernel"
input_ports: ["A:mid", "B:other"]
output_ports: ["out"]
input_ports: ["in", "other"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let inp = graph.input("in").unwrap();
let other = graph.input("other").unwrap();
for i in 0..10i32 {
inp.send(Packet::from_i64(i as i64).at(Timestamp(i as i64)))
.unwrap();
other
.send(Packet::from_i64(1000_i64).at(Timestamp(i as i64)))
.unwrap();
}
graph.close_all_inputs();
graph
.wait_done_timeout(Duration::from_secs(10))
.expect("dropped timestamps must auto-advance the bound, otherwise this would time out");
let mut got = Vec::new();
while let Some(p) = poller.try_next() {
got.push(p.timestamp().0);
}
assert_eq!(
got,
(5..10).collect::<Vec<i64>>(),
"only non-filtered timestamps should produce, and must not stall"
);
}
#[test]
fn alignment_holds_on_thread_pool() {
init();
let graph = Graph::from_yaml(
r#"
executors:
- { name: "cpu", type: "ThreadPoolExecutor", num_threads: 4 }
nodes:
- name: "z"
kernel: "ZipKernel"
executor: "cpu"
input_ports: ["A:x", "B:y"]
output_ports: ["out"]
input_ports: ["x", "y"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let a = graph.input("x").unwrap();
let b = graph.input("y").unwrap();
const N: i32 = 100;
for i in 0..N {
a.send(Packet::from_i64(i as i64).at(Timestamp(i as i64)))
.unwrap();
b.send(Packet::from_i64((i * 1000) as i64).at(Timestamp(i as i64)))
.unwrap();
}
graph.close_all_inputs();
graph.wait_done_timeout(Duration::from_secs(30)).unwrap();
let mut got = Vec::new();
while let Some(p) = poller.try_next() {
got.push((p.timestamp().0, read_int(&p)));
}
assert_eq!(got.len(), N as usize, "must not drop packets");
for (ts, v) in &got {
let i = *ts;
assert_eq!(
*v,
i + i * 1000,
"pairing at ts={ts} must be correct (no mispairing even under concurrency)"
);
}
}