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:
Samuel Onoja authored and GitHub committed 2026-10-11 12:15:28 +01:00
1 parent a447136b01
commit 91e2f227d2
18 files changed
+536 -155

No files matched your search

+10 -8
View File
@@ -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'
+6 -3
View File
@@ -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)