#![allow(clippy::expect_used, clippy::print_stdout, reason = "an example")]
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use libtmux::plan::{
Attribution, NewSession, NewWindow, Outcome, Plan, Planner, SelectPane, SendKeys, SetOption,
};
use libtmux::query::{Filterable as _, QueryIteratorExt as _};
use libtmux::test::{TestServer, TestServerBuilder};
use libtmux::{Server, blocking};
struct Row {
mode: &'static str,
feature: &'static str,
dispatches: usize,
processes: usize,
elapsed: Duration,
attribution: &'static str,
query: String,
}
struct Witness {
directory: tempfile::TempDir,
}
impl Witness {
fn new() -> Self {
let real = real_tmux();
let directory = tempfile::tempdir().expect("a scratch directory");
let witness = Self { directory };
let script = format!(
"#!/bin/sh\nprintf 'start\\n' >> '{log}'\nexec '{real}' \"$@\"\n",
log = witness.log().display(),
real = real.display(),
);
std::fs::write(witness.executable(), script).expect("the wrapper is written");
std::fs::set_permissions(witness.executable(), std::fs::Permissions::from_mode(0o755))
.expect("the wrapper is executable");
std::fs::write(witness.log(), "").expect("the log starts empty");
witness
}
fn executable(&self) -> PathBuf {
self.directory.path().join("tmux")
}
fn log(&self) -> PathBuf {
self.directory.path().join("started.log")
}
fn server(&self) -> TestServerBuilder {
TestServer::builder().tmux_executable(self.executable())
}
fn started(&self) -> usize {
std::fs::read_to_string(self.log())
.expect("the log is readable")
.lines()
.count()
}
}
fn real_tmux() -> PathBuf {
if let Some(pinned) = std::env::var_os("LIBTMUX_TEST_TMUX") {
return PathBuf::from(pinned);
}
let path = std::env::var_os("PATH").expect("PATH is set");
std::env::split_paths(&path)
.map(|directory| directory.join("tmux"))
.find(|candidate| Path::is_file(candidate))
.expect("tmux is on PATH")
}
fn workload(name: &str) -> Plan {
let mut plan = Plan::new();
let session = plan.add(NewSession::new(name).window_name("editor"));
let window = plan.add(NewWindow::new(session).name("build").focus());
plan.add(SendKeys::new(window.pane()).text("# built").enter());
plan.add(SendKeys::new(window.pane()).text("# ready").enter());
plan.add(SetOption::window(
session.window(),
"synchronize-panes",
"off",
));
plan.add(SelectPane::new(session.pane()));
plan
}
async fn query(server: &Server, session: &str) -> String {
let session = server
.session(session)
.await
.expect("the session is readable")
.expect("the workload built its session");
let windows = session.windows().await.expect("windows list");
let mut panes = Vec::new();
for window in &windows {
panes.extend(window.panes().await.expect("panes list"));
}
let fields = libtmux::Pane::filter_fields();
let active = panes.iter().matching(&fields.pane_active.eq(true)).count();
format!(
"{} panes, {} windows, {active} active",
panes.len(),
windows.len()
)
}
fn fidelity(outcomes: impl IntoIterator<Item = Outcome>, attribution: Attribution) -> &'static str {
if outcomes
.into_iter()
.any(|outcome| outcome == Outcome::Unknown)
{
return "unknown";
}
match attribution {
Attribution::PerCommand => "per-command",
Attribution::Merged => "merged",
}
}
async fn run_subprocess(mode: &'static str, planner: Planner, name: &str) -> Row {
let witness = Witness::new();
let guard = witness.server().start().await.expect("tmux starts");
let server = guard.server();
let plan = workload(name);
let before = witness.started();
let started = Instant::now();
let result = plan.run(server, planner).await.expect("the plan runs");
let elapsed = started.elapsed();
let processes = witness.started() - before;
let attribution = result
.steps()
.iter()
.map(libtmux::plan::StepOutcome::attribution)
.fold(Attribution::PerCommand, |worst, next| {
if next == Attribution::Merged {
next
} else {
worst
}
});
let row = Row {
mode,
feature: "plan",
dispatches: result.dispatches(),
processes,
elapsed,
attribution: fidelity(
result
.operations()
.iter()
.map(libtmux::plan::OperationReport::outcome),
attribution,
),
query: query(server, name).await,
};
guard.shutdown().await.expect("tmux shuts down");
row
}
#[derive(Clone, Copy)]
enum Connection {
Streaming,
Routed,
}
async fn run_control_mode(connection: Connection, name: &str) -> Row {
let witness = Witness::new();
let guard = witness.server().start().await.expect("tmux starts");
let server = guard.server();
let host = server
.new_session("control-host")
.await
.expect("host session");
let before = witness.started();
let control = libtmux::control::ControlMode::attach(server, host.id())
.await
.expect("control mode attaches");
let (sender, events) = control.split();
let plan = workload(name);
let started = Instant::now();
let (mode, result) = match connection {
Connection::Streaming => (
"control-mode/streaming",
plan.run_over_control_mode(&sender).await,
),
Connection::Routed => {
let routed = server
.over_control_mode(&sender)
.await
.expect("the connection reaches this server");
(
"control-mode/routed",
plan.run(&routed, Planner::Sequential).await,
)
}
};
let result = result.expect("the plan runs");
let elapsed = started.elapsed();
let processes = witness.started() - before;
let row = Row {
mode,
feature: "plan,control-mode",
dispatches: result.dispatches(),
processes,
elapsed,
attribution: fidelity(
result
.operations()
.iter()
.map(libtmux::plan::OperationReport::outcome),
Attribution::PerCommand,
),
query: query(server, name).await,
};
events.shutdown().await.expect("control mode shuts down");
guard.shutdown().await.expect("tmux shuts down");
row
}
fn run_blocking(name: &str) -> Row {
let runtime = blocking::Runtime::new().expect("a runtime");
runtime.run(run_subprocess(
"blocking/sequential",
Planner::Sequential,
name,
))
}
#[tokio::main]
async fn main() {
let mut rows = vec![
run_subprocess("async/sequential", Planner::Sequential, "async-seq").await,
run_subprocess("async/folded", Planner::Folding, "async-fold").await,
run_subprocess("async/marked-fold", Planner::Marked, "async-marked").await,
run_control_mode(Connection::Streaming, "control").await,
run_control_mode(Connection::Routed, "routed").await,
];
rows.insert(
0,
tokio::task::spawn_blocking(|| run_blocking("blocking-seq"))
.await
.expect("blocking run"),
);
println!(
"{:<24} {:<18} {:>10} {:>9} {:>9} {:<12} query output",
"mode", "feature", "dispatches", "processes", "wall", "attribution",
);
println!("{}", "-".repeat(118));
for row in &rows {
println!(
"{:<24} {:<18} {:>10} {:>9} {:>8.0?} {:<12} {}",
row.mode,
row.feature,
row.dispatches,
row.processes,
row.elapsed,
row.attribution,
row.query,
);
}
let queries: Vec<&str> = rows.iter().map(|row| row.query.as_str()).collect();
println!();
println!(
"every mode built the same thing: {}",
queries.windows(2).all(|pair| pair[0] == pair[1]),
);
println!(
"dispatches ranged {}..{}, processes ranged {}..{}",
rows.iter().map(|row| row.dispatches).min().unwrap_or(0),
rows.iter().map(|row| row.dispatches).max().unwrap_or(0),
rows.iter().map(|row| row.processes).min().unwrap_or(0),
rows.iter().map(|row| row.processes).max().unwrap_or(0),
);
println!();
what_a_fold_costs_when_something_fails().await;
}
async fn what_a_fold_costs_when_something_fails() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let absent: libtmux::PaneId = "%999".parse().expect("a pane id");
println!("the same failure, dispatched two ways:");
for planner in [Planner::Sequential, Planner::Folding] {
let mut plan = Plan::new();
plan.add(libtmux::plan::KillPane::new(absent.clone()));
plan.add(SendKeys::new(absent.clone()).text("never runs").enter());
let result = plan.run(server, planner).await.expect("the plan runs");
println!(
" {:<12} {} dispatch(es) -> {:?}",
format!("{planner:?}"),
result.dispatches(),
result
.operations()
.iter()
.map(libtmux::plan::OperationReport::outcome)
.collect::<Vec<_>>(),
);
}
println!(" Sequential names the failing operation; Folding cannot, because tmux");
println!(" reports one status for the group whichever member failed.");
guard.shutdown().await.expect("tmux shuts down");
}