diff --git a/docs/dev/suite-ledger.md b/docs/dev/suite-ledger.md index ab0d208b..f763d978 100644 --- a/docs/dev/suite-ledger.md +++ b/docs/dev/suite-ledger.md @@ -42,7 +42,7 @@ The suite had its own layout loop (section 2) and then called the shared `layout |---|---| | 1, the daemon answers with both outputs; five windows | extra `setup` | | 2, every layout with five windows: inside the outputs, apart, healthy | shared `layouts`; the extra repeats it at five windows, see below | -| 2, every layout with three, two and one windows | extra `layouts_by_count` | +| 2, every layout with three, two and one windows | shared `commands.layouts` and `layouts.run` (they close windows from the end and open them again); the extra still goes through the counts for the log check | | 2, each window where the daemon last asked KWin for it | extra `layouts_by_count` (KDE only: reads the daemon's log) | | 3, the layouts and the sweep of commands | shared `layouts` and `sweep` | | 3, `replace-configuration` made the window manager from the file | extra `replace_configuration` | @@ -52,9 +52,9 @@ The suite had its own layout loop (section 2) and then called the shared `layout | 4, `komorebic query ...`, `quick-save-resize`, `save-resize` | extra `queries` (candidates for the shared list) | | 5, every window is where the daemon last asked; a clean stop | extra `at_the_end` | -The extra's layout loop still goes through five windows, for the daemon's log check at that count; -it states its overlap verdict for five windows too, so that one is made twice. Dropping it from the -extra would lose the five-window `where asked` check, which is KDE's. +The extra's layout loop still goes through five, three, two and one windows, for the daemon's log +check at each count. It no longer states an overlap verdict: the shared `layouts` makes that one at +every count, with the extra's own check left to say only where KWin has the windows. ## drag-resize diff --git a/nix/tests/vm/lib/komorebi_vm/core/scenarios/commands.py b/nix/tests/vm/lib/komorebi_vm/core/scenarios/commands.py index 23935f0a..509a61e8 100644 --- a/nix/tests/vm/lib/komorebi_vm/core/scenarios/commands.py +++ b/nix/tests/vm/lib/komorebi_vm/core/scenarios/commands.py @@ -10,6 +10,7 @@ command does is checked elsewhere; this says none of them breaks a running daemo from collections.abc import Sequence from komorebi_vm.core.checks import Checks +from komorebi_vm.core.geometry import Rect from komorebi_vm.core.platform import Client, Platform LAYOUTS: tuple[str, ...] = ( @@ -285,15 +286,18 @@ def unhealthy(platform: Platform, clients: list[Client]) -> str | None: return None -def layouts(platform: Platform, checks: Checks, clients: list[Client]) -> None: - """Changes to every layout, and checks that the windows stay inside the work area and apart - from each other, and that the daemon stays healthy. The scrolling layout is a strip whose - columns can be past the edge, so its frames are not looked at.""" - area = platform.work_area() +FEWER = (3, 2, 1) +"""The counts of windows below the one `layouts` starts with that it tries every layout with.""" + + +def _each_layout( + platform: Platform, checks: Checks, clients: list[Client], label: str, area: Rect +) -> None: + """Changes to every layout, with `clients` the windows there are, and checks them.""" for layout in LAYOUTS: status, out = platform.komorebic_status("change-layout", layout) if status != 0 and NOT_A_COMMAND in out: - checks.skip(f"change-layout {layout}", "the port has no such layout") + checks.skip(f"change-layout {layout}{label}", "the port has no such layout") continue problems = [] if status != 0: @@ -313,7 +317,45 @@ def layouts(platform: Platform, checks: Checks, clients: list[Client]) -> None: for b in titles[i + 1 :] if frames[a].overlaps(frames[b]) ] - checks.check(f"change-layout {layout}: " + ("; ".join(problems) or "ok"), not problems) + checks.check( + f"change-layout {layout}{label}: " + ("; ".join(problems) or "ok"), not problems + ) + + +def layouts(platform: Platform, checks: Checks, clients: list[Client]) -> None: + """Changes to every layout, and checks that the windows stay inside the work area and apart + from each other, and that the daemon stays healthy. The scrolling layout is a strip whose + columns can be past the edge, so its frames are not looked at. + + It does the same with three, two and one windows (those of `FEWER` that `clients` has more + than): the others are closed from the end, and opened again with their own titles at the end, + with the layout `bsp`. + """ + area = platform.work_area() + _each_layout(platform, checks, clients, "", area) + closed: list[Client] = [] + for keep in (n for n in FEWER if n < len(clients)): + for client in clients[keep:]: + if client not in closed: + platform.close(client) + closed.append(client) + left = clients[:keep] + platform.wait_for(lambda n=keep: platform.state().managed() == n, timeout=20) + _each_layout(platform, checks, left, f" with {keep} windows", area) + if closed: + platform.komorebic("change-layout", "bsp") + for client in closed: + platform.spawn(client.title) + checks.check( + "the windows that were closed are opened again and managed", + platform.wait_for( + lambda: ( + unhealthy(platform, clients) is None + and platform.state().managed() == len(clients) + ), + timeout=30, + ), + ) def sweep( diff --git a/nix/tests/vm/lib/komorebi_vm/core/scenarios/layouts.py b/nix/tests/vm/lib/komorebi_vm/core/scenarios/layouts.py index 45d2043e..da2a3d74 100644 --- a/nix/tests/vm/lib/komorebi_vm/core/scenarios/layouts.py +++ b/nix/tests/vm/lib/komorebi_vm/core/scenarios/layouts.py @@ -49,9 +49,42 @@ def _check_layout(platform: Platform, checks: Checks, clients: list[Client], lay ) +FEWER = (3, 2, 1) +"""The counts of windows below the one the scenario starts with that every layout is tried with.""" + + +def _fewer(platform: Platform, checks: Checks, clients: list[Client]) -> None: + """Every layout with fewer windows: those of `FEWER` that `clients` has more than, the others + closed from the end, and opened again with their own titles at the end.""" + closed: list[Client] = [] + for keep in (n for n in FEWER if n < len(clients)): + for client in clients[keep:]: + if client not in closed: + platform.close(client) + closed.append(client) + left = clients[:keep] + checks.check( + f"{keep} windows are left managed", + platform.wait_for(lambda n=keep: platform.state().managed() == n, timeout=20), + ) + for layout in LAYOUTS: + _check_layout(platform, checks, left, layout) + platform.komorebic("change-layout", "bsp") + for client in closed: + platform.spawn(client.title) + checks.check( + "the windows that were closed are opened again and tiled", + platform.wait_for( + lambda: platform.state().managed() == len(clients) and tiled(platform, clients), + timeout=30, + ), + ) + + def run(platform: Platform, checks: Checks, clients: list[Client]) -> None: """Changes the layout of the first workspace to each of `LAYOUTS` with `clients`, which are - tiled windows in a container each, and then with two windows more, which it closes. + tiled windows in a container each, and then with two windows more, which it closes, and with + three, two and one windows of `clients`, which it closes, and opens again. The layout is `bsp` again at the end. """ @@ -77,3 +110,4 @@ def run(platform: Platform, checks: Checks, clients: list[Client]) -> None: timeout=20, ), ) + _fewer(platform, checks, clients) diff --git a/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/commands.py b/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/commands.py index 69d4af07..c71f6655 100644 --- a/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/commands.py +++ b/nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/commands.py @@ -1,9 +1,9 @@ """What only KDE can tell about the commands, round the shared `commands` scenarios. Every window the daemon has is a window KWin has, and the frame the daemon last asked KWin for is -the one KWin carries out. The shared scenarios check the layouts with five windows and that every -command is understood and leaves the daemon healthy, as the operating system sees it; what is here -reads KWin (`kdotool`) and the daemon's log. +the one KWin carries out. The shared scenarios check the layouts with five, three, two and one +windows and that every command is understood and leaves the daemon healthy, as the operating system +sees it; what is here reads KWin (`kdotool`) and the daemon's log. The suite runs `setup`, `layouts_by_count`, the shared `layouts` and `sweep`, `replace_configuration`, the shared `handover`, `title_rule` and `prints`, `queries` and @@ -60,29 +60,6 @@ def healthy(platform: KdePlatform) -> int: return 0 -def _outside_or_overlapping(platform: KdePlatform) -> list[str]: - """The windows that are outside the outputs, or on another window (2 pixels are allowed).""" - kde = platform.kde - width, height = platform.size - rects = {wid: kde.rect(wid) for wid in _live_windows(platform)} - bad = [] - for wid, r in rects.items(): - if r.x < -1 or r.y < -1 or r.x + r.w > width + 1 or r.y + r.h > height + 1: - bad.append(f"{kde.title(wid)} outside at {r}") - ids = list(rects) - for i, a in enumerate(ids): - for b in ids[i + 1 :]: - ra, rb = rects[a], rects[b] - if ( - ra.x < rb.x + rb.w - 2 - and rb.x < ra.x + ra.w - 2 - and ra.y < rb.y + rb.h - 2 - and rb.y < ra.y + ra.h - 2 - ): - bad.append(f"{kde.title(a)} on {kde.title(b)}") - return bad - - def _open_window(platform: KdePlatform, n: int) -> None: platform.spawn(f"win{n}") @@ -128,11 +105,12 @@ def setup(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None: def layouts_by_count(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None: - """Every layout with five, three, two and one windows: the windows are in their outputs and - apart, the daemon is healthy, and KWin has each window where the daemon last asked for it. + """Every layout with five, three, two and one windows: KWin has each window where the daemon + last asked for it, and the daemon is healthy. - The shared `layouts` checks five windows. The windows that were closed are opened again, and - the layout is `bsp` at the end. + The shared `layouts` checks that the windows are inside the outputs and apart, at every one of + those counts. The windows that were closed are opened again, and the layout is `bsp` at the + end. """ kde = platform.kde for layout in LAYOUTS: @@ -147,14 +125,16 @@ def layouts_by_count(platform: KdePlatform, checks: Checks, clients: list[Client lambda: all(_where_asked(platform, w) is None for w in _live_windows(platform)), timeout=10, ) - # the scrolling layout is a strip whose columns can be past the edge of the output - if layout != "scrolling": - verdict += [f"{keep}w: {bad}" for bad in _outside_or_overlapping(platform)] + verdict += [ + f"{keep}w: {bad}" + for w in _live_windows(platform) + if (bad := _where_asked(platform, w)) + ] if reason := healthy(platform): verdict.append(f"{keep}w: not healthy ({reason})") checks.check( - f"{layout}: the windows are in their outputs and do not overlap, and the daemon is" - " healthy" + "".join(f"\n {v}" for v in verdict), + f"{layout}: KWin has every window where the daemon last asked, with 5, 3, 2 and 1" + " windows" + "".join(f"\n {v}" for v in verdict), verdict == [], ) for n in range(kde.managed() + 1, len(clients) + 1): diff --git a/nix/tests/vm/lib/tests/test_moved.py b/nix/tests/vm/lib/tests/test_moved.py index 83f9e3dc..1f546252 100644 --- a/nix/tests/vm/lib/tests/test_moved.py +++ b/nix/tests/vm/lib/tests/test_moved.py @@ -2,7 +2,8 @@ are wrong in one way.""" from komorebi_vm.core.checks import Checks -from komorebi_vm.core.scenarios import handover, title_rule +from komorebi_vm.core.geometry import Rect +from komorebi_vm.core.scenarios import commands, handover, title_rule from tests.fake_platform import FakePlatform @@ -62,3 +63,29 @@ def test_a_daemon_that_does_not_manage_again_fails_handover() -> None: clients = opened(platform, checks) handover.run(platform, checks, clients) assert any("manage: " in f for f in checks.failures) + + +def test_every_layout_is_tried_with_fewer_windows_and_the_windows_are_opened_again() -> None: + platform = FakePlatform() + checks = Checks() + clients = opened(platform, checks) + base = checks.passed + commands.layouts(platform, checks, clients) + assert checks.failed == 0, checks.failures + assert checks.passed - base == len(commands.LAYOUTS) * 3 + 1 + assert platform.state().managed() == len(clients) + + +def test_a_layout_that_overlaps_two_windows_fails_commands_layouts() -> None: + class Overlapping(FakePlatform): + def komorebic(self, *args: str) -> str: + out = super().komorebic(*args) + if args == ("change-layout", "grid"): + self.tiles = [Rect(0, 0, 1000, 1080)] * len(self.containers) + return out + + platform = Overlapping() + checks = Checks() + clients = opened(platform, checks) + commands.layouts(platform, checks, clients) + assert any("grid with 2 windows" in f for f in checks.failures), checks.failures