107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""Linux host regressions: reject incomplete capture manifests and stale readiness."""
|
|
import importlib.util
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
@unittest.skipUnless(sys.platform.startswith('linux'), 'host requires Linux flock')
|
|
class HostLeaseTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
configured_source = os.environ.get('VDI_NATIVE_CURSOR_HOST')
|
|
source = Path(configured_source) if configured_source else (
|
|
Path(__file__).resolve().parents[2] / 'terminal/native-cursor/host.py')
|
|
spec = importlib.util.spec_from_file_location('cursor_host', source)
|
|
cls.host = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(cls.host)
|
|
|
|
def test_display_restart_cannot_reuse_previous_ready_or_receipt(self):
|
|
h = self.host
|
|
lease = 'a' * 32
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
run = Path(directory)
|
|
(run / 'ready').write_text('b' * 32)
|
|
(run / 'receipt').write_text('nvfbc-v1\n')
|
|
calls = []
|
|
|
|
def systemctl(*args):
|
|
calls.append(args)
|
|
if args == ('start', h.GUARD):
|
|
self.assertFalse((run / 'ready').exists())
|
|
self.assertFalse((run / 'receipt').exists())
|
|
self.assertEqual((run / 'lease').read_text(), lease)
|
|
(run / 'ready').write_text(lease)
|
|
return ''
|
|
|
|
cursor = types.SimpleNamespace(size=(2560, 1440), read=lambda: {})
|
|
hello = json.dumps(dict(version=1, lease=lease, size=[2560, 1440]))
|
|
stdin = types.SimpleNamespace(buffer=io.BytesIO((hello + '\nstop\n').encode()))
|
|
stdout = io.StringIO()
|
|
with patch.object(h, 'RUN', run), patch.object(h, 'systemctl', systemctl), \
|
|
patch.object(h, 'XCursor', lambda: cursor), patch.object(h.sys, 'stdin', stdin), \
|
|
patch.object(h.sys, 'stdout', stdout), \
|
|
patch.object(h.select, 'select', lambda *args: ([stdin], [], [])):
|
|
h.stream()
|
|
self.assertEqual(json.loads(stdout.getvalue())['lease'], lease)
|
|
self.assertEqual(calls, [('stop', h.GUARD), ('start', h.GUARD), ('stop', h.GUARD)])
|
|
self.assertFalse((run / 'lease').exists())
|
|
|
|
def test_incomplete_manifest_rejected_before_stopping_capture(self):
|
|
h = self.host
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / 'lease').write_text('a' * 32)
|
|
(root / 'manifest.json').write_text(json.dumps({'sunshine': '0' * 64}))
|
|
with patch.object(h, 'RUN', root), patch.object(h, 'ROOT', root), \
|
|
patch.object(h, 'systemctl') as systemctl:
|
|
with self.assertRaisesRegex(RuntimeError, 'incomplete pinned capture manifest'):
|
|
h.guard()
|
|
systemctl.assert_not_called()
|
|
|
|
def test_software_sprite_guard_stops_capture_before_unhide_on_failure(self):
|
|
h = self.host
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / 'lease').write_text('a' * 32)
|
|
manifest = {}
|
|
for name in ('sunshine', 'capture-no-cursor.so'):
|
|
(root / name).write_bytes(name.encode())
|
|
manifest[name] = hashlib.sha256(name.encode()).hexdigest()
|
|
(root / 'manifest.json').write_text(json.dumps(manifest))
|
|
(root / 'hide-server-cursor').touch()
|
|
events = []
|
|
|
|
class CursorOwner:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def hide_server_cursor(self):
|
|
events.append('hide')
|
|
|
|
def __exit__(self, *_):
|
|
events.append('restore')
|
|
|
|
def capture(lease):
|
|
events.append('capture')
|
|
raise RuntimeError('producer failed')
|
|
|
|
with patch.object(h, 'ROOT', root), patch.object(h, 'RUN', root), \
|
|
patch.object(h, 'XCursor', CursorOwner), \
|
|
patch.object(h, 'guard_capture', capture), \
|
|
patch.object(h, 'systemctl', lambda *args: events.append(args)):
|
|
with self.assertRaisesRegex(RuntimeError, 'producer failed'):
|
|
h.guard()
|
|
self.assertEqual(events, ['hide', 'capture', ('stop', h.SERVICE), 'restore'])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|