use std::error::Error as StdError;
use std::os::unix::process::ExitStatusExt as _;
use std::process::ExitStatus;
use super::{Error, ErrorKind, SENSITIVE_OUTPUT_WITHHELD, ServerGoneKind};
use crate::Command;
use crate::command::{CommandResult, ProcessStatus, RequestId};
#[test]
fn a_server_that_is_not_there_is_not_a_refusal() {
for (stderr, expected, retryable) in [
(
"no server running on /tmp/libtmux-rs-dev/absent",
ServerGoneKind::NotRunning,
true,
),
(
"error connecting to /tmp/libtmux-rs-dev/absent (Connection refused)",
ServerGoneKind::Unreachable,
true,
),
("server exited unexpectedly", ServerGoneKind::Lost, false),
("server exited", ServerGoneKind::Stopped, false),
] {
let error = Error::refused("list-sessions", Some(1), stderr.to_owned(), None);
assert_eq!(error.kind(), ErrorKind::ServerGone, "{stderr}");
assert!(
matches!(&error, Error::ServerGone { kind, .. } if *kind == expected),
"{stderr} should be {expected:?}, got {error:?}",
);
assert!(!error.is_object_gone(), "{stderr}");
assert_eq!(
error.is_transient(),
retryable,
"only a failure before connecting proves replay safe: {stderr}",
);
}
}
#[test]
fn only_resource_limited_spawn_failures_invite_unchanged_replay() {
let spawn = |kind| {
Error::spawn(
1,
Command::new("display-message").summary(),
std::io::Error::from(kind),
false,
)
};
for kind in [
std::io::ErrorKind::Interrupted,
std::io::ErrorKind::WouldBlock,
std::io::ErrorKind::OutOfMemory,
std::io::ErrorKind::ResourceBusy,
std::io::ErrorKind::ExecutableFileBusy,
] {
assert!(spawn(kind).is_transient(), "{kind:?} may clear on retry");
}
for kind in [
std::io::ErrorKind::PermissionDenied,
std::io::ErrorKind::InvalidData,
] {
assert!(
!spawn(kind).is_transient(),
"{kind:?} needs repair, not replay",
);
}
}
#[cfg(feature = "control-mode")]
#[test]
fn control_mode_debug_distinguishes_the_source_kind() {
let error = Error::control_mode(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
assert_eq!(
format!("{error:?}"),
"ControlMode { kind: Transport, source_kind: Some(PermissionDenied) }",
);
}
#[test]
fn an_error_after_an_effect_cannot_invite_replay_or_be_relabelled() {
let error = Error::Overloaded {
request_id: 7,
command: Command::new("list-panes").summary(),
in_flight: 1,
}
.after_effect("resize-pane");
assert_eq!(error.kind(), ErrorKind::PartialEffect);
assert!(!error.is_object_gone());
assert!(!error.is_transient());
let error = error.after_effect("select-pane");
assert!(
matches!(
&error,
Error::AfterEffect { operation: "resize-pane", source }
if source.kind() == ErrorKind::Refused && source.is_transient()
),
"the existing, more specific effect is the useful replay boundary: {error:?}",
);
}
#[test]
fn an_effect_boundary_preserves_a_redacted_source_without_growing() {
let secret = "sentinel-after-effect-secret";
let command = Command::new("send-keys").sensitive_arg(secret);
let result = CommandResult::new(
RequestId::new(9),
command.summary(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(1 << 8)),
Vec::new(),
format!("bad key: {secret}\n").into_bytes(),
);
let error = result
.refusal_for("send-keys")
.expect("the sensitive command was refused")
.after_effect("send-keys")
.after_effect("plan");
let source = StdError::source(&error).expect("the boundary exposes its source");
let source = source
.downcast_ref::<Box<Error>>()
.expect("the source owns a libtmux::Error")
.as_ref();
assert_eq!(source.kind(), ErrorKind::Refused);
assert!(source.source().is_none(), "the source chain has one edge");
assert!(matches!(
&error,
Error::AfterEffect {
operation: "send-keys",
..
}
));
for diagnostic in [
error.to_string(),
format!("{error:?}"),
source.to_string(),
format!("{source:?}"),
] {
assert!(!diagnostic.contains(secret), "{diagnostic}");
}
}
#[test]
fn a_lost_server_is_not_read_as_one_that_stopped() {
let error = Error::refused(
"new-session",
Some(1),
"server exited unexpectedly".to_owned(),
None,
);
assert!(
matches!(&error, Error::ServerGone { kind, .. } if *kind == ServerGoneKind::Lost),
"{error:?}",
);
}
#[test]
fn a_refusal_that_names_no_server_stays_a_refusal() {
let error = Error::refused(
"delete-buffer",
Some(1),
"no buffer never-existed".to_owned(),
None,
);
assert_eq!(error.kind(), ErrorKind::Refused, "{error:?}");
}
#[test]
fn withheld_refusal_uses_the_payload_appropriate_variant() {
let error = Error::refused_withheld("set-option", Some(1));
assert!(matches!(&error, Error::CommandFailed { .. }), "{error:?}");
assert_eq!(error.kind(), ErrorKind::Refused);
}
#[test]
fn a_sensitive_mismatched_target_echo_stays_withheld() {
let secret = "sentinel-sensitive-echo";
let target = std::ffi::OsStr::new("%9");
let command = Command::new("send-keys")
.arg("-t")
.arg(target)
.sensitive_arg("sentinel-sensitive-input");
let result = CommandResult::new(
RequestId::new(1),
command.summary(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(1 << 8)),
Vec::new(),
format!("can't find pane: %9 {secret}\n").into_bytes(),
);
let error = Error::from_refused_result("send-keys", &result, Some(target));
assert!(
matches!(
&error,
Error::CommandFailed { stderr, .. } if stderr == SENSITIVE_OUTPUT_WITHHELD
),
"{error:?}",
);
let diagnostic = format!("{error:?} {error}");
for sensitive in [secret, "sentinel-sensitive-input"] {
assert!(!diagnostic.contains(sensitive), "{diagnostic}");
}
}
#[test]
fn a_missing_coordinate_is_not_a_missing_object() {
for (stderr, gone) in [
("can't find window: @99", true),
("can't find pane: %99", true),
("can't find session: $99", true),
("can't find session: nosuchsession", true),
("can't find window: 9", false),
("can't find window: nosuchname", false),
("can't find pane: 9", false),
] {
let error = Error::refused(
"unlink-window",
Some(1),
stderr.to_owned(),
Some(std::ffi::OsStr::new("home:9")),
);
assert_eq!(
error.is_object_gone(),
gone,
"{stderr} should report gone={gone}, got {error:?}",
);
}
}
#[test]
fn a_missing_coordinate_reports_the_target_that_was_sent() {
let error = Error::refused(
"unlink-window",
Some(1),
"can't find window: 9".to_owned(),
Some(std::ffi::OsStr::new("home:9")),
);
assert!(
matches!(&error, Error::LinkGone { target, .. } if target == "home:9"),
"{error:?}",
);
assert_eq!(error.to_string(), "tmux has no window at home:9");
}