use std::collections::BTreeSet;
use crate::model::{DepError, Requirement};
pub(crate) fn dependency_order(
target: &str,
requires_of: impl Fn(&str) -> Option<Vec<Requirement>>,
) -> Result<Vec<Step>, DepError> {
struct Frame {
name: String,
args: Vec<String>,
deps: Vec<Requirement>,
next: usize,
}
let mut order: Vec<Step> = Vec::new();
let mut done: BTreeSet<(String, Vec<String>)> = BTreeSet::new();
let mut on_stack = BTreeSet::new();
let mut stack: Vec<Frame> = Vec::new();
let deps = requires_of(target).ok_or_else(|| DepError::Missing {
task: target.to_string(),
required_by: target.to_string(),
})?;
on_stack.insert(target.to_string());
stack.push(Frame {
name: target.to_string(),
args: Vec::new(),
deps,
next: 0,
});
loop {
let descend = {
let Some(frame) = stack.last_mut() else { break };
if frame.next < frame.deps.len() {
let dep = frame.deps[frame.next].clone();
frame.next += 1;
Some(dep)
} else {
None
}
};
match descend {
Some(dep) => {
let key = (dep.name.clone(), dep.args.clone());
if done.contains(&key) {
continue; }
if on_stack.contains(&dep.name) {
return Err(DepError::Cycle(dep.name));
}
let required_by = stack.last().expect("a top frame exists").name.clone();
let deps = requires_of(&dep.name).ok_or(DepError::Missing {
task: dep.name.clone(),
required_by,
})?;
on_stack.insert(dep.name.clone());
stack.push(Frame {
name: dep.name,
args: dep.args,
deps,
next: 0,
});
}
None => {
let frame = stack.pop().expect("a top frame exists");
on_stack.remove(&frame.name);
done.insert((frame.name.clone(), frame.args.clone()));
order.push(Step {
name: frame.name,
args: frame.args,
});
}
}
}
Ok(order)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Step {
pub(crate) name: String,
pub(crate) args: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn bare(name: &str) -> Requirement {
Requirement {
name: name.to_string(),
args: Vec::new(),
}
}
fn step_names(steps: &[Step]) -> Vec<&str> {
steps.iter().map(|s| s.name.as_str()).collect()
}
fn deps_of<'a>(map: &'a [(&str, &[&str])]) -> impl Fn(&str) -> Option<Vec<Requirement>> + 'a {
move |name| {
map.iter()
.find(|(n, _)| *n == name)
.map(|(_, ds)| ds.iter().map(|s| bare(s)).collect())
}
}
#[test]
fn dependency_order_is_deps_first_target_last() {
let g = deps_of(&[("a", &["b", "c"]), ("b", &["c"]), ("c", &[])]);
assert_eq!(
step_names(&dependency_order("a", g).unwrap()),
["c", "b", "a"]
);
}
#[test]
fn dependency_order_dedupes_a_diamond() {
let g = deps_of(&[("a", &["b", "c"]), ("b", &["d"]), ("c", &["d"]), ("d", &[])]);
let planned = dependency_order("a", g).unwrap();
let order = step_names(&planned);
assert_eq!(order.iter().filter(|n| **n == "d").count(), 1);
let pos = |n: &str| order.iter().position(|x| *x == n).unwrap();
assert!(pos("d") < pos("b") && pos("d") < pos("c"));
assert_eq!(*order.last().unwrap(), "a");
}
#[test]
fn dependency_order_detects_a_cycle() {
let g = deps_of(&[("a", &["b"]), ("b", &["a"])]);
assert_eq!(dependency_order("a", g), Err(DepError::Cycle("a".into())));
}
#[test]
fn dependency_order_flags_a_missing_dependency() {
let g = deps_of(&[("a", &["ghost"])]);
assert_eq!(
dependency_order("a", g),
Err(DepError::Missing {
task: "ghost".into(),
required_by: "a".into(),
})
);
}
#[test]
fn dependency_order_survives_a_pathologically_deep_chain() {
const N: usize = 200_000;
let order = dependency_order("t0", |n| {
let i: usize = n.strip_prefix('t')?.parse().ok()?;
Some(if i + 1 < N {
vec![bare(&format!("t{}", i + 1))]
} else {
vec![]
})
})
.unwrap();
let order = step_names(&order);
assert_eq!(order.len(), N);
assert_eq!(*order.first().unwrap(), format!("t{}", N - 1)); assert_eq!(*order.last().unwrap(), "t0"); }
}