use pipechecker::auditors::dag;
use pipechecker::models::{Job, Pipeline, Provider};
#[test]
fn test_self_loop() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![Job {
id: "A".to_string(),
name: None,
depends_on: vec!["A".to_string()], steps: vec![],
env: vec![],
container_image: None,
service_images: vec![],
timeout_minutes: None,
}],
env: vec![],
source: "jobs:\n A:\n needs: [A]".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert_eq!(issues.len(), 1);
assert!(issues[0].message.contains("Circular"));
assert_eq!(
issues[0].location.as_ref().unwrap().job,
Some("A".to_string())
);
}
#[test]
fn test_three_node_cycle() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
name: None,
depends_on: vec!["B".to_string()],
steps: vec![],
env: vec![],
container_image: None,
service_images: vec![],
timeout_minutes: None,
},
Job {
id: "B".to_string(),
name: None,
depends_on: vec!["C".to_string()],
steps: vec![],
env: vec![],
container_image: None,
service_images: vec![],
timeout_minutes: None,
},
Job {
id: "C".to_string(),
name: None,
depends_on: vec!["A".to_string()],
steps: vec![],
env: vec![],
container_image: None,
service_images: vec![],
timeout_minutes: None,
},
],
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(!issues.is_empty());
let jobs_in_issues: Vec<_> = issues
.iter()
.filter_map(|i| i.location.as_ref()?.job.as_ref())
.collect();
assert!(jobs_in_issues
.iter()
.any(|j| j == &&"A".to_string() || j == &&"B".to_string() || j == &&"C".to_string()));
}
#[test]
fn test_four_node_cycle() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
depends_on: vec!["B".to_string()],
..Default::default()
},
Job {
id: "B".to_string(),
depends_on: vec!["C".to_string()],
..Default::default()
},
Job {
id: "C".to_string(),
depends_on: vec!["D".to_string()],
..Default::default()
},
Job {
id: "D".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
],
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(!issues.is_empty());
assert!(issues.len() >= 1);
}
#[test]
fn test_multiple_independent_cycles() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
Job {
id: "B".to_string(),
depends_on: vec!["C".to_string()],
..Default::default()
},
Job {
id: "C".to_string(),
depends_on: vec!["B".to_string()],
..Default::default()
},
],
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(issues.len() >= 2);
let messages: Vec<_> = issues.iter().map(|i| i.message.clone()).collect();
assert!(messages.iter().any(|m| m.contains("A")));
assert!(messages.iter().any(|m| m.contains("B") || m.contains("C")));
}
#[test]
fn test_large_dag_no_cycles() {
let mut jobs = Vec::new();
let mut prev: Option<String> = None;
for i in 0..20 {
let id = format!("job{}", i);
let depends_on = if let Some(p) = prev.take() {
vec![p]
} else {
vec![]
};
prev = Some(id.clone());
jobs.push(Job {
id,
name: None,
depends_on,
steps: vec![],
env: vec![],
container_image: None,
service_images: vec![],
timeout_minutes: None,
});
}
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs,
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(
issues.is_empty(),
"Large DAG with no cycles should produce no issues"
);
}
#[test]
fn test_indirect_dependency_cycle() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
depends_on: vec!["B".to_string()],
..Default::default()
},
Job {
id: "B".to_string(),
depends_on: vec!["C".to_string()],
..Default::default()
},
Job {
id: "C".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
],
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(!issues.is_empty());
}
#[test]
fn test_cycle_location_points_to_job_in_cycle() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
depends_on: vec!["B".to_string()],
..Default::default()
},
Job {
id: "B".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
],
env: vec![],
source: "jobs:\n A:\n needs: [B]\n B:\n needs: [A]".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(!issues.is_empty());
let loc_job = issues[0].location.as_ref().unwrap().job.as_ref().unwrap();
assert!(loc_job == "A" || loc_job == "B");
}
#[test]
fn test_no_cycle_with_diamond_dependency() {
let pipeline = Pipeline {
provider: Provider::GitHubActions,
jobs: vec![
Job {
id: "A".to_string(),
depends_on: vec![],
..Default::default()
},
Job {
id: "B".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
Job {
id: "C".to_string(),
depends_on: vec!["A".to_string()],
..Default::default()
},
Job {
id: "D".to_string(),
depends_on: vec!["B".to_string(), "C".to_string()],
..Default::default()
},
],
env: vec![],
source: "".to_string(),
};
let issues = dag::audit(&pipeline).unwrap();
assert!(
issues.is_empty(),
"Diamond dependency DAG should have no cycles"
);
}