0ab08e8561
Add scripts/gamemode-watch.py, launched at niri startup, which subscribes to niri's event stream and toggles gamemode whenever a game window (steam_app_*, RuneLite, gamescope) is open — so Steam games trigger gamemode with zero per-game setup instead of needing gamemoderun %command%. 'winwatch' is a third reference-counted source alongside feral + manual. Pure Watcher class is unit-tested; SIGTERM/finally guarantee cleanup so the watcher stopping never leaves gamemode stuck on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.0 KiB
Python
97 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for gamemode-watch's pure event logic (no niri, no subprocess).
|
|
Loads the hyphenated script by path and drives Watcher with synthetic events,
|
|
asserting the exact sequence of add/del calls."""
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
MOD_PATH = os.path.join(HERE, "..", "gamemode-watch.py")
|
|
spec = importlib.util.spec_from_file_location("gamemode_watch", MOD_PATH)
|
|
gw = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(gw)
|
|
|
|
fails = 0
|
|
|
|
|
|
def check(desc, got, want):
|
|
global fails
|
|
if got == want:
|
|
print(f"ok - {desc}")
|
|
else:
|
|
print(f"FAIL - {desc} (got {got!r} want {want!r})")
|
|
fails = 1
|
|
|
|
|
|
# --- is_game ---------------------------------------------------------------
|
|
check("steam_app_ matches", gw.is_game("steam_app_480"), True)
|
|
check("bare steam_app_ matches", gw.is_game("steam_app_"), True)
|
|
check("RuneLite matches", gw.is_game("RuneLite"), True)
|
|
check("runelite matches", gw.is_game("runelite"), True)
|
|
check("runelite full id matches", gw.is_game("net-runelite-client-RuneLite"), True)
|
|
check("gamescope matches", gw.is_game("gamescope"), True)
|
|
check("Gamescope (case) matches", gw.is_game("Gamescope"), True)
|
|
check("Alacritty is not a game", gw.is_game("Alacritty"), False)
|
|
check("bolt-launcher excluded", gw.is_game("bolt-launcher"), False)
|
|
check("empty app_id is not a game", gw.is_game(""), False)
|
|
check("None app_id is not a game", gw.is_game(None), False)
|
|
|
|
|
|
# --- Watcher transitions ---------------------------------------------------
|
|
def drive(events):
|
|
calls = []
|
|
w = gw.Watcher(lambda action: calls.append(action))
|
|
for ev in events:
|
|
w.handle(ev)
|
|
return calls
|
|
|
|
|
|
def win(wid, app_id):
|
|
return {"WindowOpenedOrChanged": {"window": {"id": wid, "app_id": app_id}}}
|
|
|
|
|
|
def closed(wid):
|
|
return {"WindowClosed": {"id": wid}}
|
|
|
|
|
|
def snapshot(pairs):
|
|
return {"WindowsChanged": {"windows": [{"id": i, "app_id": a} for i, a in pairs]}}
|
|
|
|
|
|
check("empty snapshot: no calls", drive([snapshot([])]), [])
|
|
check("non-game window: no calls", drive([win(1, "Alacritty")]), [])
|
|
check("one game opens: add", drive([win(1, "steam_app_10")]), ["add"])
|
|
check(
|
|
"second game keeps active (single add)",
|
|
drive([win(1, "steam_app_10"), win(2, "RuneLite")]),
|
|
["add"],
|
|
)
|
|
check(
|
|
"close one of two: still active",
|
|
drive([win(1, "steam_app_10"), win(2, "RuneLite"), closed(2)]),
|
|
["add"],
|
|
)
|
|
check(
|
|
"close last game: add then del",
|
|
drive([win(1, "steam_app_10"), closed(1)]),
|
|
["add", "del"],
|
|
)
|
|
check(
|
|
"snapshot already containing a game: add (restart mid-game)",
|
|
drive([snapshot([(1, "Alacritty"), (5, "steam_app_9")])]),
|
|
["add"],
|
|
)
|
|
check(
|
|
"game window changes to non-game app_id: del",
|
|
drive([win(1, "steam_app_10"), win(1, "Alacritty")]),
|
|
["add", "del"],
|
|
)
|
|
check(
|
|
"repeated OpenedOrChanged for same game: single add",
|
|
drive([win(1, "gamescope"), win(1, "gamescope"), win(1, "gamescope")]),
|
|
["add"],
|
|
)
|
|
|
|
sys.exit(fails)
|