#![cfg(feature = "query")]
#![cfg(feature = "test-support")]
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use libtmux::query::{Filterable as _, QueryIteratorExt as _};
use libtmux::test::TestServer;
use libtmux::{
NewSessionOptions, NewWindowOptions, Pane, SessionTree, SplitDirection, SplitOptions,
WindowTree,
};
async fn populate(server: &libtmux::Server) {
let build = server
.new_session(NewSessionOptions::new("build").window_name("editor"))
.await
.expect("session");
build
.windows()
.await
.expect("windows")
.remove(0)
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane");
build
.new_window(NewWindowOptions::new("logs").command("sleep 300"))
.await
.expect("window");
server
.new_session(NewSessionOptions::new("idle").window_name("shell"))
.await
.expect("session");
}
#[tokio::test]
async fn a_session_is_selected_by_what_its_windows_hold() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
populate(server).await;
let branches = server.hierarchy().await.expect("hierarchy");
let sessions = SessionTree::filter_fields();
let windows = WindowTree::filter_fields();
let named = sessions
.windows
.any(windows.window.window_name.eq("editor"));
let matched: Vec<_> = branches
.iter()
.matching(&named)
.map(|branch| branch.session.name().to_string_lossy().into_owned())
.collect();
assert_eq!(matched, ["build"]);
let split_window = sessions.windows.any(
windows
.panes
.any(Pane::filter_fields().pane_index.eq(1_u32)),
);
let busy = sessions
.session
.session_name
.starts_with("bui")
.and(split_window);
assert_eq!(
branches.iter().matching(&busy).count(),
1,
"one session has a window with more than one pane",
);
let quiet = sessions
.windows
.none(windows.window.window_name.eq("editor"));
let quiet: Vec<_> = branches
.iter()
.matching(&quiet)
.map(|branch| branch.session.name().to_string_lossy().into_owned())
.collect();
assert_eq!(quiet, ["idle"]);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn every_window_must_satisfy_all_and_an_empty_relation_does_so_vacuously() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
populate(server).await;
let branches = server.hierarchy().await.expect("hierarchy");
let sessions = SessionTree::filter_fields();
let windows = WindowTree::filter_fields();
let uniform = sessions.windows.all(windows.window.window_name.eq("shell"));
let matched: Vec<_> = branches
.iter()
.matching(&uniform)
.map(|branch| branch.session.name().to_string_lossy().into_owned())
.collect();
assert_eq!(matched, ["idle"], "only the session with one such window");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "serde")]
#[tokio::test]
async fn a_relation_survives_the_portable_envelope() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
populate(server).await;
let sessions = SessionTree::filter_fields();
let windows = WindowTree::filter_fields();
let expression = sessions
.windows
.any(windows.window.window_name.eq("editor"));
let wire = serde_json::to_value(&expression).expect("the expression serializes");
assert_eq!(wire["target"], "session_tree");
assert_eq!(wire["expr"]["op"], "relation");
assert_eq!(wire["expr"]["quantifier"], "any");
assert_eq!(wire["expr"]["field"], "windows");
assert_eq!(wire["expr"]["expr"]["field"], "window_name");
let decoded: libtmux::query::FilterExpr<SessionTree> =
serde_json::from_value(wire).expect("the expression round-trips");
let branches = server.hierarchy().await.expect("hierarchy");
assert_eq!(
branches
.iter()
.matching(&decoded)
.map(|branch| branch.session.name().to_string_lossy().into_owned())
.collect::<Vec<_>>(),
["build"],
);
let mismatched = serde_json::json!({
"version": 1,
"target": "session_tree",
"expr": {
"op": "relation",
"quantifier": "any",
"field": "windows",
"expr": {"op": "eq", "field": "pane_index", "value": 0},
},
});
assert!(
serde_json::from_value::<libtmux::query::FilterExpr<SessionTree>>(mismatched).is_err(),
"pane_index is not a window field",
);
let unknown = serde_json::json!({
"version": 1,
"target": "session_tree",
"expr": {
"op": "relation",
"quantifier": "any",
"field": "clients",
"expr": {"op": "eq", "field": "window_name", "value": "x"},
},
});
assert!(serde_json::from_value::<libtmux::query::FilterExpr<SessionTree>>(unknown).is_err(),);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_numeric_field_compares_by_order_as_well_as_by_value() {
use libtmux::Session;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let busy = server
.new_session(NewSessionOptions::new("busy"))
.await
.expect("session");
for name in ["one", "two", "three"] {
busy.new_window(NewWindowOptions::new(name).command("sleep 300"))
.await
.expect("window");
}
server
.new_session(NewSessionOptions::new("quiet"))
.await
.expect("session");
let sessions = server.sessions().await.expect("sessions");
let fields = Session::filter_fields();
let named = |expression: &libtmux::query::FilterExpr<Session>| {
let mut names: Vec<_> = sessions
.iter()
.matching(expression)
.map(|session| session.name().to_string_lossy().into_owned())
.collect();
names.sort_unstable();
names
};
assert_eq!(named(&fields.session_windows.gt(1_u32)), ["busy"]);
assert_eq!(named(&fields.session_windows.gte(4_u32)), ["busy"]);
assert_eq!(named(&fields.session_windows.lt(4_u32)), ["quiet"]);
assert_eq!(named(&fields.session_windows.lte(1_u32)), ["quiet"]);
assert_eq!(
named(&fields.session_windows.gte(1_u32)),
["busy", "quiet"],
"a bound is inclusive where the name says so",
);
assert!(named(&fields.session_windows.gt(9_u32)).is_empty());
let ten = server
.new_session(NewSessionOptions::new("ten"))
.await
.expect("session");
for index in 0..9 {
ten.new_window(NewWindowOptions::new(format!("w{index}").as_str()).command("sleep 300"))
.await
.expect("window");
}
let sessions = server.sessions().await.expect("sessions");
let over_nine: Vec<_> = sessions
.iter()
.matching(&fields.session_windows.gt(9_u32))
.map(|session| session.name().to_string_lossy().into_owned())
.collect();
assert_eq!(over_nine, ["ten"]);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "serde")]
#[test]
fn ordering_travels_the_wire_and_is_refused_where_it_has_no_meaning() {
use libtmux::Session;
let expression = Session::filter_fields().session_windows.gt(2_u32);
let wire = serde_json::to_value(&expression).expect("the expression serializes");
assert_eq!(wire["expr"]["op"], "gt");
assert_eq!(wire["expr"]["value"], "2");
let decoded: libtmux::query::FilterExpr<Session> =
serde_json::from_value(wire).expect("the expression round-trips");
assert_eq!(decoded, expression);
let on_text = serde_json::json!({
"version": 1,
"target": "session",
"expr": {"op": "gt", "field": "session_name", "value": "a"},
});
assert!(serde_json::from_value::<libtmux::query::FilterExpr<Session>>(on_text).is_err());
let above_nine = serde_json::json!({
"version": 1,
"target": "session",
"expr": {"op": "gt", "field": "session_windows", "value": "9"},
});
let above_nine: libtmux::query::FilterExpr<Session> =
serde_json::from_value(above_nine).expect("the expression decodes");
assert_eq!(
above_nine,
Session::filter_fields().session_windows.gt(9_u32),
"the wire form and the authored form are the same expression",
);
let two_bounds = serde_json::json!({
"version": 1,
"target": "session",
"expr": {"op": "gt", "field": "session_windows", "value": ["1", "2"]},
});
assert!(serde_json::from_value::<libtmux::query::FilterExpr<Session>>(two_bounds).is_err());
}