srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/platform/src/ipc/tests.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/platform/src/ipc/tests.rs')
-rw-r--r--crates/platform/src/ipc/tests.rs630
1 files changed, 630 insertions, 0 deletions
diff --git a/crates/platform/src/ipc/tests.rs b/crates/platform/src/ipc/tests.rs
new file mode 100644
index 0000000..cb56a7c
--- /dev/null
+++ b/crates/platform/src/ipc/tests.rs
@@ -0,0 +1,630 @@
+use super::*;
+use std::cell::RefCell;
+use std::io::BufRead;
+use std::rc::Rc;
+
+/// Takes a reader the caller already owns, rather than building a fresh
+/// `BufReader` around a cloned handle every call (what this used to do):
+/// `BufRead::read_line` is free to read further ahead than one line in
+/// a single syscall whenever more is already sitting in the kernel
+/// socket buffer - true the moment a caller (like `"subscribe"`'s
+/// two-line initial reply, added alongside the workspace event) writes
+/// more than one line in one `write_all`. A fresh `BufReader` built
+/// per call has nowhere to keep whatever it over-read once the call
+/// returns and it's dropped - that data is already gone from the
+/// kernel's queue, so the *next* fresh `BufReader`'s read blocks
+/// forever waiting for bytes that already arrived and were silently
+/// discarded. Hung an entire test run silently, with no compiler error
+/// and no panic to point at it, until the underlying `cargo test`
+/// process was found sitting at 0% CPU with no explanation. One
+/// `BufReader`, reused for every `read_line` call in a test, keeps
+/// whatever it over-reads available for the next call instead.
+fn read_line(reader: &mut std::io::BufReader<UnixStream>) -> String {
+ let mut line = String::new();
+ reader.read_line(&mut line).unwrap();
+ line
+}
+
+#[test]
+fn subscribe_gets_an_immediate_snapshot() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"subscribe\"}\n").unwrap();
+ server.poll(&wm);
+
+ // Four lines, not one: a client snapshot, a workspace snapshot, a
+ // keyboard-layout snapshot, and a monitor snapshot, same as every
+ // later push - see the `"subscribe"` match arm's own doc comment
+ // for why all four are sent immediately rather than waiting for
+ // the first real change of each kind.
+ let clients_line = read_line(&mut reader);
+ assert!(clients_line.contains(r#""event":"clients""#));
+ assert!(clients_line.contains(r#""clients":[]"#));
+
+ let workspaces_line = read_line(&mut reader);
+ assert!(workspaces_line.contains(r#""event":"workspaces""#));
+ assert!(workspaces_line.contains(r#""id":1"#));
+ assert!(workspaces_line.contains(r#""active":true"#));
+
+ let layout_line = read_line(&mut reader);
+ assert!(layout_line.contains(r#""event":"keyboard_layout""#));
+ assert!(layout_line.contains(r#""layout":""#));
+
+ let monitors_line = read_line(&mut reader);
+ assert!(monitors_line.contains(r#""event":"monitors""#));
+ assert!(monitors_line.contains(r#""monitors":[]"#));
+}
+
+#[test]
+fn subscribe_then_a_monitor_change_pushes_a_fresh_monitors_event() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"subscribe\"}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader); // clients
+ let _ = read_line(&mut reader); // workspaces
+ let _ = read_line(&mut reader); // keyboard_layout
+ let _ = read_line(&mut reader); // monitors (empty)
+
+ wm.borrow_mut().set_monitors(vec![{
+ let mut m = srdwm_core::Monitor::new(0, "HDMI-A-1", srdwm_core::Rect::new(0, 0, 1920, 1080));
+ m.primary = true;
+ m
+ }]);
+ server.poll(&wm);
+
+ // A real monitor existing for the first time also changes which
+ // monitor (if any) `WorkspaceInfo::monitor` reports for the
+ // now-visible workspace - `None` (no monitor existed to show it)
+ // to `Some(0)` - so a "workspaces" event fires too, ahead of
+ // "monitors" in `poll`'s own emission order. Drained here, not
+ // asserted on: this test is about the monitors event specifically.
+ let workspaces_line = read_line(&mut reader);
+ assert!(workspaces_line.contains(r#""event":"workspaces""#));
+
+ let line = read_line(&mut reader);
+ assert!(line.contains(r#""event":"monitors""#));
+ assert!(line.contains(r#""name":"HDMI-A-1""#));
+}
+
+#[test]
+fn subscribe_then_a_window_change_pushes_a_fresh_snapshot() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"subscribe\"}\n").unwrap();
+ server.poll(&wm);
+ let _initial_clients = read_line(&mut reader);
+ let _initial_workspaces = read_line(&mut reader);
+ let _initial_layout = read_line(&mut reader);
+ let _initial_monitors = read_line(&mut reader);
+
+ {
+ let mut wm = wm.borrow_mut();
+ let id = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(id, "hello"));
+ }
+ server.poll(&wm);
+
+ let pushed = read_line(&mut reader);
+ assert!(pushed.contains(r#""event":"clients""#));
+ assert!(pushed.contains(r#""title":"hello""#));
+}
+
+#[test]
+fn subscribe_then_a_workspace_switch_pushes_a_fresh_workspaces_event() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().add_workspace("2", "dynamic");
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"subscribe\"}\n").unwrap();
+ server.poll(&wm);
+ let _initial_clients = read_line(&mut reader);
+ let _initial_workspaces = read_line(&mut reader);
+ let _initial_layout = read_line(&mut reader);
+ let _initial_monitors = read_line(&mut reader);
+
+ wm.borrow_mut().switch_workspace(2);
+ server.poll(&wm);
+
+ // Switching touches no window, so the clients list is unchanged --
+ // the next line waiting must be the workspaces push, not a
+ // clients one that never comes.
+ let pushed = read_line(&mut reader);
+ assert!(pushed.contains(r#""event":"workspaces""#));
+ assert!(pushed.contains(r#""id":2,"name":"2","layout":"dynamic","active":true"#));
+}
+
+#[test]
+fn a_poll_with_no_real_change_pushes_nothing_new() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"subscribe\"}\n").unwrap();
+ server.poll(&wm);
+ let _initial_clients = read_line(&mut reader);
+ let _initial_workspaces = read_line(&mut reader);
+ let _initial_layout = read_line(&mut reader);
+ let _initial_monitors = read_line(&mut reader);
+
+ // Nothing changed between these two polls - a second push would
+ // show up as a second readable line the client isn't expecting.
+ server.poll(&wm);
+ server.poll(&wm);
+ client.set_nonblocking(true).unwrap();
+ let mut buf = [0u8; 16];
+ match client.read(&mut buf) {
+ Err(e) if e.kind() == ErrorKind::WouldBlock => {}
+ other => panic!("expected no further data, got {other:?}"),
+ }
+}
+
+#[test]
+fn workspaces_command_reports_the_default_workspace() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"workspaces\"}\n").unwrap();
+ server.poll(&wm);
+
+ let line = read_line(&mut reader);
+ assert!(!line.contains(r#""event""#), "one-shot command, same plain shape as \"clients\"");
+ assert!(line.contains(r#""id":1,"name":"1","layout":"dynamic","active":true"#));
+}
+
+#[test]
+fn workspaces_and_monitors_agree_on_which_monitor_shows_which_workspace() {
+ // `WorkspaceInfo::monitor` and `MonitorInfo::active_workspace` are
+ // the same fact from either direction - requested by an AGS peer
+ // session so a workspace pill or a per-monitor picker can each
+ // read it from whichever side it already has in hand. Default
+ // `WindowManager::new()` starts on workspace `1` with no real
+ // monitor set up yet; adding one real monitor (id `0`) must make
+ // workspace `1`'s own `monitor` read back `0`, and that monitor's
+ // `active_workspace` read back `1`.
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "eDP-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ // Two separate connections, not one reused - a one-shot command's
+ // connection closes after its reply (see `a_oneshot_clients_
+ // request_still_closes_the_connection_as_before`), so a second
+ // command on the same `client` would just hit a broken pipe.
+ let mut workspaces_client = UnixStream::connect(&server.path).unwrap();
+ let mut workspaces_reader = std::io::BufReader::new(workspaces_client.try_clone().unwrap());
+ workspaces_client.write_all(b"{\"cmd\":\"workspaces\"}\n").unwrap();
+ server.poll(&wm);
+ let workspaces_line = read_line(&mut workspaces_reader);
+ assert!(workspaces_line.contains(r#""id":1,"name":"1","layout":"dynamic","active":true,"monitor":0"#));
+
+ let mut monitors_client = UnixStream::connect(&server.path).unwrap();
+ let mut monitors_reader = std::io::BufReader::new(monitors_client.try_clone().unwrap());
+ monitors_client.write_all(b"{\"cmd\":\"monitors\"}\n").unwrap();
+ server.poll(&wm);
+ let monitors_line = read_line(&mut monitors_reader);
+ assert!(monitors_line.contains(r#""name":"eDP-1""#));
+ assert!(monitors_line.contains(r#""active_workspace":1"#));
+}
+
+#[test]
+fn settings_command_reflects_a_prior_set() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ // A fresh connection per command - one-shot commands close the
+ // connection after replying (see `a_oneshot_clients_request_
+ // still_closes_the_connection_as_before`), so a second write on
+ // the same client after its first reply hits a closed socket.
+ let mut set_client = UnixStream::connect(&server.path).unwrap();
+ set_client.write_all(b"{\"cmd\":\"set\",\"key\":\"night_light\",\"value\":true}\n").unwrap();
+ server.poll(&wm);
+ let _set_reply = read_line(&mut std::io::BufReader::new(set_client.try_clone().unwrap()));
+
+ let mut settings_client = UnixStream::connect(&server.path).unwrap();
+ settings_client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut std::io::BufReader::new(settings_client));
+ assert!(!line.contains(r#""event""#), "one-shot command, same plain shape as \"clients\"");
+ assert!(line.contains(r#""night_light":true"#));
+ assert!(line.contains(r#""reading_mode":false"#), "night_light and reading_mode share one slot, so setting one leaves the other off");
+}
+
+#[test]
+fn setting_night_light_then_reading_mode_clears_night_light() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ client.write_all(b"{\"cmd\":\"set\",\"key\":\"night_light\",\"value\":true}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut std::io::BufReader::new(client));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ client.write_all(b"{\"cmd\":\"set\",\"key\":\"reading_mode\",\"value\":true}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut std::io::BufReader::new(client));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut std::io::BufReader::new(client));
+ assert!(line.contains(r#""night_light":false"#));
+ assert!(line.contains(r#""reading_mode":true"#));
+}
+
+#[test]
+fn keyboard_layout_command_reports_the_current_layout() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_keyboard_layout("English (US)");
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"keyboard_layout\"}\n").unwrap();
+ server.poll(&wm);
+
+ let line = read_line(&mut reader);
+ assert!(!line.contains(r#""event""#), "one-shot command, same plain shape as \"clients\"");
+ assert!(line.contains(r#""layout":"English (US)""#));
+}
+
+#[test]
+fn cycle_keyboard_layout_dispatch_queues_a_request_for_main_rs_to_act_on() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"cycle_keyboard_layout\"}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ // `IpcServer` has no seat/keyboard of its own to actually cycle --
+ // it can only queue the intent for `main.rs`'s `sync()` to act on,
+ // same as `close_requests`/`activate_workspace`. This is as far as
+ // this crate can verify the request landed.
+ assert_eq!(wm.borrow_mut().take_keyboard_layout_cycle_requests(), 1);
+}
+
+#[test]
+fn activate_workspace_dispatch_switches_the_current_workspace() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().add_workspace("2", "dynamic");
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ // `id: 2`, not `1` - `WindowManager::new`'s default workspace is
+ // id 1 (already current), and `switch_workspace` no-ops when asked
+ // to "switch" to the already-current workspace, so activating `1`
+ // would silently test nothing: the assertion below would pass
+ // whether or not this dispatch actually worked at all.
+ client.write_all(b"{\"cmd\":\"activate_workspace\",\"id\":2}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert_eq!(wm.borrow().current_workspace(), 2);
+}
+
+#[test]
+fn set_output_position_resolves_a_monitor_name_to_its_id() {
+ // The CLI (`srd dispatch set output position <name> <x> <y>`)
+ // sends a `name`, not an `id`, whenever the caller didn't already
+ // have a numeric id handy - `srd monitors` reports names, not an
+ // arbitrary index, so a display-arrangement UI reasonably works
+ // in those terms.
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![
+ srdwm_core::Monitor::new(0, "EmbeddedDisplayPort-1", srdwm_core::Rect::new(0, 0, 1920, 1080)),
+ srdwm_core::Monitor::new(1, "HDMI-A-1", srdwm_core::Rect::new(1920, 0, 1920, 1080)),
+ ]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_output_position\",\"name\":\"HDMI-A-1\",\"x\":1920,\"y\":0}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ let queued = wm.borrow_mut().drain_output_position_requests();
+ assert_eq!(queued, vec![(1, 1920, 0)], "must resolve to monitor id 1, not treat the name as missing");
+}
+
+#[test]
+fn set_output_position_with_an_unknown_name_errors_instead_of_silently_no_opping() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "EmbeddedDisplayPort-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_output_position\",\"name\":\"nonexistent-output\",\"x\":0,\"y\":0}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+
+ assert!(line.contains(r#""error""#), "an unresolvable name must error, not silently queue nothing");
+ assert!(wm.borrow_mut().drain_output_position_requests().is_empty());
+}
+
+#[test]
+fn set_output_enabled_accepts_a_name_directly() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "HDMI-A-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_output_enabled\",\"name\":\"HDMI-A-1\",\"enabled\":false}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert_eq!(wm.borrow_mut().drain_output_enable_requests(), vec![("HDMI-A-1".to_string(), false)]);
+}
+
+#[test]
+fn set_output_enabled_resolves_an_id_to_its_name_for_the_disable_direction() {
+ // `id` only ever resolves against the *live* monitor list - fine
+ // for disabling something currently connected, which is the only
+ // case where a stale-index concern doesn't apply yet.
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(3, "HDMI-A-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_output_enabled\",\"id\":3,\"enabled\":false}\n").unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert_eq!(wm.borrow_mut().drain_output_enable_requests(), vec![("HDMI-A-1".to_string(), false)]);
+}
+
+#[test]
+fn set_output_enabled_with_neither_name_nor_a_resolvable_id_errors() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"set_output_enabled\",\"enabled\":true}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+
+ assert!(line.contains(r#""error""#));
+ assert!(wm.borrow_mut().drain_output_enable_requests().is_empty());
+}
+
+#[test]
+fn monitors_query_lists_a_disabled_output_alongside_live_ones() {
+ // What the AGS peer session asked for directly: a disabled output
+ // must not just vanish from `srd monitors` - it needs a row
+ // (name + `enabled: false`) to offer turning back on.
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "EmbeddedDisplayPort-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+ wm.borrow_mut().set_disabled_monitor("HDMI-A-1".to_string(), srdwm_core::Rect::new(1920, 0, 1920, 1080), srdwm_core::Rect::new(1920, 0, 1920, 1080), false);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"monitors\"}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+
+ assert!(line.contains(r#""name":"EmbeddedDisplayPort-1""#));
+ assert!(line.contains(r#""enabled":true"#), "live outputs must report enabled:true");
+ assert!(line.contains(r#""name":"HDMI-A-1""#), "disabled output must still be listed");
+ assert!(line.contains(r#""enabled":false"#), "the disabled output's own row must say so");
+}
+
+#[test]
+fn monitors_query_marks_a_split_part_so_a_client_does_not_treat_it_as_a_real_output() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ let whole = srdwm_core::Monitor::new(0, "eDP-1", srdwm_core::Rect::new(0, 0, 1920, 1080));
+ let mut half = srdwm_core::Monitor::new(1, "eDP-1-1", srdwm_core::Rect::new(0, 0, 960, 1080));
+ half.split = true;
+ wm.borrow_mut().set_monitors(vec![whole, half]);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"monitors\"}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+
+ let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
+ let monitors = parsed["monitors"].as_array().unwrap();
+ let whole = monitors.iter().find(|m| m["name"] == "eDP-1").unwrap();
+ let half = monitors.iter().find(|m| m["name"] == "eDP-1-1").unwrap();
+ assert_eq!(whole["split"], false, "an ordinary output must not be marked as a split part");
+ assert_eq!(half["split"], true, "a split part must be marked so a client can tell it apart from a real output");
+}
+
+#[test]
+fn lock_dispatch_queues_a_lock_request_with_no_id_needed() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ assert!(!wm.borrow_mut().drain_lock_request(), "nothing queued before the dispatch");
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"lock\"}\n").unwrap();
+ server.poll(&wm);
+ let response = read_line(&mut reader);
+
+ assert!(!response.contains(r#""ok":false"#), "lock must not require an id: {response}");
+ assert!(wm.borrow_mut().drain_lock_request(), "the dispatch must have queued a lock request");
+}
+
+#[test]
+fn toggle_floating_dispatch_flips_the_windows_floating_flag() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ let id = {
+ let mut wm = wm.borrow_mut();
+ let id = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(id, "a"));
+ id
+ };
+ assert!(!wm.borrow().is_floating(id));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(format!("{{\"cmd\":\"toggle_floating\",\"id\":{id}}}\n").as_bytes()).unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert!(wm.borrow().is_floating(id));
+}
+
+#[test]
+fn toggle_pinned_dispatch_flips_always_on_top() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ let id = {
+ let mut wm = wm.borrow_mut();
+ let id = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(id, "a"));
+ id
+ };
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(format!("{{\"cmd\":\"toggle_pinned\",\"id\":{id}}}\n").as_bytes()).unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ assert!(wm.borrow().window(id).unwrap().always_on_top);
+}
+
+#[test]
+fn move_window_dispatch_swaps_with_the_neighbour_and_focuses_the_target_first() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "primary", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
+ let (a, b) = {
+ let mut wm = wm.borrow_mut();
+ let a = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(b, "b"));
+ // Set geometry *after* `add_window`, not before - a dynamic-
+ // layout workspace's own `SmartPlacement` grid overrides
+ // whatever geometry a freshly constructed `Window` already
+ // carried in, the same lesson `crates/core/src/manager/
+ // tests.rs`'s decoration-mode tests already ran into.
+ wm.window_mut(a).unwrap().geometry = srdwm_core::Rect::new(0, 0, 400, 300);
+ wm.window_mut(b).unwrap().geometry = srdwm_core::Rect::new(500, 0, 400, 300);
+ (a, b)
+ };
+ // Focus `a` first, then ask to move `b` - the dispatch must focus
+ // `b` itself before swapping, not silently move whatever was
+ // already focused.
+ wm.borrow_mut().focus_window(a);
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(format!("{{\"cmd\":\"move_window\",\"id\":{b},\"direction\":\"left\"}}\n").as_bytes()).unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ let wm = wm.borrow();
+ assert_eq!(wm.window(b).unwrap().geometry.x, 0, "b must have swapped into a's old position");
+ assert_eq!(wm.window(a).unwrap().geometry.x, 500, "a must have swapped into b's old position");
+}
+
+#[test]
+fn move_to_workspace_dispatch_moves_the_window_without_switching_the_current_workspace() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ wm.borrow_mut().add_workspace("2", "dynamic");
+ let id = {
+ let mut wm = wm.borrow_mut();
+ let id = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(id, "a"));
+ id
+ };
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(format!("{{\"cmd\":\"move_to_workspace\",\"id\":{id},\"workspace\":2}}\n").as_bytes()).unwrap();
+ server.poll(&wm);
+ let _ = read_line(&mut reader);
+
+ let wm = wm.borrow();
+ assert_eq!(wm.window(id).unwrap().workspace, 2);
+ assert_eq!(wm.current_workspace(), 1, "moving a window to a workspace must not also switch to it");
+}
+
+#[test]
+fn a_oneshot_clients_request_still_closes_the_connection_as_before() {
+ let dir = tempfile::tempdir().unwrap();
+ let mut server = IpcServer::bind_in(dir.path(), "test").unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+
+ let mut client = UnixStream::connect(&server.path).unwrap();
+ let mut reader = std::io::BufReader::new(client.try_clone().unwrap());
+ client.write_all(b"{\"cmd\":\"clients\"}\n").unwrap();
+ server.poll(&wm);
+ let line = read_line(&mut reader);
+ // The plain, pre-existing shape - no `"event"` field - so
+ // existing one-shot polling consumers (`crates/ctl`) see no change.
+ assert!(!line.contains(r#""event""#));
+ assert!(line.contains(r#""clients""#));
+
+ {
+ let mut wm = wm.borrow_mut();
+ let id = wm.alloc_window_id();
+ wm.add_window(srdwm_core::Window::new(id, "later"));
+ }
+ server.poll(&wm);
+ client.set_nonblocking(true).unwrap();
+ let mut buf = [0u8; 16];
+ let result = client.read(&mut buf);
+ // A one-shot connection was never registered as a subscriber, so a
+ // later change must not be pushed to it - and the server already
+ // closed its end after the single reply, so a read either sees EOF
+ // (0 bytes) or, depending on how quickly the close propagates,
+ // WouldBlock; either is correct, actual new data would not be.
+ match result {
+ Ok(n) => assert_eq!(n, 0, "expected EOF, got {n} bytes of unexpected data"),
+ Err(e) => assert_eq!(e.kind(), ErrorKind::WouldBlock),
+ }
+}