#[derive(Debug, Clone)]
pub struct SdpViolation {
pub from_module: String,
pub to_module: String,
pub from_instability: f64,
pub to_instability: f64,
pub suppressed: bool,
}
pub(super) fn check_sdp(
graph: &super::ModuleGraph,
metrics: &[super::CouplingMetrics],
) -> Vec<SdpViolation> {
let instability: std::collections::HashMap<&str, f64> = metrics
.iter()
.map(|m| (m.module_name.as_str(), m.instability))
.collect();
let mut violations = Vec::new();
for (from_idx, deps) in graph.forward.iter().enumerate() {
let from_name = &graph.modules[from_idx];
let from_inst = instability.get(from_name.as_str()).copied().unwrap_or(0.0);
for &to_idx in deps {
let to_name = &graph.modules[to_idx];
let to_inst = instability.get(to_name.as_str()).copied().unwrap_or(0.0);
if from_inst < to_inst {
violations.push(SdpViolation {
from_module: from_name.clone(),
to_module: to_name.clone(),
from_instability: from_inst,
to_instability: to_inst,
suppressed: false,
});
}
}
}
violations
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coupling::{metrics::compute_coupling_metrics, ModuleGraph};
#[test]
fn test_no_violations_all_same_instability() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into()],
forward: vec![vec![1], vec![0]],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert!(
violations.is_empty(),
"Equal instability should not trigger SDP"
);
}
#[test]
fn test_violation_stable_depends_on_unstable() {
let graph = ModuleGraph {
modules: vec![
"a".into(),
"b".into(),
"x".into(),
"y".into(),
"p".into(),
"q".into(),
],
forward: vec![
vec![1], vec![4, 5], vec![0], vec![0], vec![], vec![], ],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].from_module, "a");
assert_eq!(violations[0].to_module, "b");
assert!(violations[0].from_instability < violations[0].to_instability);
}
#[test]
fn test_no_violation_unstable_depends_on_stable() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into()],
forward: vec![vec![1], vec![]],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert!(
violations.is_empty(),
"Unstable depending on stable is correct SDP"
);
}
#[test]
fn test_no_violations_empty_graph() {
let graph = ModuleGraph {
modules: vec![],
forward: vec![],
};
let violations = check_sdp(&graph, &[]);
assert!(violations.is_empty());
}
#[test]
fn test_no_violations_single_module() {
let graph = ModuleGraph {
modules: vec!["a".into()],
forward: vec![vec![]],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert!(violations.is_empty());
}
#[test]
fn test_multiple_violations() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into(), "c".into(), "d".into(), "e".into()],
forward: vec![
vec![1, 2], vec![], vec![], vec![0], vec![0], ],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert!(violations.is_empty());
}
#[test]
fn test_violation_details() {
let graph = ModuleGraph {
modules: vec![
"a".into(),
"b".into(),
"x".into(),
"y".into(),
"p".into(),
"q".into(),
],
forward: vec![
vec![1], vec![4, 5], vec![0], vec![0], vec![], vec![], ],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert_eq!(violations.len(), 1);
let v = &violations[0];
assert_eq!(v.from_module, "a");
assert_eq!(v.to_module, "b");
assert!(v.from_instability < 0.5);
assert!(v.to_instability > 0.5);
}
#[test]
fn test_sdp_violation_default_not_suppressed() {
let graph = ModuleGraph {
modules: vec![
"a".into(),
"b".into(),
"x".into(),
"y".into(),
"p".into(),
"q".into(),
],
forward: vec![vec![1], vec![4, 5], vec![0], vec![0], vec![], vec![]],
};
let metrics = compute_coupling_metrics(&graph);
let violations = check_sdp(&graph, &metrics);
assert_eq!(violations.len(), 1);
assert!(
!violations[0].suppressed,
"SDP violations should default to not suppressed"
);
}
}