use std::collections::HashMap;
use std::ops::Range;
use crate::graph::{LevelGraph, Source};
use crate::pipeline::PipelineNode;
use super::routed::{SourceKey, consumer_counts};
pub(super) fn segments(graph: &LevelGraph<'_>) -> Vec<Range<usize>> {
let counts = consumer_counts(graph);
let mut segments: Vec<Range<usize>> = Vec::new();
for (i, node) in graph.order.iter().enumerate() {
match segments.last_mut() {
Some(segment) if continues(graph, &counts, graph.order[i - 1], node) => {
segment.end = i + 1;
}
_ => segments.push(i..i + 1),
}
}
segments
}
fn continues(
graph: &LevelGraph<'_>,
counts: &HashMap<SourceKey<'_>, usize>,
prev: &PipelineNode,
next: &PipelineNode,
) -> bool {
let only_prev = graph.inputs[next.id.as_str()] == [Source::Node(&prev.id)];
only_prev && counts.get(&Some(prev.id.as_str())) == Some(&1)
}
pub(super) fn as_chain<'g, 'a>(graph: &'g LevelGraph<'a>) -> Option<&'g [&'a PipelineNode]> {
let order = graph.order.as_slice();
let (Some(first), Some(last)) = (order.first(), order.last()) else {
return (graph.outputs == [Source::LevelInput]).then_some(order);
};
let fed = graph.inputs[first.id.as_str()] == [Source::LevelInput];
let output = graph.outputs == [Source::Node(&last.id)];
(fed && output && segments(graph).len() == 1).then_some(order)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{Flow, level_flow};
use crate::pipeline::PipelineDefinition;
fn definition(json: &str) -> PipelineDefinition {
serde_json::from_str(json).unwrap()
}
fn with_graph<R>(json: &str, check: impl Fn(&LevelGraph<'_>) -> R) -> R {
let def = definition(json);
let Ok(Flow::Graph(graph)) = level_flow(&def.nodes, &def.edges) else {
panic!("expected a graph");
};
check(&graph)
}
const CHAIN: &str = r#"{
"nodes": [
{ "id": "input", "type": "input" },
{ "id": "a", "type": "x" }, { "id": "b", "type": "x" },
{ "id": "output", "type": "output" }
],
"edges": [
{ "id": "e1", "source": "input", "target": "a" },
{ "id": "e2", "source": "a", "target": "b" },
{ "id": "e3", "source": "b", "target": "output" }
]
}"#;
const FAN_OUT: &str = r#"{
"nodes": [
{ "id": "input", "type": "input" },
{ "id": "a", "type": "x" }, { "id": "b", "type": "x" },
{ "id": "c", "type": "x" }, { "id": "d", "type": "x" },
{ "id": "output", "type": "output" }
],
"edges": [
{ "id": "e1", "source": "input", "target": "a" },
{ "id": "e2", "source": "a", "target": "b" },
{ "id": "e3", "source": "a", "target": "c" },
{ "id": "e4", "source": "c", "target": "d" },
{ "id": "e5", "source": "b", "target": "output" },
{ "id": "e6", "source": "d", "target": "output" }
]
}"#;
#[test]
fn test_a_chain_is_one_segment_and_runs_as_today() {
with_graph(CHAIN, |g| {
assert_eq!(segments(g), vec![0..2]);
assert_eq!(as_chain(g).map(<[_]>::len), Some(2));
});
}
#[test]
fn test_fan_out_ends_a_segment_and_a_linear_tail_joins_up() {
with_graph(FAN_OUT, |g| {
assert_eq!(segments(g), vec![0..1, 1..2, 2..4]);
assert!(as_chain(g).is_none());
});
}
#[test]
fn test_a_chain_that_skips_input_is_not_todays_chain() {
let json = CHAIN.replace(
r#""source": "input", "target": "a""#,
r#""source": "a", "target": "output""#,
);
with_graph(&json, |g| assert!(as_chain(g).is_none()));
}
#[test]
fn test_input_straight_to_output_runs_as_an_empty_chain() {
let json = r#"{
"nodes": [ { "id": "input", "type": "input" }, { "id": "output", "type": "output" } ],
"edges": [ { "id": "e1", "source": "input", "target": "output" } ]
}"#;
with_graph(json, |g| assert_eq!(as_chain(g).map(<[_]>::len), Some(0)));
}
}