#![cfg(feature = "test-support")]
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use libtmux::test::TestServer;
use libtmux::{NewSessionOptions, NewWindowOptions, SplitDirection, SplitOptions, TmuxText};
fn text(value: Option<&TmuxText>) -> Vec<u8> {
value.expect("tmux reports the value").as_bytes().to_vec()
}
#[tokio::test]
async fn creating_an_object_returns_it_hydrated_in_one_command() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session("work")
.await
.expect("session is created");
assert_eq!(session.name().as_bytes().to_vec(), b"work".to_vec());
assert_eq!(session.window_count(), 1);
assert!(session.created() > 0);
let window = session
.new_window(NewWindowOptions::new("editor").command("sleep 300"))
.await
.expect("window is created");
assert_eq!(window.name().as_bytes().to_vec(), b"editor".to_vec());
assert_eq!(window.session_id(), session.id());
assert_eq!(window.pane_count(), 1);
let pane = window
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane is created");
assert_eq!(pane.window_id(), window.id());
assert!(pane.pid() > 0);
assert_eq!(server.sessions().await.expect("sessions").len(), 1);
assert_eq!(server.windows().await.expect("windows").len(), 2);
assert_eq!(server.panes().await.expect("panes").len(), 3);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn creation_options_reach_tmux() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let directory = tempfile::tempdir().expect("temporary directory");
let session = server
.new_session(
NewSessionOptions::new("configured")
.window_name("first")
.start_directory(directory.path())
.size(120, 40)
.command("sleep 300"),
)
.await
.expect("session is created");
let windows = session.windows().await.expect("windows list");
assert_eq!(windows[0].name().as_bytes().to_vec(), b"first".to_vec());
assert_eq!(
session
.get_option("default-size")
.await
.expect("read")
.expect("new-session -x -y sets it")
.as_bytes(),
b"120x40",
);
let panes = session.panes().await.expect("panes list");
assert_eq!(
text(panes[0].current_path()),
directory
.path()
.canonicalize()
.expect("canonical path")
.as_os_str()
.as_encoded_bytes(),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_new_window_does_not_steal_selection_unless_asked() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session("work")
.await
.expect("session is created");
let first = session
.active_window()
.await
.expect("active window resolves")
.expect("a session always has an active window");
session
.new_window(NewWindowOptions::new("background").command("sleep 300"))
.await
.expect("window is created");
let still_first = session
.active_window()
.await
.expect("active window resolves")
.expect("a session always has an active window");
assert_eq!(still_first.id(), first.id(), "creation does not select");
let selected = session
.new_window(
NewWindowOptions::new("foreground")
.command("sleep 300")
.select(),
)
.await
.expect("window is created");
let active = session
.active_window()
.await
.expect("active window resolves")
.expect("a session always has an active window");
assert_eq!(active.id(), selected.id(), "select() makes it active");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn renaming_updates_the_handle_that_performed_it() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut session = server
.new_session("before")
.await
.expect("session is created");
session.rename("after").await.expect("rename succeeds");
assert_eq!(session.name().as_bytes().to_vec(), b"after".to_vec());
let mut window = session
.new_window(NewWindowOptions::new("old").command("sleep 300"))
.await
.expect("window is created");
window.rename("new").await.expect("rename succeeds");
assert_eq!(window.name().as_bytes().to_vec(), b"new".to_vec());
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn killing_removes_the_object_and_strands_other_handles() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session("doomed")
.await
.expect("session is created");
server
.new_session("survivor")
.await
.expect("session is created");
let mut stale = session.clone();
session.kill().await.expect("kill succeeds");
assert_eq!(server.sessions().await.expect("sessions").len(), 1);
let error = stale.refresh().await.expect_err("a killed session is gone");
assert!(matches!(
error,
libtmux::Error::ObjectGone {
kind: libtmux::ObjectKind::Session,
..
},
));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn unlink_removes_one_link_while_kill_removes_the_window() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let origin = server
.new_session("origin")
.await
.expect("session is created");
server
.new_session("borrower")
.await
.expect("session is created");
let shared = origin
.new_window(NewWindowOptions::new("shared").command("sleep 300"))
.await
.expect("window is created");
server
.cmd(
libtmux::Command::new("link-window")
.arg("-s")
.arg(shared.id().to_string())
.arg("-t")
.arg("borrower:9"),
)
.await
.expect("link succeeds");
let links: Vec<_> = server
.windows()
.await
.expect("windows list")
.into_iter()
.filter(|window| window.id() == shared.id())
.collect();
assert_eq!(links.len(), 2);
links[0].clone().unlink().await.expect("unlink succeeds");
let remaining: Vec<_> = server
.windows()
.await
.expect("windows list")
.into_iter()
.filter(|window| window.id() == shared.id())
.collect();
assert_eq!(
remaining.len(),
1,
"the window survives in its other session"
);
remaining[0].clone().kill().await.expect("kill succeeds");
assert!(
server
.windows()
.await
.expect("windows list")
.iter()
.all(|window| window.id() != shared.id()),
"the window is gone everywhere",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn pane_input_and_capture_round_trip_through_a_shell() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session(NewSessionOptions::new("io").command("sh"))
.await
.expect("session is created");
let mut pane = session
.panes()
.await
.expect("panes list")
.into_iter()
.next()
.expect("one pane");
pane.send_keys("printf marker-8fa1\n")
.await
.expect("keys are sent");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let captured = loop {
let lines = pane.capture().await.expect("capture succeeds");
if lines.iter().any(|line| {
line.as_bytes()
.windows(11)
.any(|window| window == b"marker-8fa1")
}) {
break lines;
}
assert!(
std::time::Instant::now() < deadline,
"the shell did not echo the marker before the deadline",
);
tokio::task::yield_now().await;
};
assert!(!captured.is_empty());
let window = pane
.window()
.await
.expect("parent resolves")
.expect("the pane's window exists");
window
.split(SplitOptions::new(SplitDirection::Right).command("sleep 300"))
.await
.expect("pane is created");
pane.refresh().await.expect("the pane still exists");
let full_width = pane.width();
pane.resize(20, 8).await.expect("resize succeeds");
assert_eq!(pane.width(), 20);
assert!(full_width > 20, "the split left room to shrink into");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn killing_the_server_leaves_nothing_running() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server().clone();
server
.new_session("work")
.await
.expect("session is created");
assert!(server.is_alive().await);
server.kill().await.expect("kill-server succeeds");
assert!(!server.is_alive().await);
server.kill().await.expect("kill-server is idempotent");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn pane_operations_reshape_the_hierarchy() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("shaping").await.expect("session");
let window = session
.windows()
.await
.expect("windows")
.into_iter()
.next()
.expect("one window");
let second = window
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane is created");
let mut first = window
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
first.set_title("named").await.expect("title is set");
assert_eq!(first.title().as_bytes(), b"named",);
first.clear_history().await.expect("history is cleared");
let before = first.index();
first.swap_with(&second).await.expect("panes are swapped");
assert_ne!(first.index(), before, "the pane moved");
let windows_before = session.windows().await.expect("windows").len();
second.break_out().await.expect("pane is broken out");
assert_eq!(
session.windows().await.expect("windows").len(),
windows_before + 1,
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn window_operations_move_and_resize() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("moving").await.expect("session");
let mut window = session
.new_window(NewWindowOptions::new("mover").command("sleep 300"))
.await
.expect("window is created");
window.move_to(&session, 20).await.expect("window is moved");
assert_eq!(window.index(), 20);
window.resize(100, 30).await.expect("window is resized");
assert_eq!(window.width(), 100);
assert_eq!(window.height(), 30);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_session_environment_is_set_read_and_removed() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("environed").await.expect("session");
assert!(
session.environment("EDITOR").await.expect("read").is_none(),
"a variable nobody set is absent rather than empty",
);
session
.set_environment("EDITOR", "hx")
.await
.expect("the variable is set");
assert!(matches!(
session.environment("EDITOR").await.expect("read"),
Some(libtmux::EnvironmentEntry::Set(value)) if value.as_bytes() == b"hx",
));
let window = session
.new_window(NewWindowOptions::new("later").command("sleep 300"))
.await
.expect("window");
let pane = window.panes().await.expect("panes").remove(0);
let seen = server
.format(Some(&pane), "#{?#{==:#{EDITOR},},unset,set}")
.await
.expect("a format expands");
assert_eq!(seen.as_bytes(), b"set");
session
.unset_environment("EDITOR")
.await
.expect("the variable is removed");
assert!(session.environment("EDITOR").await.expect("read").is_none());
session
.set_environment("PAGER", "less")
.await
.expect("the variable is set");
session
.hide_environment("PAGER")
.await
.expect("the variable is hidden");
assert_eq!(
session.environment("PAGER").await.expect("read"),
Some(libtmux::EnvironmentEntry::Removed),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_window_linked_into_two_sessions_is_one_window() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let source = server.new_session("linking-from").await.expect("session");
let target = server.new_session("linking-to").await.expect("session");
let mut window = source
.new_window(NewWindowOptions::new("shared").command("sleep 300"))
.await
.expect("window");
window
.link_to(&target, None)
.await
.expect("the window is linked");
let linked = target
.windows()
.await
.expect("windows")
.into_iter()
.find(|other| other.id() == window.id())
.expect("the link exists in the target session");
assert!(linked.is_linked());
window
.rename("renamed")
.await
.expect("the window is renamed");
assert_eq!(
linked
.refreshed()
.await
.expect("the link still exists")
.name()
.as_bytes(),
b"renamed",
);
linked.unlink().await.expect("the link is removed");
assert!(
!target
.windows()
.await
.expect("windows")
.iter()
.any(|other| other.id() == window.id()),
);
assert!(
source
.windows()
.await
.expect("windows")
.iter()
.any(|other| other.id() == window.id()),
"the window itself is still there, only the second link is gone",
);
let occupied = target
.windows()
.await
.expect("windows")
.first()
.expect("a window")
.index();
assert!(window.link_to(&target, Some(occupied)).await.is_err());
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn objects_are_found_by_their_tmux_identity() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("identified").await.expect("session");
let window = session
.new_window(NewWindowOptions::new("found").command("sleep 300"))
.await
.expect("window");
let pane = window.panes().await.expect("panes").remove(0);
assert_eq!(
server
.session_by_id(session.id())
.await
.expect("the lookup runs")
.expect("the session exists")
.id(),
session.id(),
);
assert_eq!(
server
.window_by_id(window.id())
.await
.expect("the lookup runs")
.expect("the window exists")
.id(),
window.id(),
);
assert_eq!(
server
.pane_by_id(pane.id())
.await
.expect("the lookup runs")
.expect("the pane exists")
.id(),
pane.id(),
);
let gone = window.id().clone();
window.kill().await.expect("the window is killed");
assert!(
server
.window_by_id(&gone)
.await
.expect("the lookup runs")
.is_none(),
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_failure_says_what_to_do_about_it() {
use libtmux::{ErrorKind, Server};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("classified").await.expect("session");
let window = session
.new_window(NewWindowOptions::new("doomed").command("sleep 300"))
.await
.expect("window");
let mut stale = window.clone();
window.kill().await.expect("the window is killed");
let gone = stale
.rename("gone")
.await
.expect_err("the window is killed");
assert_eq!(gone.kind(), ErrorKind::ObjectGone);
assert!(gone.is_object_gone());
assert!(!gone.is_transient(), "looking again will not bring it back");
let pane = session.panes().await.expect("panes").remove(0);
let stale_pane = pane.clone();
let gone_session = session.clone();
session.kill().await.expect("the session is killed");
for (kind, error) in [
(
ErrorKind::ObjectGone,
stale_pane.capture().await.expect_err("the pane is gone"),
),
(
ErrorKind::ObjectGone,
gone_session
.windows()
.await
.expect_err("the session is gone"),
),
] {
assert_eq!(error.kind(), kind, "{error}");
}
assert!(
server
.format(Some(&stale_pane), "#{pane_id}")
.await
.expect("tmux does not refuse a format against a target that has gone")
.as_bytes()
.is_empty(),
);
let refused = server
.delete_buffer("never-existed")
.await
.expect_err("tmux refuses to delete a buffer it does not have");
assert_eq!(refused.kind(), ErrorKind::Refused);
assert!(
!refused.is_transient(),
"tmux will answer the same way again"
);
let absent = Server::builder()
.tmux_executable("tmux-that-is-not-installed")
.socket_path("/tmp/libtmux-rs-unreachable.sock")
.build()
.expect("the configuration is valid")
.sessions()
.await
.expect_err("there is no such executable");
assert_eq!(absent.kind(), ErrorKind::Unreachable);
assert!(!absent.is_transient());
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_name_tmux_cannot_address_is_rejected_before_it_is_created() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.cmd(
libtmux::Command::new("new-session")
.arg("-d")
.arg("-s")
.arg("a:b"),
)
.await
.expect("tmux accepts the name");
let found = server
.cmd(libtmux::Command::new("has-session").arg("-t").arg("a:b"))
.await
.expect("the command runs");
assert!(
!found.success(),
"tmux cannot find the session it just made"
);
assert!(
found.stderr_lossy().contains("can't find window"),
"it split the name at the colon: {:?}",
found.stderr_lossy(),
);
assert_eq!(server.sessions().await.expect("sessions").len(), 1);
assert!(libtmux::SessionName::new("a:b").is_err());
assert!(libtmux::SessionName::new("c.d").is_err());
assert!(libtmux::SessionName::new("").is_err());
assert_eq!(
libtmux::SessionName::new("has,comma")
.expect("a comma is addressable")
.as_str(),
"has,comma",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_taken_session_name_is_classified_rather_than_a_bare_refusal() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("taken").await.expect("first session");
let error = server
.new_session("taken")
.await
.map(|_| ())
.expect_err("tmux refuses a duplicate name");
assert!(
matches!(&error, libtmux::Error::SessionExists { name } if name == "taken"),
"the refusal names what was taken: {error:?}",
);
assert_eq!(error.kind(), libtmux::ErrorKind::Refused);
assert_eq!(server.sessions().await.expect("sessions").len(), 1);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn directional_focus_follows_the_layout_and_wraps_at_the_edge() {
use libtmux::{NewWindowOptions, PaneDirection, SplitDirection};
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard
.server()
.new_session("directions")
.await
.expect("session");
let stacked = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
let mut top = stacked
.active_pane()
.await
.expect("panes")
.expect("a window has a pane");
let bottom = stacked
.split(SplitDirection::Below)
.await
.expect("split below");
top.select().await.expect("focus the top pane");
assert_eq!(
stacked
.focus_direction(PaneDirection::Below)
.await
.expect("focus moves")
.id(),
bottom.id(),
"down from the top",
);
assert_eq!(
stacked
.focus_direction(PaneDirection::Above)
.await
.expect("focus moves")
.id(),
top.id(),
"up from the bottom",
);
assert_eq!(
stacked
.focus_direction(PaneDirection::Above)
.await
.expect("the edge is not an error")
.id(),
bottom.id(),
"up from the top wraps to the bottom",
);
let side = session
.new_window(NewWindowOptions::new("side").command("sleep 300"))
.await
.expect("window created");
let mut left = side
.active_pane()
.await
.expect("panes")
.expect("a window has a pane");
let right = side
.split(SplitDirection::Right)
.await
.expect("split right");
left.select().await.expect("focus the left pane");
assert_eq!(
side.focus_direction(PaneDirection::Right)
.await
.expect("focus moves")
.id(),
right.id(),
"right from the left",
);
assert_eq!(
side.focus_direction(PaneDirection::Left)
.await
.expect("focus moves")
.id(),
left.id(),
"left from the right",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}