Add configurable mouse and wheel bindings, window move and resize, tab drops, cross-output dragging, and device settings. Adjust display scaling in 0.05 increments. Preserve opposite-edge positioning during scrolling resizes and show rows for edge drops into tabbed columns.
135 lines
7.0 KiB
Python
135 lines
7.0 KiB
Python
"""Verify visibility-gated callbacks and idle inhibition on a private desktop.
|
|
|
|
FERESE_TEST_PRESENTATION=1 python3 scripts/tests/test_presentation_isolated.py
|
|
Requires built debug compositor/control binaries, a Wayland host, cc and wayland-scanner.
|
|
"""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from session_socket import ipc_environment, ipc_socket
|
|
import signal
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
@unittest.skipUnless(os.environ.get("FERESE_TEST_PRESENTATION") == "1", "requires a Wayland host")
|
|
class Presentation(unittest.TestCase):
|
|
def test_covered_and_unmapped_inhibitors_stop_inhibiting(self):
|
|
processes = []
|
|
with tempfile.TemporaryDirectory(prefix="ferese-presentation-") as directory:
|
|
root = Path(directory)
|
|
protocols = subprocess.check_output(["pkg-config", "--variable=pkgdatadir", "wayland-protocols"], text=True).strip()
|
|
sources = []
|
|
for name, relative in [("xdg-shell", "stable/xdg-shell/xdg-shell.xml"),
|
|
("idle-inhibit", "unstable/idle-inhibit/idle-inhibit-unstable-v1.xml"),
|
|
("idle-notify", "staging/ext-idle-notify/ext-idle-notify-v1.xml")]:
|
|
xml = str(Path(protocols) / relative)
|
|
for kind, suffix in [("client-header", "client-protocol.h"), ("private-code", "protocol.c")]:
|
|
subprocess.run(["wayland-scanner", kind, xml, str(root / f"{name}-{suffix}")], check=True)
|
|
sources.append(str(root / f"{name}-protocol.c"))
|
|
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/presentation.c"),
|
|
*sources, "-o", str(root / "client"), *flags], check=True)
|
|
runtime = root / "runtime"
|
|
runtime.mkdir(mode=0o700)
|
|
config = root / "config/ferese/config.kdl"
|
|
config.parent.mkdir(parents=True)
|
|
config.write_text('window-rule app-id="ferese.test.presentation" floating=#true\n'
|
|
'animations { enabled #false; }\n'
|
|
'theme { geometry { window-radius 0; border-width 0; focus-ring-width 0; }; }\n')
|
|
# Remembered geometry deliberately places both clients together;
|
|
# ordinary placement now avoids overlap. Keep host state isolated.
|
|
state = root / "state/ferese"
|
|
state.mkdir(parents=True)
|
|
(state / "floating.json").write_text(json.dumps({
|
|
"ferese.test.presentation": {
|
|
"output": "ferese-winit", "fractions": [.1, .1, .4, .4],
|
|
},
|
|
}))
|
|
display = Path(os.environ["WAYLAND_DISPLAY"])
|
|
if not display.is_absolute():
|
|
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
|
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), XDG_STATE_HOME=str(root / "state"), WAYLAND_DISPLAY=str(display), FERESE_ENABLE_SCREENCOPY="1")
|
|
env.pop("WAYLAND_SOCKET", None)
|
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
|
log = (root / "compositor.log").open("w")
|
|
try:
|
|
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
|
processes.append(compositor)
|
|
deadline = time.monotonic() + 10
|
|
while not (ipc_socket(runtime)).exists():
|
|
if compositor.poll() is not None or time.monotonic() > deadline:
|
|
self.fail((root / "compositor.log").read_text())
|
|
time.sleep(0.02)
|
|
sockets = list(runtime.glob("wayland-*"))
|
|
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
|
|
|
def call(*args):
|
|
return subprocess.check_output([str(REPO / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=10)
|
|
|
|
def windows(count):
|
|
deadline = time.monotonic() + 10
|
|
while time.monotonic() < deadline:
|
|
result = json.loads(call("get-windows"))
|
|
if len(result) == count and all(window["mapped"] and window["width"] == 640 for window in result):
|
|
return result
|
|
time.sleep(0.02)
|
|
self.fail((root / "compositor.log").read_text())
|
|
|
|
events = (root / "events").open("w")
|
|
target = subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdin=subprocess.PIPE,
|
|
stdout=events, stderr=log, start_new_session=True)
|
|
processes.append(target)
|
|
windows(1)
|
|
time.sleep(.4)
|
|
|
|
def count(event):
|
|
return (root / "events").read_text().splitlines().count(event)
|
|
|
|
before = count("frame")
|
|
idle = count("idle")
|
|
time.sleep(.5)
|
|
self.assertGreater(count("frame"), before, "visible empty commits must get callbacks")
|
|
self.assertEqual(count("idle"), idle, "visible inhibitor must keep the notifier active")
|
|
|
|
cover = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log,
|
|
stderr=log, start_new_session=True)
|
|
processes.append(cover)
|
|
current = windows(2)
|
|
self.assertEqual((current[0]["x"], current[0]["y"]), (current[1]["x"], current[1]["y"]))
|
|
time.sleep(.5)
|
|
before = count("frame")
|
|
time.sleep(.5)
|
|
self.assertEqual(count("frame"), before, "fully occluded client must stop receiving callbacks")
|
|
self.assertGreater(count("idle"), idle, "occluded inhibitor must stop inhibiting")
|
|
|
|
cover.terminate()
|
|
cover.wait(timeout=5)
|
|
windows(1)
|
|
time.sleep(.5)
|
|
self.assertGreater(count("frame"), before, "revealed client must resume callbacks")
|
|
idle = count("idle")
|
|
time.sleep(.4)
|
|
self.assertEqual(count("idle"), idle)
|
|
target.stdin.write(b"u")
|
|
target.stdin.flush()
|
|
windows(0)
|
|
time.sleep(.6)
|
|
self.assertGreater(count("idle"), idle, "unmapped inhibitor must stop inhibiting")
|
|
target.stdin.close()
|
|
events.close()
|
|
finally:
|
|
for process in reversed(processes):
|
|
if process.poll() is None:
|
|
os.killpg(process.pid, signal.SIGTERM)
|
|
process.wait(timeout=5)
|
|
log.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|