#![cfg(all(feature = "plan", feature = "test-support"))]
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use std::collections::BTreeSet;
use libtmux::plan::{
Attribution, CapturePane, KillPane, NewSession, NewWindow, Outcome, Plan, Planner, SelectPane,
SendKeys, SetOption, SplitWindow, StepReason,
};
use libtmux::test::TestServer;
use libtmux::{Command, PaneId, Server, WindowId};
fn build_plan(name: &str) -> Plan {
let mut plan = Plan::new();
let session = plan.add(NewSession::new(name));
let pane = plan.add(SplitWindow::new(session.window()).focus());
plan.add(SendKeys::new(pane).text("true").enter());
plan.add(SelectPane::new(session.pane()));
plan
}
#[test]
fn a_planner_changes_what_a_plan_costs_and_not_what_it_records() {
let plan = build_plan("counted");
for planner in [Planner::Sequential, Planner::Folding, Planner::Marked] {
let covered: Vec<usize> = planner
.steps(&plan)
.iter()
.flat_map(|step| step.indices().to_vec())
.collect();
assert_eq!(
covered,
[0, 1, 2, 3],
"{planner:?} runs every operation once"
);
}
assert_eq!(Planner::Sequential.steps(&plan).len(), 4);
assert!(Planner::Marked.steps(&plan).len() < Planner::Sequential.steps(&plan).len());
}
#[test]
fn an_operation_that_reads_output_never_shares_an_invocation() {
let pane: PaneId = "%1".parse().expect("a pane id");
let mut plan = Plan::new();
plan.add(SendKeys::new(pane.clone()).text("ls").enter());
plan.add(CapturePane::new(pane.clone()));
plan.add(SendKeys::new(pane).text("clear").enter());
let steps = Planner::Folding.steps(&plan);
assert_eq!(steps.len(), 3, "the capture splits its neighbours apart");
let reasons: Vec<StepReason> = Planner::Folding
.explain(&plan)
.into_iter()
.map(|(_, reason)| reason)
.collect();
assert_eq!(reasons[1], StepReason::ReadsOutput);
}
#[test]
fn a_boundary_stops_a_fold_without_changing_what_runs() {
let pane: PaneId = "%1".parse().expect("a pane id");
let mut plan = Plan::new();
for text in ["one", "two", "three"] {
plan.add(SendKeys::new(pane.clone()).text(text).enter());
}
assert_eq!(Planner::Folding.steps(&plan).len(), 1);
let bounded = Planner::Folding.steps_bounded(&plan, &BTreeSet::from([0]));
assert_eq!(bounded.len(), 2);
let covered: Vec<usize> = bounded
.iter()
.flat_map(|step| step.indices().to_vec())
.collect();
assert_eq!(covered, [0, 1, 2], "splitting regroups, it does not drop");
}
#[test]
fn a_detached_split_does_not_take_the_marked_fold() {
let window: WindowId = "@1".parse().expect("a window id");
let mut detached = Plan::new();
let pane = detached.add(SplitWindow::new(window.clone()));
detached.add(SendKeys::new(pane).text("go").enter());
assert_eq!(Planner::Marked.steps(&detached).len(), 2);
let mut focused = Plan::new();
let pane = focused.add(SplitWindow::new(window).focus());
focused.add(SendKeys::new(pane).text("go").enter());
assert_eq!(Planner::Marked.steps(&focused).len(), 1);
}
#[test]
fn a_plan_renders_what_it_can_before_it_runs() {
let plan = build_plan("previewed");
let rendered = plan.preview();
assert_eq!(rendered.len(), 4);
assert!(
rendered[0]
.as_ref()
.is_some_and(|command| command.summary().to_string().contains("new-session")),
"an operation naming nothing unbuilt renders now",
);
assert!(
rendered[1].is_none(),
"an operation targeting an object no step has made yet cannot render yet",
);
}
async fn pane_count(server: &Server) -> usize {
server.panes().await.expect("panes list").len()
}
#[tokio::test]
async fn every_planner_leaves_the_same_tmux_state_for_a_different_price() {
let mut costs = Vec::new();
let mut shapes = Vec::new();
for (index, planner) in [Planner::Sequential, Planner::Folding, Planner::Marked]
.into_iter()
.enumerate()
{
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let plan = build_plan(&format!("priced-{index}"));
let result = plan.run(server, planner).await.expect("the plan runs");
assert!(
result.is_complete(),
"{planner:?} completed every operation: {:?}",
result.outcomes(),
);
costs.push((planner, result.dispatches()));
shapes.push(pane_count(server).await);
guard.shutdown().await.expect("tmux fixture shuts down");
}
assert!(
shapes.windows(2).all(|pair| pair[0] == pair[1]),
"the planner did not change the tmux state that resulted: {shapes:?}",
);
let dispatches: Vec<usize> = costs.iter().map(|(_, count)| *count).collect();
assert!(
dispatches[0] > dispatches[2],
"folding costs fewer tmux invocations than one per operation: {costs:?}",
);
}
#[tokio::test]
async fn a_slot_addresses_an_object_the_plan_has_not_made_yet() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut plan = Plan::new();
let session = plan.add(NewSession::new("forward"));
let window = plan.add(NewWindow::new(session).name("built"));
plan.add(SetOption::window(window, "synchronize-panes", "on"));
let result = plan.run(server, Planner::Sequential).await.expect("runs");
assert!(result.is_complete(), "{:?}", result.outcomes());
let created = result.created(1).expect("the window bound an id");
let synchronized = server
.cmd(
Command::new("show-options")
.arg("-w")
.arg("-v")
.arg("-t")
.arg(created)
.arg("synchronize-panes"),
)
.await
.expect("tmux reports the option");
assert_eq!(synchronized.stdout_lossy().trim(), "on");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_failure_alone_is_named_and_a_failure_in_a_fold_is_not() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let absent: PaneId = "%999".parse().expect("a pane id");
let mut plan = Plan::new();
plan.add(NewSession::new("named"));
plan.add(KillPane::new(absent.clone()));
plan.add(SendKeys::new(absent.clone()).text("never").enter());
let sequential = plan.run(server, Planner::Sequential).await.expect("runs");
assert_eq!(sequential.outcomes()[0], Outcome::Complete);
assert_eq!(sequential.outcomes()[1], Outcome::Failed);
assert_eq!(sequential.outcomes()[2], Outcome::Skipped);
assert_eq!(sequential.steps()[1].attribution(), Attribution::PerCommand,);
let mut folded_plan = Plan::new();
folded_plan.add(KillPane::new(absent.clone()));
folded_plan.add(SendKeys::new(absent).text("never").enter());
let folded = folded_plan
.run(server, Planner::Folding)
.await
.expect("runs");
assert_eq!(folded.steps().len(), 1, "the two shared an invocation");
assert_eq!(folded.steps()[0].attribution(), Attribution::Merged);
assert_eq!(
folded.outcomes(),
[Outcome::Unknown, Outcome::Unknown],
"a merged failure names no member, and unknown is not success",
);
assert!(!folded.is_complete());
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "serde")]
#[test]
fn a_plan_survives_a_round_trip_through_json() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt as _;
let mut plan = Plan::new();
let session = plan.add(NewSession::new("wired"));
let window = plan.add(NewWindow::new(session).name("build").focus());
plan.add(SendKeys::new(window.pane()).text("cargo test").enter());
plan.add(SendKeys::new(window.pane()).text(OsString::from_vec(vec![0xff, b'x'])));
plan.add(SetOption::window(window, "synchronize-panes", "on"));
let json = serde_json::to_string(&plan).expect("a plan serialises");
assert!(json.contains("\"cargo test\""), "{json}");
let restored: Plan = serde_json::from_str(&json).expect("a plan deserialises");
assert_eq!(restored.len(), plan.len());
let before: Vec<_> = plan.preview().iter().map(|c| format!("{c:?}")).collect();
let after: Vec<_> = restored
.preview()
.iter()
.map(|c| format!("{c:?}"))
.collect();
assert_eq!(before, after, "a round trip changes nothing that renders");
assert_eq!(
Planner::Marked.steps(&restored).len(),
Planner::Marked.steps(&plan).len(),
);
}