Fix config upgrade failures, save races, lock confirmation and release validation (#23)
* Coordinate config writes and gate releases on desktop validation * Use toolkit rc.2 with painting and session lock fixes * Ignore retired desktop widget settings in existing configs
This commit is contained in:
1 parent
a447136b01
commit
91e2f227d2
18 files changed
+536
-155
No files matched your search
@@ -1,4 +1,4 @@
|
||||
"""Run native regressions against a private, headless Weston instance."""
|
||||
"""Run native regressions on a private software-rendered Weston host."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -9,22 +9,34 @@ import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def stop(process):
|
||||
if process.poll() is None:
|
||||
os.killpg(process.pid, signal.SIGINT)
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--log-dir", type=Path, required=True)
|
||||
parser.add_argument("--suite", choices=("panel-xwayland", "desktop"), default="panel-xwayland")
|
||||
args = parser.parse_args()
|
||||
logs = args.log_dir.resolve()
|
||||
logs.mkdir(parents=True, exist_ok=True)
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
|
||||
# No inherited session endpoints or activation settings reach the test host.
|
||||
env = {key: value for key, value in os.environ.items()
|
||||
if not key.startswith("FERESE_") or key.startswith("FERESE_TEST_")}
|
||||
for key in ("DISPLAY", "WAYLAND_DISPLAY", "WAYLAND_SOCKET", "XAUTHORITY",
|
||||
"DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", "EGL_PLATFORM"):
|
||||
env.pop(key, None)
|
||||
env.update(LIBGL_ALWAYS_SOFTWARE="1", FERESE_TEST_LOG_DIR=str(logs))
|
||||
|
||||
for key, value in {"FERESE_TEST_BINARY": "target/debug/ferese",
|
||||
"FERESE_TEST_CTL": "target/debug/feresectl",
|
||||
"FERESE_TEST_SHELL": "target/debug/ferese-shell"}.items():
|
||||
env.setdefault(key, value)
|
||||
with tempfile.TemporaryDirectory(prefix="ferese-native-host-") as temporary:
|
||||
root = Path(temporary)
|
||||
runtime = root / "runtime"
|
||||
@@ -34,9 +46,10 @@ def main():
|
||||
XDG_CACHE_HOME=str(root / "cache"))
|
||||
with (logs / "weston.log").open("w") as log:
|
||||
host = subprocess.Popen([
|
||||
env.get("FERESE_TEST_WESTON", "weston"), "--backend=headless-backend.so",
|
||||
"--renderer=pixman", "--width=1600", "--height=1000",
|
||||
"--socket=ferese-test-host", "--idle-time=0", "--no-config",
|
||||
env.get("FERESE_TEST_WESTON", "weston"),
|
||||
"--backend=headless-backend.so", "--renderer=pixman",
|
||||
"--width=1600", "--height=1000", "--socket=ferese-test-host",
|
||||
"--idle-time=0", "--no-config",
|
||||
], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
try:
|
||||
deadline = time.monotonic() + 15
|
||||
@@ -44,22 +57,34 @@ def main():
|
||||
if host.poll() is not None or time.monotonic() >= deadline:
|
||||
raise RuntimeError((logs / "weston.log").read_text())
|
||||
time.sleep(.05)
|
||||
for layout in ("continuous", "islands"):
|
||||
subprocess.run([
|
||||
sys.executable, "scripts/tests/test_shell_motion_isolated.py", "-v",
|
||||
], cwd=repo, env=dict(env, FERESE_TEST_SHELL_MOTION="1",
|
||||
FERESE_TEST_BAR_LAYOUT=layout), check=True)
|
||||
subprocess.run([
|
||||
sys.executable, "scripts/tests/test_xwayland_isolated.py", "-v",
|
||||
], cwd=repo, env=dict(env, FERESE_TEST_XWAYLAND="1"), check=True)
|
||||
cases = [
|
||||
("shell-continuous", "shell_motion", {"FERESE_TEST_SHELL_MOTION": "1", "FERESE_TEST_BAR_LAYOUT": "continuous"}),
|
||||
("shell-islands", "shell_motion", {"FERESE_TEST_SHELL_MOTION": "1", "FERESE_TEST_BAR_LAYOUT": "islands"}),
|
||||
]
|
||||
if args.suite == "desktop":
|
||||
cases.insert(0, ("lock", "lock", {"FERESE_TEST_LOCK": "1", "FERESE_TEST_BIN_DIR": "target/debug"}))
|
||||
cases.extend([
|
||||
("presentation", "presentation", {"FERESE_TEST_PRESENTATION": "1"}),
|
||||
("resize", "resize_dependencies", {"FERESE_TEST_RESIZE": "1"}),
|
||||
])
|
||||
else:
|
||||
cases.append(("xwayland", "xwayland", {"FERESE_TEST_XWAYLAND": "1"}))
|
||||
for name, script, options in cases:
|
||||
print(f"Running {name}", flush=True)
|
||||
with (logs / f"{name}-test.log").open("w") as output:
|
||||
test = subprocess.Popen([
|
||||
sys.executable, f"scripts/tests/test_{script}_isolated.py", "-v",
|
||||
], cwd=repo, env=dict(env, **options), stdout=output, stderr=output,
|
||||
start_new_session=True)
|
||||
try:
|
||||
status = test.wait(timeout=180)
|
||||
if status:
|
||||
raise RuntimeError((logs / f"{name}-test.log").read_text())
|
||||
finally:
|
||||
stop(test)
|
||||
print(f"Passed {name}", flush=True)
|
||||
finally:
|
||||
if host.poll() is None:
|
||||
os.killpg(host.pid, signal.SIGTERM)
|
||||
try:
|
||||
host.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(host.pid, signal.SIGKILL)
|
||||
host.wait(timeout=5)
|
||||
stop(host)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Opt-in lock protocol smoke test. Opens a temporary nested compositor.
|
||||
Run: FERESE_TEST_LOCK=1 python3 scripts/tests/test_lock_isolated.py
|
||||
Requires built binaries and bwrap. FERESE_TEST_BIN_DIR selects debug/release. Host PAM is never modified.
|
||||
Requires built binaries. FERESE_TEST_BIN_DIR selects debug/release.
|
||||
No credentials are submitted; this test exercises protocol ownership, not PAM.
|
||||
"""
|
||||
import os
|
||||
import unittest
|
||||
@@ -16,12 +17,12 @@ class NativeLockTest(unittest.TestCase):
|
||||
runtime=root/'runtime';runtime.mkdir(mode=0o700)
|
||||
config=root/'config'/'ferese';config.mkdir(parents=True)
|
||||
(config/'config.kdl').write_text('')
|
||||
pam=root/'pam';pam.mkdir()
|
||||
# Deny-only policy confined to a mount namespace; never changes host PAM.
|
||||
(pam/'ferese-lock').write_text('auth required pam_deny.so\naccount required pam_deny.so\n')
|
||||
host_display=os.environ.get('WAYLAND_DISPLAY','wayland-1')
|
||||
if not host_display.startswith('/'):host_display=str(pathlib.Path(os.environ['XDG_RUNTIME_DIR'])/host_display)
|
||||
env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1')
|
||||
env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),
|
||||
XDG_STATE_HOME=str(root/'state'),XDG_CACHE_HOME=str(root/'cache'),
|
||||
WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1')
|
||||
env.pop('WAYLAND_SOCKET', None)
|
||||
binary_dir = Path(os.environ.get('FERESE_TEST_BIN_DIR', 'target/release'))
|
||||
log=(root/'compositor.log').open('w')
|
||||
compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=ipc_environment(env),stdout=log,stderr=subprocess.STDOUT)
|
||||
@@ -35,7 +36,7 @@ class NativeLockTest(unittest.TestCase):
|
||||
sockets=[p for p in runtime.glob('wayland-*') if not p.name.endswith('.lock')]
|
||||
if sockets:break
|
||||
time.sleep(.1)
|
||||
childenv=dict(env,WAYLAND_DISPLAY=str(sockets[0]),FERESE_LOCK_READY='1')
|
||||
childenv=dict(env,WAYLAND_DISPLAY=str(sockets[0]),FERESE_LOCK_READY='1',WAYLAND_DEBUG='1')
|
||||
def capture(destination, timeout=10):
|
||||
with open(destination,'wb') as png:
|
||||
return subprocess.run([str(ctl),'screenshot'],env=ipc_environment(childenv),stdout=png,stderr=subprocess.PIPE,timeout=timeout)
|
||||
@@ -44,8 +45,9 @@ class NativeLockTest(unittest.TestCase):
|
||||
assert result.returncode == 0, f'Capture failed while unlocked: {result.stderr.decode()}'
|
||||
assert (root/'unlocked.png').stat().st_size > 0, 'Capture produced an empty file'
|
||||
print('Native capture succeeded while unlocked',flush=True)
|
||||
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||
locker=subprocess.Popen([str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
||||
self.assertRegex((root/'locker.log').read_text(), r'ext_session_lock_v1[@#]\d+\.locked\(')
|
||||
print('Native locker received compositor confirmation',flush=True)
|
||||
result=capture(root/'locked.png')
|
||||
assert result.returncode != 0, 'Capture unexpectedly succeeded during lock'
|
||||
@@ -55,7 +57,7 @@ class NativeLockTest(unittest.TestCase):
|
||||
result=capture(root/'after-crash.png')
|
||||
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker crash'
|
||||
print('Capture remains denied after locker crash',flush=True)
|
||||
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||
locker=subprocess.Popen([str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
||||
result=capture(root/'replacement.png')
|
||||
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker takeover'
|
||||
|
||||
@@ -174,7 +174,9 @@ class ShellMotionTest(unittest.TestCase):
|
||||
surface = wait_for(lambda: layer_surface("ferese-shell-notifications"))
|
||||
time.sleep(1.5)
|
||||
opening_frames = frames(surface)
|
||||
self.assertGreater(opening_frames, 10, log_path.read_text()[-8000:])
|
||||
# Require multiple animation frames, without imposing a
|
||||
# hardware-dependent frame rate on software-rendered CI.
|
||||
self.assertGreaterEqual(opening_frames, 4, log_path.read_text()[-8000:])
|
||||
time.sleep(1)
|
||||
for attempt in range(3):
|
||||
trace_start = len(log_path.read_text())
|
||||
@@ -191,7 +193,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
material_updates = log_path.read_text().count('.set_presentation(')
|
||||
dbus("CloseNotification", notice)
|
||||
wait_for(lambda: destroyed(surface), timeout=5)
|
||||
self.assertGreater(log_path.read_text().count('.set_presentation(') - material_updates, 5,
|
||||
self.assertGreater(log_path.read_text().count('.set_presentation(') - material_updates, 1,
|
||||
"notification materials did not follow their fade")
|
||||
print(f"Toast: {opening_frames} opening callback requests, {idle_frames / 2:g} settled commits/s; destroyed", flush=True)
|
||||
|
||||
@@ -199,9 +201,10 @@ class ShellMotionTest(unittest.TestCase):
|
||||
modal = wait_for(lambda: layer_surface("ferese-system-modal"))
|
||||
wait_for(lambda: frames(modal) >= 4)
|
||||
time.sleep(.3)
|
||||
before_close = frames(modal)
|
||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=ipc_environment(env), check=True, capture_output=True, timeout=5)
|
||||
wait_for(lambda: destroyed(modal), timeout=5)
|
||||
self.assertGreater(frames(modal), 10, "modal close did not follow frames")
|
||||
self.assertGreater(frames(modal), before_close, "modal close did not follow frames")
|
||||
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
||||
self.assertNotIn("panicked at", log_path.read_text())
|
||||
print(f"Modal: {frames(modal)} callbacks; interrupted opening closed and destroyed", flush=True)
|
||||
|
||||
Reference in new issue
Block a user