#![cfg(all(feature = "test-support", feature = "tracing"))]
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use libtmux::test::TestServer;
use libtmux::{NewWindowOptions, SplitDirection, SplitOptions};
use tracing::subscriber::Subscriber;
use tracing_subscriber::layer::{Context, Layer, SubscriberExt as _};
use tracing_subscriber::registry::LookupSpan;
#[derive(Clone, Default)]
struct CommandCounter {
count: Arc<AtomicUsize>,
}
impl CommandCounter {
fn commands(&self) -> usize {
self.count.load(Ordering::Relaxed)
}
fn reset(&self) {
self.count.store(0, Ordering::Relaxed);
}
}
impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for CommandCounter {
fn on_event(&self, event: &tracing::Event<'_>, _context: Context<'_, S>) {
struct Message<'a>(&'a mut bool);
impl tracing::field::Visit for Message<'_> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" && format!("{value:?}").contains("requested") {
*self.0 = true;
}
}
}
let mut requested = false;
event.record(&mut Message(&mut requested));
if requested {
self.count.fetch_add(1, Ordering::Relaxed);
}
}
}
async fn populate(server: &libtmux::Server, prefix: &str, sessions: usize, windows: usize) {
for session in 0..sessions {
let session = server
.new_session(format!("{prefix}-{session}").as_str())
.await
.expect("session");
for window in 0..windows {
let created = session
.new_window(
NewWindowOptions::new(format!("window-{window}").as_str()).command("sleep 300"),
)
.await
.expect("window");
created
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane");
}
}
}
#[tokio::test]
async fn the_hierarchy_costs_the_same_however_large_it_is() {
let counter = CommandCounter::default();
let subscriber = tracing_subscriber::registry().with(counter.clone());
let _guard = tracing::subscriber::set_default(subscriber);
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
populate(server, "small", 1, 1).await;
counter.reset();
let small = server.hierarchy().await.expect("hierarchy");
let small_commands = counter.commands();
populate(server, "large", 3, 5).await;
counter.reset();
let large = server.hierarchy().await.expect("hierarchy");
let large_commands = counter.commands();
assert!(large.len() > small.len(), "the second hierarchy is larger");
assert!(
large.iter().map(|tree| tree.windows.len()).sum::<usize>() > 10,
"and larger by enough for a per-object cost to show",
);
assert_eq!(small_commands, 3, "sessions, windows, and panes");
assert_eq!(
large_commands, small_commands,
"the cost does not grow with the hierarchy",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn walking_down_costs_a_command_for_every_step() {
let counter = CommandCounter::default();
let subscriber = tracing_subscriber::registry().with(counter.clone());
let _guard = tracing::subscriber::set_default(subscriber);
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
populate(server, "walked", 2, 3).await;
counter.reset();
let sessions = server.sessions().await.expect("sessions");
let mut windows = 0;
for session in &sessions {
for window in session.windows().await.expect("windows") {
windows += 1;
let _ = window.panes().await.expect("panes");
}
}
let walked = counter.commands();
counter.reset();
server.hierarchy().await.expect("hierarchy");
let gathered = counter.commands();
assert_eq!(
walked,
1 + sessions.len() + windows,
"one listing per session and per window, plus the first",
);
assert_eq!(gathered, 3);
assert!(
walked > gathered * 3,
"walking cost {walked} commands where the hierarchy cost {gathered}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn asking_a_client_what_it_is_attached_to_costs_one_command_each() {
let counter = CommandCounter::default();
let subscriber = tracing_subscriber::registry().with(counter.clone());
let _guard = tracing::subscriber::set_default(subscriber);
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("attached").await.expect("session");
let control = libtmux::control::ControlMode::attach(server, session.id())
.await
.expect("a control client");
let client = server
.clients()
.await
.expect("clients")
.into_iter()
.next()
.expect("one client");
counter.reset();
let attached = client.attached_session().await.expect("a session");
let session_commands = counter.commands();
counter.reset();
let window = client.attached_window().await.expect("a window");
let window_commands = counter.commands();
counter.reset();
let pane = client.attached_pane().await.expect("a pane");
let pane_commands = counter.commands();
assert_eq!(attached.expect("a session").id(), session.id());
assert!(window.is_some(), "the session has a current window");
assert!(pane.is_some(), "that window has an active pane");
assert_eq!(
session_commands, 1,
"one display-message, no second listing"
);
assert_eq!(window_commands, 1, "the same for the window");
assert_eq!(pane_commands, 1, "and for the pane");
control.shutdown().await.expect("control shuts down");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "query")]
#[tokio::test]
async fn reading_a_listed_pane_field_costs_no_command() {
use libtmux::query::Filterable as _;
use libtmux::{Availability, Command, NewSessionOptions, Pane, PaneWait};
let counter = CommandCounter::default();
let subscriber = tracing_subscriber::registry().with(counter.clone());
let _guard = tracing::subscriber::set_default(subscriber);
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session(NewSessionOptions::new("read").command("seq 1 200; printf abc; exec cat"))
.await
.expect("session");
let mut pane = session.panes().await.expect("panes").remove(0);
let arrived = pane
.wait_for_text("abc", std::time::Duration::from_secs(10))
.await
.expect("wait");
assert_eq!(arrived, PaneWait::Arrived);
let fields = Pane::filter_fields();
for scrolled in [None, Some(5)] {
if let Some(lines) = scrolled {
pane.copy_mode().await.expect("copy mode");
pane.cmd(
Command::new("send-keys")
.arg("-X")
.arg("-N")
.arg(lines.to_string())
.arg("scroll-up"),
)
.await
.expect("scroll");
}
pane.refresh().await.expect("listing");
counter.reset();
let cursor_x = pane.get(fields.cursor_x);
let cursor_y = pane.get(fields.cursor_y);
let pane_mode = pane.get(fields.pane_mode);
let scroll_position = pane.get(fields.scroll_position);
assert_eq!(counter.commands(), 0, "a read sends tmux nothing");
assert_eq!(cursor_x, Availability::Available(3), "after `abc`");
if let Some(lines) = scrolled {
assert!(pane_mode.is_available());
assert_eq!(scroll_position, Availability::Available(lines));
} else {
assert_eq!(pane_mode, Availability::Absent);
assert_eq!(scroll_position, Availability::Absent);
}
let reads = [
("#{cursor_x}", cursor_x.available().map(|x| x.to_string())),
("#{cursor_y}", cursor_y.available().map(|y| y.to_string())),
(
"#{pane_mode}",
pane_mode
.available()
.map(|mode| mode.to_string_lossy().into_owned()),
),
(
"#{scroll_position}",
scroll_position.available().map(|lines| lines.to_string()),
),
];
for (format, read) in reads {
let asked = pane.format(format).await.expect("display-message");
assert_eq!(
asked.to_string_lossy(),
read.unwrap_or_default(),
"{format} with scroll {scrolled:?}",
);
}
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn replacing_hooks_clears_and_writes_in_one_command() {
use libtmux::{IndexedHooks, ReplaceMode, TmuxText};
let counter = CommandCounter::default();
let subscriber = tracing_subscriber::registry().with(counter.clone());
let _guard = tracing::subscriber::set_default(subscriber);
let guard = TestServer::builder().start().await.expect("tmux starts");
let session = guard
.server()
.new_session("hooks")
.await
.expect("the session is created");
let mut entries = std::collections::BTreeMap::new();
entries.insert(0, TmuxText::from(b"display-message one".to_vec()));
entries.insert(1, TmuxText::from(b"display-message two".to_vec()));
let written = IndexedHooks::from(entries);
session
.set_hooks("alert-bell", &written, ReplaceMode::Replace)
.await
.expect("the hooks are written");
counter.reset();
session
.set_hooks("alert-bell", &written, ReplaceMode::Replace)
.await
.expect("the hooks are written");
assert_eq!(
counter.commands(),
1,
"the clear and both entries travel together",
);
let read = session
.hook("alert-bell")
.await
.expect("the hook reads")
.expect("the hook is set");
assert_eq!(read.len(), 2);
}