Files
feresaria/scripts/tests/test_idle_inhibition_isolated.py

209 lines
10 KiB
Python

"""Test playback and window visibility on a private bus and nested desktop.
FERESE_TEST_IDLE_INHIBITION=1 python3 scripts/tests/test_idle_inhibition_isolated.py
Requires Wayland, PyGObject, dbus-run-session, cc and wayland-scanner.
FERESE_TEST_BINARY and FERESE_TEST_CTL can select release binaries.
"""
import json
import os
from pathlib import Path
from session_socket import ipc_environment, ipc_socket
import signal
import subprocess
import sys
import tempfile
import threading
import time
import unittest
REPO = Path(__file__).resolve().parents[2]
BINARY = REPO / os.environ.get("FERESE_TEST_BINARY", "target/debug/ferese")
CTL = REPO / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
NAME = "org.mpris.MediaPlayer2.ferese_test"
PATH = "/org/mpris/MediaPlayer2"
PLAYER = "org.mpris.MediaPlayer2.Player"
APP = "ferese.test.window-capture"
@unittest.skipUnless(os.environ.get("FERESE_TEST_IDLE_INHIBITION") == "1", "requires a Wayland host")
class IdleInhibition(unittest.TestCase):
def test_private_playback_and_visibility(self):
result = subprocess.run(["dbus-run-session", "--", sys.executable, __file__, "--private"],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def private_checks():
import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib
loop = GLib.MainLoop()
threading.Thread(target=loop.run, daemon=True).start()
class FakePlayer:
def __init__(self, status="Paused"):
self.status = status
self.connection = Gio.DBusConnection.new_for_address_sync(
os.environ["DBUS_SESSION_BUS_ADDRESS"],
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
None, None)
node = Gio.DBusNodeInfo.new_for_xml(f'''<node>
<interface name="org.mpris.MediaPlayer2"><property name="DesktopEntry" type="s" access="read"/></interface>
<interface name="{PLAYER}"><property name="PlaybackStatus" type="s" access="read"/></interface>
</node>''')
self.registrations = [self.connection.register_object(PATH, interface, None, self.property, None)
for interface in node.interfaces]
self.connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
"RequestName", GLib.Variant("(su)", (NAME, 0)), None,
Gio.DBusCallFlags.NONE, 2000, None)
def property(self, connection, sender, path, interface, name):
return GLib.Variant("s", self.status if name == "PlaybackStatus" else APP)
def set(self, status, invalidated=False):
self.status = status
changed = {} if invalidated else {"PlaybackStatus": GLib.Variant("s", status)}
fields = ["PlaybackStatus"] if invalidated else []
self.connection.emit_signal(None, PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged",
GLib.Variant("(sa{sv}as)", (PLAYER, changed, fields)))
self.connection.flush_sync(None)
def close(self):
self.connection.close_sync(None)
processes = []
player = FakePlayer()
try:
with tempfile.TemporaryDirectory(prefix="ferese-idle-") as directory:
root = Path(directory)
runtime = root / "runtime"
runtime.mkdir(mode=0o700)
config = root / "config/ferese/config.kdl"
config.parent.mkdir(parents=True)
base = f'animations {{ reduced-motion #true; }}\nwindow-rule app-id="{APP}" floating=#true\n'
config.write_text(base)
protocols = subprocess.check_output(["pkg-config", "--variable=pkgdatadir", "wayland-protocols"], text=True).strip()
xml = str(Path(protocols) / "stable/xdg-shell/xdg-shell.xml")
for kind, output in [("client-header", "xdg-shell-client-protocol.h"), ("private-code", "xdg-shell-protocol.c")]:
subprocess.run(["wayland-scanner", kind, xml, str(root / output)], check=True)
flags = subprocess.check_output(["pkg-config", "--cflags", "--libs", "wayland-client"], text=True).split()
subprocess.run(["cc", "-Wall", "-Wextra", "-I", str(root), str(REPO / "scripts/tests/fixtures/window-capture.c"),
str(root / "xdg-shell-protocol.c"), "-o", str(root / "client"), *flags], check=True)
display = Path(os.environ["WAYLAND_DISPLAY"])
if not display.is_absolute():
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
env = dict(os.environ, WAYLAND_DISPLAY=str(display), XDG_RUNTIME_DIR=str(runtime),
XDG_CONFIG_HOME=str(root / "config"), XDG_STATE_HOME=str(root / "state"))
for variable in ["WAYLAND_SOCKET", "FERESE_SOCKET", "FERESE_SHELL_CONTROL_SOCKET"]:
env.pop(variable, None)
with (root / "compositor.log").open("w") as log:
try:
compositor = subprocess.Popen([str(BINARY), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
processes.append(compositor)
def wait(check, message):
deadline = time.monotonic() + 8
while time.monotonic() < deadline:
if compositor.poll() is not None:
raise AssertionError((root / "compositor.log").read_text())
if check():
return
time.sleep(0.02)
raise AssertionError(message + "\n" + (root / "compositor.log").read_text())
wait(lambda: (ipc_socket(runtime)).exists(), "compositor startup")
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")))
def call(*args):
return json.loads(subprocess.check_output([str(CTL), "-j", *args], env=ipc_environment(env), timeout=5))
def inhibited(expected):
def matches():
state = call("get-idle-inhibition")
return state["automatic"] == expected and state["inhibited"] == expected
wait(matches, f"idle inhibition != {expected}")
wait(lambda: len(call("get-idle-inhibition")["players"]) == 1, "MPRIS discovery")
client = subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
processes.append(client)
wait(lambda: len(call("get-windows")) == 1, "window map")
call("toggle-fullscreen")
inhibited(False)
player.set("Playing")
inhibited(True)
player.set("Paused")
inhibited(False)
player.set("Playing", invalidated=True)
inhibited(True)
player.set("Stopped")
inhibited(False)
player.set("Playing")
inhibited(True)
call("workspace", "2")
inhibited(False)
call("workspace", "1")
inhibited(True)
player.close()
inhibited(False)
player = FakePlayer("Paused")
wait(lambda: len(call("get-idle-inhibition")["players"]) == 1, "replacement discovery")
inhibited(False)
player.set("Playing")
inhibited(True)
ambiguous = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env),
stdout=log, stderr=log, start_new_session=True)
processes.append(ambiguous)
wait(lambda: len(call("get-windows")) == 2, "second player window")
inhibited(False)
os.killpg(ambiguous.pid, signal.SIGTERM)
ambiguous.wait(timeout=5)
wait(lambda: len(call("get-windows")) == 1, "ambiguous window closure")
inhibited(True)
config.write_text(base + f'window-rule app-id="{APP}" idle-inhibit="none"\n')
call("reload-config")
inhibited(False)
config.write_text(base)
call("reload-config")
inhibited(True)
call("toggle-fullscreen")
inhibited(False)
config.write_text(base + f'idle-inhibit {{ fullscreen-playback #false; }}\nwindow-rule app-id="{APP}" idle-inhibit="visible"\n')
call("reload-config")
inhibited(True)
player.set("Stopped")
inhibited(True)
call("workspace", "2")
inhibited(False)
call("workspace", "1")
inhibited(True)
config.write_text(base + 'idle-inhibit { fullscreen-playback #false; }\n')
call("reload-config")
inhibited(False)
config.write_text(base + f'window-rule app-id="{APP}" idle-inhibit="visible"\n')
call("reload-config")
inhibited(True)
os.killpg(client.pid, signal.SIGTERM)
client.wait(timeout=5)
wait(lambda: not call("get-windows"), "closed window remained registered")
inhibited(False)
finally:
for process in reversed(processes):
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
process.wait(timeout=5)
finally:
player.close()
loop.quit()
if __name__ == "__main__":
if "--private" in sys.argv:
private_checks()
else:
unittest.main()