rustqual 1.0.0

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
use crate::adapters::analyzers::coupling::sdp::*;
use crate::adapters::analyzers::coupling::{metrics::compute_coupling_metrics, ModuleGraph};

#[test]
fn test_no_violations_all_same_instability() {
    // A → B, both have same structure → no SDP violation
    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() {
    // A(stable) → B(unstable), C → A (makes A stable)
    // C → A → B
    // A: Ca=1, Ce=1, I=0.5
    // B: Ca=1, Ce=0, I=0.0
    // C: Ca=0, Ce=1, I=1.0
    // Edge C→A: C(1.0) → A(0.5) — C is unstable, depends on more stable A → no violation
    // Edge A→B: A(0.5) → B(0.0) — A depends on more stable B → no violation
    // No violations here. Let me construct a case where there IS a violation.

    // A: Ca=2, Ce=0, I=0.0 (very stable)
    // B: Ca=0, Ce=2, I=1.0 (very unstable)
    // A → B would be an SDP violation
    // We need: X → A, Y → A (gives A Ca=2)
    //          B → P, B → Q (gives B Ce=2)
    //          A → B (the violating edge)
    let graph = ModuleGraph {
        modules: vec![
            "a".into(),
            "b".into(),
            "x".into(),
            "y".into(),
            "p".into(),
            "q".into(),
        ],
        forward: vec![
            vec![1],    // a → b
            vec![4, 5], // b → p, b → q
            vec![0],    // x → a
            vec![0],    // y → a
            vec![],     // p
            vec![],     // q
        ],
    };
    let metrics = compute_coupling_metrics(&graph);
    let violations = check_sdp(&graph, &metrics);
    // A: Ca=2, Ce=1, I=1/3 ≈ 0.33
    // B: Ca=1, Ce=2, I=2/3 ≈ 0.67
    // A → B: 0.33 < 0.67 → violation
    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() {
    // A(unstable) → B(stable) — this is correct per SDP
    // A: Ca=0, Ce=1, I=1.0
    // B: Ca=1, Ce=0, I=0.0
    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_zero_violations_for_stable_leaves() {
    // Edge-case regression: the label "unstable leaf" is a common
    // intuition trap. B and C have Ce=0 and Ca=1 → instability = 0.0
    // (maximally stable). A → B is therefore NOT an SDP violation even
    // though the test name suggests multiple. The assertion locks the
    // correct "zero violations" behaviour so future refactors can't
    // silently change it.
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into(), "c".into(), "d".into(), "e".into()],
        forward: vec![
            vec![1, 2], // a → b, a → c
            vec![],     // b (Ca=1, Ce=0 → I=0.0, stable)
            vec![],     // c (Ca=1, Ce=0 → I=0.0, stable)
            vec![0],    // d → a
            vec![0],    // e → a
        ],
    };
    let metrics = compute_coupling_metrics(&graph);
    let violations = check_sdp(&graph, &metrics);
    // A: Ca=2, Ce=2, I=0.5
    // B: Ca=1, Ce=0, I=0.0 (stable)
    // A (0.5) → B (0.0) is not a violation: the callee is more stable.
    assert!(violations.is_empty());
}

#[test]
fn test_violation_details() {
    // Make a clear violation: stable A depends on unstable B
    // Setup: X→A, Y→A gives A high Ca
    // B→P, B→Q gives B high Ce
    // A→B is the violation
    let graph = ModuleGraph {
        modules: vec![
            "a".into(),
            "b".into(),
            "x".into(),
            "y".into(),
            "p".into(),
            "q".into(),
        ],
        forward: vec![
            vec![1],    // a → b
            vec![4, 5], // b → p, q
            vec![0],    // x → a
            vec![0],    // y → a
            vec![],     // p
            vec![],     // q
        ],
    };
    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");
    // A: Ca=2, Ce=1, I≈0.33
    // B: Ca=1, Ce=2, I≈0.67
    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"
    );
}

#[test]
fn test_sdp_violation_suppressed_when_from_module_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 mut metrics = compute_coupling_metrics(&graph);
    metrics[0].suppressed = true; // "a" is the from_module
    let violations = check_sdp(&graph, &metrics);
    assert_eq!(violations.len(), 1);
    assert!(
        violations[0].suppressed,
        "from_module suppressed → violation created suppressed"
    );
}

#[test]
fn test_sdp_violation_suppressed_when_to_module_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 mut metrics = compute_coupling_metrics(&graph);
    metrics[1].suppressed = true; // "b" is the to_module
    let violations = check_sdp(&graph, &metrics);
    assert_eq!(violations.len(), 1);
    assert!(
        violations[0].suppressed,
        "to_module suppressed → violation created suppressed"
    );
}