#![cfg(feature = "test-support")]
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use libtmux::test::TestServer;
use libtmux::{EnvironmentEntry, OptionValue};
use libtmux::{NewWindowOptions, TmuxText};
fn bytes(value: Option<impl Into<TmuxText>>) -> Vec<u8> {
value
.expect("tmux reports a value")
.into()
.as_bytes()
.to_vec()
}
#[tokio::test]
async fn option_values_survive_bytes_that_tmux_would_quote_for_display() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
for value in [
"plain",
"a b c",
"has\"quote",
"has\\back",
"has;semi",
"has'single",
"tab\tsep",
] {
server
.set_option("@probe", value)
.await
.expect("option is set");
assert_eq!(
bytes(server.typed_option("@probe").await.expect("option is read")),
value.as_bytes(),
"{value:?} round-trips exactly",
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn an_unknown_option_is_an_error_while_an_unset_one_is_absent() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
assert!(
server
.typed_option("@absent")
.await
.expect("an unset user option is absent")
.is_none(),
);
let error = server
.typed_option("no-such-built-in")
.await
.expect_err("an unknown built-in name is refused");
assert!(matches!(
error,
libtmux::Error::OptionRejected {
kind: libtmux::OptionErrorKind::Unknown,
..
},
));
assert!(
server
.typed_global_option("after-kill-pane[0]")
.await
.expect("a known hook name is accepted")
.is_none(),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn options_are_scoped_to_the_object_that_set_them() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("scoped").await.expect("session created");
let window = session
.new_window(NewWindowOptions::new("scoped").command("sleep 300"))
.await
.expect("window created");
let pane = window
.panes()
.await
.expect("panes list")
.into_iter()
.next()
.expect("one pane");
server.set_option("@where", "server").await.expect("set");
session.set_option("@where", "session").await.expect("set");
window.set_option("@where", "window").await.expect("set");
pane.set_option("@where", "pane").await.expect("set");
assert_eq!(
bytes(server.typed_option("@where").await.expect("read")),
b"server"
);
assert_eq!(
bytes(session.typed_option("@where").await.expect("read")),
b"session"
);
assert_eq!(
bytes(window.typed_option("@where").await.expect("read")),
b"window"
);
assert_eq!(
bytes(pane.typed_option("@where").await.expect("read")),
b"pane"
);
window.unset_option("@where").await.expect("unset");
assert!(window.typed_option("@where").await.expect("read").is_none());
assert_eq!(
bytes(session.typed_option("@where").await.expect("read")),
b"session"
);
assert_eq!(
bytes(pane.typed_option("@where").await.expect("read")),
b"pane"
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn appending_extends_a_value_rather_than_replacing_it() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session("appending")
.await
.expect("session created");
session.set_option("@parts", "one").await.expect("set");
session
.append_option("@parts", "-two")
.await
.expect("append");
assert_eq!(
bytes(session.typed_option("@parts").await.expect("read")),
b"one-two",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn option_names_lists_what_is_set_without_guessing_at_values() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_option("@listed", "a value with spaces")
.await
.expect("option is set");
let names = server.option_names().await.expect("names are listed");
assert!(names.iter().any(|name| name == "@listed"));
assert!(
names.iter().any(|name| name.starts_with("command-alias[")),
"array options keep their index",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_hook_is_stored_as_an_indexed_option() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_hook("after-new-window", "display-message hooked")
.await
.expect("hook is set");
assert_eq!(
bytes(
server
.typed_global_option("after-new-window[0]")
.await
.expect("hook is read"),
),
b"display-message hooked",
);
server
.unset_hook("after-new-window")
.await
.expect("hook is removed");
assert!(
server
.typed_global_option("after-new-window[0]")
.await
.expect("hook is read")
.is_none(),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn option_values_decode_into_flags_and_numbers() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("typed").await.expect("session");
let status = server
.typed_global_option("status")
.await
.expect("read")
.expect("status is set");
assert_eq!(TmuxText::from(status).as_flag(), Some(true));
let limit = server
.typed_global_option("history-limit")
.await
.expect("read")
.expect("history-limit is set");
assert!(
TmuxText::from(limit)
.parse::<u32>()
.is_some_and(|value| value > 0)
);
server.set_option("@prose", "sometimes").await.expect("set");
let prose = TmuxText::from(
server
.typed_option("@prose")
.await
.expect("read")
.expect("the option is set"),
);
assert_eq!(prose.as_flag(), None);
assert_eq!(prose.parse::<u32>(), None);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn options_decode_by_declared_kind_rather_than_by_shape() {
use libtmux::{OptionKind, OptionValue, option_schema};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("typed").await.expect("session");
assert_eq!(
option_schema("mouse").map(OptionSchemaKind::kind),
Some(OptionKind::Flag),
);
server.set_global_option("mouse", "off").await.expect("set");
assert_eq!(
server.typed_global_option("mouse").await.expect("read"),
Some(OptionValue::Flag(false)),
);
assert!(matches!(
server
.typed_global_option("history-limit")
.await
.expect("read"),
Some(OptionValue::Number(limit)) if limit > 0,
));
assert_eq!(
option_schema("status").map(OptionSchemaKind::kind),
Some(OptionKind::Choice),
);
assert!(matches!(
server.typed_global_option("status").await.expect("read"),
Some(OptionValue::Text(_)),
));
assert_eq!(option_schema("@mine"), None);
server.set_option("@mine", "on").await.expect("set");
assert!(matches!(
server.typed_option("@mine").await.expect("read"),
Some(OptionValue::Text(_)),
));
guard.shutdown().await.expect("tmux fixture shuts down");
}
use libtmux::OptionSchema as OptionSchemaKind;
#[tokio::test]
async fn hooks_read_back_the_commands_they_were_set_to() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
assert!(
server.hook("alert-bell").await.expect("read").is_none(),
"a hook holding nothing is absent, not present and empty",
);
server
.set_hook("alert-bell", r#"run-shell "echo a b""#)
.await
.expect("the hook is set");
let read = server
.hook("alert-bell")
.await
.expect("read")
.expect("the hook is set");
assert_eq!(read.len(), 1);
assert_eq!(
read.first().expect("a command").as_bytes(),
br#"run-shell "echo a b""#,
"the exact bytes come back, double space and all",
);
let all = server.hooks().await.expect("listing");
assert!(all.contains_key("alert-bell"));
assert!(
all.len() < 20,
"only the hooks holding something are listed, not every name tmux knows: {}",
all.len(),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_hook_keeps_the_indices_tmux_stores_it_under() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_hook("alert-bell", "display-message zero")
.await
.expect("slot 0");
server
.set_hook("alert-bell[3]", "display-message three")
.await
.expect("slot 3");
let read = server
.hook("alert-bell")
.await
.expect("read")
.expect("the hook is set");
assert_eq!(read.len(), 2);
let indices: Vec<u32> = read.iter().map(|(index, _)| *index).collect();
assert_eq!(indices, [0, 3], "the gap tmux keeps is kept here too");
assert!(read.get(1).is_none(), "nothing is invented for the gap");
assert_eq!(
read.first()
.map(|value| value.to_string_lossy().into_owned()),
Some("display-message zero".to_owned()),
"the lowest index is what a hook set without one holds",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_hook_set_on_a_window_is_read_back_by_name() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("scoped").await.expect("session");
let window = session
.active_window()
.await
.expect("active window")
.expect("a session has a window");
window
.set_hook("pane-died", "display-message window")
.await
.expect("window hook");
let read = window
.hook("pane-died")
.await
.expect("read")
.expect("the window holds what was set on it");
assert_eq!(
read.first()
.map(|value| value.to_string_lossy().into_owned()),
Some("display-message window".to_owned()),
);
server
.set_hook("alert-silence", "display-message server")
.await
.expect("server hook");
assert!(
server
.hooks()
.await
.expect("listing")
.contains_key("alert-silence"),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_typed_listing_decodes_every_option_it_reports() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("listed").await.expect("session");
session
.set_option("status-left-length", "30")
.await
.expect("a number");
session.set_option("status", "on").await.expect("a flag");
session
.set_option("@marker", "kept")
.await
.expect("a user option");
let options = session.options().await.expect("listing");
assert_eq!(
options.get("status-left-length"),
Some(&OptionValue::Number(30)),
);
assert!(matches!(
options.get("@marker"),
Some(OptionValue::Text(text)) if text.as_bytes() == b"kept",
));
assert!(options.contains_key("status"));
assert!(!options.is_empty());
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_listing_keeps_the_indices_an_array_option_is_stored_under() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let options = server.options().await.expect("listing");
let aliased: Vec<&String> = options
.keys()
.filter(|name| name.starts_with("command-alias["))
.collect();
assert!(
!aliased.is_empty(),
"tmux ships command-alias entries: {:?}",
options.keys().take(5).collect::<Vec<_>>(),
);
let fresh = server
.new_session("inheriting")
.await
.expect("session")
.options()
.await
.expect("listing");
assert!(
fresh.is_empty(),
"a session that set nothing has nothing of its own: {fresh:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn an_environment_listing_tells_a_removal_from_a_value() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard.server().new_session("env").await.expect("session");
session
.set_environment("EDITOR", "vi")
.await
.expect("a value");
session
.hide_environment("PAGER")
.await
.expect("a removal mark");
let environment = session.environment_all().await.expect("listing");
assert!(matches!(
environment.get("EDITOR"),
Some(EnvironmentEntry::Set(value)) if value.as_bytes() == b"vi",
));
assert_eq!(environment.get("PAGER"), Some(&EnvironmentEntry::Removed));
assert_eq!(environment.get("NEVER_SET"), None);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn an_environment_value_survives_what_a_line_listing_would_split() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard
.server()
.new_session("awkward")
.await
.expect("session");
session
.set_environment("MULTILINE", "first\nDECOY=not-a-variable")
.await
.expect("a value with a newline");
session
.set_environment("SPACED", "x y")
.await
.expect("a value with runs of spaces");
session
.set_environment("FRAMED", "a\"; export FRAMED;\nFORGED=\"b\\$c`d\\\\")
.await
.expect("a value shaped like framing");
let environment = session.environment_all().await.expect("listing");
for name in ["MULTILINE", "SPACED", "FRAMED"] {
assert_eq!(
environment.get(name),
session.environment(name).await.expect("read").as_ref(),
"{name} reads the same whole as alone",
);
}
assert_eq!(environment.get("FORGED"), None, "framing inside a value");
assert!(matches!(
environment.get("MULTILINE"),
Some(EnvironmentEntry::Set(value)) if value.as_bytes() == b"first\nDECOY=not-a-variable",
));
assert!(matches!(
environment.get("SPACED"),
Some(EnvironmentEntry::Set(value)) if value.as_bytes() == b"x y",
));
assert_eq!(
environment.get("DECOY"),
None,
"a continuation line is not a variable, however much it looks like one",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn writing_a_whole_hook_replaces_or_merges_as_asked() {
use libtmux::{IndexedHooks, ReplaceMode, TmuxText};
use std::collections::BTreeMap;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_hook("alert-bell[7]", "display-message existing")
.await
.expect("an entry to survive or not");
let mut entries = BTreeMap::new();
entries.insert(0, TmuxText::from(b"display-message first".to_vec()));
entries.insert(3, TmuxText::from(b"display-message fourth".to_vec()));
let written = IndexedHooks::from(entries);
server
.set_hooks("alert-bell", &written, ReplaceMode::Merge)
.await
.expect("merge");
let merged = server
.hook("alert-bell")
.await
.expect("read")
.expect("the hook is set");
assert_eq!(merged.len(), 3);
assert!(
merged.get(7).is_some(),
"merge kept the entry it did not name"
);
server
.set_hooks("alert-bell", &written, ReplaceMode::Replace)
.await
.expect("replace");
let replaced = server
.hook("alert-bell")
.await
.expect("read")
.expect("the hook is set");
assert_eq!(
replaced.iter().map(|(index, _)| *index).collect::<Vec<_>>(),
[0, 3],
"the sparse indices are kept and nothing else survives",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_later_bulk_hook_refusal_reports_the_hook_already_written() {
use libtmux::{Error, ErrorKind, IndexedHooks, ReplaceMode, TmuxText};
use std::collections::BTreeMap;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut entries = BTreeMap::new();
entries.insert(0, TmuxText::from("display-message first"));
entries.insert(1, TmuxText::from("no-such-tmux-command"));
let error = server
.set_hooks(
"alert-bell",
&IndexedHooks::from(entries),
ReplaceMode::Merge,
)
.await
.expect_err("tmux refuses the second hook command");
let hooks = server
.hook("alert-bell")
.await
.expect("the partly written hook can be read")
.expect("the first hook remains set");
assert_eq!(
hooks.get(0).map(TmuxText::as_bytes),
Some(b"display-message first".as_slice()),
);
assert!(hooks.get(1).is_none(), "the refused hook was not stored");
assert_eq!(error.kind(), ErrorKind::PartialEffect);
assert!(matches!(
error,
Error::AfterEffect {
operation: "set-hooks",
source,
..
} if source.kind() == ErrorKind::Refused
));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_first_bulk_hook_refusal_has_no_partial_effect() {
use libtmux::{ErrorKind, IndexedHooks, ReplaceMode, TmuxText};
use std::collections::BTreeMap;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut entries = BTreeMap::new();
entries.insert(0, TmuxText::from("no-such-tmux-command"));
entries.insert(1, TmuxText::from("display-message never-written"));
let error = server
.set_hooks(
"alert-bell",
&IndexedHooks::from(entries),
ReplaceMode::Merge,
)
.await
.expect_err("tmux refuses the first hook command");
assert_eq!(error.kind(), ErrorKind::Refused);
assert!(
server
.hook("alert-bell")
.await
.expect("the hook can be read")
.is_none(),
"the member after the refusal never ran",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn tmux_refusing_an_option_says_which_of_three_things_went_wrong() {
use libtmux::OptionErrorKind;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let cases = [
("no-such-option", "x", OptionErrorKind::Unknown),
("status-l", "x", OptionErrorKind::Ambiguous),
("mouse", "notabool", OptionErrorKind::BadValue),
(
"status-left-length",
"notanumber",
OptionErrorKind::BadValue,
),
];
for (name, value, expected) in cases {
let error = server
.set_global_option(name, value)
.await
.expect_err("tmux refuses it");
assert!(
matches!(&error, libtmux::Error::OptionRejected { kind, .. } if *kind == expected),
"{name}={value} should be {expected:?}, got {error:?}",
);
assert_eq!(error.kind(), libtmux::ErrorKind::Refused);
}
server
.set_global_option("status-left-length", "30")
.await
.expect("a valid option and value");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn rejected_sensitive_values_are_absent_from_errors() {
use libtmux::{Error, OptionErrorKind};
const SECRET: &str = "libtmux-sentinel-sensitive-value";
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
for name in ["mouse", "status-keys"] {
let error = server
.set_global_option(name, SECRET)
.await
.expect_err("tmux refuses the value");
assert!(
matches!(
&error,
Error::OptionRejected {
kind: OptionErrorKind::BadValue,
detail,
} if detail == name
),
"the error names the option and classifies its value: {error:?}",
);
assert!(!error.to_string().contains(SECRET), "{error}");
assert!(!format!("{error:?}").contains(SECRET), "{error:?}");
}
let error = server
.set_hook("after-new-window", SECRET)
.await
.expect_err("tmux refuses an unknown hook command");
assert!(error.to_string().contains("set-hook"), "{error}");
assert!(!error.to_string().contains(SECRET), "{error}");
assert!(!format!("{error:?}").contains(SECRET), "{error:?}");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn the_server_environment_is_separate_from_every_session_one() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_environment("SERVER_ONLY", "yes")
.await
.expect("a server value");
let before = server.new_session("before").await.expect("session");
let after = server.new_session("after").await.expect("session");
for (label, session) in [("before", &before), ("after", &after)] {
assert_eq!(
session.environment("SERVER_ONLY").await.expect("read"),
None,
"the {label} session has no entry of its own for a server variable",
);
}
after
.set_environment("SESSION_ONLY", "mine")
.await
.expect("a session value");
assert_eq!(
server.environment("SESSION_ONLY").await.expect("read"),
None
);
server.hide_environment("HIDDEN").await.expect("a mark");
assert_eq!(
server.environment("HIDDEN").await.expect("read"),
Some(EnvironmentEntry::Removed),
);
let listing = server.environment_all().await.expect("listing");
assert!(matches!(
listing.get("SERVER_ONLY"),
Some(EnvironmentEntry::Set(value)) if value.as_bytes() == b"yes",
));
assert_eq!(listing.get("HIDDEN"), Some(&EnvironmentEntry::Removed));
assert_eq!(listing.get("SESSION_ONLY"), None);
server
.unset_environment("SERVER_ONLY")
.await
.expect("removal");
assert_eq!(server.environment("SERVER_ONLY").await.expect("read"), None);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_started_process_gets_the_server_and_session_environments_merged() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_environment("BOTH", "from_server")
.await
.expect("set");
server
.set_environment("ONLY_SERVER", "reaches")
.await
.expect("set");
server
.hide_environment("HIDDEN_GLOBALLY")
.await
.expect("mark");
let session = server.new_session("merged").await.expect("session");
session
.set_environment("BOTH", "from_session")
.await
.expect("set");
let window = session
.new_window(
NewWindowOptions::new("merged")
.command("sh -c 'printf \"%s|%s|%s\" \"$BOTH\" \"$ONLY_SERVER\" \"${HIDDEN_GLOBALLY-absent}\"; sleep 30'"),
)
.await
.expect("window created");
let pane = window
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
let mut captured = String::new();
for _ in 0..50 {
let lines = pane.capture().await.expect("capture");
captured = lines
.iter()
.map(|line| line.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("\n");
if captured.contains('|') {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
}
assert!(
captured.contains("from_session|reaches|absent"),
"merged environment, got {captured:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn an_array_option_keeps_the_gaps_tmux_leaves() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_array_option("command-alias", 30, "thirty=display -p 30")
.await
.expect("index 30 is set");
server
.set_array_option("command-alias", 35, "five=display -p 35")
.await
.expect("index 35 is set");
let aliases = server.array_option("command-alias").await.expect("read");
assert_eq!(
bytes(aliases.get(30).cloned()),
b"thirty=display -p 30",
"the value is stored under the index asked for",
);
assert_eq!(aliases.get(31), None, "nothing was written between them");
assert!(
aliases.indices().any(|index| *index == 35),
"the higher index is listed: {:?}",
aliases.indices().collect::<Vec<_>>(),
);
let before = aliases.len();
server
.append_array_option("command-alias", 30, " extra")
.await
.expect("append");
let appended = server.array_option("command-alias").await.expect("read");
assert_eq!(appended.len(), before, "appending did not add an entry");
assert_eq!(
bytes(appended.get(30).cloned()),
b"thirty=display -p 30 extra",
);
server
.unset_array_option("command-alias", 35)
.await
.expect("unset");
let after = server.array_option("command-alias").await.expect("read");
assert_eq!(after.get(35), None, "the entry is gone");
assert_eq!(
bytes(after.get(30).cloned()),
b"thirty=display -p 30 extra",
"the entry below it did not move",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_name_shaped_like_a_flag_is_refused_not_obeyed() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("flags").await.expect("session");
session
.set_option("@kept", "original")
.await
.expect("a plain user option is set");
session
.set_option("-u", "@kept")
.await
.expect_err("a name that is a flag is refused");
assert_eq!(
bytes(
session
.typed_option("@kept")
.await
.expect("the option reads")
),
b"original".to_vec(),
"the option a flag name pointed at survived",
);
session
.set_environment("KEPT", "original")
.await
.expect("a plain variable is set");
session
.set_environment("-u", "KEPT")
.await
.expect("a variable named like a flag is a variable");
let kept = session
.environment("KEPT")
.await
.expect("the variable reads");
assert!(
matches!(&kept, Some(EnvironmentEntry::Set(value)) if value.as_bytes() == b"original"),
"the variable a flag name pointed at survived, got {kept:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_whole_hook_write_refuses_bytes_rather_than_substituting_them() {
use libtmux::{IndexedHooks, ReplaceMode, TmuxText};
use std::collections::BTreeMap;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut command = b"set-environment -g MARK \"a".to_vec();
command.push(0xff);
command.extend_from_slice(b"b\"");
let mut entries = BTreeMap::new();
entries.insert(0, TmuxText::from(command));
server
.set_hooks(
"alert-bell",
&IndexedHooks::from(entries),
ReplaceMode::Replace,
)
.await
.expect_err("tmux refuses a command it cannot read");
assert!(
server
.hook("alert-bell")
.await
.expect("the hook reads")
.is_none_or(|hook| hook.get(0).is_none()),
"nothing was stored under a substituted value",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn setting_one_hook_leaves_the_other_slots_alone() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
for slot in [0_usize, 2, 5] {
server
.set_hook(
&format!("alert-bell[{slot}]"),
format!("display-message {slot}"),
)
.await
.expect("a slot is written");
}
server
.set_hook("alert-bell", "display-message replaced")
.await
.expect("the hook is set");
let hooks = server
.hook("alert-bell")
.await
.expect("the hook reads")
.expect("the hook is set");
assert_eq!(
hooks.get(2).map(|command| command.to_string_lossy()),
Some("display-message 2".into()),
"a slot nobody wrote to survives: {hooks:?}",
);
assert_eq!(
hooks.get(5).map(|command| command.to_string_lossy()),
Some("display-message 5".into()),
"and so does the last one: {hooks:?}",
);
assert_eq!(
hooks.get(0).map(|command| command.to_string_lossy()),
Some("display-message replaced".into()),
"while slot 0 is the one that changed: {hooks:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn the_schema_agrees_with_where_tmux_accepts_a_write() {
use libtmux::{OptionScope, option_schema};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("scoped").await.expect("a session");
let window = session
.windows()
.await
.expect("windows")
.into_iter()
.next()
.expect("one window");
let schema = option_schema("pane-died").expect("a documented hook");
assert!(schema.accepts(OptionScope::Window), "{:?}", schema.scopes());
window
.set_hook("pane-died", "display-message gone")
.await
.expect("tmux accepts a pane hook at window scope");
assert!(
window
.hook("pane-died")
.await
.expect("the hook reads")
.is_some(),
"and reads it back there",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_write_tmux_would_place_elsewhere_is_refused() {
use libtmux::{ErrorKind, OptionScope};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("scoped").await.expect("a session");
let pane = session
.active_window()
.await
.expect("active window")
.expect("a session has a window")
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
let error = pane
.set_option("mouse", "on")
.await
.expect_err("a session option is not the pane's to set");
assert_eq!(error.kind(), ErrorKind::InvalidInput);
assert!(
matches!(
error,
libtmux::Error::OptionScopeMismatch {
requested: OptionScope::Pane,
..
}
),
"the error names the scope that was asked for: {error:?}",
);
pane.set_option("remain-on-exit", "on")
.await
.expect("a pane option is the pane's to set");
pane.set_option("@mine", "value")
.await
.expect("a user option is written wherever it was aimed");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_name_tmux_would_resolve_is_guarded_like_the_one_it_resolves_to() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("scoped").await.expect("a session");
let pane = session
.active_window()
.await
.expect("active window")
.expect("a session has a window")
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
pane.set_option("mous", "on")
.await
.expect_err("a prefix reaches the option it names");
pane.set_option("display-panes-color", "red")
.await
.expect_err("an alias reaches the option it maps to");
let ambiguous = pane
.set_option("pane-b", "on")
.await
.expect_err("tmux refuses an ambiguous name");
assert!(
!matches!(ambiguous, libtmux::Error::OptionScopeMismatch { .. }),
"and it is tmux's refusal, not this crate's: {ambiguous:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_late_pane_scope_is_granted_to_whichever_tmux_actually_has_it() {
let mut builder = TestServer::builder();
if let Some(executable) = std::env::var_os("LIBTMUX_TEST_TMUX") {
builder = builder.tmux_executable(executable);
}
let guard = builder.start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("late-scope").await.expect("a session");
let pane = session
.active_window()
.await
.expect("active window")
.expect("a session has a window")
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
pane.set_option("pane-border-format", "#{pane_index}")
.await
.expect("every supported release accepts the write");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_typed_write_reads_back_as_the_value_written() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("typed").await.expect("a session");
let window = session
.active_window()
.await
.expect("active window")
.expect("a session has a window");
let cases = [
("mouse", OptionValue::Flag(true)),
("history-limit", OptionValue::Number(4321)),
("status-keys", OptionValue::from("vi")),
("status-left", OptionValue::from("typed #[fg=red]")),
];
for (name, value) in cases {
session
.set_typed_option(name, value.clone())
.await
.unwrap_or_else(|error| panic!("{name} takes {value:?}: {error}"));
assert_eq!(
session.typed_option(name).await.expect("read"),
Some(value),
"{name} reads back as written",
);
}
window
.set_typed_option("synchronize-panes", true)
.await
.expect("a window flag");
assert_eq!(
window
.typed_option("synchronize-panes")
.await
.expect("read"),
Some(OptionValue::Flag(true)),
);
server
.set_typed_option("escape-time", 15)
.await
.expect("a server number");
assert_eq!(
server.typed_option("escape-time").await.expect("read"),
Some(OptionValue::Number(15)),
);
server
.set_typed_global_window_option("mode-keys", "vi")
.await
.expect("a global window choice");
assert_eq!(
server
.typed_global_window_option("mode-keys")
.await
.expect("read"),
Some(OptionValue::from("vi")),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_value_the_table_refuses_never_reaches_tmux() {
use libtmux::{Error, ErrorKind, OptionKind, OptionValueRefusal};
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard
.server()
.new_session("refused")
.await
.expect("a session");
session
.set_typed_option("mouse", false)
.await
.expect("mouse starts off");
session
.set_typed_option("status-keys", "emacs")
.await
.expect("status-keys starts at emacs");
session
.set_typed_option("history-limit", 1234)
.await
.expect("history-limit starts at 1234");
let cases = [
(
"mouse",
OptionValue::Number(1),
OptionValueRefusal::WrongKind {
expected: OptionKind::Flag,
},
OptionValue::Flag(false),
),
(
"status-keys",
OptionValue::from("dvorak"),
OptionValueRefusal::NotAChoice {
choices: &["emacs", "vi"],
},
OptionValue::from("emacs"),
),
(
"history-limit",
OptionValue::Number(-1),
OptionValueRefusal::OutOfRange {
range: 0..=i64::from(i32::MAX),
},
OptionValue::Number(1234),
),
];
for (name, value, expected, unchanged) in cases {
let result = session.set_typed_option(name, value).await;
assert_eq!(
session.typed_option(name).await.expect("read"),
Some(unchanged),
"{name} is as it was",
);
let error = result.expect_err("the table refuses the value");
assert!(
matches!(
&error,
Error::OptionValueRefused { option, reason }
if *option == name && *reason == expected
),
"{name}: refused before dispatch, for the declared reason: {error:?}",
);
assert_eq!(error.kind(), ErrorKind::InvalidInput);
assert!(!error.is_transient(), "the same value is refused again");
let reported = format!("{error} {error:?}");
assert!(
!reported.contains("dvorak"),
"the value stays out of the error: {reported}",
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_typed_refusal_needs_no_server() {
use libtmux::Error;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server().clone();
guard.shutdown().await.expect("tmux fixture shuts down");
let valid = server
.set_typed_global_option("history-limit", 10)
.await
.expect_err("nothing is there to take the write");
assert!(
!matches!(valid, Error::OptionValueRefused { .. }),
"a valid value reaches dispatch, and dispatch fails: {valid:?}",
);
let invalid = server
.set_typed_global_option("history-limit", -1)
.await
.expect_err("the table refuses the value");
assert!(
matches!(invalid, Error::OptionValueRefused { .. }),
"refused before any dispatch was attempted: {invalid:?}",
);
}
#[tokio::test]
async fn a_typed_user_option_is_stored_as_text() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard.server().new_session("user").await.expect("a session");
let cases = [
("@flag", OptionValue::Flag(true), "on"),
("@number", OptionValue::Number(-3), "-3"),
("@text", OptionValue::from("dvorak"), "dvorak"),
];
for (name, value, stored) in cases {
session
.set_typed_option(name, value)
.await
.unwrap_or_else(|error| panic!("{name} takes any variant: {error}"));
let read = session.typed_option(name).await.expect("read");
assert!(
matches!(read, Some(OptionValue::Text(_))),
"{name} reads back as text: {read:?}",
);
assert_eq!(
String::from_utf8_lossy(&bytes(read)),
stored,
"{name} is stored as the text a read shows",
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}