Files
saphid--frame-control/tests/test_frame_decky.py
T
Alex SouthwellandSAPHID 509ddbdc95 Add experimental Decky Loader manager for Steam Frame (#195)
* Add sandboxed local Frame Control extensions

* Add experimental Decky Loader manager for Steam Frame

* ci: verify Decky PR against main

---------

Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-25 03:14:42 +00:00

95 lines
4.4 KiB
Python

"""Decky release validation and the removable user-service transaction."""
import sandbox # noqa: F401
import hashlib
import io
import json
import os
import secrets
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'ui'))
import frame_decky
def arm_binary(fill=b'A'):
data = bytearray(b'\x7fELF' + fill * 2044)
data[18:20] = (183).to_bytes(2, 'little')
return bytes(data)
class DeckyRelease(unittest.TestCase):
def test_official_arm_release_digest_is_checked(self):
data = arm_binary()
digest = hashlib.sha256(data).hexdigest()
url = f'https://github.com/{frame_decky.REPO}/releases/download/v3.3.0/PluginLoader-arm64'
info = {'tag_name':'v3.3.0', 'assets':[{'name':'PluginLoader-arm64', 'size':len(data),
'digest':'sha256:'+digest, 'browser_download_url':url}]}
with patch.object(frame_decky.urllib.request, 'urlopen', side_effect=[io.BytesIO(json.dumps(info).encode()), io.BytesIO(data)]):
self.assertEqual(frame_decky.release_binary(), ('v3.3.0', digest, data))
info['assets'][0]['digest'] = 'sha256:' + '0'*64
with patch.object(frame_decky.urllib.request, 'urlopen', side_effect=[io.BytesIO(json.dumps(info).encode()), io.BytesIO(data)]):
with self.assertRaisesRegex(ValueError, 'digest'):
frame_decky.release_binary()
def test_install_rejects_incompatible_headset_before_download(self):
state = {'arch':'x86_64','variant':'vr','cef':True,'managed':False,'upstreamInstall':False}
with patch.object(frame_decky, 'release_binary') as download:
with self.assertRaisesRegex(ValueError, 'Steam Frame'):
frame_decky.change(lambda *a, **k: json.dumps(state), 'install')
download.assert_not_called()
@unittest.skipIf(os.name == 'nt', 'remote service script runs on Linux')
class DeckyServiceScript(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
home = Path(self.tmp.name)
fake = home / 'systemctl'
fake.write_text('#!/bin/sh\nif [ "$FRAME_DECKY_TEST_FAIL" = "1" ] && [ "$2" = "enable" ]; then echo blocked >&2; exit 1; fi\nexit 0\n')
fake.chmod(0o700)
self.env = {**os.environ, 'HOME': self.tmp.name, 'PATH': self.tmp.name + os.pathsep + os.environ['PATH']}
self.base = home / '.local/share/frame-control/decky'
def apply(self, action, data=None, version='v3.3.0', fail=False):
token = secrets.token_hex(12)
stage = Path('/tmp/frame-control-decky-' + token)
if data is not None:
stage.write_bytes(data)
digest = hashlib.sha256(data or b'').hexdigest()
env = {**self.env, 'FRAME_DECKY_TEST_FAIL': '1' if fail else '0'}
try:
return subprocess.run([sys.executable, '-', action, token, digest, version],
input=frame_decky.APPLY, capture_output=True, text=True, env=env, timeout=15)
finally:
stage.unlink(missing_ok=True)
def test_install_update_rollback_and_remove(self):
first = arm_binary(b'A')
result = self.apply('install', first)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
receipt = self.base / '.frame-control/receipt.json'
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
self.assertTrue((Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service').is_file())
second = arm_binary(b'B')
result = self.apply('update', second, version='v3.3.1', fail=True)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
result = self.apply('update', second, version='v3.3.1')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual((self.base / 'PluginLoader').read_bytes(), second)
result = self.apply('remove')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse((self.base / 'PluginLoader').exists())
self.assertFalse(receipt.exists())
self.assertTrue((self.base / 'plugins').is_dir())