debtmap 0.22.0

Code complexity and technical debt analyzer
Documentation
use debtmap::output::unified::{UNIFIED_FORMAT_VERSION, UnifiedOutput};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn manifest_path(relative: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)
}

fn load_json(relative: &str) -> Value {
    serde_json::from_str(&fs::read_to_string(manifest_path(relative)).unwrap()).unwrap()
}

fn assert_valid(schema: &Value, instance: &Value) {
    let validator = jsonschema::validator_for(schema).unwrap();
    let errors = validator
        .iter_errors(instance)
        .map(|error| error.to_string())
        .collect::<Vec<_>>();
    assert!(errors.is_empty(), "{}", errors.join("\n"));
}

#[test]
fn committed_fixture_matches_schema_and_serde() {
    let schema = load_json("schemas/debtmap-output-v3.schema.json");
    assert_eq!(
        schema["properties"]["format_version"]["const"],
        UNIFIED_FORMAT_VERSION
    );

    for path in [
        "tests/fixtures/output/unified-v3-minimal.json",
        "tests/fixtures/output/unified-v3-items.json",
    ] {
        let fixture = load_json(path);
        assert_valid(&schema, &fixture);
        let typed: UnifiedOutput = serde_json::from_value(fixture.clone()).unwrap();
        assert_eq!(serde_json::to_value(typed).unwrap(), fixture);
    }

    let fixture = load_json("tests/fixtures/output/unified-v3-minimal.json");
    let mut wrong_version = fixture.clone();
    wrong_version["format_version"] = Value::String("4.0".to_string());
    assert!(
        !jsonschema::validator_for(&schema)
            .unwrap()
            .is_valid(&wrong_version)
    );

    let mut missing_summary = fixture;
    missing_summary.as_object_mut().unwrap().remove("summary");
    assert!(
        !jsonschema::validator_for(&schema)
            .unwrap()
            .is_valid(&missing_summary)
    );

    let mut wrong_uncovered_lines = load_json("tests/fixtures/output/unified-v3-items.json");
    wrong_uncovered_lines["items"][0]["metrics"]["uncovered_lines"] = Value::from(11);
    assert!(
        !jsonschema::validator_for(&schema)
            .unwrap()
            .is_valid(&wrong_uncovered_lines)
    );
}

#[test]
fn cli_json_matches_v3_schema_with_and_without_details() {
    let directory = tempfile::tempdir().unwrap();
    fs::write(
        directory.path().join("sample.rs"),
        "pub fn hotspot(x: i32) -> i32 { if x > 0 { if x > 1 { if x > 2 { if x > 3 { if x > 4 { if x > 5 { x } else { 0 } } else { 0 } } else { 0 } } else { 0 } } else { 0 } } else { 0 } }\n",
    )
    .unwrap();
    let schema = load_json("schemas/debtmap-output-v3.schema.json");

    for (name, verbose) in [("default.json", false), ("detailed.json", true)] {
        let output_path = directory.path().join(name);
        let mut command = Command::new(env!("CARGO_BIN_EXE_debtmap"));
        command
            .current_dir(directory.path())
            .env("HOME", directory.path().join(".test-home"))
            .env("XDG_CONFIG_HOME", directory.path().join(".test-config"))
            .env_remove("DEBTMAP_CONFIG")
            .args([
                "analyze",
                ".",
                "--format",
                "json",
                "--quiet",
                "--no-tui",
                "--no-context-aware",
                "--no-parallel",
                "--min-score",
                "0",
                "--output",
            ])
            .arg(&output_path);
        if verbose {
            command.arg("-vv");
        }
        let output = command.output().unwrap();
        assert!(
            output.status.success(),
            "{}",
            String::from_utf8_lossy(&output.stderr)
        );
        let report: Value =
            serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
        assert_valid(&schema, &report);
        assert_eq!(report["format_version"], UNIFIED_FORMAT_VERSION);
        assert_eq!(
            report["summary"]["total_items"],
            report["items"].as_array().unwrap().len()
        );
        assert_eq!(report["items"][0].get("scoring_details").is_some(), verbose);
    }
}

#[test]
fn compare_consumes_v3_summary_and_rejects_unknown_versions() {
    let directory = tempfile::tempdir().unwrap();
    let output_path = directory.path().join("comparison.json");
    let output = Command::new(env!("CARGO_BIN_EXE_debtmap"))
        .args(["compare", "--before"])
        .arg(manifest_path(
            "tests/fixtures/output/unified-v3-minimal.json",
        ))
        .arg("--after")
        .arg(manifest_path("tests/fixtures/output/unified-v3-items.json"))
        .args(["--format", "json", "--output"])
        .arg(&output_path)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let comparison: Value =
        serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
    assert_eq!(
        comparison["project_health"]["after"]["total_debt_score"],
        75.0
    );

    let unsupported = directory.path().join("unsupported.json");
    let mut report = load_json("tests/fixtures/output/unified-v3-minimal.json");
    report["format_version"] = Value::String("4.0".to_string());
    fs::write(&unsupported, serde_json::to_vec_pretty(&report).unwrap()).unwrap();
    let rejected = Command::new(env!("CARGO_BIN_EXE_debtmap"))
        .args(["compare", "--before"])
        .arg(&unsupported)
        .arg("--after")
        .arg(manifest_path(
            "tests/fixtures/output/unified-v3-minimal.json",
        ))
        .args(["--format", "json"])
        .output()
        .unwrap();
    assert!(!rejected.status.success());
    assert!(
        String::from_utf8_lossy(&rejected.stderr).contains("Unsupported debtmap format version")
    );

    report["format_version"] = Value::from(3);
    fs::write(&unsupported, serde_json::to_vec_pretty(&report).unwrap()).unwrap();
    let malformed = Command::new(env!("CARGO_BIN_EXE_debtmap"))
        .args(["compare", "--before"])
        .arg(&unsupported)
        .arg("--after")
        .arg(manifest_path(
            "tests/fixtures/output/unified-v3-minimal.json",
        ))
        .args(["--format", "json"])
        .output()
        .unwrap();
    assert!(!malformed.status.success());
    assert!(String::from_utf8_lossy(&malformed.stderr).contains("Invalid debtmap format version"));
}