Files
psxrecomp/tools/tests/test_package_title_release.py
Matthew Stanley 56a6cfe782 Add one shared title release command with a required channel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:46:47 -07:00

334 lines
13 KiB
Python

#!/usr/bin/env python3
"""Unit tests for tools/package_title_release.py (no build, no disc, no game).
Pins the release contract every title now shares: --channel is required with
no default, owned CMake flags cannot be overridden by a title, artifact names
follow the channel, forbidden content fails the payload gates, diagnostics
defaults follow the channel, and a finished ZIP is re-verified by extraction.
"""
import contextlib
import hashlib
import io
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
import zipfile
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import package_title_release as ptr # noqa: E402
DIAG = '''format_version = 5
id = "fixture.diagnostics"
version = "1.0.0"
name = "Playtest Diagnostics"
channel = "stable"
[[target]]
game_id = "SLUS-00000"
[[feature]]
id = "recording"
default_enabled = {on}
[[feature]]
id = "osd"
default_enabled = {on}
[[plugin]]
feature = "recording"
id = "fixture.diagnostics"
[[plugin]]
feature = "osd"
id = "fixture.diagnostics.osd"
'''
CONFIG = '''schema = 1
[product]
name = "FixtureRecomp"
display_name = "Fixture Recompiled"
exe_name = "Fixture"
version_file = "VERSION"
[build]
cmake_args = ["-DPSX_PGXP_VARIANT=OFF"]
[diagnostics]
game_id = "SLUS-00000"
package_id = "fixture.diagnostics"
[payload]
start_here = "START_HERE.txt"
files = ["README.md", {{ from = "notes/input.ini", to = "input.ini" }}]
{extra}
'''
class Fixture:
def __init__(self, extra=''):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / 'VERSION').write_text('v1.2.3\n')
(self.root / 'START_HERE.txt').write_text('Fixture @VERSION@\n')
(self.root / 'README.md').write_text('readme\n')
(self.root / 'game.toml').write_text('[game]\nid = "SLUS-00000"\n')
(self.root / 'notes').mkdir()
(self.root / 'notes/input.ini').write_text('[controller]\n')
(self.root / 'release.toml').write_text(CONFIG.format(extra=extra))
def config(self):
return ptr.Config(self.root / 'release.toml', self.root)
def close(self):
self.tmp.cleanup()
def write_stage(stage, channel, version='v1.2.3', exe='Fixture.exe'):
stage.mkdir(parents=True)
(stage / exe).write_bytes(b'MZ fixture')
(stage / 'psx_game_version.txt').write_text(version + '\n')
(stage / 'START_HERE.txt').write_text('Fixture %s\n' % version)
(stage / 'game.toml').write_text('[game]\nid = "SLUS-00000"\n')
(stage / 'replay-diagnostics.py').write_text('# helper\n')
(stage / 'bios').mkdir()
(stage / 'bios/openbios.bin').write_bytes(b'\0' * ptr.OPENBIOS_SIZE)
package = stage / 'mods/bundled/fixture.diagnostics/1.0.0'
package.mkdir(parents=True)
(package / 'manifest.toml').write_text(
DIAG.format(on='true' if channel == 'playtest' else 'false'))
files = {p.relative_to(stage).as_posix(): ptr.sha256(p)
for p in sorted(stage.rglob('*')) if p.is_file()}
manifest = dict(channel=channel, version=version, product='FixtureRecomp', executable=exe,
mod_packages=['fixture.diagnostics'],
playtest_diagnostics_default_enabled=channel == 'playtest', files=files)
(stage / 'RELEASE_MANIFEST.json').write_text(json.dumps(manifest))
def zip_stage(stage, artifact):
with zipfile.ZipFile(artifact, 'w') as archive:
for path in sorted(stage.rglob('*')):
if path.is_file():
archive.write(path, path.relative_to(stage).as_posix())
Path(str(artifact) + '.sha256').write_text(
'%s %s\n' % (hashlib.sha256(artifact.read_bytes()).hexdigest(), artifact.name))
class ChannelIsRequired(unittest.TestCase):
def run_main(self, argv):
err = io.StringIO()
with contextlib.redirect_stderr(err), self.assertRaises(SystemExit) as raised:
ptr.main(argv)
return raised.exception.code, err.getvalue()
def test_missing_channel_fails_loudly(self):
code, err = self.run_main(['--platform', 'windows'])
self.assertNotEqual(code, 0)
self.assertIn('PACKAGING FAILED', err)
self.assertIn('--channel', err)
def test_unknown_channel_fails(self):
code, err = self.run_main(['--channel', 'release'])
self.assertNotEqual(code, 0)
self.assertIn('PACKAGING FAILED', err)
def test_verify_also_requires_channel(self):
code, _ = self.run_main(['verify', 'x.zip'])
self.assertNotEqual(code, 0)
def test_default_subcommand_is_package(self):
args = ptr.parse_args(['--channel', 'playtest'])
self.assertEqual((args.command, args.channel), ('package', 'playtest'))
class ConfigTests(unittest.TestCase):
def setUp(self):
self.fixture = Fixture()
self.addCleanup(self.fixture.close)
def test_loads(self):
config = self.fixture.config()
self.assertEqual(config.version(), 'v1.2.3')
self.assertEqual([rel for _, rel in config.files], ['README.md', 'input.ini'])
self.assertEqual(config.aot_mode, 'none')
self.assertTrue(config.diagnostics['replay_helper'])
def test_owned_cmake_flags_are_refused(self):
for flag in ('PSX_PUBLIC_RELEASE', 'PSX_RELEASE_CHANNEL', 'PSX_DEBUG_TOOLS',
'PSXRECOMP_BIOS_STEMS', 'CMAKE_BUILD_TYPE'):
text = CONFIG.format(extra='').replace(
'"-DPSX_PGXP_VARIANT=OFF"', '"-D%s=ON"' % flag)
(self.fixture.root / 'release.toml').write_text(text)
with self.assertRaisesRegex(ptr.PackagingError, 'packager owns'):
self.fixture.config()
def test_unknown_key_is_an_error(self):
(self.fixture.root / 'release.toml').write_text(
CONFIG.format(extra='').replace('[build]', '[build]\nskip_audit = true'))
with self.assertRaisesRegex(ptr.PackagingError, 'unknown key'):
self.fixture.config()
def test_missing_payload_file_is_an_error(self):
(self.fixture.root / 'README.md').unlink()
with self.assertRaisesRegex(ptr.PackagingError, 'not found'):
self.fixture.config()
def test_package_path_cannot_escape(self):
(self.fixture.root / 'release.toml').write_text(
CONFIG.format(extra='').replace('to = "input.ini"', 'to = "../input.ini"'))
with self.assertRaisesRegex(ptr.PackagingError, 'relative path'):
self.fixture.config()
class NamingTests(unittest.TestCase):
def test_artifact_names_follow_channel(self):
fixture = Fixture()
self.addCleanup(fixture.close)
config = fixture.config()
self.assertEqual(ptr.artifact_name(config, 'v1.2.3', 'public', 'windows'),
'FixtureRecomp-v1.2.3-windows-x64.zip')
self.assertEqual(ptr.artifact_name(config, '1.2.3', 'playtest', 'windows'),
'FixtureRecomp-v1.2.3-playtest-windows-x64.zip')
self.assertEqual(ptr.artifact_name(config, '0.2.4-alpha', 'playtest', 'linux'),
'FixtureRecomp-v0.2.4-alpha-playtest-linux-x86_64.AppImage')
class GateTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.stage = Path(self.tmp.name) / 'stage'
write_stage(self.stage, 'public')
def test_clean_stage_passes(self):
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')
def assert_refused(self, rel, data=b'x', message=None):
path = self.stage / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
with self.assertRaises(ptr.PackagingError) as raised:
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')
if message:
self.assertIn(message, str(raised.exception))
top = self.stage / Path(rel).parts[0]
if top.is_dir() and top.name not in ('bios', 'mods'):
shutil.rmtree(top)
else:
path.unlink()
def test_forbidden_content(self):
self.assert_refused('Game (USA).cue', message='disc')
self.assert_refused('bios/SCPH1001.BIN', message='image')
self.assert_refused('saves/card1.mcd')
self.assert_refused('settings.toml', message='personal')
self.assert_refused('mods/installed/x.psxmod', message='personal')
self.assert_refused('state.pst', message='disc')
self.assert_refused('lib/game_full_1.c', message='source')
self.assert_refused('generated/x.h', message='build-kit')
def test_toolchain_sources_are_allowed(self):
(self.stage / 'overlay_toolchain/tcc/examples').mkdir(parents=True)
(self.stage / 'overlay_toolchain/tcc/examples/fib.c').write_text('int x;')
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')
def test_openbios_required(self):
(self.stage / 'bios/openbios.bin').write_bytes(b'short')
with self.assertRaisesRegex(ptr.PackagingError, 'OpenBIOS'):
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')
def test_windows_binary_in_linux_package(self):
(self.stage / 'Fixture').write_bytes(b'\x7fELF')
(self.stage / 'extra.dll').write_bytes(b'MZ')
with self.assertRaisesRegex(ptr.PackagingError, 'Windows binary'):
ptr.payload_gates(self.stage, 'linux', 'Fixture')
def test_template_tokens_must_all_be_stamped(self):
template = Path(self.tmp.name) / 'AppRun'
template.write_text('echo @VERSION@ @UNKNOWN_TOKEN@\n')
with self.assertRaisesRegex(ptr.PackagingError, 'UNKNOWN_TOKEN'):
ptr.render_template(template, {'VERSION': '1'})
template.write_text('echo @VERSION@\n')
self.assertEqual(ptr.render_template(template, {'VERSION': '1'}), 'echo 1\n')
class DiagnosticsTests(unittest.TestCase):
def setUp(self):
self.fixture = Fixture()
self.addCleanup(self.fixture.close)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
def ctx(self, channel):
ctx = ptr.Context()
ctx.config, ctx.channel = self.fixture.config(), channel
return ctx
def test_defaults_follow_channel(self):
for staged, channel, ok in (('public', 'public', True), ('playtest', 'playtest', True),
('playtest', 'public', False), ('public', 'playtest', False)):
stage = Path(self.tmp.name) / ('%s-%s' % (staged, channel))
write_stage(stage, staged)
if ok:
ptr.check_diagnostics(self.ctx(channel), stage)
else:
with self.assertRaisesRegex(ptr.PackagingError, 'default_enabled'):
ptr.check_diagnostics(self.ctx(channel), stage)
def test_undeclared_diagnostics_package_fails(self):
(self.fixture.root / 'release.toml').write_text(
CONFIG.format(extra='').replace(
'[diagnostics]\ngame_id = "SLUS-00000"\npackage_id = "fixture.diagnostics"\n', ''))
stage = Path(self.tmp.name) / 'stage'
write_stage(stage, 'public')
with self.assertRaisesRegex(ptr.PackagingError, 'no \\[diagnostics\\]'):
ptr.check_diagnostics(self.ctx('public'), stage)
class VerifyZipTests(unittest.TestCase):
def setUp(self):
self.fixture = Fixture()
self.addCleanup(self.fixture.close)
self.config = self.fixture.config()
self.out = Path(self.fixture.root) / 'dist'
self.out.mkdir()
def build(self, channel, staged_channel=None):
stage = Path(self.fixture.root) / ('stage-' + channel)
write_stage(stage, staged_channel or channel)
artifact = self.out / ptr.artifact_name(self.config, 'v1.2.3', channel, 'windows')
zip_stage(stage, artifact)
return artifact
def test_public_and_playtest_verify(self):
for channel in ptr.CHANNELS:
manifest = ptr.verify_artifact(self.build(channel), self.config, channel, 'v1.2.3')
self.assertEqual(manifest['channel'], channel)
def test_wrong_channel_fails(self):
artifact = self.build('public')
with self.assertRaises(ptr.PackagingError):
ptr.verify_artifact(artifact, self.config, 'playtest', 'v1.2.3')
def test_tampered_payload_fails(self):
artifact = self.build('public')
with zipfile.ZipFile(artifact, 'a') as archive:
archive.writestr('saves/card.mcd', b'card')
Path(str(artifact) + '.sha256').write_text(
'%s %s\n' % (ptr.sha256(artifact), artifact.name))
with self.assertRaises(ptr.PackagingError):
ptr.verify_artifact(artifact, self.config, 'public', 'v1.2.3')
def test_stale_checksum_fails(self):
artifact = self.build('public')
Path(str(artifact) + '.sha256').write_text('0' * 64 + ' x\n')
with self.assertRaisesRegex(ptr.PackagingError, 'sha256'):
ptr.verify_artifact(artifact, self.config, 'public', 'v1.2.3')
if __name__ == '__main__':
unittest.main()