lazyBoy/tests/runtime-probe-gui.test.py

112 lines
5.1 KiB
Python

"""Real GUI service fault/restart acceptance, inside a disposable Computer container.
Run via scripts/test-runtime-gui.py; never targets an existing Computer.
"""
import json
import os
from pathlib import Path
import signal
import resource
import subprocess
import tempfile
import time
source = Path('/fixture/runtime_probe.py')
def probe(slot):
# Match the production helper's limits and clean environment, so a healthy
# service cannot pass only because the acceptance process has more resources.
def limits():
resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024,) * 2)
resource.setrlimit(resource.RLIMIT_CPU, (20, 20))
resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
result = subprocess.run(['python3', '-I', str(source), json.dumps(slot)],
env={'PATH': '/usr/local/bin:/usr/bin:/bin',
'HOME': '/home/lazyboy', 'LANG': 'C.UTF-8'},
capture_output=True, timeout=8, preexec_fn=limits, check=True)
assert len(result.stdout) <= 4096
return json.loads(result.stdout)
def main():
assert os.getuid() == 1000
processes = {}
with tempfile.TemporaryDirectory(prefix='runtime-gui-') as directory:
root = Path(directory)
env = dict(os.environ, DISPLAY=':8', HOME=directory,
DBUS_SESSION_BUS_ADDRESS=f'unix:path={root / "bus"}')
commands = {
'bus': ['dbus-daemon', '--session', '--nofork', f'--address=unix:path={root / "bus"}'],
'x': ['Xvfb', ':8', '-screen', '0', '1024x768x24', '-ac', '-nolisten', 'tcp'],
'wm': ['xfwm4', '--replace', '--compositor=off'],
'browser': ['chromium', '--no-sandbox', '--disable-dev-shm-usage',
'--no-first-run', '--disable-background-networking',
'--remote-debugging-port=9229', f'--user-data-dir={root / "profile"}', 'about:blank'],
'rfb': ['x11vnc', '-display', ':8', '-forever', '-shared', '-nopw',
'-listen', '127.0.0.1', '-rfbport', '5907'],
'viewer': ['websockify', '--web=/usr/share/novnc', '127.0.0.1:6087', '127.0.0.1:5907'],
}
def start(name):
with (root / f'{name}.log').open('ab') as log:
processes[name] = subprocess.Popen(commands[name], env=env, stdout=log,
stderr=subprocess.STDOUT, start_new_session=True)
def stop(name):
process = processes.pop(name)
try:
os.killpg(process.pid, signal.SIGTERM)
process.wait(timeout=5)
except ProcessLookupError:
pass
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=5)
def expect(label, desktop, browser, viewer):
wanted = dict(version=1, slot=7, runner=True, desktop=desktop, browser=browser, viewer=viewer)
deadline = time.monotonic() + 20
while True:
actual = probe(7)
if actual == wanted:
print(json.dumps({'case': label, 'observation': actual}), flush=True)
return
if time.monotonic() >= deadline:
logs = {p.name: p.read_text(errors='replace')[-5000:] for p in root.glob('*.log')}
raise AssertionError((label, wanted, actual, logs))
time.sleep(.2)
try:
assert probe(None) == dict(version=1, slot=None, runner=True, desktop=None, browser=None, viewer=None)
expect('cold', False, False, False)
start('x')
deadline = time.monotonic() + 10
while subprocess.run(['xdpyinfo', '-display', ':8'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL).returncode:
assert time.monotonic() < deadline, 'Xvfb startup timeout'
time.sleep(.1)
start('bus')
deadline = time.monotonic() + 5
while not (root / 'bus').exists():
assert time.monotonic() < deadline, 'session bus startup timeout'
time.sleep(.1)
for name in ['wm', 'browser', 'rfb', 'viewer']:
start(name)
expect('all-real-services', True, True, True)
for service, expected in [('browser', (True, False, True)),
('viewer', (True, True, False)),
('rfb', (True, True, False)),
('wm', (False, True, True))]:
stop(service)
expect(f'{service}-stopped', *expected)
start(service)
expect(f'{service}-restarted', True, True, True)
assert probe(None)['desktop'] is None, 'unassigned probe must not borrow the live screen'
finally:
for name in list(reversed(processes)):
stop(name)
if __name__ == '__main__':
main()