simit 0.16.0

Semver-aware git commit helper for Rust projects
Documentation
use std::env;
use std::fs;
use std::process::Command;
use std::sync::{Mutex, OnceLock};

use simit::cli::{Platform, Runtime};
use simit::user_config::UserConfig;
use tempfile::TempDir;

fn env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

fn valid_config() -> &'static str {
    r#"[ci.runners.atlas]
platform = "forgejo"
labels = ["atlas"]
os = "linux"
arch = "x86_64"
runtimes = ["cargo", "nix"]
trusted = true

[ci.runners.windows_atlas]
platform = "forgejo"
labels = ["windows-atlas"]
os = "windows"
arch = "x86_64"
runtimes = ["cargo"]

[ci.defaults.forgejo]
cargo = "atlas"
nix = "atlas"
release = "atlas"
windows = "windows_atlas"
"#
}

fn write_config(root: &TempDir, text: &str) {
    let dir = root.path().join("simit");
    fs::create_dir_all(&dir).unwrap();
    fs::write(dir.join("config.toml"), text).unwrap();
}

fn simit_with_xdg(root: &TempDir) -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_simit"));
    command
        .env("XDG_CONFIG_HOME", root.path())
        .env("XDG_DATA_HOME", root.path().join("data"))
        .env("HOME", root.path().join("home"));
    command
}

#[test]
fn user_config_path_uses_xdg_config_home() {
    let _guard = env_lock().lock().unwrap();
    let temp = TempDir::new().unwrap();
    let old_xdg = env::var_os("XDG_CONFIG_HOME");
    let old_home = env::var_os("HOME");
    unsafe {
        env::set_var("XDG_CONFIG_HOME", temp.path());
        env::set_var("HOME", temp.path().join("home"));
    }

    assert_eq!(UserConfig::path(), temp.path().join("simit/config.toml"));

    unsafe {
        match old_xdg {
            Some(value) => env::set_var("XDG_CONFIG_HOME", value),
            None => env::remove_var("XDG_CONFIG_HOME"),
        }
        match old_home {
            Some(value) => env::set_var("HOME", value),
            None => env::remove_var("HOME"),
        }
    }
}

#[test]
fn user_config_path_falls_back_to_home_config() {
    let _guard = env_lock().lock().unwrap();
    let temp = TempDir::new().unwrap();
    let old_xdg = env::var_os("XDG_CONFIG_HOME");
    let old_home = env::var_os("HOME");
    unsafe {
        env::remove_var("XDG_CONFIG_HOME");
        env::set_var("HOME", temp.path());
    }

    assert_eq!(
        UserConfig::path(),
        temp.path().join(".config/simit/config.toml")
    );

    unsafe {
        match old_xdg {
            Some(value) => env::set_var("XDG_CONFIG_HOME", value),
            None => env::remove_var("XDG_CONFIG_HOME"),
        }
        match old_home {
            Some(value) => env::set_var("HOME", value),
            None => env::remove_var("HOME"),
        }
    }
}

#[test]
fn validates_and_resolves_multi_runtime_runner() {
    let config: UserConfig = toml_edit::de::from_str(valid_config()).unwrap();
    config.validate().unwrap();

    let runners = config
        .resolve_ci_runners(Platform::Forgejo, Runtime::Nix, None, None, true)
        .unwrap();

    assert_eq!(runners.ci.labels, vec!["atlas"]);
    assert_eq!(runners.release.labels, vec!["atlas"]);
    assert_eq!(
        runners.windows.as_ref().unwrap().labels,
        vec!["windows-atlas"]
    );
}

#[test]
fn rejects_default_that_points_to_wrong_runtime() {
    let config =
        valid_config().replace("runtimes = [\"cargo\", \"nix\"]", "runtimes = [\"cargo\"]");
    let config: UserConfig = toml_edit::de::from_str(&config).unwrap();

    let err = config.validate().unwrap_err();

    assert!(format!("{err:#}").contains("does not support runtime `nix`"));
}

#[test]
fn rejects_empty_labels() {
    let config = valid_config().replace("labels = [\"atlas\"]", "labels = []");
    let config: UserConfig = toml_edit::de::from_str(&config).unwrap();

    let err = config.validate().unwrap_err();

    assert!(format!("{err:#}").contains("must define at least one label"));
}

#[test]
fn config_cli_path_show_check_and_init_work() {
    let temp = TempDir::new().unwrap();

    let path_output = simit_with_xdg(&temp)
        .args(["config", "path"])
        .output()
        .unwrap();
    assert!(path_output.status.success());
    assert_eq!(
        String::from_utf8(path_output.stdout).unwrap().trim(),
        temp.path().join("simit/config.toml").display().to_string()
    );

    let init_output = simit_with_xdg(&temp)
        .args(["config", "init"])
        .output()
        .unwrap();
    assert!(init_output.status.success());
    assert!(temp.path().join("simit/config.toml").exists());

    let check_output = simit_with_xdg(&temp)
        .args(["config", "check"])
        .output()
        .unwrap();
    assert!(check_output.status.success());
    assert!(
        String::from_utf8(check_output.stdout)
            .unwrap()
            .contains("is valid")
    );

    let show_output = simit_with_xdg(&temp)
        .args(["config", "show"])
        .output()
        .unwrap();
    assert!(show_output.status.success());
    let show = String::from_utf8(show_output.stdout).unwrap();
    assert!(show.contains("[ci.runners.forgejo_linux]"));

    let second_init = simit_with_xdg(&temp)
        .args(["config", "init"])
        .output()
        .unwrap();
    assert!(!second_init.status.success());
    assert!(
        String::from_utf8(second_init.stderr)
            .unwrap()
            .contains("already exists")
    );
}

#[test]
fn config_cli_check_rejects_invalid_config() {
    let temp = TempDir::new().unwrap();
    write_config(
        &temp,
        r#"[ci.runners.bad]
platform = "forgejo"
labels = []
os = "linux"
arch = "x86_64"
runtimes = ["cargo"]
"#,
    );

    let output = simit_with_xdg(&temp)
        .args(["config", "check"])
        .output()
        .unwrap();

    assert!(!output.status.success());
    assert!(
        String::from_utf8(output.stderr)
            .unwrap()
            .contains("must define at least one label")
    );
}