From a37a9faf3db688f3dea1291bb394ffbb9e9bf671 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 19:35:37 -0700 Subject: [PATCH] refactor(nix): a platform-neutral core in the VM library Checks, Rect and the pixel helpers depend on no compositor. They move to komorebi_vm.core, which may not import komorebi_vm.kde, so that the package can go to the Windows port unchanged. komorebi_vm re-exports them and no suite changes. This is the first step of the cross-platform e2e plan. --- docs/dev/vm-tests.md | 4 +++ nix/tests/vm/lib/komorebi_vm/__init__.py | 12 ++++--- nix/tests/vm/lib/komorebi_vm/core/__init__.py | 5 +++ .../vm/lib/komorebi_vm/{ => core}/checks.py | 0 nix/tests/vm/lib/komorebi_vm/core/geometry.py | 32 +++++++++++++++++++ .../vm/lib/komorebi_vm/{ => core}/pixels.py | 2 +- nix/tests/vm/lib/komorebi_vm/daemon.py | 2 +- nix/tests/vm/lib/komorebi_vm/kde.py | 31 +----------------- nix/tests/vm/lib/komorebi_vm/latency.py | 2 +- nix/tests/vm/lib/pyproject.toml | 2 +- nix/tests/vm/lib/tests/test_pixels.py | 2 +- 11 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 nix/tests/vm/lib/komorebi_vm/core/__init__.py rename nix/tests/vm/lib/komorebi_vm/{ => core}/checks.py (100%) create mode 100644 nix/tests/vm/lib/komorebi_vm/core/geometry.py rename nix/tests/vm/lib/komorebi_vm/{ => core}/pixels.py (98%) diff --git a/docs/dev/vm-tests.md b/docs/dev/vm-tests.md index 42e1657e..b76e2eb1 100644 --- a/docs/dev/vm-tests.md +++ b/docs/dev/vm-tests.md @@ -139,6 +139,10 @@ check also runs `ruff` and `ty` over it. The driver type checks every `test.py` with `ty` and `ruff` too, so a typo in a suite fails the build, not the run. +- The package `komorebi_vm.core` is what does not depend on the platform: `Checks`, + `Rect` and the pixel helpers. It imports nothing of KDE, so that it can move to + the Windows port unchanged. `komorebi_vm` re-exports them, so that a suite + imports them from the same place as before. - `Checks` counts checks and fails once, at the end, so that one failure does not hide the ones after it. `finish()` prints `passed: N failed: M`, and its error names the checks that failed, which `nix build` shows in the last lines diff --git a/nix/tests/vm/lib/komorebi_vm/__init__.py b/nix/tests/vm/lib/komorebi_vm/__init__.py index 3b676f2f..8b0e9e45 100644 --- a/nix/tests/vm/lib/komorebi_vm/__init__.py +++ b/nix/tests/vm/lib/komorebi_vm/__init__.py @@ -2,7 +2,7 @@ A suite imports what it needs from here: -* [`Checks`](komorebi_vm.checks.Checks) counts checks and fails at the end. +* [`Checks`](komorebi_vm.core.checks.Checks) counts checks and fails at the end. * [`Kde`](komorebi_vm.kde.Kde) is a KWin session inside a test machine, with the tools to look at it (`kdotool`, `busctl`, `kscreen-doctor`, screenshots) and the komorebi daemon that runs in it. @@ -11,7 +11,7 @@ A suite imports what it needs from here: * [`Keys`](komorebi_vm.keys.Keys) presses keys on the machine's keyboard. * [`daemon`](komorebi_vm.daemon) reads what the daemon logs about the events it handles and the frames it asks for. -* [`pixels`](komorebi_vm.pixels) reads a screenshot: the color of a pixel, and +* [`pixels`](komorebi_vm.core.pixels) reads a screenshot: the color of a pixel, and the ring around a window. * [`latency`](komorebi_vm.latency) times the daemon's commands from a recording of the session bus. @@ -25,9 +25,11 @@ protocol, not on the test driver, so `nix/tests/vm/lib/tests` can run it against a fake machine (the `vm-lib` check does). """ -from komorebi_vm import daemon, latency, pixels, uinput -from komorebi_vm.checks import Checks -from komorebi_vm.kde import Kde, Outputs, Rect +from komorebi_vm import daemon, latency, uinput +from komorebi_vm.core import pixels +from komorebi_vm.core.checks import Checks +from komorebi_vm.core.geometry import Rect +from komorebi_vm.kde import Kde, Outputs from komorebi_vm.keys import Keys from komorebi_vm.machine import Machine from komorebi_vm.pointer import Pointer diff --git a/nix/tests/vm/lib/komorebi_vm/core/__init__.py b/nix/tests/vm/lib/komorebi_vm/core/__init__.py new file mode 100644 index 00000000..3a09e014 --- /dev/null +++ b/nix/tests/vm/lib/komorebi_vm/core/__init__.py @@ -0,0 +1,5 @@ +"""What does not depend on the platform: the vocabulary and the helpers a scenario is made of. + +Nothing in this package may import from `komorebi_vm.kde` or know the name of a compositor, +so that it can move to the Windows port unchanged. +""" diff --git a/nix/tests/vm/lib/komorebi_vm/checks.py b/nix/tests/vm/lib/komorebi_vm/core/checks.py similarity index 100% rename from nix/tests/vm/lib/komorebi_vm/checks.py rename to nix/tests/vm/lib/komorebi_vm/core/checks.py diff --git a/nix/tests/vm/lib/komorebi_vm/core/geometry.py b/nix/tests/vm/lib/komorebi_vm/core/geometry.py new file mode 100644 index 00000000..30805608 --- /dev/null +++ b/nix/tests/vm/lib/komorebi_vm/core/geometry.py @@ -0,0 +1,32 @@ +"""Rectangles, in the pixels of a screen.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Rect: + """A window's position and size, in pixels.""" + + x: int + y: int + w: int + h: int + + def overlaps(self, other: "Rect") -> bool: + """Returns true if and only if the two rectangles share at least one pixel.""" + return ( + self.x < other.x + other.w + and other.x < self.x + self.w + and self.y < other.y + other.h + and other.y < self.y + self.h + ) + + def inside(self, area: "Rect", slack: int = 0) -> bool: + """Returns true if and only if every pixel of this rectangle is a pixel of `area`, give + or take `slack` pixels at each edge.""" + return ( + self.x >= area.x - slack + and self.y >= area.y - slack + and self.x + self.w <= area.x + area.w + slack + and self.y + self.h <= area.y + area.h + slack + ) diff --git a/nix/tests/vm/lib/komorebi_vm/pixels.py b/nix/tests/vm/lib/komorebi_vm/core/pixels.py similarity index 98% rename from nix/tests/vm/lib/komorebi_vm/pixels.py rename to nix/tests/vm/lib/komorebi_vm/core/pixels.py index 832746b6..047adf79 100644 --- a/nix/tests/vm/lib/komorebi_vm/pixels.py +++ b/nix/tests/vm/lib/komorebi_vm/core/pixels.py @@ -2,7 +2,7 @@ from PIL import Image -from komorebi_vm.kde import Rect +from komorebi_vm.core.geometry import Rect Color = tuple[int, int, int] diff --git a/nix/tests/vm/lib/komorebi_vm/daemon.py b/nix/tests/vm/lib/komorebi_vm/daemon.py index 22b6ec41..fdaf21bd 100644 --- a/nix/tests/vm/lib/komorebi_vm/daemon.py +++ b/nix/tests/vm/lib/komorebi_vm/daemon.py @@ -10,7 +10,7 @@ and its title, and every frame the daemon asks KWin for is a debug line. import re from dataclasses import dataclass -from komorebi_vm.kde import Rect +from komorebi_vm.core.geometry import Rect _EVENT = re.compile(r"processing event: (\w+) for window (\d+) with notification (\w+)") _ACTIVATED = re.compile(r"KWin activated window (\d+) \((.*)\)$", re.MULTILINE) diff --git a/nix/tests/vm/lib/komorebi_vm/kde.py b/nix/tests/vm/lib/komorebi_vm/kde.py index 70fc98e3..38248154 100644 --- a/nix/tests/vm/lib/komorebi_vm/kde.py +++ b/nix/tests/vm/lib/komorebi_vm/kde.py @@ -5,43 +5,14 @@ import re import shlex import time from collections.abc import Callable -from dataclasses import dataclass from typing import Any from PIL import Image +from komorebi_vm.core.geometry import Rect from komorebi_vm.machine import Machine -@dataclass(frozen=True) -class Rect: - """A window's position and size, in pixels.""" - - x: int - y: int - w: int - h: int - - def overlaps(self, other: "Rect") -> bool: - """Returns true if and only if the two rectangles share at least one pixel.""" - return ( - self.x < other.x + other.w - and other.x < self.x + self.w - and self.y < other.y + other.h - and other.y < self.y + self.h - ) - - def inside(self, area: "Rect", slack: int = 0) -> bool: - """Returns true if and only if every pixel of this rectangle is a pixel of `area`, give - or take `slack` pixels at each edge.""" - return ( - self.x >= area.x - slack - and self.y >= area.y - slack - and self.x + self.w <= area.x + area.w + slack - and self.y + self.h <= area.y + area.h + slack - ) - - class Outputs: """The outputs of a KWin session, as `kscreen-doctor` lists and changes them. diff --git a/nix/tests/vm/lib/komorebi_vm/latency.py b/nix/tests/vm/lib/komorebi_vm/latency.py index 61ae57b6..ba557a62 100644 --- a/nix/tests/vm/lib/komorebi_vm/latency.py +++ b/nix/tests/vm/lib/komorebi_vm/latency.py @@ -19,7 +19,7 @@ import re from dataclasses import dataclass from typing import Any -from komorebi_vm.kde import Rect +from komorebi_vm.core.geometry import Rect _HEADER = re.compile(r"^(method call|method return|signal|error) time=(\d+\.\d+) (.*)$") _STRING = ' string "' diff --git a/nix/tests/vm/lib/pyproject.toml b/nix/tests/vm/lib/pyproject.toml index 3378f686..f5721153 100644 --- a/nix/tests/vm/lib/pyproject.toml +++ b/nix/tests/vm/lib/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.12" dependencies = ["pillow"] [tool.setuptools] -packages = ["komorebi_vm"] +packages = ["komorebi_vm", "komorebi_vm.core"] [tool.setuptools.package-data] komorebi_vm = ["py.typed"] diff --git a/nix/tests/vm/lib/tests/test_pixels.py b/nix/tests/vm/lib/tests/test_pixels.py index daab8ecc..cdc396f4 100644 --- a/nix/tests/vm/lib/tests/test_pixels.py +++ b/nix/tests/vm/lib/tests/test_pixels.py @@ -1,7 +1,7 @@ from PIL import Image, ImageDraw from komorebi_vm import Rect -from komorebi_vm.pixels import leftmost, near, pixel, ring +from komorebi_vm.core.pixels import leftmost, near, pixel, ring BLUE = (66, 165, 245) GRAY = (128, 128, 128)