use super::*;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
static ENV_LOCK: Mutex<()> = Mutex::new(());
static UNIQUE_CRATE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn semver_newer_returns_true() {
assert!(is_newer("0.20.0", "0.19.0"));
assert!(is_newer("1.0.0", "0.99.99"));
assert!(is_newer("0.19.1", "0.19.0"));
}
#[test]
fn semver_equal_returns_false() {
assert!(!is_newer("0.19.0", "0.19.0"));
}
#[test]
fn semver_older_returns_false() {
assert!(!is_newer("0.18.0", "0.19.0"));
assert!(!is_newer("0.19.0", "1.0.0"));
}
#[test]
fn semver_prerelease_stripped() {
assert!(!is_newer("0.19.0-beta.1", "0.19.0"));
assert!(is_newer("0.20.0-alpha.1", "0.19.0"));
}
#[test]
fn semver_parse_strips_prerelease() {
assert_eq!(parse_version("1.2.3-beta.1"), Some((1, 2, 3)));
assert_eq!(parse_version("1.2.3+build.42"), Some((1, 2, 3)));
assert_eq!(parse_version("1.2.3-rc.1+sha.abc"), Some((1, 2, 3)));
}
#[test]
fn semver_parse_handles_missing_patch() {
assert_eq!(parse_version("1.2"), Some((1, 2, 0)));
assert_eq!(parse_version("1"), Some((1, 0, 0)));
}
#[test]
fn semver_parse_rejects_garbage() {
assert_eq!(parse_version("not-a-version"), None);
assert_eq!(parse_version(""), None);
}
#[test]
fn notice_formats_correctly() {
let info = UpdateInfo {
crate_name: "trusty-search".to_owned(),
current: "0.19.0".to_owned(),
latest: "0.20.0".to_owned(),
};
let n = notice(&info);
assert!(n.contains("trusty-search"), "crate name missing: {n}");
assert!(n.contains("0.20.0"), "latest version missing: {n}");
assert!(n.contains("0.19.0"), "current version missing: {n}");
assert!(n.contains("cargo install"), "install command missing: {n}");
assert!(n.contains("--locked"), "--locked flag missing: {n}");
}
#[tokio::test]
async fn check_throttled_skips_when_no_update_check_set() {
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::set_var(NO_UPDATE_CHECK_ENV, "1") };
}
let result = check_throttled("trusty-search", "0.19.0").await;
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::remove_var(NO_UPDATE_CHECK_ENV) };
}
assert!(
result.is_none(),
"expected None when {NO_UPDATE_CHECK_ENV} is set"
);
}
#[tokio::test]
async fn check_throttled_skips_when_ci_set() {
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::set_var(CI_ENV, "true") };
}
let result = check_throttled("trusty-search", "0.19.0").await;
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::remove_var(CI_ENV) };
}
assert!(result.is_none(), "expected None when CI is set");
}
async fn run_cache_freshness_test(
last_check_unix: u64,
latest_version: &str,
current_version: &str,
expected_is_some: bool,
) {
let unique_crate = format!(
"test-crate-{}-{}",
std::process::id(),
UNIQUE_CRATE_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let entry = CacheEntry {
last_check_unix,
latest_version: latest_version.to_owned(),
};
write_cache(&unique_crate, &entry);
let result = check_throttled(&unique_crate, current_version).await;
assert_eq!(
result.is_some(),
expected_is_some,
"freshness={expected_is_some}: latest={latest_version} current={current_version}"
);
let _ = std::fs::remove_file(cache_path(&unique_crate));
}
#[tokio::test]
async fn cache_fresh_returns_some_when_newer() {
run_cache_freshness_test(now_unix_secs() - 3600, "1.0.0", "0.19.0", true).await;
}
#[tokio::test]
async fn cache_fresh_returns_none_when_current() {
run_cache_freshness_test(now_unix_secs() - 3600, "0.19.0", "0.19.0", false).await;
}
#[test]
fn corrupt_cache_returns_none() {
let unique_crate = format!("corrupt-test-{}", std::process::id());
let path = cache_path(&unique_crate);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&path, b"this is not valid json {{{{");
let result = read_cache(&unique_crate);
let _ = std::fs::remove_file(&path);
assert!(result.is_none(), "corrupt cache must yield None");
}
#[test]
fn missing_cache_returns_none() {
let unique_crate = format!("missing-test-{}", std::process::id());
let _ = std::fs::remove_file(cache_path(&unique_crate));
let result = read_cache(&unique_crate);
assert!(result.is_none(), "missing cache must yield None");
}
#[test]
fn cache_round_trip() {
let unique_crate = format!("roundtrip-{}", std::process::id());
let entry = CacheEntry {
last_check_unix: 1_700_000_000,
latest_version: "9.9.9".to_owned(),
};
write_cache(&unique_crate, &entry);
let back = read_cache(&unique_crate);
let _ = std::fs::remove_file(cache_path(&unique_crate));
let back = back.expect("cache round-trip should succeed");
assert_eq!(back.last_check_unix, 1_700_000_000);
assert_eq!(back.latest_version, "9.9.9");
}
#[tokio::test]
#[ignore]
async fn perform_upgrade_fails_cleanly_on_nonexistent_crate() {
let result = super::perform_upgrade("___trusty_test_crate_does_not_exist___").await;
assert!(
result.is_err(),
"expected Err for nonexistent crate, got Ok"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("cargo install") || msg.contains("status") || msg.contains("exited"),
"unexpected error message: {msg}"
);
}
#[tokio::test]
#[ignore]
async fn verify_installed_binary_passes_for_cargo() {
let result = super::verify_installed_binary("cargo").await;
assert!(
result.is_ok(),
"expected Ok for `cargo --version`, got: {:?}",
result
);
}
#[tokio::test]
async fn verify_installed_binary_fails_for_missing_binary() {
let result = super::verify_installed_binary("___no_such_binary_xyz_999___").await;
assert!(
result.is_err(),
"expected Err for non-existent binary, got Ok"
);
}
#[test]
fn candidate_bin_dirs_falls_back_to_dot_cargo() {
let home = std::path::Path::new("/home/tester");
let dirs = super::upgrade::candidate_bin_dirs(Some(home), None);
assert_eq!(dirs[0], home.join(".cargo").join("bin"));
}
#[test]
fn candidate_bin_dirs_prefers_cargo_home_override() {
let home = std::path::Path::new("/home/tester");
let dirs = super::upgrade::candidate_bin_dirs(Some(home), Some("/opt/custom-cargo"));
assert_eq!(
dirs[0],
std::path::PathBuf::from("/opt/custom-cargo").join("bin"),
"CARGO_HOME override must take priority over ~/.cargo/bin"
);
}
#[test]
fn candidate_bin_dirs_ignores_empty_cargo_home() {
let home = std::path::Path::new("/home/tester");
let dirs = super::upgrade::candidate_bin_dirs(Some(home), Some(""));
assert_eq!(
dirs[0],
home.join(".cargo").join("bin"),
"empty CARGO_HOME must fall back to ~/.cargo/bin, not be treated as a path"
);
}
#[test]
fn candidate_bin_dirs_includes_local_bin() {
let home = std::path::Path::new("/home/tester");
let dirs = super::upgrade::candidate_bin_dirs(Some(home), None);
assert!(
dirs.contains(&home.join(".local").join("bin")),
"~/.local/bin (the prebuilt installer's default) must be a candidate: {dirs:?}"
);
let local_idx = dirs
.iter()
.position(|d| d == &home.join(".local").join("bin"))
.expect("local bin present");
let cargo_idx = dirs
.iter()
.position(|d| d == &home.join(".cargo").join("bin"))
.expect("cargo bin present");
assert!(cargo_idx < local_idx, "cargo bin must be checked first");
}
#[test]
fn candidate_bin_dirs_empty_without_home_or_cargo_home() {
let dirs = super::upgrade::candidate_bin_dirs(None, None);
assert!(
dirs.is_empty(),
"no home and no CARGO_HOME override must yield zero candidates"
);
}
#[cfg(unix)]
fn write_fake_binary(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt;
std::fs::write(path, b"#!/bin/sh\necho fake 1.0.0\nexit 0\n").expect("write fake binary");
let mut perms = std::fs::metadata(path)
.expect("stat fake binary")
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod fake binary");
}
#[cfg(unix)]
struct EnvSnapshot {
home: Option<String>,
cargo_home: Option<String>,
path: Option<String>,
}
#[cfg(unix)]
fn set_home_env(home: &std::path::Path, cargo_home: Option<&str>) -> EnvSnapshot {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let snapshot = EnvSnapshot {
home: std::env::var("HOME").ok(),
cargo_home: std::env::var("CARGO_HOME").ok(),
path: std::env::var("PATH").ok(),
};
unsafe {
std::env::set_var("HOME", home);
match cargo_home {
Some(v) => std::env::set_var("CARGO_HOME", v),
None => std::env::remove_var("CARGO_HOME"),
}
}
snapshot
}
#[cfg(unix)]
fn restore_home_env(snapshot: EnvSnapshot) {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
match snapshot.home {
Some(h) => std::env::set_var("HOME", h),
None => std::env::remove_var("HOME"),
}
match snapshot.cargo_home {
Some(h) => std::env::set_var("CARGO_HOME", h),
None => std::env::remove_var("CARGO_HOME"),
}
match snapshot.path {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}
}
}
#[cfg(unix)]
#[tokio::test]
async fn verify_installed_binary_finds_binary_in_cargo_bin() {
let tmp = tempfile::tempdir().expect("tempdir");
let cargo_bin = tmp.path().join(".cargo").join("bin");
std::fs::create_dir_all(&cargo_bin).expect("mkdir .cargo/bin");
write_fake_binary(&cargo_bin.join("fake_trusty_bin_cargo"));
let snapshot = set_home_env(tmp.path(), None);
let result = super::verify_installed_binary("fake_trusty_bin_cargo").await;
restore_home_env(snapshot);
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[cfg(unix)]
#[tokio::test]
async fn verify_installed_binary_finds_binary_in_local_bin() {
let tmp = tempfile::tempdir().expect("tempdir");
let local_bin = tmp.path().join(".local").join("bin");
std::fs::create_dir_all(&local_bin).expect("mkdir .local/bin");
write_fake_binary(&local_bin.join("fake_trusty_bin_local"));
let snapshot = set_home_env(tmp.path(), None);
let result = super::verify_installed_binary("fake_trusty_bin_local").await;
restore_home_env(snapshot);
assert!(
result.is_ok(),
"expected Ok when binary only exists in ~/.local/bin, got {result:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn verify_installed_binary_honours_cargo_home_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let custom_cargo_home = tmp.path().join("custom-cargo-home");
let custom_bin = custom_cargo_home.join("bin");
std::fs::create_dir_all(&custom_bin).expect("mkdir custom cargo bin");
write_fake_binary(&custom_bin.join("fake_trusty_bin_cargo_home"));
let home_dir = tmp.path().join("home");
std::fs::create_dir_all(&home_dir).expect("mkdir home");
let snapshot = set_home_env(
&home_dir,
Some(custom_cargo_home.to_str().expect("utf8 path")),
);
let result = super::verify_installed_binary("fake_trusty_bin_cargo_home").await;
restore_home_env(snapshot);
assert!(
result.is_ok(),
"expected Ok when binary is under $CARGO_HOME/bin, got {result:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn verify_installed_binary_finds_binary_via_path() {
let home_tmp = tempfile::tempdir().expect("home tempdir");
let path_tmp = tempfile::tempdir().expect("path tempdir");
write_fake_binary(&path_tmp.path().join("fake_trusty_bin_path"));
let snapshot = set_home_env(home_tmp.path(), None);
let prev_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{prev_path}", path_tmp.path().display());
unsafe { std::env::set_var("PATH", &new_path) };
let result = super::verify_installed_binary("fake_trusty_bin_path").await;
restore_home_env(snapshot);
assert!(
result.is_ok(),
"expected Ok via PATH fallback, got {result:?}"
);
}
#[test]
fn is_launchd_supervised_returns_false_in_test_env() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("TERM_PROGRAM", "TestRunner");
std::env::remove_var("XPC_SERVICE_NAME");
}
let result = super::is_launchd_supervised();
unsafe {
std::env::remove_var("TERM_PROGRAM");
}
assert!(
!result,
"is_launchd_supervised returned true inside a test terminal env"
);
}
#[tokio::test]
#[ignore]
async fn live_crates_io_with_old_version_returns_some() {
let result = check_crates_io("trusty-search", "0.0.1").await;
assert!(
result.is_some(),
"expected Some(UpdateInfo) for old version 0.0.1 — is network available?"
);
let info = result.unwrap();
println!("crates.io returned: latest={}", info.latest);
assert!(
!info.latest.is_empty(),
"latest version should not be empty"
);
let n = notice(&info);
println!("Notice: {n}");
assert!(
n.contains("cargo install trusty-search --locked"),
"notice missing install cmd: {n}"
);
assert!(n.contains(&info.latest), "notice missing latest version");
assert!(n.contains("0.0.1"), "notice missing current version");
}