mod common;
use lmflow::{Graph, Packet, State, Timestamp};
fn init() {
common::register_test_kernels();
}
fn drive_passthrough(graph: &Graph) {
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+timestamp should pass through the expanded subgraph unchanged"
);
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(graph.state(), State::Terminated);
}
#[test]
fn subgraph_expands_to_equivalent_pipeline() {
init();
let graph = Graph::from_yaml(
r#"
subgraphs:
PassPair:
nodes:
- { name: a, kernel: PassThrough, input_ports: ["sin"], output_ports: ["mid"] }
- { name: b, kernel: PassThrough, input_ports: ["mid"], output_ports: ["sout"] }
input_ports: ["sin"]
output_ports: ["sout"]
nodes:
- { name: p, type: PassPair, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
drive_passthrough(&graph);
}
#[test]
fn nested_subgraph_expands() {
init();
let graph = Graph::from_yaml(
r#"
subgraphs:
Inner:
nodes:
- { name: k, kernel: PassThrough, input_ports: ["i"], output_ports: ["o"] }
input_ports: ["i"]
output_ports: ["o"]
Outer:
nodes:
- { name: x, type: Inner, input_ports: ["oi"], output_ports: ["oo"] }
input_ports: ["oi"]
output_ports: ["oo"]
nodes:
- { name: p, type: Outer, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
drive_passthrough(&graph);
}
#[test]
fn subgraph_instantiated_twice_is_namespaced() {
init();
let graph = Graph::from_yaml(
r#"
subgraphs:
PassPair:
nodes:
- { name: a, kernel: PassThrough, input_ports: ["sin"], output_ports: ["m"] }
- { name: b, kernel: PassThrough, input_ports: ["m"], output_ports: ["sout"] }
input_ports: ["sin"]
output_ports: ["sout"]
nodes:
- { name: p, type: PassPair, input_ports: ["in"], output_ports: ["mid"] }
- { name: q, type: PassPair, input_ports: ["mid"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.expect("two instances of the same subgraph must namespace their internal edge `m`");
drive_passthrough(&graph);
}
#[test]
fn include_merges_subgraph_library_from_file() {
init();
let dir = std::env::temp_dir().join("lmflow_include_e2e_ok");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("lib.yml"),
r#"
subgraphs:
PassPair:
nodes:
- { name: a, kernel: PassThrough, input_ports: ["sin"], output_ports: ["mid"] }
- { name: b, kernel: PassThrough, input_ports: ["mid"], output_ports: ["sout"] }
input_ports: ["sin"]
output_ports: ["sout"]
"#,
)
.unwrap();
std::fs::write(
dir.join("main.yml"),
r#"
include: ["lib.yml"]
nodes:
- { name: p, type: PassPair, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let graph = Graph::from_yaml_file(dir.join("main.yml").to_str().unwrap()).unwrap();
drive_passthrough(&graph);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn missing_include_file_is_rejected() {
init();
let dir = std::env::temp_dir().join("lmflow_include_e2e_missing");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("main.yml"),
r#"
include: ["nope.yml"]
nodes:
- { name: p, kernel: PassThrough, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let err = Graph::from_yaml_file(dir.join("main.yml").to_str().unwrap()).unwrap_err();
assert!(
format!("{err:?}").contains("nope.yml"),
"missing include must be reported: {err:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn include_in_text_is_rejected() {
let err = Graph::from_yaml(
r#"
include: ["lib.yml"]
nodes:
- { name: p, kernel: PassThrough, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap_err();
assert!(
format!("{err:?}").contains("from_yaml_file"),
"text-mode include must point users at from_yaml_file: {err:?}"
);
}
#[test]
fn to_dot_renders_subgraph_clusters_and_executor_affinity() {
init();
let graph = Graph::from_yaml(
r#"
executors:
- { name: cpu, type: ThreadPoolExecutor, num_threads: 2, affinity: [0, 1] }
subgraphs:
PassPair:
nodes:
- { name: a, kernel: PassThrough, executor: cpu, input_ports: ["sin"], output_ports: ["mid"] }
- { name: b, kernel: PassThrough, input_ports: ["mid"], output_ports: ["sout"] }
input_ports: ["sin"]
output_ports: ["sout"]
nodes:
- { name: p, type: PassPair, input_ports: ["in"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let dot = graph.to_dot();
assert!(dot.contains("digraph lmflow"), "{dot}");
assert!(dot.contains("rankdir=LR"), "{dot}");
assert!(
dot.contains("subgraph cluster_"),
"expected a namespace cluster:\n{dot}"
);
assert!(
dot.contains("label=\"p\""),
"cluster should be labelled p:\n{dot}"
);
assert!(dot.contains("@cpu"), "{dot}");
assert!(dot.contains("@default"), "{dot}");
assert!(dot.contains("cluster_legend"), "{dot}");
assert!(
dot.contains("cores[0,1]"),
"affinity cores must be shown:\n{dot}"
);
assert!(dot.contains("2t"), "thread count must be shown:\n{dot}");
assert!(
dot.contains("label=\"p/mid\""),
"namespaced internal edge label:\n{dot}"
);
assert!(
dot.contains("shape=cds"),
"graph ports drawn as distinct shape:\n{dot}"
);
}
#[test]
fn to_dot_plain_graph_has_no_namespace_clusters() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- { name: n1, kernel: PassThrough, input_ports: ["in"], output_ports: ["mid"] }
- { name: n2, kernel: PassThrough, input_ports: ["mid"], output_ports: ["out"] }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let dot = graph.to_dot();
assert!(dot.contains("digraph lmflow"), "{dot}");
assert!(
!dot.contains("subgraph cluster_n"),
"plain graph must have no namespace clusters:\n{dot}"
);
assert!(
dot.contains("@default"),
"nodes without an explicit executor run on the default executor:\n{dot}"
);
}