use petgraph::algo::tarjan_scc;
use petgraph::visit::EdgeRef;
use thiserror::Error;
use super::{AgmGraph, depends_subgraph};
#[derive(Debug, Clone, PartialEq, Error)]
#[error("cycle detected in depends graph: {}", .cycle.join(" -> "))]
pub struct CycleError {
pub cycle: Vec<String>,
}
impl CycleError {
#[must_use]
pub fn new(mut path: Vec<String>) -> Self {
if path.len() >= 2 && path.first() != path.last() {
let first = path[0].clone();
path.push(first);
}
Self { cycle: path }
}
#[must_use]
pub fn display_path(&self) -> String {
self.cycle.join(" -> ")
}
}
#[must_use]
pub fn detect_cycles(graph: &AgmGraph) -> Vec<Vec<String>> {
let sub = depends_subgraph(graph);
let sccs = tarjan_scc(&sub);
let mut cycles: Vec<Vec<String>> = Vec::new();
for scc in sccs {
if scc.len() > 1 {
let ids: Vec<String> = scc.iter().map(|&idx| sub[idx].clone()).collect();
cycles.push(ids);
} else if scc.len() == 1 {
let node_idx = scc[0];
let has_self_loop = sub.edges(node_idx).any(|e| e.target() == node_idx);
if has_self_loop {
cycles.push(vec![sub[node_idx].clone()]);
}
}
}
cycles
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::build::build_graph;
use crate::graph::test_helpers::*;
#[test]
fn test_detect_cycles_acyclic_returns_empty() {
let mut a = make_node("a");
let b = make_node("b");
let c = make_node("c");
a.depends = Some(vec!["b".to_owned(), "c".to_owned()]);
let graph = build_graph(&make_file(vec![a, b, c]));
assert!(detect_cycles(&graph).is_empty());
}
#[test]
fn test_detect_cycles_self_loop_detected() {
let mut a = make_node("a");
a.depends = Some(vec!["a".to_owned()]);
let graph = build_graph(&make_file(vec![a]));
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0], vec!["a"]);
}
#[test]
fn test_detect_cycles_two_node_cycle_detected() {
let mut a = make_node("a");
let mut b = make_node("b");
a.depends = Some(vec!["b".to_owned()]);
b.depends = Some(vec!["a".to_owned()]);
let graph = build_graph(&make_file(vec![a, b]));
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
let mut ids = cycles[0].clone();
ids.sort();
assert_eq!(ids, vec!["a", "b"]);
}
#[test]
fn test_detect_cycles_multiple_sccs_detected() {
let mut a = make_node("a");
let mut b = make_node("b");
let mut c = make_node("c");
let mut d = make_node("d");
a.depends = Some(vec!["b".to_owned()]);
b.depends = Some(vec!["a".to_owned()]);
c.depends = Some(vec!["d".to_owned()]);
d.depends = Some(vec!["c".to_owned()]);
let graph = build_graph(&make_file(vec![a, b, c, d]));
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 2);
}
}