#![cfg(feature = "builtin-kernels")]
use lmflow::{Graph, Packet, State, Timestamp};
fn init() {
lmflow::register_builtin_kernels();
}
#[test]
fn feedback_self_loop_accumulates_and_terminates() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: acc
kernel: FeedbackAddKernel
input_ports: ["in", "out"]
output_ports: ["out"]
back_edges: ["out"]
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.expect("a cycle broken by a back_edge must build");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
assert_eq!(graph.state(), State::Running);
let input = graph.input("in").unwrap();
for i in 0..5i64 {
input.send(Packet::from_i64(1).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap(); assert_eq!(graph.state(), State::Terminated);
let got: Vec<i64> = std::iter::from_fn(|| poller.next().map(|p| p.as_i64().unwrap())).collect();
assert_eq!(
got,
vec![1, 2, 3, 4, 5],
"running sum via feedback register"
);
}
#[test]
fn unmarked_cycle_is_rejected() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: p, kernel: PassThroughKernel, input_ports: ["x"], output_ports: ["x"] }
"#,
)
.unwrap_err();
assert!(err.to_string().contains("cycle"), "{err}");
}
#[test]
fn node_with_no_forward_input_is_rejected() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- { name: p, kernel: PassThroughKernel, input_ports: ["x"], output_ports: ["x"], back_edges: ["x"] }
"#,
)
.unwrap_err();
assert!(
err.to_string().contains("forward input"),
"should reject an all-back-edge node: {err}"
);
}
#[test]
fn back_edge_must_name_an_input_port() {
init();
let err = Graph::from_yaml(
r#"
nodes:
- name: acc
kernel: FeedbackAddKernel
input_ports: ["in", "out"]
output_ports: ["out"]
back_edges: ["nope"]
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap_err();
assert!(
err.to_string()
.contains("not one of this node's input ports"),
"{err}"
);
}
#[test]
fn feedback_inside_subgraph_expands_and_runs() {
init();
let graph = Graph::from_yaml(
r#"
subgraphs:
Acc:
nodes:
- name: a
kernel: FeedbackAddKernel
input_ports: ["sin", "loop"]
output_ports: ["loop"]
back_edges: ["loop"]
input_ports: ["sin"]
output_ports: ["loop"]
nodes:
- { name: acc, type: Acc, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.expect("subgraph with an internal back_edge must expand and build");
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..4i64 {
input.send(Packet::from_i64(2).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(graph.state(), State::Terminated);
let got: Vec<i64> = std::iter::from_fn(|| poller.next().map(|p| p.as_i64().unwrap())).collect();
assert_eq!(
got,
vec![2, 4, 6, 8],
"feedback loop survives subgraph expansion"
);
}
#[test]
fn feedback_loop_on_thread_pool() {
init();
let graph = Graph::from_yaml(
r#"
executors:
- { name: cpu, type: ThreadPoolExecutor, num_threads: 2 }
nodes:
- name: acc
kernel: FeedbackAddKernel
executor: cpu
input_ports: ["in", "out"]
output_ports: ["out"]
back_edges: ["out"]
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..6i64 {
input.send(Packet::from_i64(1).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(graph.state(), State::Terminated);
let got: Vec<i64> = std::iter::from_fn(|| poller.next().map(|p| p.as_i64().unwrap())).collect();
assert_eq!(got, vec![1, 2, 3, 4, 5, 6], "running sum on a thread pool");
}