use gwm::config::{
global_config_path, global_config_path_in, resolve_global_config_path, resolve_gwm_config_file, Config,
};
use std::path::Path;
use std::sync::Mutex;
use tempfile::TempDir;
fn env_lock() -> &'static Mutex<()> {
static LOCK: Mutex<()> = Mutex::new(());
&LOCK
}
fn write_repo(dir: &Path, contents: &str) {
std::fs::write(dir.join(".gwm.toml"), contents).unwrap();
}
fn write_global(dir: &Path, contents: &str) -> std::path::PathBuf {
let p = dir.join("config.toml");
std::fs::write(&p, contents).unwrap();
p
}
#[test]
fn global_config_path_lives_under_gwm_config_toml() {
let home = Path::new("/tmp/xdg-home");
assert_eq!(global_config_path_in(home), home.join("gwm").join("config.toml"));
}
fn touch_dotconfig(home: &Path) -> std::path::PathBuf {
let dir = home.join(".config").join("gwm");
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("config.toml");
std::fs::write(&cfg, "").unwrap();
cfg
}
#[test]
fn resolver_prefers_dotconfig_when_it_exists() {
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap(); let cfg = touch_dotconfig(home.path());
let resolved = resolve_global_config_path(None, Some(home.path()), Some(platform.path()), |p| p.exists());
assert_eq!(
resolved,
Some(cfg),
"the documented ~/.config path must win when present"
);
}
#[test]
fn resolver_falls_back_to_platform_dir_when_only_it_exists() {
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap();
let cfg = global_config_path_in(platform.path());
std::fs::create_dir_all(cfg.parent().unwrap()).unwrap();
std::fs::write(&cfg, "").unwrap();
let resolved = resolve_global_config_path(None, Some(home.path()), Some(platform.path()), |p| p.exists());
assert_eq!(
resolved,
Some(cfg),
"an existing platform-dir config must still resolve"
);
}
#[test]
fn resolver_returns_canonical_dotconfig_when_nothing_exists() {
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap();
let resolved = resolve_global_config_path(None, Some(home.path()), Some(platform.path()), |_| false);
assert_eq!(
resolved,
Some(global_config_path_in(&home.path().join(".config"))),
"with nothing on disk the canonical ~/.config path is reported"
);
}
#[test]
fn resolver_honours_xdg_outright_even_when_dotconfig_exists() {
let xdg = TempDir::new().unwrap();
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap();
touch_dotconfig(home.path());
let resolved = resolve_global_config_path(Some(xdg.path()), Some(home.path()), Some(platform.path()), |p| {
p.exists()
});
assert_eq!(resolved, Some(global_config_path_in(xdg.path())));
}
#[test]
fn resolver_linux_dotconfig_equals_platform_dir_is_unchanged() {
let home = TempDir::new().unwrap();
let platform = home.path().join(".config"); let resolved = resolve_global_config_path(None, Some(home.path()), Some(&platform), |_| false);
assert_eq!(resolved, Some(global_config_path_in(&platform)));
}
#[test]
fn shared_resolver_honours_filename_and_dotconfig() {
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap();
let dir = home.path().join(".config").join("gwm");
std::fs::create_dir_all(&dir).unwrap();
let cfg = dir.join("aliases.toml");
std::fs::write(&cfg, "").unwrap();
let resolved = resolve_gwm_config_file("aliases.toml", None, Some(home.path()), Some(platform.path()), |p| {
p.exists()
});
assert_eq!(
resolved,
Some(cfg),
"an arbitrary filename resolves under ~/.config when present"
);
let xdg = TempDir::new().unwrap();
let via_xdg = resolve_gwm_config_file(
"aliases.toml",
Some(xdg.path()),
Some(home.path()),
Some(platform.path()),
|p| p.exists(),
);
assert_eq!(via_xdg, Some(xdg.path().join("gwm").join("aliases.toml")));
}
#[test]
fn gwm_no_global_config_env_forces_repo_only() {
let _guard = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prev_flag = std::env::var("GWM_NO_GLOBAL_CONFIG").ok();
let prev_xdg = std::env::var("XDG_CONFIG_HOME").ok();
let xdg = TempDir::new().unwrap();
unsafe {
std::env::set_var("XDG_CONFIG_HOME", xdg.path());
std::env::set_var("GWM_NO_GLOBAL_CONFIG", "1");
}
assert_eq!(global_config_path(), None, "opt-out must suppress the global path");
unsafe {
std::env::set_var("GWM_NO_GLOBAL_CONFIG", "0");
}
assert_eq!(
global_config_path(),
Some(xdg.path().join("gwm").join("config.toml")),
"a falsey value must not opt out (path resolves under XDG)"
);
unsafe {
match prev_flag {
Some(v) => std::env::set_var("GWM_NO_GLOBAL_CONFIG", v),
None => std::env::remove_var("GWM_NO_GLOBAL_CONFIG"),
}
match prev_xdg {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
}
#[cfg(unix)]
#[test]
fn non_utf8_xdg_config_home_still_wins_outright() {
use std::os::unix::ffi::OsStrExt;
let _guard = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prev_flag = std::env::var_os("GWM_NO_GLOBAL_CONFIG");
let prev_xdg = std::env::var_os("XDG_CONFIG_HOME");
let raw = std::ffi::OsStr::from_bytes(b"/tmp/xdg-\xff-home");
let expected = Path::new(raw).join("gwm").join("config.toml");
unsafe {
std::env::remove_var("GWM_NO_GLOBAL_CONFIG");
std::env::set_var("XDG_CONFIG_HOME", raw);
}
let got = global_config_path();
unsafe {
match prev_flag {
Some(v) => std::env::set_var("GWM_NO_GLOBAL_CONFIG", v),
None => std::env::remove_var("GWM_NO_GLOBAL_CONFIG"),
}
match prev_xdg {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
assert_eq!(got, Some(expected), "non-UTF-8 XDG must win outright, not be dropped");
}
#[test]
fn no_files_resolve_to_default() {
let repo = TempDir::new().unwrap();
let missing = repo.path().join("nope.toml");
let cfg = Config::load_layered(repo.path(), Some(&missing)).unwrap();
assert!(cfg.theme.preset.is_none());
assert!(cfg.labels.is_empty());
}
#[test]
fn global_only_applies_when_repo_absent() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r#"
[theme]
preset = "catppuccin"
"#,
);
let repo = TempDir::new().unwrap(); let cfg = Config::load_layered(repo.path(), Some(&global)).unwrap();
assert_eq!(cfg.theme.preset.as_deref(), Some("catppuccin"));
}
#[test]
fn repo_only_ignores_a_missing_global() {
let repo = TempDir::new().unwrap();
write_repo(
repo.path(),
r#"
[theme]
preset = "gruvbox"
"#,
);
let missing = repo.path().join("no-global.toml");
let cfg = Config::load_layered(repo.path(), Some(&missing)).unwrap();
assert_eq!(cfg.theme.preset.as_deref(), Some("gruvbox"));
}
#[test]
fn repo_scalar_wins_over_global() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r#"
[theme]
preset = "catppuccin"
"#,
);
let repo = TempDir::new().unwrap();
write_repo(
repo.path(),
r#"
[theme]
preset = "tokyo-night"
"#,
);
let cfg = Config::load_layered(repo.path(), Some(&global)).unwrap();
assert_eq!(
cfg.theme.preset.as_deref(),
Some("tokyo-night"),
"repo .gwm.toml must win on a conflicting scalar"
);
}
#[test]
fn disjoint_tables_from_both_files_coexist() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r#"
[theme]
preset = "catppuccin"
"#,
);
let repo = TempDir::new().unwrap();
write_repo(
repo.path(),
r#"
[worktree]
base = "/tmp/custom-worktrees"
"#,
);
let cfg = Config::load_layered(repo.path(), Some(&global)).unwrap();
assert_eq!(cfg.theme.preset.as_deref(), Some("catppuccin"), "global theme survives");
assert_eq!(cfg.worktree.base, "/tmp/custom-worktrees", "repo worktree survives");
}
#[test]
fn nested_table_merges_key_by_key() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r##"
[theme]
preset = "catppuccin"
branch = "#111111"
"##,
);
let repo = TempDir::new().unwrap();
write_repo(
repo.path(),
r##"
[theme]
accent = "#222222"
"##,
);
let cfg = Config::load_layered(repo.path(), Some(&global)).unwrap();
assert_eq!(cfg.theme.preset.as_deref(), Some("catppuccin"), "global preset kept");
assert_eq!(
cfg.theme.overrides.get("branch").map(String::as_str),
Some("#111111"),
"untouched global override kept"
);
assert_eq!(
cfg.theme.overrides.get("accent").map(String::as_str),
Some("#222222"),
"repo override merged in"
);
}
#[test]
fn arrays_are_replaced_not_unioned() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r#"
[[labels]]
name = "global-label"
"#,
);
let repo = TempDir::new().unwrap();
write_repo(
repo.path(),
r#"
[[labels]]
name = "repo-label"
"#,
);
let cfg = Config::load_layered(repo.path(), Some(&global)).unwrap();
let names: Vec<&str> = cfg.labels.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, vec!["repo-label"], "repo array replaces global array");
}
#[test]
fn merged_result_is_validated() {
let gdir = TempDir::new().unwrap();
let global = write_global(
gdir.path(),
r#"
[theme]
accent = "not-a-color"
"#,
);
let repo = TempDir::new().unwrap(); let err = Config::load_layered(repo.path(), Some(&global)).unwrap_err();
assert!(
err.to_string().to_lowercase().contains("color"),
"expected a colour validation error, got: {err}"
);
}