diff --git a/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/placement.py b/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/placement.py new file mode 100644 index 00000000..09591420 --- /dev/null +++ b/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/placement.py @@ -0,0 +1,95 @@ +"""What only KDE can tell about the placement rules of the config, beside the shared `placement`. + +The rules match a window by what it is called: its Wayland app id (`Class`) and its program +(`Exe`), as well as its title. The shared scenario checks the places, by number and by title; these +check that the names a rule matches are the ones a real KWin window has, and that `foot.exe` is +`foot`. + +The suite runs the shared scenario, then `by_app_id` and `by_program`, with no window open. +""" + +from typing import Any + +from komorebi_vm.core.checks import Checks +from komorebi_vm.core.platform import Client +from komorebi_vm.kde_platform import KdePlatform + + +def _config(rules: Any) -> dict[str, Any]: + return { + "default_workspace_padding": 10, + "default_container_padding": 10, + "monitors": [ + { + "workspaces": [ + {"name": "a", "layout": "BSP", "initial_window_placement_rules": rules} + ] + } + ], + } + + +def _order(platform: KdePlatform) -> list[str]: + return [titles[0] for titles in platform.state().containers()] + + +def _open(platform: KdePlatform, opened: list[Client], title: str, app_id: str | None) -> None: + """Opens a window, and waits until the daemon has put it in a container and given it the focus: + "after the one in focus" depends on it.""" + opened.append(platform.spawn(title, app_id=app_id)) + platform.wait_for(lambda: platform.state().managed() == len(opened), timeout=20) + platform.wait_for(lambda: platform.state().focused_title() == title, timeout=20) + + +def _close(platform: KdePlatform, opened: list[Client]) -> None: + for client in opened: + platform.close(client) + platform.wait_for(lambda: platform.state().managed() == 0, timeout=20) + + +def by_app_id(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None: + """A window with the app id `special` goes first, one titled `chat` goes last, and one no rule + matches goes after the one in focus.""" + platform.restart_with( + _config( + { + "Primary": {"kind": "Class", "id": "special", "matching_strategy": "Equals"}, + "Last": [{"kind": "Title", "id": "chat", "matching_strategy": "Contains"}], + } + ) + ) + opened: list[Client] = [] + for title in ("one", "two"): + _open(platform, opened, title, None) + _open(platform, opened, "three", "special") + checks.check( + f"the window with the app id special is first ({_order(platform)})", + _order(platform) == ["three", "one", "two"], + ) + _open(platform, opened, "a chat room", None) + checks.check( + f"the chat room is last ({_order(platform)})", + _order(platform) == ["three", "one", "two", "a chat room"], + ) + _open(platform, opened, "four", None) + checks.check( + f"a window no rule matches goes after the one in focus, the chat room ({_order(platform)})", + _order(platform) == ["three", "one", "two", "a chat room", "four"], + ) + _close(platform, opened) + + +def by_program(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None: + """A rule for the program `foot.exe`, the name of a Windows executable, matches `foot`, which + is what opens every window here, so every window goes to the primary tile.""" + platform.restart_with( + _config({"Primary": {"kind": "Exe", "id": "foot.exe", "matching_strategy": "Equals"}}) + ) + opened: list[Client] = [] + for title in ("a", "b", "c"): + _open(platform, opened, title, None) + checks.check( + f"foot.exe is foot, so every window is a primary one: c, b, a ({_order(platform)})", + _order(platform) == ["c", "b", "a"], + ) + _close(platform, opened) diff --git a/nix/tests/vm/suites/placement/test.py b/nix/tests/vm/suites/placement/test.py index 6b3bb64b..6d6b67a1 100644 --- a/nix/tests/vm/suites/placement/test.py +++ b/nix/tests/vm/suites/placement/test.py @@ -1,17 +1,15 @@ # `initial_window_placement_rules` in a real (headless) KWin: where in the layout a window that -# opens goes. The daemon reads the rules from a `komorebi.json` it is started with, and the rules -# match a window by what it is called: its program (`Exe`), its Wayland app id (`Class`) and its -# title. `komorebi/tests/placement.rs` checks the same against the fake; this checks that the names -# the rules match are the ones a real window has, and that the places of a file are the places the -# windows get. -import json -import shlex -import time -from typing import Any - +# opens goes. The daemon reads the rules from a `komorebi.json` it is started with. The shared +# scenario (`komorebi_vm.core.scenarios.placement`, which runs on Windows too) checks the places, by +# number and by title. What only a real KWin can tell is in +# `komorebi_vm.platforms.kde.extras.placement`: that the names the rules match are the ones a real +# window has (its Wayland app id, its program, and `foot.exe` being `foot`). +# `komorebi/tests/placement.rs` checks the same against the fake. from komorebi_vm import Checks, Kde - -CONFIG = "/tmp/placement.json" +from komorebi_vm.core.scenarios import placement +from komorebi_vm.core.suite import platform_only, run_suite, shared +from komorebi_vm.kde_platform import KdePlatform +from komorebi_vm.platforms.kde.extras import placement as kde_placement start_all() machine.wait_for_unit("multi-user.target") @@ -19,113 +17,18 @@ machine.wait_for_unit("user@1000.service") checks = Checks() kde = Kde.headless(machine, "main", size=(1920, 1080), scale=SCALE) -opened: list[str] = [] +platform = KdePlatform(kde, (1920, 1080)) +platform.start_komorebi() - -def order() -> list[str]: - """The title of the window of each container of the first workspace, in the layout's order.""" - return [titles[0] for titles in kde.containers()] - - -def open_window(title: str, app_id: str = "foot") -> None: - """Opens a window, and waits until the daemon has put it in a container.""" - kde.spawn(f"foot -a {shlex.quote(app_id)} -T {shlex.quote(title)} sleep 3000") - opened.append(title) - kde.wait_for(lambda: kde.wid(title) != "", timeout=30) - kde.wait_for(lambda: kde.managed() == len(opened), timeout=20) - # the window that has just opened has the focus once the daemon has told KWin so - kde.wait_for(lambda: kde.focused_title() == title, timeout=20) - time.sleep(0.5 * SCALE) - - -def fresh(rules: Any) -> None: - """Starts the daemon again, with `rules` as the placement rules of the workspace's windows. - - The windows of the run before are closed. `None` leaves the key out of the file. - """ - if kde.komorebi_running(): - kde.komorebic("stop") - kde.wait_for(lambda: not kde.komorebi_running(), timeout=30) - for title in opened: - wid = kde.wid(title) - if wid != "": - kde.close(wid) - kde.wait_for(lambda: all(kde.wid(title) == "" for title in opened), timeout=30) - opened.clear() - - workspace: dict[str, Any] = {"name": "a", "layout": "BSP"} - if rules is not None: - workspace["initial_window_placement_rules"] = rules - config = { - "default_workspace_padding": 10, - "default_container_padding": 10, - "monitors": [{"workspaces": [workspace]}], - } - kde.sh(f"printf %s {shlex.quote(json.dumps(config))} > {CONFIG}") - kde.start_komorebi("--config", CONFIG) - checks.check( - "the daemon answers komorebic", - kde.wait_for(lambda: kde.run("komorebic state")[0] == 0, timeout=30), - ) - - -print("== 1. no rules: after the one in focus") -fresh(None) -for title in ("a", "b", "c"): - open_window(title) -checks.check(f"a, b, c ({order()})", order() == ["a", "b", "c"]) - -print("== 2. every window to the primary tile") -fresh("Primary") -for title in ("a", "b", "c"): - open_window(title) -checks.check(f"c, b, a ({order()})", order() == ["c", "b", "a"]) - -print("== 3. by the number of the place, counted from 1, as a number and as text") -fresh(2) -for title in ("a", "b", "c"): - open_window(title) -checks.check(f"a, c, b ({order()})", order() == ["a", "c", "b"]) -fresh("2") -for title in ("a", "b", "c"): - open_window(title) -checks.check( - f"text that is a number is the same place: a, c, b ({order()})", order() == ["a", "c", "b"] +args = (platform, checks, []) +run_suite( + checks, + shared(placement.run, platform, checks), + platform_only("kde", kde_placement.by_app_id, *args), + platform_only("kde", kde_placement.by_program, *args), ) -print("== 4. by the app id and the title") -fresh( - { - "Primary": {"kind": "Class", "id": "special", "matching_strategy": "Equals"}, - "Last": [{"kind": "Title", "id": "chat", "matching_strategy": "Contains"}], - } -) -for title in ("one", "two"): - open_window(title) -open_window("three", "special") -checks.check( - f"the window with the app id special is first ({order()})", order() == ["three", "one", "two"] -) -open_window("a chat room") -checks.check( - f"the chat room is last ({order()})", order() == ["three", "one", "two", "a chat room"] -) -open_window("four") -checks.check( - f"a window no rule matches goes after the one in focus, the chat room ({order()})", - order() == ["three", "one", "two", "a chat room", "four"], -) - -print("== 5. by the program, with the name of a Windows executable") -fresh({"Primary": {"kind": "Exe", "id": "foot.exe", "matching_strategy": "Equals"}}) -for title in ("a", "b", "c"): - open_window(title) -checks.check( - f"foot.exe is foot, so every window is a primary one: c, b, a ({order()})", - order() == ["c", "b", "a"], -) - -kde.komorebic("stop") -checks.check("a clean stop", kde.wait_for(lambda: not kde.komorebi_running(), timeout=30)) +kde.stop_komorebi() +checks.check("a clean stop", not kde.komorebi_running()) kde.stop() checks.finish()