#![cfg(feature = "builtin-kernels")]
use std::sync::Mutex;
use lmflow::{Graph, Packet, State, Timestamp};
static LOG_LOCK: Mutex<()> = Mutex::new(());
fn log_guard() -> std::sync::MutexGuard<'static, ()> {
LOG_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn init() {
lmflow::register_builtin_kernels();
}
#[test]
fn passthrough_pipeline() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "n1"
kernel: "PassThroughKernel"
input_ports: ["in"]
output_ports: ["mid"]
- name: "n2"
kernel: "PassThroughKernel"
input_ports: ["mid"]
output_ports: ["out"]
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
assert_eq!(graph.state(), State::Running);
let input = graph.input("in").unwrap();
let mut got = Vec::new();
for i in 0..10i32 {
input.send(Packet::new(i).at(Timestamp(i as i64))).unwrap();
let p = poller.next().expect("should have output");
got.push((*p.get::<i32>().unwrap(), p.timestamp().0));
}
assert_eq!(
got,
(0..10).map(|i| (i, i as i64)).collect::<Vec<_>>(),
"value and timestamp should pass through both passthrough stages unchanged"
);
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(graph.state(), State::Terminated);
assert!(
poller.next().is_none(),
"poller should return None after the graph finishes"
);
}
#[test]
fn cpp_kernel_reads_options_and_types() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "s"
kernel: "ScaleKernel"
input_ports: ["in"]
output_ports: ["out"]
options: { factor: 7 }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
graph
.input("in")
.unwrap()
.send(Packet::from_i64(6i32 as i64).at(Timestamp(0)))
.unwrap();
graph.close_all_inputs();
graph.wait_done().unwrap();
let out = poller.try_next().expect("should have output");
assert_eq!(
out.type_id(),
lmflow::packet::type_id::I64,
"output should be the builtin integer type"
);
assert_eq!(out.as_i64(), Some(42), "6 * factor(7) = 42");
}
#[test]
fn typed_contract_rejects_mismatch() {
init();
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(Packet::new(1i32).at(Timestamp(0)))
.unwrap();
graph.close_all_inputs();
let err = graph.wait_done().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("type mismatch"), "{msg}");
assert!(
msg.contains("input port"),
"the error should indicate which port: {msg}"
);
}
#[test]
fn stateful_sum_emits_on_close() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "pass"
kernel: "PassThroughKernel"
input_ports: ["in"]
output_ports: ["mid"]
- name: "sink"
kernel: "SinkKernel"
input_ports: ["mid"]
output_ports: []
input_ports: ["in"]
output_ports: ["mid"]
"#,
)
.unwrap();
let poller = graph.add_poller("mid").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..3i32 {
input.send(Packet::new(i).at(Timestamp(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, 3, "all three packets should reach the graph output");
}
#[test]
fn fanout_delivers_to_every_consumer() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "sp"
kernel: "SplitKernel"
input_ports: ["in"]
output_ports: ["a", "b"]
- name: "pa"
kernel: "PassThroughKernel"
input_ports: ["a"]
output_ports: ["oa"]
- name: "pb"
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 input = graph.input("in").unwrap();
input.send(Packet::new(42i32).at(Timestamp(0))).unwrap();
graph.close_all_inputs();
graph.wait_done().unwrap();
let a = pa.try_next().expect("branch a should receive");
let b = pb.try_next().expect("branch b should receive");
assert_eq!(a.get::<i32>(), Some(&42));
assert_eq!(b.get::<i32>(), Some(&42));
}
#[test]
fn multiple_pollers_on_same_port() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "p", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let p1 = graph.add_poller("out").unwrap();
let p2 = graph.add_poller("out").unwrap();
graph.start().unwrap();
graph
.input("in")
.unwrap()
.send(Packet::new(1i32).at(Timestamp(0)))
.unwrap();
graph.close_all_inputs();
graph.wait_done().unwrap();
assert!(p1.try_next().is_some(), "poller 1 should receive");
assert!(
p2.try_next().is_some(),
"poller 2 should also independently receive a copy"
);
}
#[test]
fn rejects_cycle() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "PassThroughKernel", input_ports: ["y"], output_ports: ["x"] }
- { name: "b", kernel: "PassThroughKernel", input_ports: ["x"], output_ports: ["y"] }
"#,
)
.unwrap_err();
assert!(err.to_string().contains("cycle"), "{err}");
}
#[test]
fn empty_graph_terminates_cleanly() {
init();
let graph = Graph::from_yaml("nodes: []").unwrap();
graph.start().unwrap();
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(
graph.state(),
State::Terminated,
"an empty graph should also terminate cleanly"
);
}
#[test]
fn dangling_output_is_allowed() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "n", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["dangling"] }
input_ports: ["in"]
"#,
)
.unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..5i32 {
input.send(Packet::new(i).at(Timestamp(i as i64))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(
graph.node_stats(0).unwrap().processed,
5,
"the kernel should still process every packet"
);
}
#[test]
fn rejects_graph_output_with_no_producer() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "n", kernel: "SinkKernel", input_ports: ["in"], output_ports: [] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("no node produces"), "{err}");
}
#[test]
fn mux_kernel_forwards_selected_data_port() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "m"
kernel: "MuxKernel"
input_ports: ["sel", "a", "b"]
output_ports: ["out"]
input_ports: ["sel", "a", "b"]
output_ports: ["out"]
"#,
)
.unwrap();
let out = graph.add_poller("out").unwrap();
graph.start().unwrap();
let sel = graph.input("sel").unwrap();
let a = graph.input("a").unwrap();
let b = graph.input("b").unwrap();
for (ts, k, av, bv) in [(0i64, 0i64, 100i64, 200i64), (1, 1, 101, 201)] {
sel.send(Packet::from_i64(k).at(Timestamp(ts))).unwrap();
a.send(Packet::from_i64(av).at(Timestamp(ts))).unwrap();
b.send(Packet::from_i64(bv).at(Timestamp(ts))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
let mut got = Vec::new();
while let Some(p) = out.try_next() {
got.push(p.as_i64().unwrap());
}
assert_eq!(
got,
vec![100, 201],
"ts0 selects data port 0 (a=100), ts1 selects data port 1 (b=201)"
);
}
#[test]
fn mux_kernel_rejects_out_of_range_selector() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "m", kernel: "MuxKernel", input_ports: ["sel", "a"], output_ports: ["out"] }
input_ports: ["sel", "a"]
output_ports: ["out"]
"#,
)
.unwrap();
graph.add_poller("out").unwrap();
graph.start().unwrap();
graph
.input("sel")
.unwrap()
.send(Packet::from_i64(5).at(Timestamp(0)))
.unwrap();
graph
.input("a")
.unwrap()
.send(Packet::from_i64(1).at(Timestamp(0)))
.unwrap();
graph.close_all_inputs();
let err = graph.wait_done().unwrap_err();
assert!(err.to_string().contains("out of range"), "{err}");
}
#[test]
fn rejects_duplicate_producer() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["dup"] }
- { name: "b", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["dup"] }
input_ports: ["in"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("multiple producers"), "{err}");
}
#[test]
fn rejects_unconnected_input() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "PassThroughKernel", input_ports: ["nowhere"], output_ports: ["out"] }
"#,
)
.unwrap_err();
assert!(err.to_string().contains("no producer"), "{err}");
}
#[test]
fn rejects_name_clash_between_graph_input_and_node_output() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["in"] }
input_ports: ["in"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("name conflict"), "{err}");
}
#[test]
fn source_node_requires_executor() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "src", kernel: "RangeSourceKernel", input_ports: [], output_ports: ["out"] }
output_ports: ["out"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("requires an executor"), "{err}");
}
#[test]
fn source_node_produces_and_terminates() {
init();
let graph = Graph::from_yaml(
r#"
executors:
- { name: "cpu", type: "ThreadPoolExecutor", num_threads: 2 }
nodes:
- { name: "src", kernel: "RangeSourceKernel", input_ports: [], output_ports: ["out"], executor: "cpu", options: { count: 5 } }
output_ports: ["out"]
"#,
)
.unwrap();
let out = graph.add_poller("out").unwrap();
graph.start().unwrap();
graph
.wait_done_timeout(std::time::Duration::from_secs(30))
.unwrap();
assert_eq!(graph.state(), State::Terminated);
let mut got = Vec::new();
while let Some(p) = out.try_next() {
got.push(p.as_i64().expect("i64 packet"));
}
assert_eq!(
got,
vec![0, 1, 2, 3, 4],
"source should emit 0..count in order"
);
}
#[test]
fn rejects_unknown_executor() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["out"], executor: "ghost" }
input_ports: ["in"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("undefined executor"), "{err}");
}
#[test]
fn rejects_unregistered_kernel_and_lists_available() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: "a", kernel: "NoSuchKernel", input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
"#,
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("not registered"), "{msg}");
assert!(
msg.contains("PassThroughKernel"),
"the error should list available kernels to guide the user: {msg}"
);
}
#[test]
fn send_before_start_is_state_error() {
init();
let graph = simple_graph();
let err = graph
.input("in")
.unwrap()
.send(Packet::new(1i32).at(Timestamp(0)))
.unwrap_err();
assert_eq!(err.code(), lmflow::status::code::STATE, "{err}");
}
#[test]
fn double_start_is_state_error() {
init();
let graph = simple_graph();
graph.start().unwrap();
let err = graph.start().unwrap_err();
assert_eq!(err.code(), lmflow::status::code::STATE, "{err}");
}
#[test]
fn add_poller_after_start_is_rejected() {
init();
let graph = simple_graph();
graph.start().unwrap();
assert!(graph.add_poller("out").is_err());
}
#[test]
fn send_after_close_is_closed_error() {
init();
let graph = simple_graph();
graph.start().unwrap();
let input = graph.input("in").unwrap();
graph.close_all_inputs();
let err = input.send(Packet::new(1i32).at(Timestamp(0))).unwrap_err();
assert_eq!(err.code(), lmflow::status::code::CLOSED, "{err}");
}
#[test]
fn unset_timestamp_on_graph_input_is_rejected() {
init();
let graph = simple_graph();
graph.start().unwrap();
let err = graph
.input("in")
.unwrap()
.send(Packet::new(1i32)) .unwrap_err();
assert!(err.to_string().contains("timestamp"), "{err}");
}
#[test]
fn timestamps_must_be_strictly_increasing() {
init();
let graph = simple_graph();
graph.start().unwrap();
let input = graph.input("in").unwrap();
input.send(Packet::new(1i32).at(Timestamp(5))).unwrap();
let err = input.send(Packet::new(2i32).at(Timestamp(5))).unwrap_err();
assert!(err.to_string().contains("increasing"), "{err}");
let err = input.send(Packet::new(3i32).at(Timestamp(4))).unwrap_err();
assert!(err.to_string().contains("increasing"), "{err}");
}
#[test]
fn unknown_port_names_are_not_found() {
init();
let graph = simple_graph();
assert_eq!(
graph.input("ghost").unwrap_err().code(),
lmflow::status::code::NOT_FOUND
);
assert_eq!(
graph.add_poller("ghost").unwrap_err().code(),
lmflow::status::code::NOT_FOUND
);
}
#[test]
fn cancel_makes_wait_done_report_cancelled() {
init();
let graph = simple_graph();
graph.start().unwrap();
graph.cancel();
let err = graph.wait_done().unwrap_err();
assert_eq!(err.code(), lmflow::status::code::CANCELLED, "{err}");
}
#[test]
fn introspection_reports_topology() {
init();
let graph = simple_graph();
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.node_name(0), Some("p"));
assert_eq!(graph.input_port_names(), vec!["in"]);
assert_eq!(graph.output_port_names(), vec!["out"]);
assert_eq!(graph.queue_depth("in"), Some(0));
assert!(
graph.dump().contains("node"),
"dump should contain the node table"
);
}
#[test]
fn node_stats_track_processing() {
init();
let graph = simple_graph();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..3i32 {
input.send(Packet::new(i).at(Timestamp(i as i64))).unwrap();
poller.next().unwrap();
}
let st = graph.node_stats(0).unwrap();
assert_eq!(st.node_name, "p");
assert_eq!(st.kernel_name, "PassThroughKernel");
assert_eq!(st.processed, 3);
assert_eq!(st.errors, 0);
assert!(
!st.running,
"callback has finished, should not show running"
);
}
#[test]
fn global_watermark_blocks_input_when_exceeded() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "p", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
max_queued_packets: 2
"#,
)
.unwrap();
graph.start().unwrap(); let input = graph.input("in").unwrap();
let mut sent = 0;
for i in 0..10i32 {
if input
.try_send(Packet::new(i).at(Timestamp(i as i64)))
.is_err()
{
break;
}
sent += 1;
}
assert!(
sent < 10,
"the global watermark must stop unbounded growth, actually sent {sent}"
);
}
mod slow_sink_kernel {
use lmflow::ffi::LMFlowContext;
use std::ffi::c_void;
use std::time::Duration;
unsafe extern "C" fn process(_s: *mut c_void, _ctx: *mut LMFlowContext) -> i32 {
std::thread::sleep(Duration::from_millis(3));
0
}
pub fn register() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let vt = lmflow::ffi::LMFlowKernelVTable {
create: None,
get_contract: None,
open: None,
process: Some(process),
close: None,
destroy: None,
};
let vt: &'static _ = Box::leak(Box::new(vt));
let name = std::ffi::CString::new("SlowSink").unwrap();
let rc = unsafe {
lmflow::ffi::lmflow_register_kernel(name.as_ptr(), vt, std::ptr::null_mut())
};
assert_eq!(rc, 0, "failed to register SlowSink");
});
}
}
#[test]
fn blocking_send_applies_backpressure_on_pool_instead_of_erroring() {
init();
slow_sink_kernel::register();
let graph = Graph::from_yaml(
r#"
executors:
- { name: "cpu", type: "ThreadPoolExecutor", num_threads: 2 }
nodes:
- { name: "s", kernel: "SlowSink", executor: "cpu", input_ports: ["in"], output_ports: [] }
input_ports: ["in"]
max_queued_packets: 4
"#,
)
.unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..30i32 {
input
.send(Packet::new(i).at(Timestamp(i as i64)))
.unwrap_or_else(|e| panic!("blocking send #{i} should not fail (should become backpressure waiting), but got {e}"));
}
graph.close_all_inputs();
graph
.wait_done_timeout(std::time::Duration::from_secs(30))
.unwrap();
assert_eq!(
graph.node_stats(0).unwrap().processed,
30,
"under backpressure all 30 must be processed, no loss no error"
);
}
#[test]
fn watchdog_warns_on_slow_kernel() {
use std::ffi::{c_char, c_void, CStr};
let _lg = log_guard(); init();
slow_sink_kernel::register();
static MSGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
MSGS.lock().unwrap_or_else(|e| e.into_inner()).clear();
unsafe extern "C" fn sink(_u: *mut c_void, _lv: i32, msg: *const c_char) {
if !msg.is_null() {
let s = unsafe { CStr::from_ptr(msg) }
.to_string_lossy()
.into_owned();
MSGS.lock().unwrap_or_else(|e| e.into_inner()).push(s);
}
}
lmflow::ffi::lmflow_set_log_callback(Some(sink), std::ptr::null_mut());
let graph = Graph::from_yaml(
r#"
executors:
- { name: "cpu", type: "ThreadPoolExecutor", num_threads: 1 }
nodes:
- { name: "s", kernel: "SlowSink", executor: "cpu", input_ports: ["in"], output_ports: [] }
input_ports: ["in"]
watchdog_ms: 1
"#,
)
.unwrap();
graph.start().unwrap();
graph
.input("in")
.unwrap()
.send(Packet::new(0i32).at(Timestamp(0)))
.unwrap();
graph.close_all_inputs();
graph
.wait_done_timeout(std::time::Duration::from_secs(10))
.unwrap();
lmflow::ffi::lmflow_set_log_callback(None, std::ptr::null_mut());
let msgs = MSGS.lock().unwrap_or_else(|e| e.into_inner());
assert!(
msgs.iter().any(|m| m.contains("watchdog")),
"a 3ms kernel under watchdog_ms=1 must emit a WARN, actual logs: {:?}",
*msgs
);
}
fn simple_graph() -> Graph {
Graph::from_yaml(
r#"
nodes:
- { name: "p", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap()
}
#[test]
fn multiple_observers_and_poller_all_receive() {
use std::sync::{Arc, Mutex};
init();
let graph = simple_graph();
let seen_a = Arc::new(Mutex::new(Vec::new()));
let seen_b = Arc::new(Mutex::new(Vec::new()));
let a = seen_a.clone();
let b = seen_b.clone();
graph
.observe("out", move |p| {
a.lock().unwrap().push(*p.get::<i32>().unwrap())
})
.unwrap();
graph
.observe("out", move |p| {
b.lock().unwrap().push(*p.get::<i32>().unwrap())
})
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..5i32 {
input.send(Packet::new(i).at(Timestamp(i as i64))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
let want: Vec<i32> = (0..5).collect();
assert_eq!(
*seen_a.lock().unwrap(),
want,
"observer A should receive all"
);
assert_eq!(
*seen_b.lock().unwrap(),
want,
"observer B should independently receive all"
);
let mut got = Vec::new();
while let Some(p) = poller.try_next() {
got.push(*p.get::<i32>().unwrap());
}
assert_eq!(got, want, "poller should also independently receive all");
}
#[test]
fn dropping_graph_still_closes_opened_kernels() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: "s", kernel: "SinkKernel", input_ports: ["in"], output_ports: [] }
input_ports: ["in"]
"#,
)
.unwrap();
graph.start().unwrap();
graph
.input("in")
.unwrap()
.send(Packet::new(1i32).at(Timestamp(0)))
.unwrap();
assert_eq!(
graph.counter_value("sink.closed"),
0,
"not closed yet at this point"
);
let shared = graph.shared_for_inspection();
drop(graph);
assert_eq!(
shared.counter_value("sink.closed"),
1,
"graph destruction must still call the kernel's Close"
);
}
#[test]
fn missing_required_side_packet_is_rejected_at_start() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "norm"
kernel: "NormalizeKernel"
input_ports: ["in"]
output_ports: ["out"]
options: { scale: 0.5 }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let err = graph.start().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("calibration"),
"should indicate which one is missing: {msg}"
);
assert!(
msg.contains("norm"),
"should indicate which node requires it: {msg}"
);
}
#[test]
fn side_packet_is_visible_to_kernel() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "norm"
kernel: "NormalizeKernel"
input_ports: ["in"]
output_ports: ["out"]
options: { scale: 0.5, mean: [1.0, 2.0] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
graph
.set_side_packet("calibration", Packet::new(vec![1.0f64, 2.0]))
.unwrap();
graph.start().unwrap();
assert!(graph.set_side_packet("late", Packet::new(0u8)).is_err());
graph.close_all_inputs();
graph.wait_done().unwrap();
}
#[test]
fn missing_required_option_fails_with_reason() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: "norm"
kernel: "NormalizeKernel"
input_ports: ["in"]
output_ports: ["out"]
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
graph
.set_side_packet("calibration", Packet::new(0u8))
.unwrap();
let err = graph.start().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("scale"),
"should indicate which parameter: {msg}"
);
assert!(msg.contains("norm"), "should carry the node name: {msg}");
}
#[test]
fn timestamp_monotonicity_survives_queue_drain() {
init();
let graph = simple_graph();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
input.send(Packet::new(1i32).at(Timestamp(100))).unwrap();
assert!(poller.next().is_some());
assert_eq!(
graph.queue_depth("in"),
Some(0),
"the queue should be drained"
);
let err = input.send(Packet::new(2i32).at(Timestamp(50))).unwrap_err();
assert!(
err.to_string().contains("increasing"),
"must still reject going backward after drain: {err}"
);
let err = input
.send(Packet::new(3i32).at(Timestamp(100)))
.unwrap_err();
assert!(
err.to_string().contains("increasing"),
"must still reject duplicates after drain: {err}"
);
input.send(Packet::new(4i32).at(Timestamp(101))).unwrap();
}
#[test]
fn warns_about_unconsumed_ports() {
use std::ffi::{c_char, c_void, CStr};
use std::sync::Mutex;
static WARNINGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
unsafe extern "C" fn sink(_u: *mut c_void, level: i32, msg: *const c_char) {
if level == 1 {
let s = unsafe { CStr::from_ptr(msg) }
.to_string_lossy()
.into_owned();
WARNINGS.lock().expect("lock poisoned").push(s);
}
}
init();
let _log = log_guard();
WARNINGS.lock().expect("lock poisoned").clear();
lmflow::ffi::lmflow_set_log_callback(Some(sink), std::ptr::null_mut());
let _graph = Graph::from_yaml(
r#"
nodes:
- { name: "p", kernel: "PassThroughKernel", input_ports: ["in"], output_ports: ["nowhere"] }
input_ports: ["in", "unused"]
"#,
)
.unwrap();
lmflow::ffi::lmflow_set_log_callback(None, std::ptr::null_mut());
let w = WARNINGS.lock().expect("lock poisoned").join("\n");
assert!(
w.contains("unused"),
"an unconsumed graph input port should warn: {w}"
);
assert!(
w.contains("nowhere"),
"an output port with no receiver should warn: {w}"
);
}