Files
DroidDeck/tools/tests/test_runtime_inputs.py
T
The412Banner cf06f89c11 Merge #570 (PowerVR + Xclipse system Vulkan) into the non-Adreno test branch
# Conflicts:
#	.github/workflows/build.yml
#	app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt
#	app/src/main/java/com/droiddeck/launcher/session/SessionService.kt
#	tools/build_local.sh
#	tools/dxvk/build.sh
#	tools/linuxfs/overlay/usr/local/bin/droiddeck-session
#	tools/release/runtime_inputs.py
#	tools/tests/test_runtime_inputs.py
2026-10-12 18:34:02 -04:00

136 lines
6.1 KiB
Python

import importlib.util
import gzip
import io
from pathlib import Path
import subprocess
import tarfile
import tempfile
import unittest
from unittest.mock import patch
from zipfile import ZipFile
spec = importlib.util.spec_from_file_location('runtime_inputs', Path(__file__).resolve().parents[1] / 'release/runtime_inputs.py')
runtime = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runtime)
class RuntimeInputsTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
# Component archive validation has its own binary/archive fixtures.
component_check = patch.object(runtime, 'verify_powervr')
component_check.start()
self.addCleanup(component_check.stop)
paths = runtime.outputs(self.root)
for name in runtime.required():
path = self.root / paths.get(name, 'app/src/main/' + name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(gzip.compress(b'inventory') if path.suffix == '.gzip' else b'prepared runtime')
self.source = self.root / 'tools/linuxfs/preload/test.c'
self.source.parent.mkdir(parents=True)
self.source.write_text('current source')
self.audio(['pactl', 'modules/arm64/module-aaudio-sink.so', 'modules/arm64/module-directaudio-native-sink.so'])
def audio(self, names):
stream = io.BytesIO()
with tarfile.open(fileobj=stream, mode='w') as archive:
for name in names:
entry = tarfile.TarInfo('./' + name)
entry.size = 1
archive.addfile(entry, io.BytesIO(b'x'))
path = self.root / 'app/build/prepared-assets/pulseaudio.tzst'
path.write_bytes(subprocess.run(['zstd', '-q', '-c'], input=stream.getvalue(), capture_output=True, check=True).stdout)
def test_missing_proot_cannot_be_attested(self):
(self.root / 'app/src/main/jniLibs/arm64-v8a/libproot.so').unlink()
with self.assertRaisesRegex(ValueError, 'libproot.so'):
runtime.record(self.root)
def test_base_audio_without_compiled_sinks_is_rejected(self):
self.audio(['pactl'])
with self.assertRaisesRegex(ValueError, 'module-aaudio-sink'):
runtime.record(self.root)
def test_changed_native_source_rejects_old_outputs(self):
runtime.record(self.root)
self.source.write_text('new implementation')
with self.assertRaisesRegex(ValueError, 'sources changed'):
runtime.verify(self.root)
def test_changed_quest_source_rejects_old_outputs(self):
source = self.root / 'tools/quest/kgsl_compat.c'
source.parent.mkdir(parents=True)
source.write_text('old compatibility code')
runtime.record(self.root)
source.write_text('new compatibility code')
with self.assertRaisesRegex(ValueError, 'sources changed'):
runtime.verify(self.root)
def test_missing_quest_driver_cannot_be_attested(self):
(self.root / 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst').unlink()
with self.assertRaisesRegex(ValueError, 'quest1.tzst'):
runtime.record(self.root)
def test_changed_powervr_wrapper_rejects_old_outputs(self):
wrapper = self.root / 'tools/hybris/run-proton.py'
wrapper.parent.mkdir(parents=True)
wrapper.write_text('current wrapper')
runtime.record(self.root)
wrapper.write_text('changed wrapper')
with self.assertRaisesRegex(ValueError, 'sources changed'):
runtime.verify(self.root)
def test_missing_powervr_bundle_cannot_be_attested(self):
(self.root / 'app/src/main/assets/hybris/runtime.tzst').unlink()
with self.assertRaisesRegex(ValueError, 'assets/hybris/runtime.tzst'):
runtime.record(self.root)
def test_modified_prebuilt_binary_is_rejected(self):
runtime.record(self.root)
(self.root / 'app/src/main/jniLibs/arm64-v8a/libproot.so').write_bytes(b'old APK binary')
with self.assertRaisesRegex(ValueError, 'changed runtime input'):
runtime.verify(self.root)
def test_an_old_apk_is_rejected_even_when_staging_is_current(self):
runtime.record(self.root)
data = runtime.verify(self.root)
apk = self.root / 'old.apk'
with ZipFile(apk, 'w') as package:
for name, output in data['outputs'].items():
content = b'stale' if name == 'assets/linuxfs/libssbs.so' else (self.root / output['path']).read_bytes()
package.writestr(name, content)
with self.assertRaisesRegex(ValueError, 'stale runtime input'):
runtime.check_apk(apk, self.root)
def test_complete_current_payload_passes(self):
runtime.record(self.root)
data = runtime.verify(self.root)
apk = self.root / 'current.apk'
with ZipFile(apk, 'w') as package:
for name, output in data['outputs'].items():
package.writestr(name, (self.root / output['path']).read_bytes())
runtime.check_apk(apk, self.root)
def test_android_expanding_or_renaming_a_compressed_inventory_is_rejected(self):
runtime.record(self.root)
data = runtime.verify(self.root)
apk = self.root / 'expanded.apk'
with ZipFile(apk, 'w') as package:
for name, output in data['outputs'].items():
content = (self.root / output['path']).read_bytes()
if name.endswith('.gzip'):
name = name.removesuffix('.gzip')
content = gzip.decompress(content)
package.writestr(name, content)
with self.assertRaisesRegex(ValueError, 'APK missing assets/desktop-removal/'):
runtime.check_apk(apk, self.root)
def test_malformed_removal_inventory_cannot_be_attested(self):
path = self.root / 'app/src/main/assets/desktop-removal/kde-remove.tsv.gzip'
path.write_bytes(b'not a gzip stream')
with self.assertRaisesRegex(ValueError, 'Invalid desktop removal inventory'):
runtime.record(self.root)