rustqual 1.4.1

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
use super::*;

// ── Smoke tests ────────────────────────────────────────────────────

#[test]
fn ai_toon_render_on_empty_analysis_emits_zero_findings() {
    let analysis = empty_analysis();
    let config = Config::default();
    let reporter = AiReporter {
        config: &config,
        data: &analysis.data,
        format: AiOutputFormat::Toon,
    };
    let output = reporter.render(&analysis.findings, &analysis.data);
    assert!(
        output.contains("findings"),
        "TOON output must include the findings key; got {output}"
    );
    assert!(
        !output.contains("findings_by_file:"),
        "no findings_by_file when zero findings; got {output}"
    );
}

#[test]
fn ai_json_render_on_empty_analysis_emits_zero_findings() {
    let analysis = empty_analysis();
    let config = Config::default();
    let value = build_ai_value(&analysis, &config);
    assert_eq!(
        value["findings"], 0,
        "empty analysis must produce zero findings; got {value}"
    );
}

#[test]
fn ai_value_includes_complexity_finding_metric_and_location() {
    let mut analysis = empty_analysis();
    analysis.findings.complexity = vec![ComplexityFinding {
        common: Finding {
            file: "test.rs".into(),
            line: 4,
            column: 0,
            dimension: crate::findings::Dimension::Complexity,
            rule_id: "complexity/magic_number".into(),
            message: "magic number 42 in f".into(),
            severity: crate::domain::Severity::Medium,
            suppressed: false,
        },
        kind: ComplexityFindingKind::MagicNumber,
        metric_value: 1,
        threshold: 0,
        hotspot: None,
    }];
    let config = Config::default();
    let value = build_ai_value(&analysis, &config);
    assert_eq!(value["findings"], 1, "one complexity finding: got {value}");
    let by_file = value["findings_by_file"]
        .as_object()
        .expect("findings_by_file present");
    let entries = by_file
        .get("test.rs")
        .and_then(|v| v.as_array())
        .expect("entries for test.rs");
    let magic_entries: Vec<_> = entries
        .iter()
        .filter(|e| e["category"].as_str() == Some("magic_number"))
        .collect();
    assert_eq!(
        magic_entries.len(),
        1,
        "magic_number category preserved; got {value}"
    );
    assert_eq!(
        magic_entries[0]["line"], 4,
        "line number preserved; got {value}"
    );
}

#[test]
fn ai_reporter_includes_orphan_entries_via_snapshot_view() {
    use crate::domain::findings::OrphanSuppression;
    let mut analysis = empty_analysis();
    analysis.findings.orphan_suppressions = vec![OrphanSuppression {
        file: "src/foo.rs".into(),
        line: 42,
        dimensions: vec![crate::findings::Dimension::Srp],
        reason: Some("legacy".into()),
    }];
    let config = Config::default();
    let value = build_ai_value(&analysis, &config);
    assert_eq!(
        value["findings"], 1,
        "orphan must count as a finding, got value: {value}"
    );
    let by_file = value["findings_by_file"]
        .as_object()
        .expect("findings_by_file present");
    let entries = by_file
        .get("src/foo.rs")
        .and_then(|v| v.as_array())
        .expect("entries for src/foo.rs");
    let orphan = entries
        .iter()
        .find(|e| e["category"] == "orphan_suppression")
        .expect("orphan entry under src/foo.rs");
    assert_eq!(orphan["line"], 42);
}