#![cfg(target_family = "unix")]
use std::process::Command;
use tempfile::tempdir;
fn bin_path() -> &'static str {
env!("CARGO_BIN_EXE_runex")
}
fn bash_available() -> bool {
let Ok(path) = which::which("bash") else { return false };
let out = Command::new(path)
.args(["--norc", "--noprofile", "-c", "echo $BASH_VERSION"])
.output();
let Ok(out) = out else { return false };
let ver = String::from_utf8_lossy(&out.stdout);
ver.trim()
.split('.')
.next()
.and_then(|s| s.parse::<u32>().ok())
.map(|major| major >= 4)
.unwrap_or(false)
}
fn run_init_bash(home: &std::path::Path) {
let bin = bin_path();
let cfg = home.join("config.toml");
std::fs::write(
&cfg,
"version = 1\n\n[keybind.trigger]\ndefault = \"space\"\n\n[[abbr]]\nkey = \"gst\"\nexpand = \"echo EXPANDED\"\n",
)
.unwrap();
let out = Command::new(bin)
.env("HOME", home)
.env("USERPROFILE", home)
.env("XDG_CACHE_HOME", home.join(".cache"))
.env("XDG_CONFIG_HOME", home.join(".config"))
.env_remove("PSModulePath")
.env("SHELL", "/bin/bash")
.args(["--config", cfg.to_str().unwrap(), "init", "bash", "--yes"])
.output()
.unwrap();
assert!(
out.status.success(),
"runex init bash --yes must succeed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn cache_is_no_op_in_non_interactive_bash() {
if !bash_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
run_init_bash(home);
let cache = home.join(".cache").join("runex").join("integration.bash");
assert!(cache.is_file(), "expected cache file at {}", cache.display());
let out = Command::new("bash")
.args([
"--norc",
"--noprofile",
"-c",
&format!(
"source {}; declare -F __runex_expand 2>/dev/null && echo DEFINED || echo UNDEFINED",
cache.display()
),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.trim_end().ends_with("UNDEFINED"),
"non-interactive bash must NOT define __runex_expand: stdout=`{stdout}`"
);
}
#[test]
fn cache_defines_expand_in_interactive_bash() {
if !bash_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
run_init_bash(home);
let cache = home.join(".cache").join("runex").join("integration.bash");
let out = Command::new("bash")
.args([
"--norc",
"--noprofile",
"-i",
"-c",
&format!(
"source {}; declare -F __runex_expand 2>/dev/null | grep -q __runex_expand && echo DEFINED || echo UNDEFINED",
cache.display()
),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("DEFINED"),
"interactive bash must define __runex_expand after sourcing the cache: stdout=`{stdout}`"
);
}
#[test]
fn cache_header_pins_version_and_bin() {
if !bash_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
run_init_bash(home);
let cache = home.join(".cache").join("runex").join("integration.bash");
let body = std::fs::read_to_string(&cache).unwrap();
let head: Vec<&str> = body.lines().take(3).collect();
assert!(
head.iter().any(|l| l.contains("runex-integration-version:")),
"cache must contain version header: head=\n{head:#?}"
);
assert!(
head.iter().any(|l| l.contains("runex-bin:")),
"cache must contain runex-bin: header: head=\n{head:#?}"
);
assert!(
head.iter().any(|l| l.contains("do not edit")),
"cache must contain `do not edit` notice: head=\n{head:#?}"
);
}
#[test]
fn rcfile_source_line_points_at_cache() {
if !bash_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
run_init_bash(home);
let bashrc = home.join(".bashrc");
let body = std::fs::read_to_string(&bashrc).unwrap();
assert!(
body.contains("# runex-init"),
"bashrc must contain the runex-init marker: {body}"
);
let cache = home.join(".cache").join("runex").join("integration.bash");
assert!(
body.contains(cache.to_str().unwrap()),
"bashrc must reference the cache path {}:\n{}",
cache.display(),
body
);
}
#[test]
fn init_cleans_up_stale_temp_from_previous_crash() {
if !bash_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
let cache_dir = home.join(".cache").join("runex");
std::fs::create_dir_all(&cache_dir).unwrap();
let stale_tmp = cache_dir.join(".integration.bash.runex.tmp");
std::fs::write(&stale_tmp, "leftover from a previous crash").unwrap();
run_init_bash(home);
let cache = cache_dir.join("integration.bash");
assert!(cache.is_file(), "init must produce the cache file");
assert!(
!stale_tmp.exists(),
"stale temp must be removed after a successful init"
);
}