#![allow(non_snake_case)]
#![allow(clippy::await_holding_lock)]
#[path = "common/mod.rs"]
mod common;
use std::time::Duration;
use rusty_autossh::pidfile::write_pid;
use rusty_autossh::{CompatibilityMode, MonitorMode, SshSupervisorBuilder, SupervisorEvent};
use tokio::sync::mpsc;
#[test]
fn pidfile_atomic_write_format() {
let (_td, root) = common::sandbox();
let path = common::temp_pidfile_path(&root);
let guard = write_pid(path.clone(), 12345).expect("write_pid succeeds");
let contents = std::fs::read_to_string(&path).expect("pidfile readable");
assert_eq!(contents, "12345\n", "decimal pid + newline format");
assert_eq!(guard.path(), path.as_path());
drop(guard);
assert!(
!path.exists(),
"PidfileGuard::drop removes the pidfile on normal exit"
);
}
#[test]
fn pidfile_guard_drops_on_panic() {
let (_td, root) = common::sandbox();
let path = common::temp_pidfile_path(&root);
let path_clone = path.clone();
let result = std::panic::catch_unwind(move || {
let _guard = write_pid(path_clone, 99999).expect("write_pid succeeds");
assert!(_guard.path().exists());
panic!("intentional panic to exercise Drop unwinding");
});
assert!(result.is_err(), "panic was caught");
assert!(
!path.exists(),
"Drop ran during unwinding and removed the pidfile"
);
}
#[test]
fn stale_pidfile_overwritten_at_startup() {
let (_td, root) = common::sandbox();
let path = common::temp_pidfile_path(&root);
std::fs::write(&path, b"garbage contents from prior run\n").expect("seed stale pidfile");
assert!(path.exists());
let guard = write_pid(path.clone(), 4242).expect("write_pid overwrites stale");
let contents = std::fs::read_to_string(&path).expect("pidfile readable");
assert_eq!(contents, "4242\n");
drop(guard);
}
#[test]
fn dash_f_forces_gatetime_zero_default_mode() {
use rusty_autossh::clock::{ClockFlags, EnvSnapshot, PollClock};
use std::collections::HashMap;
use std::ffi::OsString;
let mut vars = HashMap::new();
vars.insert("AUTOSSH_GATETIME".to_string(), OsString::from("99"));
let env = EnvSnapshot { vars };
let flags = ClockFlags {
gate_time: Some(Duration::from_secs(99)),
..ClockFlags::default()
};
let clock = PollClock::resolve_from_env_and_flags(&env, &flags, true);
assert_eq!(
clock.gate_time,
Duration::ZERO,
"default mode: -f overrides env + flag → gate_time = 0"
);
}
#[test]
fn dash_f_forces_gatetime_zero_strict_mode() {
use rusty_autossh::clock::{ClockFlags, EnvSnapshot, PollClock};
use std::collections::HashMap;
use std::ffi::OsString;
let mut vars = HashMap::new();
vars.insert("AUTOSSH_GATETIME".to_string(), OsString::from("99"));
let env = EnvSnapshot { vars };
let clock = PollClock::resolve_from_env_and_flags(&env, &ClockFlags::default(), true);
assert_eq!(
clock.gate_time,
Duration::ZERO,
"strict mode: -f overrides env → gate_time = 0"
);
}
#[test]
fn logfile_default_mode_writer_initialized() {
let (_td, root) = common::sandbox();
let log_path = common::temp_logfile_path(&root);
let guard =
rusty_autossh::logging::init_logfile(Some(log_path.clone()), CompatibilityMode::Default)
.expect("init_logfile succeeds for writable path");
assert!(
guard.is_some(),
"Default mode wraps writer in tracing-appender non_blocking → returns WorkerGuard"
);
drop(guard);
}
#[test]
fn logfile_strict_mode_no_worker_guard() {
let (_td, root) = common::sandbox();
let log_path = common::temp_logfile_path(&root);
let guard =
rusty_autossh::logging::init_logfile(Some(log_path.clone()), CompatibilityMode::Strict)
.expect("init_logfile succeeds for writable path");
assert!(
guard.is_none(),
"Strict mode does NOT install tracing-appender → returns None"
);
assert!(
log_path.exists(),
"Strict mode init_logfile creates the file via OpenOptions::append"
);
}
#[test]
fn logfile_unwritable_falls_back_to_stderr() {
let (_td, root) = common::sandbox();
let nonexistent = root.join("does_not_exist").join("subdir").join("log");
let guard = rusty_autossh::logging::init_logfile(Some(nonexistent), CompatibilityMode::Default)
.expect("init_logfile returns Ok(None) on unwritable path per FR-032");
assert!(
guard.is_none(),
"fallback path returns None; supervisor continues without logfile"
);
}
#[cfg(unix)]
#[test]
fn dash_f_pidfile_lifecycle_unix() {
use std::time::Instant;
let _lock = common::env_lock().lock().unwrap_or_else(|p| p.into_inner());
let (_td, root) = common::sandbox();
let pid_path = common::temp_pidfile_path(&root);
let echo = common::echo_child_path();
let mut cmd = common::rusty_autossh_cmd();
cmd.env("AUTOSSH_PATH", echo)
.env("AUTOSSH_PIDFILE", &pid_path)
.env("AUTOSSH_MAXLIFETIME", "30")
.args(["-f", "-M", "0", "user@host"]);
let start = Instant::now();
let output = cmd.timeout(Duration::from_secs(5)).output();
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(5),
"parent should exit within 5s after daemonize"
);
let _ = output;
if pid_path.exists() {
if let Ok(s) = std::fs::read_to_string(&pid_path) {
if let Ok(pid) = s.trim().parse::<i32>() {
unsafe {
libc_kill(pid, 15);
}
}
}
let deadline = Instant::now() + Duration::from_secs(2);
while Instant::now() < deadline && pid_path.exists() {
std::thread::sleep(Duration::from_millis(100));
}
}
}
#[cfg(unix)]
unsafe extern "C" {
#[link_name = "kill"]
fn libc_kill(pid: i32, sig: i32) -> i32;
}
#[tokio::test(flavor = "current_thread")]
async fn max_lifetime_clean_exit_removes_pidfile() {
let _lock = common::env_lock().lock().unwrap_or_else(|p| p.into_inner());
let (_td, root) = common::sandbox();
let pid_path = common::temp_pidfile_path(&root);
let echo = common::echo_child_path();
let (tx, _rx) = mpsc::channel::<SupervisorEvent>(32);
let _g = common::env_guard("RUSTY_AUTOSSH_TEST_BEHAVIOR", Some("exit_zero"));
let mut sup = SshSupervisorBuilder::new()
.ssh_args(vec![])
.ssh_path(echo)
.monitor_mode(MonitorMode::None)
.max_lifetime(Some(Duration::from_secs(10)))
.event_sender(tx)
.pidfile_path(pid_path.clone())
.build()
.expect("builder ok");
let result = tokio::time::timeout(Duration::from_secs(10), sup.run()).await;
assert!(
result.is_ok(),
"supervisor.run() should return within 10s on exit_zero + MonitorMode::None"
);
drop(sup);
assert!(
!pid_path.exists(),
"pidfile must be removed on clean exit (PidfileGuard::Drop runs)"
);
drop(_g);
}