treestat 1.1.0

A CLI that displays source file counts in a tree view by directory and language
Documentation
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn bin_path() -> String {
    env!("CARGO_BIN_EXE_treestat").to_string()
}

fn make_temp_dir() -> std::path::PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!("treestat-test-{nanos}"));
    fs::create_dir_all(&dir).unwrap();
    dir
}

fn write(path: &Path, content: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(path, content).unwrap();
}

#[test]
fn text_tree_output_for_rust() {
    let root = make_temp_dir();
    write(&root.join("src/main.rs"), "fn main(){}\n");
    write(&root.join("src/nested/lib.rs"), "pub fn a(){}\n");
    write(&root.join("tests/test.rs"), "#[test] fn t(){}\n");
    write(&root.join("README.md"), "x\n");

    let out = Command::new(bin_path())
        .arg(&root)
        .arg("--lang")
        .arg("rust")
        .arg("--count-mode")
        .arg("tree")
        .output()
        .unwrap();

    assert!(out.status.success());
    let s = String::from_utf8_lossy(&out.stdout);
    assert!(s.contains("Total matching files: 3"));
    assert!(s.contains("src/ (2)"));
    assert!(s.contains("Languages (Top 5): rust=3"));
    assert!(!s.contains("Extensions:"));

    let _ = fs::remove_dir_all(root);
}

#[test]
fn max_depth_limits_tree_but_not_counts() {
    let root = make_temp_dir();
    write(&root.join("src/main.rs"), "fn main(){}\n");
    write(&root.join("src/nested/lib.rs"), "pub fn a(){}\n");
    write(&root.join("src/nested/deeper/mod.rs"), "pub fn b(){}\n");

    let out = Command::new(bin_path())
        .arg(&root)
        .arg("--lang")
        .arg("rust")
        .arg("--count-mode")
        .arg("tree")
        .arg("--max-depth")
        .arg("1")
        .output()
        .unwrap();

    assert!(out.status.success());
    let s = String::from_utf8_lossy(&out.stdout);
    // 所有 Rust 文件都会被递归统计到 total_files 里
    assert!(s.contains("Total matching files: 3"));
    // 根下的 src 目录展示的是整个子树的文件数(3 个)
    assert!(s.contains("src/ (3)"));
    // 但因为 max-depth=1,嵌套子目录不应该出现在树形输出里
    assert!(!s.contains("nested/ ("));

    let _ = fs::remove_dir_all(root);
}

#[test]
fn json_output_shape() {
    let root = make_temp_dir();
    write(&root.join("pkg/a.py"), "print(1)\n");

    let out = Command::new(bin_path())
        .arg(&root)
        .arg("--lang")
        .arg("python")
        .arg("--format")
        .arg("json")
        .output()
        .unwrap();

    assert!(out.status.success());
    let s = String::from_utf8_lossy(&out.stdout);
    assert!(s.contains("\"total_files\":1"));
    assert!(s.contains("\"count_mode\":\"tree\""));
    assert!(s.contains("\"children\":"));
    assert!(s.contains("\"language_counts\""));
    assert!(s.contains("\"python\":1") || s.contains("\"python\": 1"));
    assert!(!s.contains("\"extensions\":"));

    let _ = fs::remove_dir_all(root);
}

#[test]
fn multi_language_count_includes_c_cpp_and_headers() {
    let root = make_temp_dir();
    write(
        &root.join("src/a.c"),
        "int main() { return 0; }
",
    );
    write(
        &root.join("src/b.cpp"),
        "int x = 0;
",
    );
    write(
        &root.join("include/a.h"),
        "#pragma once
",
    );
    write(
        &root.join("include/b.hpp"),
        "#pragma once
",
    );
    write(
        &root.join("src/ignore.py"),
        "print(1)
",
    );

    let out = Command::new(bin_path())
        .arg(&root)
        .arg("--lang")
        .arg("c,cpp")
        .output()
        .unwrap();

    assert!(out.status.success());
    let s = String::from_utf8_lossy(&out.stdout);
    assert!(s.contains("Total matching files: 4"));
    assert!(s.contains("Languages (Top 5):"));
    assert!(s.contains("c=") || s.contains("c++="));

    let _ = fs::remove_dir_all(root);
}