#![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<TmuxText>) -> Vec<u8> {
value.expect("tmux reports a value").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.get_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
.get_option("@absent")
.await
.expect("an unset user option is absent")
.is_none(),
);
let error = server
.get_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
.get_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.get_option("@where").await.expect("read")),
b"server"
);
assert_eq!(
bytes(session.get_option("@where").await.expect("read")),
b"session"
);
assert_eq!(
bytes(window.get_option("@where").await.expect("read")),
b"window"
);
assert_eq!(
bytes(pane.get_option("@where").await.expect("read")),
b"pane"
);
window.unset_option("@where").await.expect("unset");
assert!(window.get_option("@where").await.expect("read").is_none());
assert_eq!(
bytes(session.get_option("@where").await.expect("read")),
b"session"
);
assert_eq!(
bytes(pane.get_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.get_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
.get_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
.get_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
.get_global_option("status")
.await
.expect("read")
.expect("status is set");
assert_eq!(status.as_flag(), Some(true));
let limit = server
.get_global_option("history-limit")
.await
.expect("read")
.expect("history-limit is set");
assert!(limit.parse::<u32>().is_some_and(|value| value > 0));
server.set_option("@prose", "sometimes").await.expect("set");
let prose = server
.get_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),
);
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("alert-bell", "display-message window")
.await
.expect("window hook");
let read = window
.hook("alert-bell")
.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");
let environment = session.environment_all().await.expect("listing");
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 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 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");
}