Isolate nested IPC sockets and define session ownership policy
This commit is contained in:
1 parent
97026a0db7
commit
ce024d4794
42 files changed
+635
-147
No files matched your search
@@ -0,0 +1,17 @@
|
||||
"""Find the compositor endpoint in an isolated test runtime."""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ipc_socket(runtime):
|
||||
runtime = Path(runtime)
|
||||
default = runtime / "ferese/control.sock"
|
||||
instances = sorted(path for path in runtime.glob("ferese/instances/*/control.sock") if path.is_socket())
|
||||
if default.is_socket():
|
||||
instances.append(default)
|
||||
if len(instances) > 1:
|
||||
raise RuntimeError("Isolated test runtime contains multiple compositor instances")
|
||||
return instances[0] if instances else default
|
||||
|
||||
|
||||
def ipc_environment(environment):
|
||||
return dict(environment, FERESE_SOCKET=str(ipc_socket(environment["XDG_RUNTIME_DIR"])))
|
||||
@@ -6,6 +6,7 @@ Requires a Wayland host, wayland-scanner and a C compiler.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
@@ -56,7 +57,7 @@ class BlurredPopupTest(unittest.TestCase):
|
||||
with log_path.open("w") as log:
|
||||
compositor = subprocess.Popen(
|
||||
[str(binary), "--backend", "nested", "--grant-effects", "--", str(client)],
|
||||
env=env, stdout=log, stderr=log,
|
||||
env=ipc_environment(env), stdout=log, stderr=log,
|
||||
)
|
||||
try:
|
||||
deadline = time.monotonic() + 30
|
||||
@@ -66,7 +67,7 @@ class BlurredPopupTest(unittest.TestCase):
|
||||
self.fail("Blurred popup stopped presenting frames:\n" + log_path.read_text())
|
||||
time.sleep(.025)
|
||||
control = repo / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
||||
subprocess.run([str(control), "-j", "outputs"], env=env, check=True,
|
||||
subprocess.run([str(control), "-j", "outputs"], env=ipc_environment(env), check=True,
|
||||
stdout=subprocess.DEVNULL, timeout=5)
|
||||
phases = log_path.read_text().splitlines()
|
||||
for name, count in [("parent", 1), ("open", 3), ("update", 9), ("close", 3)]:
|
||||
|
||||
@@ -7,6 +7,7 @@ the compositor and FERESE_TEST_CTL at feresectl when they are not siblings.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
@@ -50,7 +51,7 @@ class FloatingSizeTest(unittest.TestCase):
|
||||
self.fail(f"missing {ctl}; build it with cargo build -p feresectl")
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
process = subprocess.Popen([str(binary), "--backend", "nested", "--", str(root / "client")],
|
||||
env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
env=ipc_environment(env), stdout=log, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
for _ in range(100):
|
||||
if process.poll() is not None:
|
||||
@@ -62,7 +63,7 @@ class FloatingSizeTest(unittest.TestCase):
|
||||
self.assertTrue(sockets, "nested Wayland socket did not appear")
|
||||
childenv = dict(env, WAYLAND_DISPLAY=str(sockets[0]))
|
||||
time.sleep(2)
|
||||
listed = subprocess.run([str(ctl), "-j", "outputs"], env=childenv,
|
||||
listed = subprocess.run([str(ctl), "-j", "outputs"], env=ipc_environment(childenv),
|
||||
check=True, capture_output=True, timeout=10, text=True)
|
||||
enabled = [o for o in json.loads(listed.stdout) if o.get("enabled")]
|
||||
if not enabled:
|
||||
@@ -74,7 +75,7 @@ class FloatingSizeTest(unittest.TestCase):
|
||||
screenshot = root / "frame.png"
|
||||
with screenshot.open("wb") as png:
|
||||
captured = subprocess.run([str(ctl), "screenshot", "-g", geometry],
|
||||
env=childenv, stdout=png, stderr=subprocess.PIPE, timeout=15)
|
||||
env=ipc_environment(childenv), stdout=png, stderr=subprocess.PIPE, timeout=15)
|
||||
if captured.returncode != 0:
|
||||
self.fail(f"capture of {geometry} failed: {captured.stderr.decode()}")
|
||||
if screenshot.stat().st_size == 0:
|
||||
|
||||
@@ -6,6 +6,7 @@ Requires built debug ferese/feresectl, cc, wayland-scanner and a Wayland host.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -63,12 +64,12 @@ class FloatingPlacementTest(unittest.TestCase):
|
||||
children = []
|
||||
|
||||
def launch(command, **kwargs):
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
def command(*args):
|
||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
try:
|
||||
@@ -76,7 +77,7 @@ class FloatingPlacementTest(unittest.TestCase):
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||
deadline = time.monotonic() + 10
|
||||
while not (runtime / "ferese/control.sock").is_socket():
|
||||
while not (ipc_socket(runtime)).is_socket():
|
||||
if compositor.poll() is not None or time.monotonic() > deadline:
|
||||
self.fail((root / "compositor.log").read_text())
|
||||
time.sleep(.025)
|
||||
|
||||
@@ -6,6 +6,7 @@ Requires built debug ferese/feresectl, foot, dbus-daemon, and a Wayland host.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -34,13 +35,13 @@ class FocusHistoryTest(unittest.TestCase):
|
||||
children = []
|
||||
|
||||
def launch(command, **kwargs):
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
def command(*args):
|
||||
return json.loads(subprocess.check_output(
|
||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5
|
||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5
|
||||
))
|
||||
|
||||
def wait_for(predicate):
|
||||
@@ -62,7 +63,7 @@ class FocusHistoryTest(unittest.TestCase):
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||
launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||
command("focus-last-window")
|
||||
self.assertIsNone(focused())
|
||||
env["WAYLAND_DISPLAY"] = str(next(
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -99,7 +100,7 @@ def private_checks():
|
||||
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
try:
|
||||
compositor = subprocess.Popen([str(BINARY), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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):
|
||||
@@ -112,11 +113,11 @@ def private_checks():
|
||||
time.sleep(0.02)
|
||||
raise AssertionError(message + "\n" + (root / "compositor.log").read_text())
|
||||
|
||||
wait(lambda: (runtime / "ferese/control.sock").exists(), "compositor startup")
|
||||
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=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(CTL), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
def inhibited(expected):
|
||||
def matches():
|
||||
@@ -126,7 +127,7 @@ def private_checks():
|
||||
wait(matches, f"idle inhibition != {expected}")
|
||||
|
||||
wait(lambda: len(call("get-idle-inhibition")["players"]) == 1, "MPRIS discovery")
|
||||
client = subprocess.Popen([str(root / "client")], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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")
|
||||
@@ -153,7 +154,7 @@ def private_checks():
|
||||
inhibited(False)
|
||||
player.set("Playing")
|
||||
inhibited(True)
|
||||
ambiguous = subprocess.Popen([str(root / "client"), "cover"], env=env,
|
||||
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")
|
||||
|
||||
@@ -6,6 +6,7 @@ Uses a nested compositor and mock logind; never suspends or ends the host sessio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
@@ -141,13 +142,13 @@ def private_test():
|
||||
env.pop("WAYLAND_SOCKET", None)
|
||||
log = (root / "test.log").open("w")
|
||||
try:
|
||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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 (runtime / "ferese/control.sock").exists():
|
||||
while not (ipc_socket(runtime)).exists():
|
||||
assert compositor.poll() is None and time.monotonic() < deadline, (root / "test.log").read_text()
|
||||
time.sleep(0.02)
|
||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
processes.append(backend)
|
||||
deadline = time.monotonic() + 10
|
||||
while True:
|
||||
@@ -164,7 +165,7 @@ def private_test():
|
||||
except GLib.Error as error:
|
||||
assert "AccessDenied" in str(error)
|
||||
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
||||
admin = IPC(runtime / "ferese/control.sock")
|
||||
admin = IPC(ipc_socket(runtime))
|
||||
def inhibit(path, flags):
|
||||
call(INTERFACE, "Inhibit", GLib.Variant("(ossua{sv})", (path, "test.media", "", flags, {"reason": GLib.Variant("s", "Playing media")})))
|
||||
inhibit(request, 8)
|
||||
@@ -180,7 +181,7 @@ def private_test():
|
||||
assert call(INTERFACE, "CreateMonitor", GLib.Variant("(ooss)", (PATH + "/request/test/monitor", session, "test.media", "")), "(u)") == (0,)
|
||||
wait_for(lambda: any(value["session-state"] == 1 for value in changes), "Missing initial Running")
|
||||
assert call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.Session", "version")), "(v)", session) == (1,)
|
||||
query = IPC(runtime / "ferese/control.sock")
|
||||
query = IPC(ipc_socket(runtime))
|
||||
state = query.call("begin-session-end")
|
||||
assert state["query-ready"] is False
|
||||
wait_for(lambda: any(value["session-state"] == 2 for value in changes), "Missing QueryEnd")
|
||||
|
||||
@@ -5,6 +5,7 @@ Requires built binaries and bwrap. FERESE_TEST_BIN_DIR selects debug/release. Ho
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
|
||||
@unittest.skipUnless(os.environ.get("FERESE_TEST_LOCK") == "1", "visible nested lock test is opt-in")
|
||||
class NativeLockTest(unittest.TestCase):
|
||||
@@ -23,7 +24,7 @@ class NativeLockTest(unittest.TestCase):
|
||||
env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1')
|
||||
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=env,stdout=log,stderr=subprocess.STDOUT)
|
||||
compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=ipc_environment(env),stdout=log,stderr=subprocess.STDOUT)
|
||||
ctl=binary_dir / 'feresectl'
|
||||
assert ctl.is_file(), f'missing {ctl}; build it with cargo build --release -p feresectl'
|
||||
locker=None
|
||||
@@ -37,13 +38,13 @@ class NativeLockTest(unittest.TestCase):
|
||||
childenv=dict(env,WAYLAND_DISPLAY=str(sockets[0]),FERESE_LOCK_READY='1')
|
||||
def capture(destination, timeout=10):
|
||||
with open(destination,'wb') as png:
|
||||
return subprocess.run([str(ctl),'screenshot'],env=childenv,stdout=png,stderr=subprocess.PIPE,timeout=timeout)
|
||||
return subprocess.run([str(ctl),'screenshot'],env=ipc_environment(childenv),stdout=png,stderr=subprocess.PIPE,timeout=timeout)
|
||||
time.sleep(.6)
|
||||
result=capture(root/'unlocked.png')
|
||||
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=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=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)
|
||||
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
||||
print('Native locker received compositor confirmation',flush=True)
|
||||
result=capture(root/'locked.png')
|
||||
@@ -54,7 +55,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=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=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)
|
||||
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'
|
||||
|
||||
@@ -5,6 +5,7 @@ Requires built debug compositor binaries, a Wayland host and dbus-daemon.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -34,17 +35,17 @@ class MaterialLifetime(unittest.TestCase):
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
try:
|
||||
bus = subprocess.Popen(["dbus-daemon", "--session", "--nofork", "--print-address=1"],
|
||||
env=env, stdout=subprocess.PIPE, text=True, start_new_session=True)
|
||||
env=ipc_environment(env), stdout=subprocess.PIPE, text=True, start_new_session=True)
|
||||
children.append(bus)
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||
compositor = subprocess.Popen([str(ROOT / "target/debug/ferese"), "--backend", "nested"],
|
||||
env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
children.append(compositor)
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
self.assertIsNone(compositor.poll(), (root / "compositor.log").read_text())
|
||||
sockets = [p for p in runtime.glob("wayland-*") if p.is_socket()]
|
||||
if sockets and (runtime / "ferese/control.sock").is_socket():
|
||||
if sockets and (ipc_socket(runtime)).is_socket():
|
||||
break
|
||||
time.sleep(.05)
|
||||
else:
|
||||
@@ -54,7 +55,7 @@ class MaterialLifetime(unittest.TestCase):
|
||||
"cargo", "test", "--workspace", "--locked",
|
||||
"material::tests::attachment_acknowledgement_and_last_clone_release",
|
||||
"--", "--ignored", "--exact",
|
||||
], cwd=ROOT, env=env, capture_output=True, text=True, timeout=120)
|
||||
], cwd=ROOT, env=ipc_environment(env), capture_output=True, text=True, timeout=120)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("1 passed", result.stdout)
|
||||
self.assertIsNone(compositor.poll())
|
||||
|
||||
@@ -7,6 +7,7 @@ Requires a Wayland host, PyGObject and dbus-run-session.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
@@ -135,7 +136,7 @@ def private_checks():
|
||||
binary = REPO / os.environ.get('FERESE_TEST_BINARY', 'target/debug/ferese')
|
||||
ctl = REPO / os.environ.get('FERESE_TEST_CTL', 'target/debug/feresectl')
|
||||
with (root / 'compositor.log').open('w') as log:
|
||||
child = subprocess.Popen([str(binary), '--backend=nested'], env=env,
|
||||
child = subprocess.Popen([str(binary), '--backend=nested'], env=ipc_environment(env),
|
||||
stdout=log, stderr=log, start_new_session=True)
|
||||
|
||||
def wait(check):
|
||||
@@ -148,17 +149,17 @@ def private_checks():
|
||||
time.sleep(.02)
|
||||
raise AssertionError('Timed out\n' + (root / 'compositor.log').read_text())
|
||||
|
||||
wait(lambda: (runtime / 'ferese/control.sock').exists())
|
||||
wait(lambda: (ipc_socket(runtime)).exists())
|
||||
|
||||
def call(*args):
|
||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
def request(command, args):
|
||||
payload = json.dumps({'version': 1, 'id': 17, 'type': 'command',
|
||||
'command': command, 'args': args}).encode()
|
||||
connection = socket.socket(socket.AF_UNIX)
|
||||
connection.settimeout(3)
|
||||
connection.connect(str(runtime / 'ferese/control.sock'))
|
||||
connection.connect(str(ipc_socket(runtime)))
|
||||
connection.sendall(struct.pack('!I', len(payload)) + payload)
|
||||
return connection
|
||||
|
||||
@@ -181,7 +182,7 @@ def private_checks():
|
||||
env['WAYLAND_DISPLAY'] = str(next(path for path in runtime.glob('wayland-*') if not path.name.endswith('.lock')))
|
||||
shell_binary = os.environ.get('FERESE_TEST_SHELL')
|
||||
if shell_binary:
|
||||
shell = subprocess.Popen([str(REPO / shell_binary)], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
shell = subprocess.Popen([str(REPO / shell_binary)], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
first = players[0]
|
||||
selected(first.name)
|
||||
initial = call('media')
|
||||
|
||||
@@ -10,6 +10,7 @@ import errno
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -46,12 +47,12 @@ class OverviewDamageTest(unittest.TestCase):
|
||||
log_path = root / "compositor.log"
|
||||
|
||||
def launch(command, **kwargs):
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
def command(*args):
|
||||
return json.loads(subprocess.check_output([str(control), "-j", *args], env=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(control), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
def wait_for(predicate, seconds=12):
|
||||
deadline = time.monotonic() + seconds
|
||||
@@ -98,7 +99,7 @@ class OverviewDamageTest(unittest.TestCase):
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||
launch([str(binary), "--backend", "nested"], stdout=log, stderr=log)
|
||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||
env["WAYLAND_DISPLAY"] = str(next(
|
||||
path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")
|
||||
))
|
||||
|
||||
@@ -8,6 +8,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -55,7 +56,7 @@ class OverviewPreviews(unittest.TestCase):
|
||||
log_path = root / "compositor.log"
|
||||
|
||||
def call(*args):
|
||||
return subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=10)
|
||||
return subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||
|
||||
def wait_for(predicate):
|
||||
deadline = time.monotonic() + 10
|
||||
@@ -69,12 +70,12 @@ class OverviewPreviews(unittest.TestCase):
|
||||
|
||||
with log_path.open("w") as log:
|
||||
def launch(command):
|
||||
child = subprocess.Popen(command, env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
children.append(child)
|
||||
|
||||
try:
|
||||
launch([str(binary), "--backend", "nested"])
|
||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if path.is_socket()))
|
||||
launch([str(root / "client")])
|
||||
wait_for(lambda: len(json.loads(call("get-windows"))) == 1)
|
||||
|
||||
@@ -6,6 +6,7 @@ Requires built debug compositor/control binaries, a Wayland host, cc and wayland
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -57,10 +58,10 @@ class Presentation(unittest.TestCase):
|
||||
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=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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 (runtime / "ferese/control.sock").exists():
|
||||
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)
|
||||
@@ -68,7 +69,7 @@ class Presentation(unittest.TestCase):
|
||||
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=env, timeout=10)
|
||||
return subprocess.check_output([str(REPO / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||
|
||||
def windows(count):
|
||||
deadline = time.monotonic() + 10
|
||||
@@ -80,7 +81,7 @@ class Presentation(unittest.TestCase):
|
||||
self.fail((root / "compositor.log").read_text())
|
||||
|
||||
events = (root / "events").open("w")
|
||||
target = subprocess.Popen([str(root / "client")], env=env, stdin=subprocess.PIPE,
|
||||
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)
|
||||
@@ -95,7 +96,7 @@ class Presentation(unittest.TestCase):
|
||||
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=env, stdout=log,
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@ before the 300 ms deadline; it does not measure frame pacing or GPU performance.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import select
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -50,10 +51,10 @@ class ResizeDependencies(unittest.TestCase):
|
||||
ctl = REPO / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
try:
|
||||
compositor = subprocess.Popen([str(binary), "--backend=nested"], env=env, stdout=log, stderr=log)
|
||||
compositor = subprocess.Popen([str(binary), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log)
|
||||
processes.append(compositor)
|
||||
deadline = time.monotonic() + 10
|
||||
while not (runtime / "ferese/control.sock").exists():
|
||||
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(.02)
|
||||
@@ -61,10 +62,10 @@ class ResizeDependencies(unittest.TestCase):
|
||||
client_env = dict(env, WAYLAND_DISPLAY=str(socket))
|
||||
|
||||
def call(*args):
|
||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
def action(*args):
|
||||
subprocess.run([str(ctl), *args], env=env, check=True, capture_output=True, timeout=5)
|
||||
subprocess.run([str(ctl), *args], env=ipc_environment(env), check=True, capture_output=True, timeout=5)
|
||||
|
||||
def client(name):
|
||||
process = subprocess.Popen([str(root / "client"), name], env=client_env,
|
||||
|
||||
@@ -7,6 +7,7 @@ No host permission store, compositor configuration or portal service is changed.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
@@ -94,10 +95,10 @@ def private_test():
|
||||
env.pop("WAYLAND_SOCKET", None)
|
||||
log = (root / "test.log").open("w")
|
||||
try:
|
||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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)
|
||||
destination = runtime / "ferese/control.sock"
|
||||
wait_for(lambda: destination.exists(), "Nested compositor did not start")
|
||||
wait_for(lambda: ipc_socket(runtime).is_socket(), "Nested compositor did not start")
|
||||
destination = ipc_socket(runtime)
|
||||
admin = IPC(destination)
|
||||
outputs = admin.call("get-outputs")
|
||||
name = next(output["name"] for output in outputs if output["enabled"])
|
||||
@@ -122,7 +123,7 @@ def private_test():
|
||||
threading.Thread(target=accept, daemon=True).start()
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
processes.append(backend)
|
||||
def call(method, params=None, path=PATH, interface=INTERFACE):
|
||||
return bus.call_sync(NAME, path, interface, method, params, None, Gio.DBusCallFlags.NO_AUTO_START, 12000, None).unpack()
|
||||
@@ -180,7 +181,7 @@ def private_test():
|
||||
fallback("replacement", saved=stale)
|
||||
print("PASS: other-app and changed-device restore require fresh consent", flush=True)
|
||||
|
||||
mismatch = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream", name, "hidden", str(2**32 - 1)], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
|
||||
mismatch = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream", name, "hidden", str(2**32 - 1)], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
|
||||
processes.append(mismatch)
|
||||
mismatch.wait(timeout=8)
|
||||
error = mismatch.stderr.read()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Check IPC discovery for isolated compositor drivers."""
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
|
||||
|
||||
class SessionSocketTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.directory.cleanup)
|
||||
self.runtime = Path(self.directory.name)
|
||||
# Only path discovery is under test; endpoint permissions and actual
|
||||
# socket binding are covered by the compositor's IPC tests.
|
||||
self.socket_type = patch.object(Path, 'is_socket', Path.is_file)
|
||||
self.socket_type.start()
|
||||
self.addCleanup(self.socket_type.stop)
|
||||
|
||||
def endpoint(self, suffix):
|
||||
path = self.runtime / suffix
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch()
|
||||
return path
|
||||
|
||||
def test_discovery_changes_when_the_nested_listener_appears(self):
|
||||
self.assertEqual(ipc_socket(self.runtime), self.runtime / 'ferese/control.sock')
|
||||
nested = self.endpoint('ferese/instances/123-abc/control.sock')
|
||||
self.assertEqual(ipc_socket(self.runtime), nested)
|
||||
|
||||
def test_client_environment_replaces_an_inherited_host_target(self):
|
||||
nested = self.endpoint('ferese/instances/123-abc/control.sock')
|
||||
environment = {'XDG_RUNTIME_DIR': str(self.runtime), 'FERESE_SOCKET': '/run/user/1000/ferese/control.sock'}
|
||||
self.assertEqual(ipc_environment(environment)['FERESE_SOCKET'], str(nested))
|
||||
self.assertEqual(environment['FERESE_SOCKET'], '/run/user/1000/ferese/control.sock')
|
||||
|
||||
def test_proxy_or_drm_endpoint_remains_supported(self):
|
||||
default = self.endpoint('ferese/control.sock')
|
||||
self.assertEqual(ipc_socket(self.runtime), default)
|
||||
|
||||
def test_multiple_instances_require_an_explicit_test_target(self):
|
||||
self.endpoint('ferese/instances/123-abc/control.sock')
|
||||
self.endpoint('ferese/instances/456-def/control.sock')
|
||||
with self.assertRaises(RuntimeError):
|
||||
ipc_socket(self.runtime)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -32,6 +32,7 @@ class SessionStartupTest(unittest.TestCase):
|
||||
path.write_text('#!/bin/bash\n' + body)
|
||||
path.chmod(0o755)
|
||||
env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory,
|
||||
FERESE_SESSION_MODE='desktop',
|
||||
FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display',
|
||||
FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77',
|
||||
TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate)))
|
||||
@@ -71,6 +72,11 @@ class SessionStartupTest(unittest.TestCase):
|
||||
'--user try-restart xdg-desktop-portal-ferese.service', 'shell:77',
|
||||
'--user stop ferese-session.target'])
|
||||
|
||||
def test_direct_session_imports_its_control_socket(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_SOCKET': '/run/user/1000/ferese/control.sock'})
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn('FERESE_SOCKET=/run/user/1000/ferese/control.sock', self.imported_vars(events))
|
||||
|
||||
def test_a_managed_x11_endpoint_is_imported_for_dbus_activators(self):
|
||||
status, events = self.run_session(display=':7', xauthority='/run/user/1000/ferese-xauth-abcd')
|
||||
self.assertEqual(status, 0)
|
||||
@@ -105,6 +111,11 @@ class SessionStartupTest(unittest.TestCase):
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(events, ['shell:77'])
|
||||
|
||||
def test_embedded_policy_blocks_session_services_despite_an_inherited_import_flag(self):
|
||||
status, events = self.run_session(direct=True, extra_env={'FERESE_SESSION_MODE': 'embedded'})
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(events, ['shell:77'])
|
||||
|
||||
def test_failed_target_does_not_prevent_shell_or_stop_unowned_target(self):
|
||||
status, events = self.run_session(fail_target=True)
|
||||
self.assertEqual(status, 0)
|
||||
|
||||
@@ -7,6 +7,7 @@ Protocol traces contain only this test's shell traffic, never the host desktop.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -53,7 +54,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
'<allow receive_sender="*"/><allow own="*"/></policy></busconfig>')
|
||||
|
||||
def launch(command, **kwargs):
|
||||
process = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
process = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(process)
|
||||
return process
|
||||
|
||||
@@ -73,7 +74,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
return subprocess.run(["gdbus", "call", "--session", "--dest", "org.freedesktop.Notifications",
|
||||
"--object-path", "/org/freedesktop/Notifications", "--method",
|
||||
"org.freedesktop.Notifications." + method, *args],
|
||||
env=env, text=True, capture_output=True, timeout=5, check=check)
|
||||
env=ipc_environment(env), text=True, capture_output=True, timeout=5, check=check)
|
||||
|
||||
def layer_surface(namespace):
|
||||
matches = list(re.finditer(r'get_layer_surface\([^\n]*?wl_surface[@#](\d+)[^\n]*"' + namespace + '"', log_path.read_text()))
|
||||
@@ -132,7 +133,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
compositor = launch([str(binary), "--backend", "nested", "--grant-effects", "--grant-shell-control",
|
||||
"--", "env", "WAYLAND_DEBUG=client", "ICED_BACKEND=tiny-skia", str(shell)],
|
||||
stdout=log, stderr=log)
|
||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||
wait_for(lambda: dbus("GetServerInformation", check=False).returncode == 0)
|
||||
bar = wait_for(lambda: layer_surface("ferese-shell-top-bar"))
|
||||
if bar_layout == "islands":
|
||||
@@ -165,11 +166,11 @@ class ShellMotionTest(unittest.TestCase):
|
||||
"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)
|
||||
|
||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=env, check=True, capture_output=True)
|
||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=ipc_environment(env), check=True, capture_output=True)
|
||||
modal = wait_for(lambda: layer_surface("ferese-system-modal"))
|
||||
wait_for(lambda: frames(modal) >= 4)
|
||||
time.sleep(.3)
|
||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=env, check=True, capture_output=True)
|
||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=ipc_environment(env), check=True, capture_output=True)
|
||||
wait_for(lambda: destroyed(modal), timeout=5)
|
||||
self.assertGreater(frames(modal), 10, "modal close did not follow frames")
|
||||
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
||||
@@ -181,7 +182,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
source = config_file.read_text()
|
||||
config_file.write_text(re.sub(
|
||||
r'bar-layout "[^"]+"', f'bar-layout "{layout_name}"', source))
|
||||
subprocess.run([str(ctl), "reload-config"], env=env, check=True, capture_output=True)
|
||||
subprocess.run([str(ctl), "reload-config"], env=ipc_environment(env), check=True, capture_output=True)
|
||||
wait_for(lambda: bar_region_count(bar) == (expected_count or 2))
|
||||
|
||||
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
||||
@@ -192,7 +193,7 @@ class ShellMotionTest(unittest.TestCase):
|
||||
source = config_file.read_text()
|
||||
config_file.write_text(re.sub(
|
||||
r'bar-island-padding \d+', f'bar-island-padding {padding}', source))
|
||||
subprocess.run([str(ctl), "reload-config"], env=env, check=True, capture_output=True)
|
||||
subprocess.run([str(ctl), "reload-config"], env=ipc_environment(env), check=True, capture_output=True)
|
||||
expected_width = original_input[0][2] - 2 * (12 - padding)
|
||||
wait_for(lambda: len(bar_input_rectangles(bar)) == 2
|
||||
and bar_input_rectangles(bar)[0][2] == expected_width)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
@@ -31,12 +32,12 @@ class ShortcutConnections(unittest.TestCase):
|
||||
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), WAYLAND_DISPLAY=str(host_display))
|
||||
env.pop("WAYLAND_SOCKET", None)
|
||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||
process = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env,
|
||||
process = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env),
|
||||
stdout=log, stderr=log, start_new_session=True)
|
||||
connections = []
|
||||
try:
|
||||
deadline = time.monotonic() + 15
|
||||
while not (runtime / "ferese/control.sock").exists():
|
||||
while not (ipc_socket(runtime)).exists():
|
||||
if process.poll() is not None or time.monotonic() >= deadline:
|
||||
self.fail((root / "compositor.log").read_text())
|
||||
time.sleep(0.02)
|
||||
@@ -44,7 +45,7 @@ class ShortcutConnections(unittest.TestCase):
|
||||
def connect():
|
||||
connection = socket.socket(socket.AF_UNIX)
|
||||
connection.settimeout(2)
|
||||
connection.connect(str(runtime / "ferese/control.sock"))
|
||||
connection.connect(str(ipc_socket(runtime)))
|
||||
connections.append(connection)
|
||||
return connection
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Requires target/debug/ferese, feresectl, and a running Wayland desktop.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import select
|
||||
import signal
|
||||
import shutil
|
||||
@@ -38,12 +39,12 @@ class ThemeTest(unittest.TestCase):
|
||||
XDG_CONFIG_HOME=str(root / "config"))
|
||||
|
||||
def launch(command, **kwargs):
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
def command(*args):
|
||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5))
|
||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||
|
||||
def until(predicate, seconds=5):
|
||||
deadline = time.monotonic() + seconds
|
||||
@@ -67,7 +68,7 @@ class ThemeTest(unittest.TestCase):
|
||||
bus = launch(["dbus-daemon", "--session", "--nofork", "--print-address=1"], stdout=subprocess.PIPE, text=True)
|
||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||
until(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||
until(lambda: (ipc_socket(runtime)).is_socket())
|
||||
self.assertEqual(command("theme", "get")["theme"]["appearance"], "dark")
|
||||
subscriber = launch([str(repo / "target/debug/feresectl"), "theme", "subscribe"], stdout=subprocess.PIPE, text=True)
|
||||
self.assertTrue(select.select([subscriber.stdout], [], [], 3)[0])
|
||||
@@ -116,10 +117,10 @@ class ThemeTest(unittest.TestCase):
|
||||
command("theme", "mode", "light")
|
||||
until(lambda: command("theme", "get")["theme"]["tokens"]["colors"]["surface_base"] == "#FBF1C7")
|
||||
if shutil.which("gsettings"):
|
||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-dark"], env=env, check=True, timeout=5)
|
||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-dark"], env=ipc_environment(env), check=True, timeout=5)
|
||||
replace(source, 'theme { mode "auto"; family "everforest"; schedule { source "system"; }; }\n')
|
||||
until(lambda: (snapshot := command("theme", "get"))["mode"] == "auto" and snapshot["theme"]["appearance"] == "dark" and snapshot["fallback_note"] is None)
|
||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-light"], env=env, check=True, timeout=5)
|
||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-light"], env=ipc_environment(env), check=True, timeout=5)
|
||||
until(lambda: (snapshot := command("theme", "get"))["theme"]["appearance"] == "light" and snapshot["presented"] == snapshot["theme"])
|
||||
self.assertIsNone(compositor.poll())
|
||||
finally:
|
||||
|
||||
@@ -7,6 +7,7 @@ Requires cc, pkg-config, wayland-scanner, and wayland-protocols.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import select
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -62,7 +63,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
||||
with (root / "compositor.log").open("w") as log:
|
||||
compositor = subprocess.Popen(
|
||||
[str(compositor_binary), "--backend", "nested"],
|
||||
env=env,
|
||||
env=ipc_environment(env),
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
@@ -101,7 +102,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
||||
while True:
|
||||
result = subprocess.run(
|
||||
[str(ctl_binary), "-j", "focused-window"],
|
||||
env=env,
|
||||
env=ipc_environment(env),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
@@ -116,7 +117,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
||||
visible_ids.append(focused["id"])
|
||||
if phase == "visible":
|
||||
# Leave a configure/resize outstanding when the buffer is detached.
|
||||
subprocess.run([str(ctl_binary), "toggle-maximized"], env=env,
|
||||
subprocess.run([str(ctl_binary), "toggle-maximized"], env=ipc_environment(env),
|
||||
capture_output=True, check=True)
|
||||
time.sleep(.08)
|
||||
if phase == "detached":
|
||||
@@ -131,7 +132,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
||||
deadline = time.monotonic() + 3
|
||||
while True:
|
||||
remaining = json.loads(subprocess.check_output(
|
||||
[str(ctl_binary), "-j", "windows"], env=env, text=True
|
||||
[str(ctl_binary), "-j", "windows"], env=ipc_environment(env), text=True
|
||||
))
|
||||
if not remaining:
|
||||
break
|
||||
@@ -139,10 +140,10 @@ class UnmappedToplevelTest(unittest.TestCase):
|
||||
self.fail(f"destroyed window still managed: {remaining}")
|
||||
time.sleep(0.05)
|
||||
|
||||
subprocess.run([str(ctl_binary), "focus-last-window"], env=env,
|
||||
subprocess.run([str(ctl_binary), "focus-last-window"], env=ipc_environment(env),
|
||||
capture_output=True, check=True)
|
||||
focused = json.loads(subprocess.check_output(
|
||||
[str(ctl_binary), "-j", "focused-window"], env=env, text=True
|
||||
[str(ctl_binary), "-j", "focused-window"], env=ipc_environment(env), text=True
|
||||
))
|
||||
self.assertIsNone(focused, "focus history must not restore a destroyed window")
|
||||
deadline = time.monotonic() + 3
|
||||
|
||||
@@ -9,6 +9,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import selectors
|
||||
import subprocess
|
||||
@@ -58,10 +59,10 @@ class WindowCapture(unittest.TestCase):
|
||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||
log = (root / "compositor.log").open("w")
|
||||
try:
|
||||
compositor = subprocess.Popen([str(COMPOSITOR), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
compositor = subprocess.Popen([str(COMPOSITOR), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
processes.append(compositor)
|
||||
deadline = time.monotonic() + 10
|
||||
while not (runtime / "ferese/control.sock").exists():
|
||||
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)
|
||||
@@ -69,7 +70,7 @@ class WindowCapture(unittest.TestCase):
|
||||
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
||||
|
||||
def call(*args):
|
||||
return subprocess.check_output([str(CTL), "-j", *args], env=env, timeout=10)
|
||||
return subprocess.check_output([str(CTL), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||
|
||||
def windows(count):
|
||||
deadline = time.monotonic() + 10
|
||||
@@ -80,9 +81,9 @@ class WindowCapture(unittest.TestCase):
|
||||
time.sleep(0.02)
|
||||
self.fail((root / "compositor.log").read_text())
|
||||
|
||||
processes.append(subprocess.Popen([str(root / "client")], env=env, stdout=log, stderr=log, start_new_session=True))
|
||||
processes.append(subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True))
|
||||
target = windows(1)[0]
|
||||
processes.append(subprocess.Popen([str(root / "client"), "cover"], env=env, stdout=log, stderr=log, start_new_session=True))
|
||||
processes.append(subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True))
|
||||
current = windows(2)
|
||||
cover = next(window for window in current if window["title"] == "Occluding window")
|
||||
self.assertEqual((target["x"], target["y"], target["width"], target["height"]),
|
||||
@@ -131,7 +132,7 @@ class WindowCapture(unittest.TestCase):
|
||||
worker.stdin.close()
|
||||
worker.stdout.close()
|
||||
rejected = subprocess.run([str(CTL), "screenshot-window", str(target["id"])],
|
||||
env=env, capture_output=True, timeout=10)
|
||||
env=ipc_environment(env), capture_output=True, timeout=10)
|
||||
self.assertNotEqual(rejected.returncode, 0)
|
||||
self.assertIn(b"protected", rejected.stderr)
|
||||
self.assertTrue(next(window for window in windows(2) if window["id"] == target["id"])["mapped"])
|
||||
|
||||
@@ -7,6 +7,7 @@ Only this test's compositor, windows and streams are stopped.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import select
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -65,21 +66,21 @@ class WindowStream(unittest.TestCase):
|
||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||
log = (root / "test.log").open("w")
|
||||
try:
|
||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
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)
|
||||
socket = runtime / "ferese/control.sock"
|
||||
wait_for(socket.exists, "Nested compositor did not start")
|
||||
wait_for(lambda: ipc_socket(runtime).is_socket(), "Nested compositor did not start")
|
||||
socket = ipc_socket(runtime)
|
||||
admin = IPC(socket)
|
||||
sockets = [path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")]
|
||||
env["WAYLAND_DISPLAY"] = str(sockets[0])
|
||||
target = subprocess.Popen([str(root / "client"), "resize"], env=env, stdin=subprocess.PIPE, stdout=log, stderr=log, start_new_session=True)
|
||||
target = subprocess.Popen([str(root / "client"), "resize"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=log, stderr=log, start_new_session=True)
|
||||
processes.append(target)
|
||||
windows = wait_for(lambda: [window for window in admin.call("get-windows") if window["capture_width"] == 640], "Window fixture did not map; private manager must be hidden from ordinary clients")
|
||||
target_id = windows[0]["id"]
|
||||
cover = subprocess.Popen([str(root / "client"), "cover"], env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
cover = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||
processes.append(cover)
|
||||
wait_for(lambda: len(admin.call("get-windows")) == 2, "Occluding window did not map")
|
||||
helper = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(target_id), "hidden"], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
helper = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(target_id), "hidden"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
processes.append(helper)
|
||||
self.assertTrue(select.select([helper.stdout], [], [], 10)[0], "Stream did not announce a PipeWire node")
|
||||
ready = json.loads(helper.stdout.readline())
|
||||
@@ -126,11 +127,11 @@ class WindowStream(unittest.TestCase):
|
||||
pipeline.set_state(Gst.State.NULL)
|
||||
pipeline = None
|
||||
cover_id = next(window["id"] for window in admin.call("get-windows") if window["title"] == "Occluding window")
|
||||
paused = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(cover_id), "hidden"], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
paused = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(cover_id), "hidden"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
processes.append(paused)
|
||||
self.assertTrue(select.select([paused.stdout], [], [], 10)[0])
|
||||
self.assertGreater(json.loads(paused.stdout.readline())["width"], 0)
|
||||
locker = subprocess.Popen([str(root / "locker")], env=env, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
locker = subprocess.Popen([str(root / "locker")], env=ipc_environment(env), stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||
processes.append(locker)
|
||||
self.assertTrue(select.select([locker.stdout], [], [], 5)[0], "Nested lock request did not complete")
|
||||
self.assertIn(locker.stdout.readline().strip(), ("locked", "requested"))
|
||||
|
||||
@@ -6,6 +6,7 @@ Requires target/debug/ferese, feresectl, foot, dbus-daemon, and a Wayland host.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from session_socket import ipc_environment, ipc_socket
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -35,13 +36,13 @@ class WorkspaceHistoryTest(unittest.TestCase):
|
||||
children = []
|
||||
|
||||
def launch(command, **kwargs):
|
||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
def command(*args):
|
||||
return json.loads(subprocess.check_output(
|
||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5
|
||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5
|
||||
))
|
||||
|
||||
def active():
|
||||
@@ -55,7 +56,7 @@ class WorkspaceHistoryTest(unittest.TestCase):
|
||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"],
|
||||
stdout=log, stderr=log)
|
||||
deadline = time.monotonic() + 10
|
||||
while not (runtime / "ferese/control.sock").is_socket():
|
||||
while not (ipc_socket(runtime)).is_socket():
|
||||
if compositor.poll() is not None or time.monotonic() >= deadline:
|
||||
self.fail((root / "compositor.log").read_text())
|
||||
time.sleep(.025)
|
||||
|
||||
Reference in new issue
Block a user