use super::*;
use serial_test::serial;
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]
#[serial(update_check_throttled_env)]
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]
#[serial(update_check_throttled_env)]
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]
#[serial(update_check_throttled_env)]
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}"
);
}
#[cfg(unix)]
async fn with_real_stdout_redirected_to<Fut, T>(tmp: &std::fs::File, f: Fut) -> T
where
Fut: std::future::Future<Output = T>,
{
use std::os::unix::io::AsRawFd;
struct RestoreStdout(std::os::raw::c_int);
impl Drop for RestoreStdout {
fn drop(&mut self) {
unsafe {
libc::dup2(self.0, libc::STDOUT_FILENO);
libc::close(self.0);
}
}
}
let saved = unsafe { libc::dup(libc::STDOUT_FILENO) };
assert!(saved >= 0, "dup(STDOUT_FILENO) failed");
let _restore = RestoreStdout(saved);
let rc = unsafe { libc::dup2(tmp.as_raw_fd(), libc::STDOUT_FILENO) };
assert!(rc >= 0, "dup2(tmp, STDOUT_FILENO) failed");
f.await
}
#[cfg(unix)]
fn run_isolated_inner_test(inner_test_name: &str) {
let exe = std::env::current_exe().expect("resolve current test binary");
let output = std::process::Command::new(&exe)
.args([inner_test_name, "--exact", "--ignored", "--test-threads=1"])
.output()
.expect("re-exec test binary for isolated fd test");
assert!(
output.status.success(),
"isolated test `{inner_test_name}` failed ({}):\n--- stdout ---\n{}\n--- stderr ---\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[cfg(unix)]
#[tokio::test]
#[ignore]
async fn run_with_mode_capture_never_leaks_to_parent_stdio_inner() {
use std::io::Read;
let tmp = tempfile::NamedTempFile::new().expect("create tempfile");
let mut cmd = tokio::process::Command::new("sh");
cmd.args([
"-c",
"for i in $(seq 1 20); do echo \"LEAK_MARKER_3830 stdout $i\"; \
echo \"LEAK_MARKER_3830 stderr $i\" >&2; done; exit 0",
]);
let result = with_real_stdout_redirected_to(
tmp.as_file(),
super::upgrade::run_with_mode(
cmd,
super::upgrade::UpgradeOutput::Capture,
"`noisy-capture`",
),
)
.await;
assert!(result.is_ok(), "expected Ok, got {result:?}");
let mut contents = String::new();
tmp.reopen()
.expect("reopen tempfile")
.read_to_string(&mut contents)
.expect("read tempfile");
assert!(
contents.is_empty(),
"Capture mode leaked to the parent's real stdout fd: {contents:?}"
);
}
#[cfg(unix)]
#[test]
fn run_with_mode_capture_never_leaks_to_parent_stdio() {
run_isolated_inner_test(
"update::tests::run_with_mode_capture_never_leaks_to_parent_stdio_inner",
);
}
#[cfg(unix)]
#[tokio::test]
#[ignore]
async fn run_with_mode_inherit_leaks_to_parent_stdio_inner() {
use std::io::Read;
let tmp = tempfile::NamedTempFile::new().expect("create tempfile");
let mut cmd = tokio::process::Command::new("sh");
cmd.args(["-c", "echo 'LEAK_MARKER_3830 inherit-path'; exit 0"]);
let result = with_real_stdout_redirected_to(
tmp.as_file(),
super::upgrade::run_with_mode(
cmd,
super::upgrade::UpgradeOutput::Inherit,
"`noisy-inherit`",
),
)
.await;
assert!(result.is_ok(), "expected Ok, got {result:?}");
let mut contents = String::new();
tmp.reopen()
.expect("reopen tempfile")
.read_to_string(&mut contents)
.expect("read tempfile");
assert!(
contents.contains("LEAK_MARKER_3830 inherit-path"),
"expected Inherit mode to leak to the parent's real stdout fd (the fd-redirection \
technique may be broken — this test found nothing, which would make the sibling \
Capture test's negative result meaningless): {contents:?}"
);
}
#[cfg(unix)]
#[test]
fn run_with_mode_inherit_leaks_to_parent_stdio() {
run_isolated_inner_test("update::tests::run_with_mode_inherit_leaks_to_parent_stdio_inner");
}
#[tokio::test]
async fn run_with_mode_capture_folds_stderr_into_error() {
let mut cmd = tokio::process::Command::new("sh");
cmd.args([
"-c",
"echo 'DISTINCTIVE_MARKER_3830_COMMON: cargo install failed' >&2; exit 7",
]);
let err = super::upgrade::run_with_mode(
cmd,
super::upgrade::UpgradeOutput::Capture,
"`fake cargo install`",
)
.await
.expect_err("expected Err");
let msg = err.to_string();
assert!(
msg.contains("DISTINCTIVE_MARKER_3830_COMMON: cargo install failed"),
"error message did not fold in captured stderr: {msg}"
);
assert!(msg.contains("fake cargo install"));
}
#[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]
#[serial(update_verify_installed_binary_env)]
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]
#[serial(update_verify_installed_binary_env)]
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]
#[serial(update_verify_installed_binary_env)]
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]
#[serial(update_verify_installed_binary_env)]
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 _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
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"
);
}
#[cfg(unix)]
fn write_versioned_binary(path: &std::path::Path, version_line: &str) {
use std::os::unix::fs::PermissionsExt;
std::fs::write(path, format!("#!/bin/sh\necho '{version_line}'\nexit 0\n"))
.expect("write versioned 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)]
#[tokio::test]
async fn verify_installed_binary_at_path_reads_exact_binary_despite_path_shadow() {
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_versioned_binary(&cargo_bin.join("tm"), "trusty-mpm 0.19.26");
let local_bin = tmp.path().join(".local").join("bin");
std::fs::create_dir_all(&local_bin).expect("mkdir .local/bin");
let install_path = local_bin.join("tm");
write_versioned_binary(&install_path, "trusty-mpm 0.19.29");
let snapshot = set_home_env(tmp.path(), None);
let stale = super::verify_installed_binary("tm").await;
let stale_bin_path = super::upgrade::candidate_bin_dirs(Some(tmp.path()), None)
.into_iter()
.map(|d| d.join("tm"))
.find(|p| p.exists())
.expect("candidate_bin_dirs must find a `tm` binary");
restore_home_env(snapshot);
assert!(
stale.is_ok(),
"the stale binary must itself be a healthy exit 0"
);
assert_eq!(
stale_bin_path,
cargo_bin.join("tm"),
"name-based resolution must pick ~/.cargo/bin FIRST (the #3554 mechanism) \
— got {stale_bin_path:?}"
);
let reported = super::verify_installed_binary_at_path(&install_path)
.await
.expect("health gate must pass against the concrete install path");
assert!(
reported.contains("0.19.29"),
"must read the NEW binary's version even though an older copy exists \
at a higher-priority path; got: {reported:?}"
);
assert!(
!reported.contains("0.19.26"),
"must NOT read the stale shadowed binary; got: {reported:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn verify_installed_binary_at_path_fails_for_missing_binary() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("does-not-exist").join("tm");
let result = super::verify_installed_binary_at_path(&missing).await;
assert!(
result.is_err(),
"a missing binary must be a hard error, not a silent pass"
);
}
#[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");
}