#![cfg(feature = "test-support")]
use libtmux::test::TestServer;
use libtmux::{Command, NewWindowOptions, SplitDirection, SplitOptions};
#[tokio::test]
async fn buffers_hold_exact_bytes_and_report_absence() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
assert!(server.buffer_names().await.expect("names").is_empty());
assert!(
server.buffer("absent").await.expect("read").is_none(),
"an unknown buffer is absent rather than an error",
);
server
.set_buffer(Some("payload"), "line one\nline two\ttabbed")
.await
.expect("buffer is stored");
assert_eq!(
server.buffer("payload").await.expect("read"),
Some(b"line one\nline two\ttabbed".to_vec()),
);
let names = server.buffer_names().await.expect("names");
assert_eq!(names, ["payload"]);
server
.delete_buffer("payload")
.await
.expect("buffer is deleted");
assert!(server.buffer("payload").await.expect("read").is_none());
assert!(
server.delete_buffer("payload").await.is_err(),
"deleting a buffer that is gone is a failure, not a silent success",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn key_bindings_can_be_added_and_removed() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.bind_key("prefix", "Y", "display-message bound")
.await
.expect("key is bound");
let listed = server
.key_bindings(Some("prefix"))
.await
.expect("bindings are listed");
assert!(
listed
.iter()
.any(|line| line.contains(" Y ") && line.contains("display-message")),
"the new binding appears in tmux's own listing",
);
server
.unbind_key("prefix", "Y")
.await
.expect("key is unbound");
let listed = server
.key_bindings(Some("prefix"))
.await
.expect("bindings are listed");
assert!(!listed.iter().any(|line| line.contains(" Y ")));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn formats_expand_against_a_target() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("formatted").await.expect("session");
let pane = session
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
let name = server
.format(Some(&pane), "#{session_name}")
.await
.expect("format expands");
assert_eq!(name.as_bytes(), b"formatted");
let combined = server
.format(Some(&pane), "#{session_name}:#{session_windows}")
.await
.expect("format expands");
assert_eq!(combined.as_bytes(), b"formatted:1");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn sourcing_a_file_applies_its_commands() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let directory = tempfile::tempdir().expect("temporary directory");
let config = directory.path().join("extra.conf");
std::fs::write(&config, "set-option -s @sourced yes\n").expect("config is written");
server.source_file(&config).await.expect("file is sourced");
assert_eq!(
server
.get_option("@sourced")
.await
.expect("read")
.expect("the sourced option is set")
.as_bytes(),
b"yes",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[derive(Debug, PartialEq)]
enum Failure {
Deliberate,
Tmux(String),
}
impl From<libtmux::Error> for Failure {
fn from(error: libtmux::Error) -> Self {
Self::Tmux(error.to_string())
}
}
#[tokio::test]
async fn scoped_operations_clean_up_after_success_and_failure() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let seen = server
.with_session("scoped", async |session| {
Ok::<_, libtmux::Error>(session.id().to_string())
})
.await
.expect("the scope runs and the operation succeeds");
assert!(seen.starts_with('$'));
assert!(server.sessions().await.expect("sessions").is_empty());
let outcome = server
.with_session("failing", async |_session| {
Err::<(), Failure>(Failure::Deliberate)
})
.await;
assert_eq!(outcome, Err(Failure::Deliberate));
assert!(
server.sessions().await.expect("sessions").is_empty(),
"cleanup runs even when the operation failed",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn scoped_windows_and_panes_nest() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("nested").await.expect("session");
let before = session.windows().await.expect("windows").len();
let pane_count = session
.with_window(
NewWindowOptions::new("temporary").command("sleep 300"),
async |window| {
window
.with_pane(
SplitOptions::new(SplitDirection::Below).command("sleep 300"),
async |_pane| {
Ok::<_, libtmux::Error>(window.panes().await.expect("panes").len())
},
)
.await
},
)
.await
.expect("both scopes run and the operation succeeds");
assert_eq!(pane_count, 2, "the scoped pane existed inside its scope");
assert_eq!(
session.windows().await.expect("windows").len(),
before,
"the scoped window is gone afterwards",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn shell_commands_run_through_tmux_and_report_their_output() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let outcome = server.run_shell("printf 'first\\nsecond\\n'").await;
let Ok(lines) = outcome else {
let error = outcome.expect_err("refused");
assert!(
matches!(&error, libtmux::Error::CapabilityDefective { capability, .. }
if *capability == "run-shell output"),
"an affected release refuses with the reason, got {error:?}",
);
assert_eq!(error.kind(), libtmux::ErrorKind::UnsupportedVersion);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
};
assert_eq!(
lines
.iter()
.map(libtmux::TmuxText::as_bytes)
.collect::<Vec<_>>(),
[&b"first"[..], &b"second"[..]],
);
assert!(server.run_shell("true").await.expect("runs").is_empty());
server
.spawn_shell("false")
.await
.expect("tmux accepts a background command whatever it later does");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn wait_for_channels_lock_and_release() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.lock_channel("gate")
.await
.expect("channel is locked");
server
.unlock_channel("gate")
.await
.expect("channel is unlocked");
server
.signal_channel("gate")
.await
.expect("channel is signalled");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn client_operations_need_a_client_and_report_when_there_is_none() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("headless").await.expect("session");
assert!(server.clients().await.expect("clients list").is_empty());
assert!(
server.display_popup(None, "true").await.is_err(),
"a popup needs a client with a terminal",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn a_client_reports_itself_while_it_is_attached() {
use libtmux::control::ControlMode;
use std::time::Duration;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let first = server.new_session("client-one").await.expect("session");
let second = server.new_session("client-two").await.expect("session");
let control = ControlMode::attach(server, first.id())
.await
.expect("control mode attaches");
libtmux::test::retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the attached client is listed");
let mut client = server.clients().await.expect("clients").remove(0);
assert!(client.is_control_mode(), "this client attached with -C");
assert!(!client.name().as_bytes().is_empty());
assert!(client.pid() > 0);
client
.switch_to(&second)
.await
.expect("the client switches session");
libtmux::test::retry_until(Duration::from_secs(10), async || {
second
.refreshed()
.await
.is_ok_and(|session| session.is_attached())
})
.await
.expect("the second session reports the client");
client.refresh().await.expect("the client still exists");
client.detach().await.expect("the client detaches");
libtmux::test::retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| clients.is_empty())
})
.await
.expect("the detached client stops being listed");
let _ = control.shutdown().await;
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn pane_modes_enter_and_leave() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("modes").await.expect("session");
let mut pane = session
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
assert!(!pane.is_in_mode(), "a fresh pane has no mode open");
pane.copy_mode().await.expect("copy mode is entered");
pane.refresh().await.expect("the pane still exists");
assert!(pane.is_in_mode(), "copy mode is a pane mode");
pane.exit_mode().await.expect("the mode is cancelled");
pane.refresh().await.expect("the pane still exists");
assert!(!pane.is_in_mode(), "cancelling leaves the mode");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn interactive_commands_need_a_client() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("interactive").await.expect("session");
assert!(server.display_panes(None).await.is_err());
assert!(
server
.display_menu(
None,
"menu",
[("Item".into(), "i".into(), "kill-pane".into())]
)
.await
.is_err(),
);
assert!(
server
.command_prompt(None, Some("question"), "kill-pane")
.await
.is_err(),
);
server
.choose(libtmux::Chooser::Tree, None)
.await
.expect("tmux accepts a chooser with no client");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn retry_until_waits_for_tmux_rather_than_sleeping() {
use libtmux::test::{retry_until, unique_name};
use std::time::Duration;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let name = unique_name("retry");
let session = server.new_session(name.as_str()).await.expect("session");
assert_ne!(unique_name("retry"), name);
session
.windows()
.await
.expect("windows")
.into_iter()
.next()
.expect("one window")
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane is created");
retry_until(Duration::from_secs(5), async || {
session.panes().await.is_ok_and(|panes| panes.len() == 2)
})
.await
.expect("the second pane appears");
let waited = retry_until(Duration::from_millis(50), async || false)
.await
.expect_err("a condition that never holds times out");
assert_eq!(waited.waited(), Duration::from_millis(50));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_scoped_raw_command_targets_its_own_object() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let first = server.new_session("first").await.expect("first session");
let second = server.new_session("second").await.expect("second session");
for (session, expected) in [(&first, "first"), (&second, "second")] {
let named = session
.cmd(
Command::new("display-message")
.arg("-p")
.arg("#{session_name}"),
)
.await
.expect("the command runs");
assert_eq!(named.stdout_lossy().trim(), expected);
}
let window = second
.active_window()
.await
.expect("active window")
.expect("a session has a window");
let pane = window
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
pane.cmd(
Command::new("send-keys")
.arg("--")
.arg("echo scoped")
.arg("Enter"),
)
.await
.expect("keys are sent");
let seen = libtmux::test::retry_until(std::time::Duration::from_secs(5), async || {
pane.capture().await.is_ok_and(|lines| {
lines
.iter()
.any(|line| String::from_utf8_lossy(line.as_bytes()).contains("scoped"))
})
})
.await;
assert!(seen.is_ok(), "the keys reached this pane, not another");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn formatting_returns_text_and_displaying_returns_nothing() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("split").await.expect("session");
let window = session
.active_window()
.await
.expect("active window")
.expect("a session has a window");
assert_eq!(
session
.format("#{session_name}")
.await
.expect("format")
.to_string_lossy(),
"split",
);
assert_eq!(
window
.format("#{window_id}")
.await
.expect("format")
.to_string_lossy(),
window.id().to_string(),
);
assert!(
session
.format("#{?0,yes,}")
.await
.expect("format")
.as_bytes()
.is_empty(),
);
let pane = window
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
assert_eq!(
pane.format("#{pane_id}")
.await
.expect("format")
.to_string_lossy(),
pane.id().to_string(),
);
session.display("hello").await.expect("display");
window.display("hello").await.expect("display");
pane.display("hello").await.expect("display");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_capability_too_new_for_this_tmux_is_refused_rather_than_ignored() {
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 version = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.clone();
let supported = version.release().is_none_or(|release| {
*release >= libtmux::ReleaseVersion::new(3, 3, libtmux::ReleaseSuffix::FINAL)
});
let answer = server.prompt_history(libtmux::PromptKind::Command).await;
if supported {
assert!(answer.expect("prompt history reads").is_empty());
} else {
let error = answer.expect_err("an older tmux refuses the capability");
assert_eq!(error.kind(), libtmux::ErrorKind::UnsupportedVersion);
assert!(
error.to_string().contains("3.3"),
"the refusal says what it needs: {error}",
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn the_server_access_list_names_its_owner_and_refuses_to_unseat_them() {
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 supported = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.release()
.is_none_or(|release| {
*release >= libtmux::ReleaseVersion::new(3, 3, libtmux::ReleaseSuffix::FINAL)
});
if !supported {
assert_eq!(
server
.access_rules()
.await
.expect_err("an older tmux has no access list")
.kind(),
libtmux::ErrorKind::UnsupportedVersion,
);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
}
let rules = server.access_rules().await.expect("access list");
let owner = rules.first().expect("the owner is listed");
assert_eq!(rules.len(), 1);
assert_eq!(owner.mode(), libtmux::AccessMode::Write);
assert!(!owner.user().is_empty());
let user = owner.user().to_owned();
for attempt in [
server
.grant_access(&user, libtmux::AccessMode::ReadOnly)
.await,
server.revoke_access(&user).await,
] {
let error = attempt.expect_err("tmux refuses to change the owner");
assert!(
error.to_string().contains("owns the server"),
"the refusal says why: {error}",
);
}
assert_eq!(server.access_rules().await.expect("access list"), rules);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn real_tmux_compat_capture_line_flags_mark_prompts_when_the_shell_emits_them() {
use libtmux::{CaptureOptions, since};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("prompts").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let supported = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.meets(&since::CAPTURE_LINE_FLAGS);
if !supported {
let refused = pane.capture_lines(CaptureOptions::visible()).await;
assert!(
matches!(
refused.as_ref().map_err(libtmux::Error::kind),
Err(libtmux::ErrorKind::UnsupportedVersion),
),
"an older tmux reports the version rather than returning nothing: {refused:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
}
pane.send_keys(
r"printf '\033]133;A\007'; echo THE-PROMPT; printf '\033]133;C\007'; echo the-output",
)
.await
.expect("keys are sent");
pane.send_key_names(["Enter"]).await.expect("Enter is sent");
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
let lines = pane
.capture_lines(CaptureOptions::history())
.await
.expect("the capture succeeds");
let prompt = lines
.iter()
.position(|line| line.starts_prompt)
.expect("the prompt mark is reported");
let output = lines
.iter()
.position(|line| line.starts_output)
.expect("the output mark is reported");
assert!(prompt < output, "the prompt precedes its output");
assert_eq!(
lines[prompt].text.to_string_lossy().trim(),
"THE-PROMPT",
"the mark lands on the line the shell was drawing",
);
assert_eq!(lines[output].text.to_string_lossy().trim(), "the-output");
assert!(
lines
.iter()
.all(|line| !line.text.to_string_lossy().starts_with("- ")),
"no line keeps tmux's flag column in its text",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}