use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct DependencyGraph {
nodes: HashMap<String, HashSet<String>>,
reverse_edges: HashMap<String, HashSet<String>>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
reverse_edges: HashMap::new(),
}
}
pub fn add_node(&mut self, step_id: &str) {
self.nodes.entry(step_id.to_string()).or_default();
self.reverse_edges.entry(step_id.to_string()).or_default();
}
pub fn add_dependency(&mut self, step_id: &str, depends_on: &str) {
self.nodes
.entry(step_id.to_string())
.or_default()
.insert(depends_on.to_string());
self.nodes.entry(depends_on.to_string()).or_default();
self.reverse_edges
.entry(depends_on.to_string())
.or_default()
.insert(step_id.to_string());
self.reverse_edges.entry(step_id.to_string()).or_default();
}
pub fn get_dependencies(&self, step_id: &str) -> HashSet<String> {
self.nodes
.get(step_id)
.cloned()
.unwrap_or_else(HashSet::new)
}
pub fn get_dependents(&self, step_id: &str) -> HashSet<String> {
self.reverse_edges
.get(step_id)
.cloned()
.unwrap_or_else(HashSet::new)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn get_zero_dependency_steps(&self) -> Vec<String> {
self.nodes
.iter()
.filter(|(_, deps)| deps.is_empty())
.map(|(id, _)| id.clone())
.collect()
}
pub fn has_cycle(&self) -> bool {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for node in self.nodes.keys() {
if self.has_cycle_dfs(node, &mut visited, &mut rec_stack) {
return true;
}
}
false
}
fn has_cycle_dfs(
&self,
node: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> bool {
if rec_stack.contains(node) {
return true; }
if visited.contains(node) {
return false; }
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
if let Some(deps) = self.nodes.get(node) {
for dep in deps {
if self.has_cycle_dfs(dep, visited, rec_stack) {
return true;
}
}
}
rec_stack.remove(node);
false
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_graph_is_empty() {
let graph = DependencyGraph::new();
assert_eq!(graph.node_count(), 0);
}
#[test]
fn test_add_node() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
assert_eq!(graph.node_count(), 1);
assert!(graph.get_dependencies("step_1").is_empty());
}
#[test]
fn test_add_dependency() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_dependency("step_2", "step_1");
let deps = graph.get_dependencies("step_2");
assert_eq!(deps.len(), 1);
assert!(deps.contains("step_1"));
}
#[test]
fn test_add_dependency_creates_nodes() {
let mut graph = DependencyGraph::new();
graph.add_dependency("step_2", "step_1");
assert_eq!(graph.node_count(), 2);
assert!(graph.get_dependencies("step_2").contains("step_1"));
}
#[test]
fn test_get_dependents() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_dependency("step_2", "step_1");
let dependents = graph.get_dependents("step_1");
assert_eq!(dependents.len(), 1);
assert!(dependents.contains("step_2"));
}
#[test]
fn test_multiple_dependents() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_node("step_3");
graph.add_dependency("step_2", "step_1");
graph.add_dependency("step_3", "step_1");
let dependents = graph.get_dependents("step_1");
assert_eq!(dependents.len(), 2);
assert!(dependents.contains("step_2"));
assert!(dependents.contains("step_3"));
}
#[test]
fn test_cycle_detection_simple_cycle() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_dependency("step_1", "step_2");
graph.add_dependency("step_2", "step_1");
assert!(graph.has_cycle());
}
#[test]
fn test_cycle_detection_no_cycle() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_node("step_3");
graph.add_dependency("step_2", "step_1");
graph.add_dependency("step_3", "step_2");
assert!(!graph.has_cycle());
}
#[test]
fn test_cycle_detection_self_cycle() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_dependency("step_1", "step_1");
assert!(graph.has_cycle());
}
#[test]
fn test_cycle_detection_complex() {
let mut graph = DependencyGraph::new();
graph.add_dependency("step_2", "step_1");
graph.add_dependency("step_3", "step_1");
graph.add_dependency("step_4", "step_2");
graph.add_dependency("step_4", "step_3");
assert!(!graph.has_cycle());
}
#[test]
fn test_cycle_detection_complex_with_cycle() {
let mut graph = DependencyGraph::new();
graph.add_dependency("step_2", "step_1");
graph.add_dependency("step_3", "step_1");
graph.add_dependency("step_4", "step_2");
graph.add_dependency("step_4", "step_3");
graph.add_dependency("step_1", "step_4");
assert!(graph.has_cycle());
}
#[test]
fn test_get_zero_dependency_steps() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_node("step_3");
graph.add_dependency("step_2", "step_1");
graph.add_dependency("step_3", "step_1");
let zero_deps = graph.get_zero_dependency_steps();
assert_eq!(zero_deps.len(), 1);
assert!(zero_deps.contains(&"step_1".to_string()));
}
#[test]
fn test_get_zero_dependency_steps_multiple() {
let mut graph = DependencyGraph::new();
graph.add_node("step_1");
graph.add_node("step_2");
graph.add_node("step_3");
graph.add_dependency("step_3", "step_1");
let zero_deps = graph.get_zero_dependency_steps();
assert_eq!(zero_deps.len(), 2);
assert!(zero_deps.contains(&"step_1".to_string()));
assert!(zero_deps.contains(&"step_2".to_string()));
}
#[test]
fn test_get_dependencies_nonexistent() {
let graph = DependencyGraph::new();
let deps = graph.get_dependencies("nonexistent");
assert!(deps.is_empty());
}
#[test]
fn test_get_dependents_nonexistent() {
let graph = DependencyGraph::new();
let dependents = graph.get_dependents("nonexistent");
assert!(dependents.is_empty());
}
}