cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! End-to-end tests: run the built `cargo-treemap` binary against real crates and check the
//! JSON report. The fixture test is hermetic (a tiny generated crate + one small dep); the
//! popular-crate test is `#[ignore]`d because it builds ripgrep.

use serde_json::Value;
use std::path::{Path, PathBuf};
use std::process::Command;

fn bin() -> &'static str {
    env!("CARGO_BIN_EXE_cargo-treemap")
}

/// Run `cargo treemap --release --format json` on a manifest and parse the report.
fn analyze(manifest: &Path, extra: &[&str]) -> Value {
    let out = Command::new(bin())
        .args(["treemap", "--release", "--format", "json", "--manifest-path"])
        .arg(manifest)
        .args(extra)
        .output()
        .expect("failed to run cargo-treemap");
    assert!(
        out.status.success(),
        "cargo-treemap exited {}:\n{}",
        out.status,
        String::from_utf8_lossy(&out.stderr)
    );
    serde_json::from_slice(&out.stdout).expect("report is valid JSON")
}

fn crate_labels(report: &Value) -> Vec<String> {
    report["tree"]["groups"]
        .as_array()
        .expect("tree.groups is an array")
        .iter()
        .map(|g| g["label"].as_str().unwrap().to_string())
        .collect()
}

#[test]
fn analyzes_a_fixture_with_a_dependency() {
    let dir = std::env::temp_dir().join(format!("cargo-treemap-fixture-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(dir.join("src")).unwrap();
    std::fs::write(
        dir.join("Cargo.toml"),
        r#"[package]
name = "ctm_fixture"
version = "0.0.0"
edition = "2021"

[dependencies]
itoa = "1"
"#,
    )
    .unwrap();
    std::fs::write(
        dir.join("src/main.rs"),
        r#"fn main() {
    let mut buf = itoa::Buffer::new();
    println!("{}", buf.format(std::env::args().count()));
}
"#,
    )
    .unwrap();

    let report = analyze(&dir.join("Cargo.toml"), &[]);

    // Basic shape.
    let fmt = report["meta"]["format"].as_str().unwrap();
    assert!(["Mach-O", "ELF", "PE"].contains(&fmt), "unexpected format {fmt}");
    assert!(report["meta"]["total_text"].as_u64().unwrap() > 0);
    assert!(report["tree"]["text"].as_u64().unwrap() > 0);

    // The right crates showed up, each with real bytes.
    let labels = crate_labels(&report);
    assert!(labels.contains(&"ctm_fixture".to_string()), "crates: {labels:?}");
    assert!(labels.contains(&"itoa".to_string()), "crates: {labels:?}");
    assert!(labels.contains(&"std".to_string()), "crates: {labels:?}");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn json_report_is_stable_shaped() {
    // A no-dependency crate: still produces a well-formed report with the expected top keys.
    let dir = std::env::temp_dir().join(format!("cargo-treemap-plain-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(dir.join("src")).unwrap();
    std::fs::write(
        dir.join("Cargo.toml"),
        "[package]\nname = \"ctm_plain\"\nversion = \"0.0.0\"\nedition = \"2021\"\n",
    )
    .unwrap();
    std::fs::write(dir.join("src/main.rs"), "fn main() { println!(\"hi\"); }\n").unwrap();

    let report = analyze(&dir.join("Cargo.toml"), &[]);
    for key in ["meta", "dependencies", "crates", "tree"] {
        assert!(report.get(key).is_some(), "report missing `{key}`");
    }
    assert!(crate_labels(&report).contains(&"ctm_plain".to_string()));

    let _ = std::fs::remove_dir_all(&dir);
}

/// Popular-crate coverage. Builds ripgrep, so it's opt-in: `cargo test -- --ignored`.
/// Point it at a checkout with `RIPGREP=/path/to/ripgrep`, else it looks in /tmp/ripgrep.
#[test]
#[ignore = "builds ripgrep; run with `cargo test -- --ignored`"]
fn analyzes_ripgrep() {
    let root = std::env::var("RIPGREP").unwrap_or_else(|_| "/tmp/ripgrep".to_string());
    let manifest = PathBuf::from(&root).join("Cargo.toml");
    if !manifest.exists() {
        eprintln!("skipping: no ripgrep checkout at {root} (set RIPGREP=...)");
        return;
    }
    let report = analyze(&manifest, &["--bin", "rg"]);
    let labels = crate_labels(&report);
    // ripgrep's regex engine is its signature dependency.
    assert!(
        labels.contains(&"regex_automata".to_string()),
        "expected regex_automata in {labels:?}"
    );
    assert!(report["meta"]["total_text"].as_u64().unwrap() > 500_000);
}