From 38f12952513a46dde2eaef4d4601fa7f30c8dbb3 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 15:07:07 -0700 Subject: [PATCH] test(core): pin the state JSON of the three daemons from real fixtures tests/state_roundtrip.rs (15 tests) reads the monitors of the e2e fixtures (Windows kfw-tiled, kfw-stack, kfw-float; KDE kfde-tiled, kfde-stack) into the shared Monitor

with a platform shaped like each daemon (Windows: isize id, optional serial number, the name/device_id/workspace_names extra; KDE: u32 id, string serial number, no extra; macOS: window_hiding_position extra), writes them again and compares the parsed JSON: every fixture is written as it was read. Also pins that the corner is the last macOS monitor key and cannot be omitted, that the workspace keys are the same whatever the platform, the exact set of KDE monitor keys, and which keys an old state file may omit (23 optional keys at monitor, workspace and container level, e.g. container locked and workspace layout_options_rules) and which it may not (18 required keys, including layer, work_area_offset_rules and apply_window_based_work_area_offset: a file from before those existed does not load; pinned, not judged). The fixtures are copied into tests/common/fixtures.rs as Rust text because the nix source filters keep .rs files and drop .json; a test compares the copies with e2e/lib/tests/fixtures when the checkout has them. The macOS fixture is NOT real macOS output: it is kfde-tiled reshaped with jq to the macOS window and monitor shape of the state_pin tests (no Mac here), and says so in its comment; replace it with 'komorebic state' from a Mac when one is at hand. Verified: cargo test -p komorebi-core (27 unit + 179 integration passed, 1 ignored) with and without --features schemars; cargo clippy -p komorebi-core --all-targets has no warning in the crate. Windows clippy and macOS check not run: the tests are portable crate tests and no platform source changed. --- crates/komorebi-core/tests/common/fixtures.rs | 1037 +++++++++++++++++ crates/komorebi-core/tests/common/mod.rs | 1 + crates/komorebi-core/tests/state_roundtrip.rs | 561 +++++++++ 3 files changed, 1599 insertions(+) create mode 100644 crates/komorebi-core/tests/common/fixtures.rs create mode 100644 crates/komorebi-core/tests/state_roundtrip.rs diff --git a/crates/komorebi-core/tests/common/fixtures.rs b/crates/komorebi-core/tests/common/fixtures.rs new file mode 100644 index 00000000..f2e3c695 --- /dev/null +++ b/crates/komorebi-core/tests/common/fixtures.rs @@ -0,0 +1,1037 @@ +//! Real `komorebic state` output of the three daemons, copied from `e2e/lib/tests/fixtures` +//! (Windows `kfw-*`, KDE `kfde-*`) as Rust text, because the nix source filters keep `.rs` files and +//! not `.json`. `state_roundtrip.rs` checks the copies against the originals when those exist. + +#![allow(dead_code)] + +pub const KFW_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 332 + } + } + ], + "focused": 0 + } + }, + { + "id": "fop08rEXwYLbkqzpy3nE9", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 393, + "right": 596, + "bottom": 332 + } + } + ], + "focused": 0 + } + } + ], + "focused": 2 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 332 + }, + { + "left": 657, + "top": 393, + "right": 596, + "bottom": 332 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFW_STACK: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + }, + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 1 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFW_FLOAT: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [ + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Floating", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFDE_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1230296105, + "device": "Virtual-0", + "serial_number_id": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + }, + { + "id": "f63LOWKm57v9SDinvNb_K", + "locked": false, + "windows": { + "elements": [ + { + "id": 6, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 555, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + } + ], + "focused": 1 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 505 + }, + { + "left": 975, + "top": 555, + "right": 925, + "bottom": 505 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; + +pub const KFDE_STACK: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1230296105, + "device": "Virtual-0", + "serial_number_id": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + }, + { + "id": 6, + "rect": { + "left": 20, + "top": 20, + "right": 924, + "bottom": 1037 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 1 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 1037 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 1040 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; + +/// Shaped like the macOS state (windows as `id`, `rect`, `details` with role and subrole; the monitor +/// with `window_hiding_position` last), derived from `kfde-tiled` with jq: NOT real macOS output, which +/// cannot be produced on this host. Replace it with the output of `komorebic state` on a Mac when +/// one is at hand. +pub const MACOS_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1, + "device": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + }, + { + "id": "f63LOWKm57v9SDinvNb_K", + "locked": false, + "windows": { + "elements": [ + { + "id": 6, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 555, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + } + ], + "focused": 1 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 505 + }, + { + "left": 975, + "top": 555, + "right": 925, + "bottom": 505 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null, + "window_hiding_position": "BottomLeft", + "serial_number_id": "C02XXXXXXXX" + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; diff --git a/crates/komorebi-core/tests/common/mod.rs b/crates/komorebi-core/tests/common/mod.rs index b8482c62..7903a1dd 100644 --- a/crates/komorebi-core/tests/common/mod.rs +++ b/crates/komorebi-core/tests/common/mod.rs @@ -483,3 +483,4 @@ pub fn work_area(right: i32, bottom: i32) -> Rect { bottom, } } +pub mod fixtures; diff --git a/crates/komorebi-core/tests/state_roundtrip.rs b/crates/komorebi-core/tests/state_roundtrip.rs new file mode 100644 index 00000000..c55c92b1 --- /dev/null +++ b/crates/komorebi-core/tests/state_roundtrip.rs @@ -0,0 +1,561 @@ +//! State JSON round trips from real fixtures: `komorebic state` output of the Windows and KDE +//! daemons (copied from `e2e/lib/tests/fixtures`) and a macOS-shaped one, read into the shared +//! `Monitor

` with each platform's window, id, serial number and extra fields, written again, +//! and compared as parsed JSON. +//! +//! The platforms here are only shaped like the real ones (what the shared types are generic over); +//! the windows keep the JSON they were read from, because the real windows write what the +//! operating system knows (title, rectangle) at the moment they are serialised. + +mod common; + +use common::fixtures::*; +use komorebi_config::behaviour::WindowHidingPosition; +use komorebi_core::ContainerWindow; +use komorebi_core::Monitor; +use komorebi_core::MonitorPlatform; +use komorebi_core::NoExtra; +use komorebi_core::WorkspacePlatform; +use komorebi_core::WorkspaceWindow; +use komorebi_core::ring::Ring; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; +use std::collections::HashMap; + +/// A platform shaped like Windows, KDE or macOS: its window keeps the JSON it was read from, and +/// is named by the number under `$id_key`. +macro_rules! shaped_platform { + ($platform:ident, $window:ident, $id_key:literal, $monitor_id:ty, $serial:ty, $extra:ty) => { + #[derive(Clone, Debug, PartialEq)] + struct $window { + id: i64, + json: Value, + } + + impl Serialize for $window { + fn serialize(&self, serializer: S) -> Result { + self.json.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for $window { + fn deserialize>(deserializer: D) -> Result { + let json = Value::deserialize(deserializer)?; + let id = json[$id_key] + .as_i64() + .ok_or_else(|| serde::de::Error::custom(concat!("no ", $id_key)))?; + Ok(Self { id, json }) + } + } + + impl ContainerWindow for $window { + type Id = i64; + type HideSpot = (); + + fn id(&self) -> i64 { + self.id + } + + fn exe_name(&self) -> Option { + None + } + + fn hide_window(&self, _: ()) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn restore_window(&self) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn hide_stacked(&self, _: ()) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn restore_stacked(&self) -> color_eyre::eyre::Result<()> { + Ok(()) + } + } + + impl WorkspaceWindow for $window { + fn title(&self) -> Option { + None + } + + fn exe(&self) -> Option { + None + } + + fn class(&self) -> Option { + None + } + + fn path(&self) -> Option { + None + } + } + + #[derive(Clone, Debug, PartialEq)] + struct $platform; + + impl WorkspacePlatform for $platform { + type Window = $window; + type State = (); + + fn foreground_window_id() -> Option { + None + } + + fn cursor_position() -> Option<(i32, i32)> { + None + } + + fn should_act_window( + _: &str, + _: &str, + _: &str, + _: &str, + _: &[komorebi_config::matching::MatchingRule], + ) -> Option { + None + } + + fn should_act_window_individual( + _: &str, + _: &str, + _: &str, + _: &str, + _: &komorebi_config::matching::IdWithIdentifier, + ) -> bool { + false + } + + fn scrolling_idx_for_direction( + _: &komorebi_core::Workspace, + _: komorebi_layouts::OperationDirection, + ) -> Option { + None + } + + fn scrolling_insertion_idx(_: &komorebi_core::Workspace) -> Option { + None + } + + fn note_scrolling_departure(_: &komorebi_core::Workspace, _: usize) {} + } + + impl MonitorPlatform for $platform { + type Id = $monitor_id; + type SerialNumber = $serial; + type Extra = $extra; + #[cfg(feature = "schemars")] + type IdSchema = $monitor_id; + + fn serialize_id( + id: &$monitor_id, + serializer: S, + ) -> Result { + serializer.serialize_i64(*id as i64) + } + + fn deserialize_id<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result<$monitor_id, D::Error> { + <$monitor_id>::deserialize(deserializer) + } + } + }; +} + +/// What only a Windows monitor has, as the Windows daemon declares it. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct WindowsExtra { + name: String, + device_id: String, + #[serde(deserialize_with = "workspace_names")] + workspace_names: HashMap, +} + +/// A flattened map is read through a buffer that keeps its keys as strings. +fn workspace_names<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + HashMap::::deserialize(deserializer)? + .into_iter() + .map(|(key, name)| { + key.parse() + .map(|key| (key, name)) + .map_err(serde::de::Error::custom) + }) + .collect() +} + +/// What only a macOS monitor has. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct MacosExtra { + window_hiding_position: WindowHidingPosition, +} + +shaped_platform!( + Windows, + WindowsWindow, + "hwnd", + isize, + Option, + WindowsExtra +); +shaped_platform!(Kde, KdeWindow, "id", u32, String, NoExtra); +shaped_platform!(Macos, MacosWindow, "id", u32, String, MacosExtra); + +fn json(text: &str) -> Value { + serde_json::from_str(text).unwrap() +} + +/// Reads the monitors of a state file as platform `P`, writes them again. +fn round_trip(state: &Value) -> (Ring>, Value) +where + P::Window: Serialize + for<'de> Deserialize<'de>, +{ + let monitors: Ring> = serde_json::from_value(state["monitors"].clone()).unwrap(); + let written = serde_json::to_value(&monitors).unwrap(); + (monitors, written) +} + +// ---- Windows ---------------------------------------------------------------------------------- + +#[test] +fn a_windows_tiled_state_is_written_as_it_was_read() { + let state = json(KFW_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 65537); + assert_eq!(monitor.serial_number_id, None); + assert_eq!(monitor.extra.name, "DISPLAY1"); + assert_eq!(monitor.extra.device_id, "Default_Monitor-4&183790ca&0&UID0"); + assert_eq!(monitor.workspaces().len(), 1); + assert_eq!(monitor.workspaces()[0].containers().len(), 3); +} + +#[test] +fn a_windows_stacked_state_is_written_as_it_was_read() { + let state = json(KFW_STACK); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let stacked = monitors.elements()[0].workspaces()[0] + .containers() + .iter() + .find(|c| c.windows().len() > 1) + .expect("the fixture has a stack"); + assert_eq!(stacked.windows().len(), 2); +} + +#[test] +fn a_windows_state_with_a_floating_window_and_the_floating_layer_is_written_as_it_was_read() { + let state = json(KFW_FLOAT); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let workspace = &monitors.elements()[0].workspaces()[0]; + assert_eq!(workspace.floating_windows().len(), 1); + assert_eq!(workspace.layer, komorebi_core::WorkspaceLayer::Floating); + assert!(workspace.contains_window(workspace.floating_windows()[0].id)); +} + +// ---- KDE -------------------------------------------------------------------------------------- + +#[test] +fn a_kde_tiled_state_is_written_as_it_was_read() { + let state = json(KFDE_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 1230296105); + assert_eq!(monitor.device, "Virtual-0"); + assert_eq!(monitor.serial_number_id, "Virtual-0"); + assert_eq!(monitor.workspaces()[0].containers().len(), 3); +} + +#[test] +fn a_kde_stacked_state_is_written_as_it_was_read() { + let state = json(KFDE_STACK); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let first = &monitors.elements()[0].workspaces()[0].containers()[0]; + assert_eq!(first.windows().len(), 2); + assert_eq!(first.focused_window_idx(), 1); +} + +#[test] +fn a_kde_monitor_has_no_platform_fields_beyond_the_shared_ones() { + let state = json(KFDE_TILED); + let (_, written) = round_trip::(&state); + + let mut keys: Vec<&str> = written["elements"][0] + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + keys.sort_unstable(); + + assert_eq!( + keys, + [ + "container_padding", + "device", + "floating_layer_behaviour", + "id", + "last_focused_workspace", + "serial_number_id", + "size", + "wallpaper", + "window_based_work_area_offset", + "window_based_work_area_offset_limit", + "work_area_offset", + "work_area_size", + "workspace_padding", + "workspaces", + ] + ); +} + +// ---- macOS ------------------------------------------------------------------------------------ + +#[test] +fn a_macos_shaped_state_is_written_as_it_was_read_with_the_corner_last() { + let state = json(MACOS_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 1); + assert_eq!(monitor.serial_number_id, "C02XXXXXXXX"); + assert_eq!( + monitor.extra.window_hiding_position, + WindowHidingPosition::BottomLeft + ); + let keys: Vec<&String> = written["elements"][0].as_object().unwrap().keys().collect(); + assert_eq!(keys.last().unwrap().as_str(), "window_hiding_position"); +} + +#[test] +fn a_macos_state_cannot_be_read_without_its_corner() { + let mut state = json(MACOS_TILED); + state["monitors"]["elements"][0] + .as_object_mut() + .unwrap() + .remove("window_hiding_position"); + + assert!(serde_json::from_value::>>(state["monitors"].clone()).is_err()); +} + +// ---- shared ----------------------------------------------------------------------------------- + +#[test] +fn the_same_workspace_keys_are_written_whatever_the_platform() { + fn workspace_keys(written: &Value) -> Vec { + let mut keys: Vec = written["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap() + .keys() + .cloned() + .collect(); + keys.sort(); + keys + } + + let windows = workspace_keys(&round_trip::(&json(KFW_TILED)).1); + let kde = workspace_keys(&round_trip::(&json(KFDE_TILED)).1); + let macos = workspace_keys(&round_trip::(&json(MACOS_TILED)).1); + + assert_eq!(windows, kde); + assert_eq!(kde, macos); +} + +#[test] +fn a_container_without_locked_is_read_from_a_state_file() { + let mut state = json(KFDE_TILED); + for container in + state["monitors"]["elements"][0]["workspaces"]["elements"][0]["containers"]["elements"] + .as_array_mut() + .unwrap() + { + container.as_object_mut().unwrap().remove("locked"); + } + + let (monitors, _) = round_trip::(&state); + + assert!( + monitors.elements()[0].workspaces()[0] + .containers() + .iter() + .all(|c| !c.locked) + ); +} + +/// Reads a KDE-shaped state with `key` taken out of the object at `level`, to say whether it loads. +fn loads_without(level: &str, key: &str) -> bool { + let mut state = json(KFDE_TILED); + let monitor = &mut state["monitors"]["elements"][0]; + let target = match level { + "monitor" => monitor, + "workspace" => &mut monitor["workspaces"]["elements"][0], + _ => &mut monitor["workspaces"]["elements"][0]["containers"]["elements"][0], + }; + assert!( + target.as_object_mut().unwrap().remove(key).is_some(), + "the fixture has no {level} key {key}" + ); + + serde_json::from_value::>>(state["monitors"].clone()).is_ok() +} + +#[test] +fn an_old_state_file_without_the_optional_keys_still_loads() { + let optional = [ + ("monitor", "serial_number_id"), + ("monitor", "work_area_offset"), + ("monitor", "window_based_work_area_offset"), + ("monitor", "container_padding"), + ("monitor", "workspace_padding"), + ("monitor", "last_focused_workspace"), + ("monitor", "wallpaper"), + ("monitor", "floating_layer_behaviour"), + ("workspace", "name"), + ("workspace", "monocle_container"), + ("workspace", "maximized_window"), + ("workspace", "layout_options"), + ("workspace", "layout_options_rules"), + ("workspace", "layout_flip"), + ("workspace", "workspace_padding"), + ("workspace", "container_padding"), + ("workspace", "work_area_offset"), + ("workspace", "window_container_behaviour"), + ("workspace", "window_container_behaviour_rules"), + ("workspace", "float_override"), + ("workspace", "floating_layer_behaviour"), + ("workspace", "wallpaper"), + ("container", "locked"), + ]; + + for (level, key) in optional { + assert!( + loads_without(level, key), + "a {level} without {key} should load" + ); + } +} + +#[test] +fn a_state_file_cannot_be_read_without_the_keys_the_shared_types_require() { + // characterization: these have no default, so a file from before one of them existed (the + // layer, the work area offset rules, the window-based offset flag) is not read + let required = [ + ("monitor", "id"), + ("monitor", "device"), + ("monitor", "workspaces"), + ("monitor", "size"), + ("monitor", "work_area_size"), + ("monitor", "window_based_work_area_offset_limit"), + ("workspace", "containers"), + ("workspace", "floating_windows"), + ("workspace", "layout"), + ("workspace", "layout_rules"), + ("workspace", "latest_layout"), + ("workspace", "resize_dimensions"), + ("workspace", "tile"), + ("workspace", "work_area_offset_rules"), + ("workspace", "apply_window_based_work_area_offset"), + ("workspace", "layer"), + ("container", "id"), + ("container", "windows"), + ]; + + for (level, key) in required { + assert!( + !loads_without(level, key), + "a {level} without {key} should not load" + ); + } +} + +#[test] +fn a_workspace_written_after_a_missing_optional_key_has_the_key_again() { + let mut state = json(KFDE_TILED); + state["monitors"]["elements"][0]["workspaces"]["elements"][0] + .as_object_mut() + .unwrap() + .remove("layout_options_rules"); + + let (_, written) = round_trip::(&state); + + assert_eq!( + written["elements"][0]["workspaces"]["elements"][0]["layout_options_rules"], + serde_json::json!([]) + ); +} + +#[test] +fn the_runtime_only_parts_of_a_workspace_are_never_written() { + let (monitors, written) = round_trip::(&json(KFDE_TILED)); + let mut monitors = monitors; + let workspace = &mut monitors.elements_mut()[0].workspaces_mut()[0]; + workspace.globals.border_width = 9; + workspace.monocle_container_restore_idx = Some(1); + + let rewritten = serde_json::to_value(&monitors).unwrap(); + let keys = rewritten["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap(); + + assert!(!keys.contains_key("globals")); + assert!(!keys.contains_key("state")); + assert!(!keys.contains_key("scrolling")); + // a restore index is written once there is one, and not before + assert!(keys.contains_key("monocle_container_restore_idx")); + assert!( + !written["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap() + .contains_key("monocle_container_restore_idx") + ); +} + +/// The copies in `common/fixtures.rs` are the files of `e2e/lib/tests/fixtures` when the checkout +/// has them (the nix source of the crate does not). +#[test] +fn the_fixtures_are_the_ones_the_e2e_library_has() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../e2e/lib/tests/fixtures"); + if !dir.is_dir() { + return; + } + + for (name, copy) in [ + ("kfw-tiled", KFW_TILED), + ("kfw-stack", KFW_STACK), + ("kfw-float", KFW_FLOAT), + ("kfde-tiled", KFDE_TILED), + ("kfde-stack", KFDE_STACK), + ] { + let original = std::fs::read_to_string(dir.join(format!("{name}.json"))).unwrap(); + assert_eq!( + json(&original), + json(copy), + "{name}.json changed: update common/fixtures.rs" + ); + } +}