sem-cli 0.9.0

Semantic version control CLI. Shows what entities changed (functions, classes, methods) instead of lines.
use std::fs;
use std::path::Path;
use std::process::{Command, Output};

use tempfile::TempDir;

fn output_text(output: &Output) -> String {
    format!(
        "stdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

fn assert_success(output: Output, context: &str) -> Output {
    assert!(
        output.status.success(),
        "{context} failed with status {:?}\n{}",
        output.status.code(),
        output_text(&output)
    );
    output
}

fn git(repo: &Path, args: &[&str]) -> Output {
    assert_success(
        Command::new("git")
            .current_dir(repo)
            .args(args)
            .output()
            .unwrap(),
        &format!("git {}", args.join(" ")),
    )
}

fn contains_cache_db(path: &Path) -> bool {
    fs::read_dir(path).unwrap().any(|entry| {
        let entry = entry.unwrap();
        let path = entry.path();
        path.file_name().is_some_and(|name| name == "cache.db")
            || (path.is_dir() && contains_cache_db(&path))
    })
}

fn init_repo(repo: &Path) {
    git(repo, &["init", "-q"]);
    git(repo, &["config", "user.email", "t@t.com"]);
    git(repo, &["config", "user.name", "test"]);

    fs::write(repo.join("a.py"), "def f(a, b):\n    return a + b\n").unwrap();
    git(repo, &["add", "a.py"]);
    git(repo, &["commit", "-q", "-m", "init"]);
}

#[test]
fn graph_cache_does_not_dirty_the_working_tree() {
    let repo = TempDir::new().unwrap();
    let cache = TempDir::new().unwrap();

    init_repo(repo.path());

    assert_success(
        Command::new(env!("CARGO_BIN_EXE_sem"))
            .current_dir(repo.path())
            .env("SEM_CACHE_DIR", cache.path())
            .args(["graph", "--format", "json"])
            .output()
            .unwrap(),
        "sem graph",
    );

    let status = git(repo.path(), &["status", "--porcelain"]);
    assert_eq!(String::from_utf8_lossy(&status.stdout), "");
    assert!(!repo.path().join(".sem").exists());
    assert!(contains_cache_db(cache.path()));
}

#[test]
fn graph_ignores_repo_local_cache_override() {
    let repo = TempDir::new().unwrap();
    let home = TempDir::new().unwrap();

    init_repo(repo.path());

    assert_success(
        Command::new(env!("CARGO_BIN_EXE_sem"))
            .current_dir(repo.path())
            .env("SEM_CACHE_DIR", ".sem")
            .env("HOME", home.path())
            .env("USERPROFILE", home.path())
            .env_remove("XDG_CACHE_HOME")
            .env_remove("LOCALAPPDATA")
            .env_remove("APPDATA")
            .args(["graph", "--format", "json"])
            .output()
            .unwrap(),
        "sem graph",
    );

    let status = git(repo.path(), &["status", "--porcelain"]);
    assert_eq!(String::from_utf8_lossy(&status.stdout), "");
    assert!(!repo.path().join(".sem").exists());
    assert!(contains_cache_db(home.path()));
}

#[test]
fn graph_ignores_repo_local_xdg_cache_home() {
    let repo = TempDir::new().unwrap();
    let home = TempDir::new().unwrap();

    init_repo(repo.path());

    assert_success(
        Command::new(env!("CARGO_BIN_EXE_sem"))
            .current_dir(repo.path())
            .env("XDG_CACHE_HOME", ".sem")
            .env("HOME", home.path())
            .env("USERPROFILE", home.path())
            .env_remove("SEM_CACHE_DIR")
            .env_remove("LOCALAPPDATA")
            .env_remove("APPDATA")
            .args(["graph", "--format", "json"])
            .output()
            .unwrap(),
        "sem graph",
    );

    let status = git(repo.path(), &["status", "--porcelain"]);
    assert_eq!(String::from_utf8_lossy(&status.stdout), "");
    assert!(!repo.path().join(".sem").exists());
    assert!(contains_cache_db(home.path()));
}