use anyhow::Result;
use vcs_runner::RunError;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Listing {
Found(bool),
Hung,
}
pub(crate) fn read_listing(name: &str, result: Result<String, RunError>) -> Result<Listing> {
match result {
Ok(stdout) => Ok(Listing::Found(
stdout.lines().any(|line| line.split_whitespace().next().is_some_and(|first| first == name)),
)),
Err(ref e) if e.is_timeout() => Ok(Listing::Hung),
Err(ref e) if is_no_sessions_error(e) => Ok(Listing::Found(false)),
Err(e) => Err(e.into()),
}
}
fn is_no_sessions_error(err: &RunError) -> bool {
err.stderr().is_some_and(|s| s.contains("No active zellij sessions"))
}
pub(crate) fn delete_hung<T>(result: &Result<T, RunError>) -> bool {
matches!(result, Err(e) if e.is_timeout())
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use vcs_runner::Cmd;
fn non_zero_exit_with_stderr(stderr: &str) -> RunError {
let script = format!("printf %s {} 1>&2; exit 1", shell_single_quote(stderr));
Cmd::new("sh").args(["-c", &script]).timeout(Duration::from_secs(5)).run().expect_err("expected non-zero exit")
}
fn shell_single_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for ch in s.chars() {
if ch == '\'' {
out.push_str("'\\''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
fn timeout_error() -> RunError {
let err = Cmd::new("sleep").arg("60").timeout(Duration::from_millis(100)).run().expect_err("expected timeout");
assert!(err.is_timeout());
err
}
const LISTING: &str = "\
dev-api [Created 2h ago]
dev [Created 1h ago] (EXITED - attach to resurrect)
";
#[test]
fn a_session_is_found_only_by_its_exact_name() {
let read = |name: &str| read_listing(name, Ok(LISTING.to_string())).unwrap();
assert_eq!(read("dev"), Listing::Found(true));
assert_eq!(read("dev-api"), Listing::Found(true));
assert_eq!(read("de"), Listing::Found(false), "a prefix is not a match");
assert_eq!(read("other"), Listing::Found(false));
}
#[test]
fn a_hung_listing_is_reported_as_hung() {
assert_eq!(read_listing("dev", Err(timeout_error())).unwrap(), Listing::Hung);
}
#[test]
fn no_sessions_on_the_machine_means_not_found() {
let err = non_zero_exit_with_stderr("No active zellij sessions found.");
assert_eq!(read_listing("dev", Err(err)).unwrap(), Listing::Found(false));
}
#[test]
fn any_other_listing_failure_is_an_error() {
let err = non_zero_exit_with_stderr("some other zellij failure");
let err = read_listing("dev", Err(err)).expect_err("not a listing");
assert!(format!("{err:#}").contains("some other zellij failure"), "{err:#}");
}
#[test]
fn only_a_timed_out_delete_needs_recovery() {
assert!(delete_hung::<()>(&Err(timeout_error())));
assert!(!delete_hung::<()>(&Err(non_zero_exit_with_stderr("no such session"))));
assert!(!delete_hung(&Ok(())));
}
#[test]
fn no_sessions_error_recognized_on_fresh_machine() {
let err = non_zero_exit_with_stderr("No active zellij sessions found.");
assert!(is_no_sessions_error(&err));
}
#[test]
fn no_sessions_error_tolerates_punctuation_drift() {
let err = non_zero_exit_with_stderr("No active zellij sessions found");
assert!(is_no_sessions_error(&err));
}
#[test]
fn unrelated_non_zero_exit_is_not_no_sessions() {
let err = non_zero_exit_with_stderr("some other zellij failure");
assert!(!is_no_sessions_error(&err));
}
#[test]
fn timeout_error_is_not_no_sessions() {
assert!(!is_no_sessions_error(&timeout_error()));
}
#[test]
fn spawn_error_is_not_no_sessions() {
let err = Cmd::new("definitely-not-a-real-binary-zxqv-9001")
.timeout(Duration::from_secs(5))
.run()
.expect_err("expected spawn failure");
assert!(err.is_spawn_failure());
assert!(!is_no_sessions_error(&err));
}
}