use std::fmt;
use std::io;
use std::time::Duration;
use crate::CommandSummary;
use crate::version::{ReleaseVersion, TmuxVersion};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ServerConfigurationErrorKind {
ConflictingSocketSelectors,
InvalidSocketName,
InvalidSocketPath,
InvalidConfigPath,
InvalidColorMode,
WorkingDirectoryUnavailable,
SocketRootUnavailable,
NotInsideTmux,
MalformedTmuxVariable,
}
#[cfg_attr(
feature = "control-mode",
doc = r#"```
use libtmux::{ControlModeErrorKind, Error};
// `Closed` means the far side ended, often just the session going away. The
// rest mean the connection never worked. The variant is `#[non_exhaustive]`,
// so a caller matches it rather than building one.
fn session_ended(failure: &Error) -> bool {
matches!(
failure,
Error::ControlMode { kind: ControlModeErrorKind::Closed, .. },
)
}
let unrelated = libtmux::Server::builder()
.socket_name("named")
.socket_path("/tmp/libtmux-rs-dev/explicit")
.build()
.expect_err("two socket selectors");
assert!(!session_ended(&unrelated));
```"#
)]
#[cfg(feature = "control-mode")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ControlModeErrorKind {
Transport,
MissingPipes,
Closed,
UnrepresentableCommand,
}
pub(crate) const NO_CURRENT_TARGET: &str = "no current target";
pub(crate) const NO_SUCH_NEIGHBOUR: [&str; 4] = [
"no next window",
"no previous window",
"no last window",
"no last pane",
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OptionErrorKind {
Unknown,
Ambiguous,
BadValue,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ServerGoneKind {
NotRunning,
Unreachable,
Lost,
Stopped,
}
pub(crate) const NO_CURRENT_CLIENT: &str = "no current client";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
ObjectGone,
Refused,
ServerGone,
Timeout,
Unreachable,
UnsupportedVersion,
InvalidInput,
Transport,
Decode,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct IdParseError {
expected_sigil: char,
}
impl IdParseError {
pub(crate) const fn new(expected_sigil: char) -> Self {
Self { expected_sigil }
}
#[must_use]
pub const fn expected_sigil(self) -> char {
self.expected_sigil
}
}
impl fmt::Display for IdParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"invalid tmux ID: expected {} followed by an integer from 0 through {}",
self.expected_sigil,
u32::MAX,
)
}
}
impl std::error::Error for IdParseError {}
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[non_exhaustive]
#[error("invalid server configuration ({kind:?})")]
InvalidServerConfiguration {
kind: ServerConfigurationErrorKind,
},
#[error("invalid tmux version output")]
InvalidVersionOutput {
output_len: usize,
},
#[error("tmux {found} is below the minimum supported version {minimum}")]
UnsupportedTmuxVersion {
found: TmuxVersion,
minimum: ReleaseVersion,
},
#[error("tmux rejected the option: {detail}")]
OptionRejected {
kind: OptionErrorKind,
detail: String,
},
#[non_exhaustive]
#[error("tmux answered {format} with a value that is not an id: {detail}")]
UnreadableFormatValue {
format: &'static str,
detail: IdParseError,
},
#[error("the tmux server was replaced: expected {expected}, found {found}")]
ServerGenerationChanged {
expected: crate::ServerGeneration,
found: crate::ServerGeneration,
},
#[non_exhaustive]
#[error("{command} produced more than {limit} bytes on {stream} (request {request_id})")]
OutputLimitExceeded {
request_id: u64,
command: CommandSummary,
stream: &'static str,
limit: usize,
},
#[non_exhaustive]
#[error(
"{command} was not admitted: {in_flight} dispatches already running (request {request_id})"
)]
Overloaded {
request_id: u64,
command: CommandSummary,
in_flight: usize,
},
#[cfg(feature = "control-mode")]
#[non_exhaustive]
#[error("a control-mode {frame} grew past its {limit} byte budget")]
ControlModeFrameTooLarge {
frame: &'static str,
limit: usize,
},
#[error("a session named {name} already exists")]
SessionExists {
name: String,
},
#[error("{capability} needs tmux {needs} or newer, and this is {found}")]
UnsupportedCapability {
capability: &'static str,
needs: ReleaseVersion,
found: TmuxVersion,
},
#[error(
"tmux {found} does not implement {capability} correctly; \
releases from {broken_in} up to but not including {fixed_in} are affected"
)]
CapabilityDefective {
capability: &'static str,
found: TmuxVersion,
broken_in: ReleaseVersion,
fixed_in: ReleaseVersion,
},
#[non_exhaustive]
#[error(
"tmux version probe request {request_id} ({command}) failed with exit code {exit_code:?} and signal {signal:?}"
)]
VersionProbeFailed {
request_id: u64,
command: CommandSummary,
exit_code: Option<i32>,
signal: Option<i32>,
},
#[non_exhaustive]
#[error("invalid {input} for tmux request {request_id}")]
InvalidCommandInput {
request_id: u64,
input: &'static str,
},
#[non_exhaustive]
#[error("tmux executable was not found for request {request_id} ({command})")]
ExecutableNotFound {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("failed to start tmux request {request_id} ({command})")]
Spawn {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("failed to read {stream} for tmux request {request_id} ({command})")]
ReadOutput {
request_id: u64,
command: CommandSummary,
stream: &'static str,
kind: io::ErrorKind,
},
#[non_exhaustive]
#[error("failed to wait for tmux request {request_id} ({command})")]
WaitChild {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("tmux request {request_id} ({command}) timed out after {timeout:?}")]
Timeout {
request_id: u64,
command: CommandSummary,
timeout: Duration,
},
#[non_exhaustive]
#[error("tmux executor is shut down for request {request_id} ({command})")]
ExecutorShutdown {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux request {request_id} is already active ({command})")]
DuplicateRequest {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux supervisor was lost for request {request_id} ({command})")]
SupervisorLost {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux no longer has {kind} {id}")]
ObjectGone {
kind: ObjectKind,
id: String,
},
#[cfg(feature = "control-mode")]
#[non_exhaustive]
#[error("control mode connection failed ({kind:?})")]
ControlMode {
kind: ControlModeErrorKind,
#[source]
source: Option<io::Error>,
},
#[non_exhaustive]
#[error("could not build a runtime")]
RuntimeUnavailable {
#[source]
source: io::Error,
},
#[error("a blocking runtime cannot be driven from inside an async context")]
RuntimeNested,
#[error("tmux found no server for {command}: {stderr}")]
ServerGone {
command: &'static str,
kind: ServerGoneKind,
stderr: String,
},
#[non_exhaustive]
#[error("tmux rejected {command} (exit {exit_code:?}): {stderr}")]
CommandFailed {
command: &'static str,
exit_code: Option<i32>,
stderr: String,
},
#[non_exhaustive]
#[error("failed to decode {list_command} output: {detail}")]
DecodeListing {
list_command: &'static str,
detail: ListingDecodeError,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ObjectKind {
Session,
Window,
Pane,
Client,
}
impl fmt::Display for ObjectKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Session => "session",
Self::Window => "window",
Self::Pane => "pane",
Self::Client => "client",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ListingDecodeError {
inner: crate::formats::FormatCodecError,
}
impl ListingDecodeError {
pub(crate) const fn new(inner: crate::formats::FormatCodecError) -> Self {
Self { inner }
}
#[must_use]
pub const fn row(&self) -> Option<usize> {
self.inner.row()
}
#[must_use]
pub const fn field_name(&self) -> Option<&'static str> {
self.inner.field_name()
}
}
impl fmt::Display for ListingDecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(formatter)
}
}
impl std::error::Error for ListingDecodeError {}
impl Error {
pub(crate) fn refused(
command: &'static str,
exit_code: Option<i32>,
stderr: String,
target: Option<&std::ffi::OsStr>,
) -> Self {
const GONE: [(&str, ServerGoneKind); 4] = [
("no server running on", ServerGoneKind::NotRunning),
("error connecting to", ServerGoneKind::Unreachable),
("server exited unexpectedly", ServerGoneKind::Lost),
("server exited", ServerGoneKind::Stopped),
];
const MISSING: [(&str, ObjectKind); 4] = [
("can't find session:", ObjectKind::Session),
("can't find window:", ObjectKind::Window),
("can't find pane:", ObjectKind::Pane),
("can't find client:", ObjectKind::Client),
];
const OPTION: [(&str, OptionErrorKind); 5] = [
("invalid option:", OptionErrorKind::Unknown),
("unknown option:", OptionErrorKind::Unknown),
("ambiguous option:", OptionErrorKind::Ambiguous),
("bad value:", OptionErrorKind::BadValue),
("value is invalid:", OptionErrorKind::BadValue),
];
for (prefix, kind) in GONE {
if stderr.trim_end().starts_with(prefix) {
return Self::ServerGone {
command,
kind,
stderr,
};
}
}
for (prefix, kind) in OPTION {
if let Some(detail) = stderr.trim_end().strip_prefix(prefix) {
return Self::OptionRejected {
kind,
detail: detail.trim().to_owned(),
};
}
}
if let Some(name) = stderr.trim_end().strip_prefix("duplicate session:") {
return Self::SessionExists {
name: name.trim().to_owned(),
};
}
if let Some(target) = target.filter(|_| stderr.trim_end() == NO_CURRENT_TARGET) {
return Self::object_gone(&target.to_string_lossy());
}
for (prefix, kind) in MISSING {
if let Some(id) = stderr.trim_end().strip_prefix(prefix) {
return Self::ObjectGone {
kind,
id: id.trim().to_owned(),
};
}
}
Self::CommandFailed {
command,
exit_code,
stderr,
}
}
fn object_gone(target: &str) -> Self {
Self::ObjectGone {
kind: match target.as_bytes().first() {
Some(b'@') => ObjectKind::Window,
Some(b'%') => ObjectKind::Pane,
_ => ObjectKind::Session,
},
id: target.to_owned(),
}
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
match self {
Self::ObjectGone { .. } | Self::ServerGenerationChanged { .. } => ErrorKind::ObjectGone,
Self::ServerGone { .. } => ErrorKind::ServerGone,
Self::CommandFailed { .. }
| Self::OutputLimitExceeded { .. }
| Self::Overloaded { .. }
| Self::SessionExists { .. }
| Self::OptionRejected { .. } => ErrorKind::Refused,
Self::Timeout { .. } => ErrorKind::Timeout,
Self::ExecutableNotFound { .. }
| Self::InvalidServerConfiguration { .. }
| Self::RuntimeUnavailable { .. } => ErrorKind::Unreachable,
Self::RuntimeNested => ErrorKind::InvalidInput,
Self::UnsupportedTmuxVersion { .. }
| Self::UnsupportedCapability { .. }
| Self::CapabilityDefective { .. } => ErrorKind::UnsupportedVersion,
Self::InvalidCommandInput { .. } => ErrorKind::InvalidInput,
Self::Spawn { .. }
| Self::ReadOutput { .. }
| Self::WaitChild { .. }
| Self::VersionProbeFailed { .. }
| Self::ExecutorShutdown { .. }
| Self::DuplicateRequest { .. }
| Self::SupervisorLost { .. } => ErrorKind::Transport,
Self::InvalidVersionOutput { .. }
| Self::DecodeListing { .. }
| Self::UnreadableFormatValue { .. } => ErrorKind::Decode,
#[cfg(feature = "control-mode")]
Self::ControlModeFrameTooLarge { .. } => ErrorKind::Decode,
#[cfg(feature = "control-mode")]
Self::ControlMode { kind, .. } => match kind {
ControlModeErrorKind::UnrepresentableCommand => ErrorKind::InvalidInput,
ControlModeErrorKind::Transport
| ControlModeErrorKind::MissingPipes
| ControlModeErrorKind::Closed => ErrorKind::Transport,
},
}
}
#[must_use]
pub fn is_object_gone(&self) -> bool {
self.kind() == ErrorKind::ObjectGone
}
#[must_use]
pub fn is_transient(&self) -> bool {
matches!(self.kind(), ErrorKind::Timeout | ErrorKind::Transport)
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode(source: io::Error) -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::Transport,
source: Some(source),
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_pipes() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::MissingPipes,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_unrepresentable() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::UnrepresentableCommand,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_frame_too_large(frame: &'static str, limit: usize) -> Self {
Self::ControlModeFrameTooLarge { frame, limit }
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_closed() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::Closed,
source: None,
}
}
#[cfg(feature = "blocking")]
pub(crate) const fn runtime_unavailable(source: io::Error) -> Self {
Self::RuntimeUnavailable { source }
}
pub(crate) const fn invalid_server_configuration(kind: ServerConfigurationErrorKind) -> Self {
Self::InvalidServerConfiguration { kind }
}
pub(crate) fn version_probe_failed(
request_id: u64,
command: CommandSummary,
exit_code: Option<i32>,
signal: Option<i32>,
) -> Self {
Self::VersionProbeFailed {
request_id,
command,
exit_code,
signal,
}
}
pub(crate) fn from_invalid_version_output(output_len: usize) -> Self {
Self::InvalidVersionOutput { output_len }
}
pub(crate) fn unsupported_tmux_version(found: TmuxVersion, minimum: ReleaseVersion) -> Self {
Self::UnsupportedTmuxVersion { found, minimum }
}
pub(crate) fn invalid_command_input(request_id: u64, input: &'static str) -> Self {
Self::InvalidCommandInput { request_id, input }
}
pub(crate) fn spawn(
request_id: u64,
command: CommandSummary,
source: io::Error,
executable_not_found: bool,
) -> Self {
if executable_not_found {
Self::ExecutableNotFound {
request_id,
command,
source,
}
} else {
Self::Spawn {
request_id,
command,
source,
}
}
}
pub(crate) fn read_output(
request_id: u64,
command: CommandSummary,
stream: &'static str,
kind: io::ErrorKind,
) -> Self {
Self::ReadOutput {
request_id,
command,
stream,
kind,
}
}
pub(crate) fn wait_child(request_id: u64, command: CommandSummary, source: io::Error) -> Self {
Self::WaitChild {
request_id,
command,
source,
}
}
pub(crate) fn timeout(request_id: u64, command: CommandSummary, timeout: Duration) -> Self {
Self::Timeout {
request_id,
command,
timeout,
}
}
pub(crate) fn executor_shutdown(request_id: u64, command: CommandSummary) -> Self {
Self::ExecutorShutdown {
request_id,
command,
}
}
pub(crate) fn duplicate_request(request_id: u64, command: CommandSummary) -> Self {
Self::DuplicateRequest {
request_id,
command,
}
}
pub(crate) fn supervisor_lost(request_id: u64, command: CommandSummary) -> Self {
Self::SupervisorLost {
request_id,
command,
}
}
#[must_use]
pub fn invalid_version_output_len(&self) -> Option<usize> {
match self {
Self::InvalidVersionOutput { output_len } => Some(*output_len),
_ => None,
}
}
#[must_use]
pub fn found_version(&self) -> Option<&TmuxVersion> {
match self {
Self::UnsupportedTmuxVersion { found, .. }
| Self::UnsupportedCapability { found, .. } => Some(found),
_ => None,
}
}
#[must_use]
pub fn minimum_version(&self) -> Option<&ReleaseVersion> {
match self {
Self::UnsupportedTmuxVersion { minimum, .. } => Some(minimum),
Self::UnsupportedCapability { needs, .. } => Some(needs),
_ => None,
}
}
}
impl fmt::Debug for Error {
#[allow(
clippy::too_many_lines,
reason = "exhaustive safe formatting keeps every public error variant byte-free"
)]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RuntimeNested => formatter.debug_struct("RuntimeNested").finish(),
Self::InvalidServerConfiguration { kind } => formatter
.debug_struct("InvalidServerConfiguration")
.field("kind", kind)
.finish(),
Self::UnsupportedCapability {
capability,
needs,
found,
} => formatter
.debug_struct("UnsupportedCapability")
.field("capability", capability)
.field("needs", needs)
.field("found", found)
.finish(),
Self::CapabilityDefective {
capability,
found,
broken_in,
fixed_in,
} => formatter
.debug_struct("CapabilityDefective")
.field("capability", capability)
.field("found", found)
.field("broken_in", broken_in)
.field("fixed_in", fixed_in)
.finish(),
Self::UnreadableFormatValue { format, detail } => formatter
.debug_struct("UnreadableFormatValue")
.field("format", format)
.field("detail", detail)
.finish(),
#[cfg(feature = "control-mode")]
Self::ControlModeFrameTooLarge { frame, limit } => formatter
.debug_struct("ControlModeFrameTooLarge")
.field("frame", frame)
.field("limit", limit)
.finish(),
Self::OutputLimitExceeded {
request_id,
command,
stream,
limit,
} => formatter
.debug_struct("OutputLimitExceeded")
.field("request_id", request_id)
.field("command", command)
.field("stream", stream)
.field("limit", limit)
.finish(),
Self::Overloaded {
request_id,
command,
in_flight,
} => formatter
.debug_struct("Overloaded")
.field("request_id", request_id)
.field("command", command)
.field("in_flight", in_flight)
.finish(),
Self::ServerGenerationChanged { expected, found } => formatter
.debug_struct("ServerGenerationChanged")
.field("expected", expected)
.field("found", found)
.finish(),
Self::OptionRejected { kind, detail } => formatter
.debug_struct("OptionRejected")
.field("kind", kind)
.field("detail", detail)
.finish(),
Self::SessionExists { name } => formatter
.debug_struct("SessionExists")
.field("name", name)
.finish(),
Self::InvalidVersionOutput { output_len } => formatter
.debug_struct("InvalidVersionOutput")
.field("output_len", output_len)
.finish(),
Self::UnsupportedTmuxVersion { found, minimum } => formatter
.debug_struct("UnsupportedTmuxVersion")
.field("found", found)
.field("minimum", minimum)
.finish(),
Self::VersionProbeFailed {
request_id,
command,
exit_code,
signal,
} => formatter
.debug_struct("VersionProbeFailed")
.field("request_id", request_id)
.field("command", command)
.field("exit_code", exit_code)
.field("signal", signal)
.finish_non_exhaustive(),
Self::InvalidCommandInput { request_id, input } => formatter
.debug_struct("InvalidCommandInput")
.field("request_id", request_id)
.field("input", input)
.finish(),
Self::ExecutableNotFound {
request_id,
command,
source,
} => formatter
.debug_struct("ExecutableNotFound")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::Spawn {
request_id,
command,
source,
} => formatter
.debug_struct("Spawn")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::ReadOutput {
request_id,
command,
stream,
kind,
} => formatter
.debug_struct("ReadOutput")
.field("request_id", request_id)
.field("command", command)
.field("stream", stream)
.field("kind", kind)
.finish(),
Self::WaitChild {
request_id,
command,
source,
} => formatter
.debug_struct("WaitChild")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::Timeout {
request_id,
command,
timeout,
} => formatter
.debug_struct("Timeout")
.field("request_id", request_id)
.field("command", command)
.field("timeout", timeout)
.finish(),
Self::ExecutorShutdown {
request_id,
command,
} => formatter
.debug_struct("ExecutorShutdown")
.field("request_id", request_id)
.field("command", command)
.finish(),
Self::DuplicateRequest {
request_id,
command,
} => formatter
.debug_struct("DuplicateRequest")
.field("request_id", request_id)
.field("command", command)
.finish(),
Self::SupervisorLost {
request_id,
command,
} => formatter
.debug_struct("SupervisorLost")
.field("request_id", request_id)
.field("command", command)
.finish(),
#[cfg(feature = "control-mode")]
Self::ControlMode { kind, source } => formatter
.debug_struct("ControlMode")
.field("kind", kind)
.field("kind", &source.as_ref().map(io::Error::kind))
.finish(),
Self::RuntimeUnavailable { source } => formatter
.debug_struct("RuntimeUnavailable")
.field("kind", &source.kind())
.finish(),
Self::CommandFailed {
command,
exit_code,
stderr,
} => formatter
.debug_struct("CommandFailed")
.field("command", command)
.field("exit_code", exit_code)
.field("stderr", stderr)
.finish(),
Self::ObjectGone { kind, id } => formatter
.debug_struct("ObjectGone")
.field("kind", kind)
.field("id", id)
.finish(),
Self::ServerGone {
command,
kind,
stderr,
} => formatter
.debug_struct("ServerGone")
.field("command", command)
.field("kind", kind)
.field("stderr", stderr)
.finish(),
Self::DecodeListing {
list_command,
detail,
} => formatter
.debug_struct("DecodeListing")
.field("list_command", list_command)
.field("detail", detail)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::{Error, ErrorKind, ServerGoneKind};
#[test]
fn a_server_that_is_not_there_is_not_a_refusal() {
for (stderr, expected) in [
(
"no server running on /tmp/libtmux-rs-dev/absent",
ServerGoneKind::NotRunning,
),
(
"error connecting to /tmp/libtmux-rs-dev/absent (Connection refused)",
ServerGoneKind::Unreachable,
),
("server exited unexpectedly", ServerGoneKind::Lost),
("server exited", ServerGoneKind::Stopped),
] {
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}");
}
}
#[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:?}");
}
}
#[cfg(test)]
mod compat_tests {
#[cfg(feature = "test-support")]
#[tokio::test]
async fn real_tmux_compat_error_option_refusal_wording_is_recognized() {
use crate::test::TestServer;
use crate::{Error, ErrorKind, OptionErrorKind};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
for (name, value, expected) in [
("no-such-option", "x", OptionErrorKind::Unknown),
("status-l", "x", OptionErrorKind::Ambiguous),
("mouse", "notabool", OptionErrorKind::BadValue),
(
"status-left-length",
"notanumber",
OptionErrorKind::BadValue,
),
] {
let error = server
.set_global_option(name, value)
.await
.expect_err("tmux refuses it");
assert!(
matches!(&error, Error::OptionRejected { kind, .. } if *kind == expected),
"{name}={value} should be {expected:?}, got {error:?}",
);
assert_eq!(error.kind(), ErrorKind::Refused);
assert!(!error.is_object_gone(), "a refusal is not a missing object");
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "test-support")]
#[tokio::test]
async fn real_tmux_compat_error_missing_target_wording_is_recognized() {
use crate::ErrorKind;
use crate::test::TestServer;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("compat-missing").await.expect("session");
for (label, error) in [
(
"window",
server
.window_by_id(&"@4242".parse().expect("a window id"))
.await
.map(|found| assert!(found.is_none(), "the window does not exist"))
.err(),
),
(
"pane",
server
.pane_by_id(&"%4242".parse().expect("a pane id"))
.await
.map(|found| assert!(found.is_none(), "the pane does not exist"))
.err(),
),
] {
assert!(error.is_none(), "a lookup reports absence, not {label}");
}
let mut window = session.windows().await.expect("windows").remove(0);
let doomed = session
.new_window(crate::NewWindowOptions::new("doomed").command("sleep 300"))
.await
.expect("window");
let mut stale = doomed.clone();
doomed.kill().await.expect("the window is killed");
let error = stale.rename("gone").await.expect_err("the window is gone");
assert_eq!(
error.kind(),
ErrorKind::ObjectGone,
"tmux 'can't find window' is recognized: {error}",
);
let refused = server
.delete_buffer("never-existed")
.await
.expect_err("tmux has no such buffer");
assert_eq!(refused.kind(), ErrorKind::Refused, "{refused}");
window
.rename("last")
.await
.expect("the window still exists");
session.kill().await.expect("the session is killed");
let error = stale
.rename("still gone")
.await
.expect_err("the window is gone");
assert_eq!(
error.kind(),
ErrorKind::ObjectGone,
"tmux 'no current target' is recognized: {error}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "test-support")]
#[tokio::test]
async fn real_tmux_compat_error_absent_server_wording_is_recognized() {
use std::time::Duration;
use crate::test::{TestServer, retry_until};
use crate::{Command, ErrorKind, ServerGoneKind};
let mut guard = TestServer::builder().start().await.expect("tmux starts");
guard.session("compat-gone").await.expect("session");
guard
.server()
.cmd(Command::new("kill-server"))
.await
.expect("the server is killed");
retry_until(Duration::from_secs(5), async || {
!guard.daemon_state().is_running()
})
.await
.expect("the daemon exits");
let error = guard
.server()
.sessions()
.await
.expect_err("there is no server to list");
assert_eq!(
error.kind(),
ErrorKind::ServerGone,
"tmux 'no server running' is recognized: {error}",
);
assert!(
matches!(&error, crate::Error::ServerGone { kind, .. } if *kind == ServerGoneKind::NotRunning),
"the absence is named: {error:?}",
);
assert!(
!error.is_object_gone(),
"an absent server is not a missing object: {error}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
}