mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 13:22:19 +01:00
test(e2e): a Windows scenario bar for komorebi-bar
New extras scenario e2e/platforms/qemu_windows/extras/bar.py, registered in run.py as `bar` (runs after the tiling windows, like the other Windows extras). It starts komorebi-bar in the guest with a configuration that has storage, time, applications, komorebi workspaces and layout widgets, the Windows-only Keyboard and Systray widgets and a `mouse` block (a message and a command), and checks: `komorebi-bar --schema` equals the checked-in schema.bar.json byte for byte; the process starts and its window titled komorebi-bar appears, visible and sized; it runs 15 s (scaled); `komorebic state` answers and komorebi does not tile the bar; commands that notify the bar work; the bar survives `komorebic stop` and the daemon starting again as the same process (it reconnects: it does NOT exit when the daemon stops, so the task text "exits cleanly when komorebi stops" is checked as `komorebic stop --bar` ending daemon and bar); screenshots are kept as artefacts, no pixel assertions. No known failures. Plumbing: run.py uploads komorebi-bar.exe when the binaries directory has it (other scenarios do not need it, so older build-ref directories still work), the nix app copies it and sets KOMOREBI_E2E_BAR_SCHEMA to the checked-in schema (the e2e directory alone does not contain it), build-ref.sh builds it. README and docs/dev/bar-e2e.md document it. Verified: py_compile and ruff clean; the runner and library unit tests: 157 passed; flake.nix parses. NOT run: the scenario itself (the Windows guest is reserved for gates), so every check is untested against a real bar and may need adjusting (window title, timings, the Systray widget inside the VM). Needs gate: win-e2e `bar`.
This commit is contained in:
1 parent
e663e346ba
commit
f0a0d71b83
6 files changed
+256
-2
No files matched your search
@@ -0,0 +1,24 @@
|
||||
# Windows e2e for komorebi-bar
|
||||
|
||||
The `bar` scenario (`e2e/platforms/qemu_windows/extras/bar.py`, registered as `EXTRAS["bar"]` in
|
||||
`run.py`) is the runtime gate for changes to `komorebi-bar` and `komorebi-bar-core`: the unit tests
|
||||
and the schema oracle cannot run the bar. Run it on the golden Windows guest:
|
||||
|
||||
```sh
|
||||
E2E_GOLDEN=golden KOMOREBI_E2E_SCALE=1.5 nix run .#e2e-windows -- OUT bar
|
||||
# another commit: build-ref.sh REF bins; KOMOREBI_E2E_BINS=bins ... (bins must have komorebi-bar.exe)
|
||||
```
|
||||
|
||||
It checks: the `--schema` output equals `schema.bar.json`; the configuration (storage, time,
|
||||
applications, komorebi workspaces and layout, Keyboard, Systray, a `mouse` block with a message and
|
||||
a command) is accepted and the bar starts; its window appears; it keeps running 15 s; the daemon
|
||||
keeps answering and does not tile the bar; the bar survives the daemon stopping and starting again
|
||||
(it reconnects, it does not exit when the daemon stops); `komorebic stop --bar` ends both.
|
||||
|
||||
Artefacts in `OUT/bar/`: `bar.png`, `bar-after-restart.png` (screenshots, no pixel assertions),
|
||||
`bar-schema.json`, `bar.out`, `bar.json`, `result.json`. Not covered: clicks, text of the widgets,
|
||||
the PowerShell session of a `Command` mouse message (the bar starts it; nothing sends the click),
|
||||
macOS (see `docs/dev/macos-handcheck.md`).
|
||||
|
||||
Not run when it was written (the guest was reserved): lint only (`py_compile`, `ruff`, the runner's
|
||||
157 unit tests pass).
|
||||
@@ -70,7 +70,25 @@ What only Windows checks, run after the tiling windows with `[windows]`
|
||||
labels, from `extras/`: `windows_padding` (the border's 7 px),
|
||||
`windows_cloaking`, `windows_config_file`, `windows_tracking`,
|
||||
`windows_second_display` and `windows_display_change` (the mode of the QXL
|
||||
display changes while windows are on it).
|
||||
display changes while windows are on it). `bar` starts `komorebi-bar` next to the
|
||||
daemon (see below).
|
||||
|
||||
### The `bar` scenario
|
||||
|
||||
`E2E_GOLDEN=golden nix run .#e2e-windows -- OUT bar` (the nix app copies `komorebi-bar.exe`
|
||||
next to the other binaries and points `KOMOREBI_E2E_BAR_SCHEMA` at the checked-in
|
||||
`schema.bar.json`; `build-ref.sh` builds the bar too, and an older `KOMOREBI_E2E_BINS` directory
|
||||
without it makes the scenario fail with that message while the others still run). After the
|
||||
tiling windows it checks: `komorebi-bar --schema` is `schema.bar.json` byte for byte (the output
|
||||
is kept as `bar-schema.json`); a bar with a configuration that has storage, time, applications,
|
||||
komorebi workspaces and layout, Keyboard and Systray widgets and a `mouse` block starts, its
|
||||
window `komorebi-bar` appears, visible and with a size; it runs for 15 s (scaled); `komorebic
|
||||
state` answers and komorebi does not tile it; `toggle-pause` twice (events to the bar); the bar
|
||||
survives `komorebic stop` (it reconnects, it does not exit) and the daemon's restart, as the same
|
||||
process; `komorebic stop --bar` ends the daemon and the bar. Artefacts: `bar.png`,
|
||||
`bar-after-restart.png`, `bar.out` (what the bar printed), `bar.json`. No pixel assertions. No
|
||||
known failures.
|
||||
|
||||
|
||||
Every scenario gets its own guest on a fresh overlay of the golden
|
||||
image; each needs about 4 vCPU and 6 GB. `OUT/<scenario>/result.json` names the
|
||||
|
||||
@@ -30,7 +30,7 @@ git -C "$root" worktree add --detach "$tree" "$commit"
|
||||
cp "$root/flake.nix" "$root/flake.lock" "$tree/"
|
||||
|
||||
mkdir -p "$out"
|
||||
for package in komorebi komorebic; do
|
||||
for package in komorebi komorebic komorebi-bar; do
|
||||
path=$(nix build "$tree#$package" --no-link --print-out-paths)
|
||||
cp "$path/bin/$package.exe" "$out/"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""komorebi-bar next to the daemon: the process, its window, its schema and what
|
||||
it does to the daemon.
|
||||
|
||||
Windows only (the bar is built per platform), run after the tiling windows like the
|
||||
other extras. It needs `komorebi-bar.exe` next to the other binaries (`BAR_BINARY`);
|
||||
without it the scenario says so and fails, the others do not need it.
|
||||
|
||||
What it exercises is what a bar change can break at runtime and the unit tests cannot
|
||||
see: the configuration is read (storage, time, applications, komorebi workspaces and
|
||||
layout widgets, a `mouse` block, and the Windows-only Systray and Keyboard widgets),
|
||||
the window opens, the bar keeps running while the daemon answers, it survives the daemon
|
||||
going away and coming back (it reconnects: it does not exit when the daemon stops), and
|
||||
`komorebic stop --bar` ends it. There are no pixel assertions; the screenshot is kept as
|
||||
an artefact (`bar.png`, `bar-after-restart.png`).
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from komorebi_vm.core.checks import Checks
|
||||
from komorebi_vm.core.platform import Client
|
||||
|
||||
from ..windows_platform import GUEST_BIN, WindowsPlatform
|
||||
|
||||
BAR_BINARY = f"{GUEST_BIN}\\komorebi-bar.exe"
|
||||
BAR_CONFIG = "C:\\e2e\\bar.json"
|
||||
BAR_OUTPUT = "C:\\e2e\\bar.out"
|
||||
# the title eframe gives the window (`run_native("komorebi-bar", ...)`)
|
||||
BAR_TITLE = "komorebi-bar"
|
||||
# how long the bar must keep running, in seconds
|
||||
ALIVE_SECONDS = 15
|
||||
|
||||
CONFIG = {
|
||||
"height": 40,
|
||||
"left_widgets": [
|
||||
{"Komorebi": {
|
||||
"workspaces": {"enable": True, "hide_empty_workspaces": False},
|
||||
"layout": {"enable": True},
|
||||
}},
|
||||
{"Applications": {
|
||||
"enable": True,
|
||||
"items": [{"name": "Notepad", "command": "notepad.exe"}],
|
||||
}},
|
||||
],
|
||||
"right_widgets": [
|
||||
{"Storage": {"enable": True}},
|
||||
{"Time": {"enable": True, "format": "TwentyFourHour"}},
|
||||
{"Keyboard": {"enable": True}},
|
||||
{"Systray": {"enable": True}},
|
||||
],
|
||||
# one option of each kind: a message to the daemon and a command, which starts the
|
||||
# bar's PowerShell session
|
||||
"mouse": {
|
||||
"on_extra2_click": {"message": {"type": "Retile"}},
|
||||
"on_middle_click": {"Command": "echo komorebi-bar-e2e"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _expected_schema() -> bytes:
|
||||
"""The checked-in Windows schema.bar.json: from `KOMOREBI_E2E_BAR_SCHEMA` (the nix app
|
||||
sets it), otherwise the one at the root of this checkout."""
|
||||
given = os.environ.get("KOMOREBI_E2E_BAR_SCHEMA")
|
||||
path = Path(given) if given else Path(__file__).resolve().parents[4] / "schema.bar.json"
|
||||
return path.read_bytes()
|
||||
|
||||
|
||||
def _sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()[:16]
|
||||
|
||||
|
||||
def _bar_pids(platform: WindowsPlatform) -> list[str]:
|
||||
listing = platform.agent_exec(
|
||||
"cmd", "/c", 'tasklist /fi "imagename eq komorebi-bar.exe" /fo csv /nh'
|
||||
)["stdout"]
|
||||
return [
|
||||
line.split(",")[1].strip('"')
|
||||
for line in listing.splitlines()
|
||||
if line.lower().startswith('"komorebi-bar.exe"')
|
||||
]
|
||||
|
||||
|
||||
def _bar_window(platform: WindowsPlatform) -> dict | None:
|
||||
return platform.os_window_by_title(BAR_TITLE)
|
||||
|
||||
|
||||
def _containers(platform: WindowsPlatform) -> int:
|
||||
return len(platform.state().containers())
|
||||
|
||||
|
||||
def _alive_for(platform: WindowsPlatform, seconds: float) -> bool:
|
||||
"""The bar process runs now and at every second of `seconds`."""
|
||||
end = time.monotonic() + seconds
|
||||
while time.monotonic() < end:
|
||||
if not _bar_pids(platform):
|
||||
return False
|
||||
time.sleep(1)
|
||||
return bool(_bar_pids(platform))
|
||||
|
||||
|
||||
def run(platform: WindowsPlatform, checks: Checks, clients: list[Client]) -> None:
|
||||
"""Starts the bar on the tiled windows of `tiling` and walks through its life. The
|
||||
daemon runs at the end and the bar does not."""
|
||||
present = platform.agent_exec("cmd", "/c", f"if exist {BAR_BINARY} (echo yes) else (echo no)")
|
||||
if "yes" not in present["stdout"]:
|
||||
checks.bad(f"komorebi-bar.exe is in the guest ({BAR_BINARY}): the binaries lack it")
|
||||
return
|
||||
|
||||
# the schema the bar prints is the checked-in one, byte for byte
|
||||
expected = _expected_schema()
|
||||
printed = platform.agent_exec(BAR_BINARY, "--schema")
|
||||
got = printed["stdout"].replace("\r\n", "\n").encode()
|
||||
(platform._out / "bar-schema.json").write_bytes(got)
|
||||
checks.check(
|
||||
"`komorebi-bar --schema` prints schema.bar.json "
|
||||
f"(printed {_sha(got)}, checked in {_sha(expected)})",
|
||||
printed["status"] == 0 and got == expected,
|
||||
)
|
||||
|
||||
before = _containers(platform)
|
||||
local = platform._out / "bar.json"
|
||||
local.write_text(json.dumps(CONFIG, indent=2))
|
||||
platform._agent.put_file(local, BAR_CONFIG)
|
||||
platform.agent_spawn(
|
||||
"cmd", "/c", f"{BAR_BINARY} --config {BAR_CONFIG} > {BAR_OUTPUT} 2>&1"
|
||||
)
|
||||
|
||||
checks.check(
|
||||
"the bar process starts", platform.wait_for(lambda: bool(_bar_pids(platform)), timeout=30)
|
||||
)
|
||||
appeared = platform.wait_for(lambda: _bar_window(platform) is not None, timeout=60)
|
||||
checks.check(f"the bar's window titled {BAR_TITLE!r} appears", appeared)
|
||||
window = _bar_window(platform)
|
||||
if window is not None:
|
||||
frame = window["frame"]
|
||||
checks.check(
|
||||
"the bar's window is visible and has a size",
|
||||
bool(window["visible"])
|
||||
and frame["right"] - frame["left"] > 0
|
||||
and frame["bottom"] - frame["top"] > 0,
|
||||
)
|
||||
time.sleep(3)
|
||||
try:
|
||||
platform.screenshot("bar")
|
||||
except Exception as e:
|
||||
print(f" no screenshot: {e}")
|
||||
|
||||
checks.check(
|
||||
f"the bar keeps running for {ALIVE_SECONDS} s with this configuration (every widget)",
|
||||
_alive_for(platform, ALIVE_SECONDS * platform._scale),
|
||||
)
|
||||
checks.check(
|
||||
"`komorebic state` answers while the bar runs",
|
||||
platform.wait_for(lambda: platform.komorebic_status("state")[0] == 0),
|
||||
)
|
||||
checks.check(
|
||||
"komorebi does not tile the bar (it ignores komorebi-bar.exe)",
|
||||
_containers(platform) == before,
|
||||
)
|
||||
# a command that changes the state reaches the bar through its subscription; the bar
|
||||
# has no way to be asked, so it is that nothing breaks
|
||||
platform.komorebic("toggle-pause")
|
||||
platform.komorebic("toggle-pause")
|
||||
checks.check(
|
||||
"the daemon answers commands that notify the bar, and the bar runs on",
|
||||
platform.komorebic_status("state")[0] == 0 and bool(_bar_pids(platform)),
|
||||
)
|
||||
|
||||
# the daemon stops and comes back: the bar reconnects, it does not exit
|
||||
pids = _bar_pids(platform)
|
||||
platform.komorebic("stop")
|
||||
platform.wait_for(lambda: platform.komorebic_status("state")[0] != 0, timeout=20)
|
||||
checks.check(
|
||||
"the bar is still running after the daemon stopped (it waits to reconnect)",
|
||||
_alive_for(platform, 5 * platform._scale),
|
||||
)
|
||||
platform.start_komorebi()
|
||||
checks.check(
|
||||
"the bar runs on after the daemon is back, the same process",
|
||||
_alive_for(platform, 5 * platform._scale) and _bar_pids(platform) == pids,
|
||||
)
|
||||
try:
|
||||
platform.screenshot("bar-after-restart")
|
||||
except Exception as e:
|
||||
print(f" no screenshot: {e}")
|
||||
|
||||
# what stops the bar is `komorebic stop --bar`
|
||||
platform.komorebic("stop", "--bar")
|
||||
checks.check(
|
||||
"`komorebic stop --bar` stops the daemon",
|
||||
platform.wait_for(lambda: platform.komorebic_status("state")[0] != 0, timeout=20),
|
||||
)
|
||||
checks.check(
|
||||
"`komorebic stop --bar` ends the bar process",
|
||||
platform.wait_for(lambda: not _bar_pids(platform), timeout=20),
|
||||
)
|
||||
try:
|
||||
text = platform.agent_get_file(BAR_OUTPUT).decode(errors="replace")
|
||||
(platform._out / "bar.out").write_text(text)
|
||||
except Exception as e:
|
||||
print(f" no bar output: {e}")
|
||||
# left as the other scenarios leave it: the daemon runs
|
||||
platform.start_komorebi()
|
||||
@@ -213,6 +213,8 @@ def _commit() -> str:
|
||||
|
||||
|
||||
BINARIES = ["komorebi.exe", "komorebic.exe", "komorebi-testwin.exe"]
|
||||
# uploaded when the binaries have them: only the `bar` scenario needs the bar
|
||||
OPTIONAL_BINARIES = ["komorebi-bar.exe"]
|
||||
WITH_CLIENTS = {
|
||||
"focus", "moving", "stacks", "workspaces", "resize", "monocle", "closing",
|
||||
"commands", "hotplug", "unknown_monitor", "lifecycle",
|
||||
@@ -239,6 +241,7 @@ EXTRAS = {
|
||||
"windows_config_file": "config_file", "windows_tracking": "tracking",
|
||||
"windows_second_display": "second_display",
|
||||
"windows_display_change": "display_change",
|
||||
"bar": "bar",
|
||||
}
|
||||
|
||||
# a small configuration for the sweep's `replace-configuration`
|
||||
@@ -366,6 +369,7 @@ def run_one(
|
||||
with Guest(golden, out / "guest", state=state) as guest:
|
||||
lap("guest")
|
||||
guest.upload(bin_dir, BINARIES)
|
||||
guest.upload(bin_dir, [b for b in OPTIONAL_BINARIES if (bin_dir / b).exists()])
|
||||
lap("upload")
|
||||
platform = WindowsPlatform(
|
||||
guest.qmp, guest.agent, out,
|
||||
|
||||
@@ -457,8 +457,10 @@
|
||||
cp ${e2eBuild.komorebi}/bin/komorebi.exe "$bins"
|
||||
cp ${e2eBuild.komorebic}/bin/komorebic.exe "$bins"
|
||||
cp ${e2eBuild.komorebi-testwin}/bin/komorebi-testwin.exe "$bins"
|
||||
cp ${e2eBuild.komorebi-bar}/bin/komorebi-bar.exe "$bins"
|
||||
fi
|
||||
export OVMF_FD=${ovmfWithTpm}
|
||||
export KOMOREBI_E2E_BAR_SCHEMA=${./schema.bar.json}
|
||||
export KOMOREBI_E2E_COMMIT=''${KOMOREBI_E2E_COMMIT:-${self.rev or "dirty"}}
|
||||
export PYTHONPATH=${./e2e/lib}:${./e2e}
|
||||
exec python -m platforms.qemu_windows.run \
|
||||
|
||||
Reference in new issue
Block a user