Restore retail-only setup packaging and audit diagnostic release ZIPs

This commit is contained in:
Matthew Stanley
2026-10-17 14:04:52 -07:00
parent 309b5cdd1d
commit 444264f2aa
9 changed files with 464 additions and 8 deletions
+2 -2
View File
@@ -67,8 +67,8 @@ offline recorder does not claim to reproduce network sessions.
## Replay
Use Python 3.11+ with the recording's original native executable, disc and game
configuration. The tool copies the executable, runtime dependencies and
Use Python 3.8+ (install `tomli` before Python 3.11) with the recording's original
native executable, disc and game configuration. The tool copies the executable, runtime dependencies and
starting state into a newly created private directory. Personal settings/cards
and the original installation are untouched.
+1
View File
@@ -1131,6 +1131,7 @@ if(BUILD_TESTING)
# template, scaffold, check_generated.sh, Project Studio audit/ops).
foreach(_t IN ITEMS
aligned_lzss_banks
audit_playtest_diagnostics_package
bundled_release_layout
collect_game_misses
cursor_writers
+98
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env python3
"""Exercise the production SDK BIOS gate with synthetic files only."""
import importlib.util
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
@@ -28,6 +31,19 @@ class GateTests(unittest.TestCase):
self.asset("SCPH5552.toml")
self.assertEqual(gate.check(self.root), [str(Path("psxrecomp/bios/SCPH5552.toml"))])
def test_omit_requires_retail_recipe_and_no_openbios_assets(self):
self.recipe('[runtime]\nopenbios=true\n')
with self.assertRaisesRegex(ValueError, "requires runtime.openbios=false"):
gate.check(self.root, omit_openbios=True)
self.recipe('[runtime]\nopenbios=false\n')
self.asset("SCPH1001.toml")
self.assertEqual(len(gate.check(self.root, omit_openbios=True)), 1)
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
self.asset(name)
with self.assertRaisesRegex(ValueError, "survived"):
gate.check(self.root, omit_openbios=True)
(self.root / "psxrecomp/bios" / name).unlink()
def test_retail_missing_profile_fails(self):
self.recipe('[runtime]\nopenbios=false\n[recompiler]\nbios_config="psxrecomp/bios/SCPH5552.toml"\n')
with self.assertRaisesRegex(ValueError, "SCPH5552"):
@@ -118,5 +134,87 @@ class GateTests(unittest.TestCase):
self.assertEqual(len(gate.check(self.root)), 3)
@unittest.skipIf(os.name == "nt" or not shutil.which("bash"),
"full shell packaging fixture requires POSIX bash")
class SetupPackageTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
for path in ("build/assets/fonts/font.txt", "build/assets/img/image.txt",
"build/mods/bundled/fixture/1.0.0/manifest.toml",
"mods/preloaded/packages/fixture/1.0.0/manifest.toml",
"psxrecomp/psxrecomp_cli.py", "recomp-ui/CMakeLists.txt",
"emitters/psxrecomp-game", "emitters/psxrecomp-bios"):
self.write(path, "synthetic fixture\n")
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE", "SCPH1001.toml"):
self.write("psxrecomp/bios/" + name, "synthetic BIOS fixture")
self.write("CMakeLists.txt", "# synthetic title\n")
self.write("VERSION", "0.0.1\n")
self.write("build/psx_game_version.txt", "0.0.1\n")
self.write("build/Fixture", "synthetic executable, never run\n")
self.write("build/replay-diagnostics.py", "# staged replay helper\n")
# Packaging tests exercise staging, not a system compression utility.
# Ubuntu developer images may omit zip; write a real ZIP with stdlib.
self.write("fixture-bin/zip", '''#!/usr/bin/env python3
import pathlib, sys, zipfile
with zipfile.ZipFile(sys.argv[-2], "w") as archive:
for path in pathlib.Path(".").rglob("*"):
if path.is_file():
archive.write(path, path.as_posix())
''')
(self.root / "fixture-bin/zip").chmod(0o755)
def write(self, name, text):
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def package(self, retail):
self.write("game.toml", "[runtime]\noverlay_cache=true\nopenbios=" +
("false" if retail else "true") + "\n")
args = ["bash", str(ROOT / "tools/package_setup_host.sh"),
"--root", str(self.root), "--build-dir", "build", "--artifact", "fixture",
"--zip-prefix", "test", "--exe-name", "Fixture", "--recompiler-build", "emitters",
"--project-file", "CMakeLists.txt", "--project-file", "game.toml"]
if retail:
args += ["--omit-openbios", "--bios-hint", "a legal retail BIOS dump (required)"]
env = dict(os.environ, RELEASE_VERSION="0.0.1", EXCLUDE_DEV_MODS="0")
env["PATH"] = str(self.root / "fixture-bin") + os.pathsep + env["PATH"]
return subprocess.run(args, cwd=self.root, env=env, capture_output=True, text=True)
def test_default_and_retail_packages_use_real_sdk_gate_and_keep_helper(self):
for retail in (False, True):
with self.subTest(retail=retail):
result = self.package(retail)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
stage = self.root / "dist/stage-setup-fixture"
self.assertTrue((stage / "replay-diagnostics.py").is_file())
self.assertTrue((stage / "psxrecomp/bios/SCPH1001.toml").is_file())
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
self.assertEqual((stage / "psxrecomp/bios" / name).exists(), not retail)
readme = (stage / "README-SETUP.txt").read_text()
if retail:
self.assertIn("a legal retail BIOS dump (required)", readme)
self.assertNotIn("otherwise OpenBIOS", readme)
else:
self.assertIn("otherwise OpenBIOS is regenerated locally", readme)
def test_sdk_omission_runs_after_overlay_and_rejects_openbios_recipe(self):
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
self.write("overlay/bios/" + name, "overlay BIOS fixture")
self.write("game.toml", "[runtime]\nopenbios=false\n")
args = ["bash", str(ROOT / "tools/stage_setup_sdk.sh"), "--stage", str(self.root),
"--framework", str(self.root / "psxrecomp"), "--sdk-overlay", str(self.root / "overlay"),
"--recompiler-build", str(self.root / "emitters"), "--allow-no-toolchain", "--omit-openbios"]
result = subprocess.run(args, cwd=self.root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertFalse((self.root / "psxrecomp/bios/openbios.bin").exists())
self.write("game.toml", "[runtime]\nopenbios=true\n")
result = subprocess.run(args, cwd=self.root, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("requires runtime.openbios=false", result.stderr)
if __name__ == "__main__":
unittest.main()
+14 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""Require BIOS assets selected by the staged title recipe."""
import argparse
import sys
from pathlib import Path
try:
@@ -8,7 +9,7 @@ except ModuleNotFoundError:
import tomli as tomllib
def check(stage):
def check(stage, omit_openbios=False):
stage = Path(stage).resolve()
recipe = stage / "game.toml"
if not recipe.is_file():
@@ -18,6 +19,13 @@ def check(stage):
openbios = config.get("runtime", {}).get("openbios", True)
if not isinstance(openbios, bool):
raise ValueError("runtime.openbios must be a boolean")
if omit_openbios and openbios:
raise ValueError("--omit-openbios requires runtime.openbios=false")
if omit_openbios:
for root in (stage / "bios", stage / "psxrecomp/bios"):
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
if (root / name).exists():
raise ValueError("OpenBIOS asset survived --omit-openbios: " + str(root / name))
profile = config.get("recompiler", {}).get("bios_config")
required = []
if profile:
@@ -40,7 +48,11 @@ def check(stage):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("stage")
parser.add_argument("--omit-openbios", action="store_true")
args = parser.parse_args()
try:
print("staged BIOS policy: " + ", ".join(check(sys.argv[1])))
print("staged BIOS policy: " + ", ".join(check(args.stage, args.omit_openbios)))
except (ValueError, OSError) as error:
sys.exit("error: " + str(error))
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Audit a flat game/setup ZIP without extracting or launching its contents."""
import argparse
from pathlib import PurePosixPath
import re
import sys
import zipfile
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
HD_SURFACE = re.compile(r"hd[ ._-]*(?:textures?|packs?)|texture[ ._-]*packs?", re.I)
# These checked-in synthetic cards are SDK test inputs, never player saves.
SDK_CARD_FIXTURES = {
"psxrecomp/runtime/tests/fixtures/memcards/card1.mcd",
"psxrecomp/runtime/tests/fixtures/memcards/card2.mcd",
"psxrecomp/runtime/tests/fixtures/memcards/dummy.0.mcr",
"psxrecomp/runtime/tests/fixtures/memcards/dummy.1.mcr",
"psxrecomp/runtime/tests/test_card_synth.mcd",
"psxrecomp/runtime/tests/test_dir/card1.mcd",
}
PERSONAL_NAMES = {
"state.toml", "state.toml.tmp", "settings.toml", "keybinds.ini",
"disc.cfg", "bios.cfg", "overlay_captures.json", "diagnostics-location.txt",
}
def audit(path, game_id, diagnostics_id):
with zipfile.ZipFile(path) as archive:
files = {}
for entry in archive.infolist():
name = entry.filename.replace("\\", "/")
parts = PurePosixPath(name).parts
if name.startswith("/") or ".." in parts or (parts and ":" in parts[0]):
raise ValueError("unsafe archive path: " + name)
if not entry.is_dir():
if name in files:
raise ValueError("duplicate archive entry: " + name)
files[name] = entry
# Some publishers put the same flat payload inside one named folder.
if "game.toml" not in files:
recipes = [name for name in files if name.count("/") == 1 and name.endswith("/game.toml")]
if len(recipes) == 1:
prefix = recipes[0].rsplit("/", 1)[0] + "/"
if all(name.startswith(prefix) for name in files):
files = {name[len(prefix):]: entry for name, entry in files.items()}
def read(name):
entry = files.get(name)
if entry is None or entry.file_size == 0:
raise ValueError("missing or empty package file: " + name)
if entry.file_size > 4 * 1024 * 1024:
raise ValueError("unexpectedly large package metadata: " + name)
return archive.read(entry)
def executable(name, entry):
if "/" in name:
return False
if name.lower().endswith(".exe"):
return entry.file_size > 0
with archive.open(entry) as stream:
return stream.read(4) == b"\x7fELF"
recipe = tomllib.loads(read("game.toml").decode("utf-8-sig"))
if recipe.get("game", {}).get("id") != game_id:
raise ValueError("game.toml game ID does not match " + game_id)
read("replay-diagnostics.py")
if not any(executable(name, entry) for name, entry in files.items()):
raise ValueError("no executable adjacent to replay-diagnostics.py")
runtime = []
for name in sorted(files):
lower = name.lower()
parts = PurePosixPath(lower).parts
leaf = parts[-1]
if leaf in PERSONAL_NAMES or "saves" in parts or "diagnostics" in parts or (
lower.startswith("mods/installed/")):
raise ValueError("personal state in package: " + name)
suffix = PurePosixPath(lower).suffix
if recipe.get("runtime", {}).get("openbios") is False and lower in {
root + leaf for root in ("bios/", "psxrecomp/bios/")
for leaf in ("openbios.bin", "openbios.toml", "openbios.license")}:
raise ValueError("OpenBIOS asset in retail-BIOS-only package: " + name)
if suffix in {".cue", ".iso", ".img", ".chd", ".mcd", ".mcr", ".rom", ".pst"}:
if name not in SDK_CARD_FIXTURES:
raise ValueError("disc, BIOS or save payload in package: " + name)
if suffix == ".bin" and lower not in {"bios/openbios.bin", "psxrecomp/bios/openbios.bin"}:
raise ValueError("unexpected binary image in package: " + name)
if re.match(r"scph.*\.(bin|rom)(?:\..*)?$", leaf):
raise ValueError("retail BIOS image in package: " + name)
# The SDK retains its framework built-in authoring catalog. The
# title's CMake exclusion controls whether it reaches runtime.
if lower.startswith("mods/") and suffix in {".toml", ".json"}:
text = read(name).decode("utf-8-sig")
if HD_SURFACE.search(text) or HD_SURFACE.search(name):
raise ValueError("HD texture surface in title/runtime catalog: " + name)
if leaf == "manifest.toml":
manifest = tomllib.loads(text)
if lower.startswith("mods/bundled/" + diagnostics_id.lower() + "/") and manifest.get("id") == diagnostics_id:
runtime.append((name, manifest))
if manifest.get("id") == diagnostics_id:
validate_diagnostics(manifest, name, game_id, diagnostics_id)
if not runtime:
raise ValueError("runtime mods/bundled catalog is missing " + diagnostics_id)
return len(runtime)
def validate_diagnostics(manifest, name, game_id, diagnostics_id):
if not any(target.get("game_id") == game_id for target in manifest.get("target", [])):
raise ValueError("diagnostics manifest has the wrong game target: " + name)
features = {feature.get("id"): feature for feature in manifest.get("feature", [])}
for feature_id, plugin_id in (("recording", diagnostics_id), ("osd", diagnostics_id + ".osd")):
feature = features.get(feature_id, {})
if feature.get("default_enabled") is not True or feature.get("channel") == "developer":
raise ValueError("diagnostics " + feature_id + " must ship enabled by default: " + name)
if not any(plugin.get("feature") == feature_id and plugin.get("id") == plugin_id
for plugin in manifest.get("plugin", [])):
raise ValueError("diagnostics " + feature_id + " plugin is missing: " + name)
if manifest.get("channel") == "developer":
raise ValueError("diagnostics package cannot be developer-only: " + name)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("zip")
parser.add_argument("--game-id", required=True)
parser.add_argument("--diagnostics-id", required=True)
args = parser.parse_args()
try:
count = audit(args.zip, args.game_id, args.diagnostics_id)
except (ValueError, OSError, zipfile.BadZipFile, KeyError, UnicodeError) as error:
sys.exit("diagnostics package audit failed: " + str(error))
print("Diagnostics package audit passed: {} ({} runtime manifest(s))".format(args.diagnostics_id, count))
if __name__ == "__main__":
main()
+18 -2
View File
@@ -22,6 +22,8 @@
# [--project-file REL]... [--project-dir REL]... \
# [--runtime-dir NAME]... [--runtime-dir-optional NAME]... \
# [--disc-hint "your legally owned disc"] \
# [--bios-hint "an optional retail SCPH-1001 BIOS; otherwise OpenBIOS"] \
# [--omit-openbios] # retail-BIOS-only titles; game.toml openbios=false
# [--version-env BPE_RELEASE_VERSION] \
# [--embed-toolchain] # optional: copy PSXRECOMP_TOOLCHAIN_DIR into zip
#
@@ -69,6 +71,8 @@ DISPLAY_NAME=""
RECOMPILER_BUILD="build-recompiler"
VERSION_ENV="RELEASE_VERSION"
DISC_HINT="your legally owned game disc"
BIOS_HINT=""
OMIT_OPENBIOS=0
# Wave-5 F4: a packaged game.toml without `overlay_cache = true` ships a runtime that never
# initialises the overlay loader, so every streamed overlay runs on the interpreter (78 of
# 105 releases measured 2026-09-16). Refuse unless the caller states why.
@@ -113,6 +117,8 @@ while [[ $# -gt 0 ]]; do
--recompiler-build) RECOMPILER_BUILD="${2:?}"; shift 2 ;;
--version-env) VERSION_ENV="${2:?}"; shift 2 ;;
--disc-hint) DISC_HINT="${2:?}"; shift 2 ;;
--bios-hint) BIOS_HINT="${2:?}"; shift 2 ;;
--omit-openbios) OMIT_OPENBIOS=1; shift ;;
--project-file) PROJECT_FILES+=("${2:?}"); shift 2 ;;
--project-dir) PROJECT_DIRS+=("${2:?}"); shift 2 ;;
--no-mods) STAGE_MODS=0; shift ;;
@@ -132,6 +138,14 @@ while [[ $# -gt 0 ]]; do
esac
done
if [[ -z "${BIOS_HINT}" ]]; then
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
BIOS_HINT="a legal retail SCPH-1001 BIOS dump (required)"
else
BIOS_HINT="an optional retail SCPH-1001 BIOS; otherwise OpenBIOS is regenerated locally"
fi
fi
# Default the mods catalog into the required set unless the caller already
# named it (in either list) or opted out. Appending rather than overriding
# keeps an explicit --runtime-dir-optional mods meaningful for a title that
@@ -485,6 +499,9 @@ stage_args=(
--host-exe "${STAGE}/${EXE_BASENAME}"
--recompiler-build "${RECOMPILER_BUILD}"
)
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
stage_args+=(--omit-openbios)
fi
if [[ "${EMBED_TOOLCHAIN}" -eq 1 ]]; then
if [[ -z "${PSXRECOMP_TOOLCHAIN_DIR:-${TOOLCHAIN_DIR:-${BPE_TOOLCHAIN_DIR:-}}}" ]]; then
echo "error: --embed-toolchain requires PSXRECOMP_TOOLCHAIN_DIR (or TOOLCHAIN_DIR)" >&2
@@ -519,8 +536,7 @@ BIOS dumps, pre-generated game C, or a portable cmake/clang pack. Emitters
Standalone:
1. Install Python 3.
2. Run ${EXE_BASENAME}.
3. Provide ${DISC_HINT} (and optional retail SCPH-1001 BIOS; otherwise
OpenBIOS is regenerated locally).
3. Provide ${DISC_HINT} and ${BIOS_HINT}.
4. Follow the Generate & rebuild wizard. On first rebuild the host downloads
cmake-clang-v1 from RetroPortingToolKit/RetroPorting-Toolchains (or you can
pick a local cmake-clang-v1-*.zip for offline builds). System cmake/ninja
+4 -1
View File
@@ -29,7 +29,10 @@ import re
import shutil
import subprocess
import time
import tomllib
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
FORMATS = (b'# psxrecomp-input-v1', b'# psxrecomp-input-v2')
ENV_PREFIXES = ('PSX_', 'RNET_', 'LNG_', 'PSX_DIAGNOSTIC_', 'RECOMP_')
+13 -1
View File
@@ -19,6 +19,7 @@
# --runtime-bin DIR MinGW runtime DLL search dir (repeatable)
# --search-dir DIR Extra DLL search dir (repeatable)
# --require-cli Require psxrecomp_cli.py after overlays (default on)
# --omit-openbios Remove OpenBIOS assets for a retail-BIOS-only recipe
#
# Env aliases for toolchain dir (first wins):
# PSXRECOMP_TOOLCHAIN_DIR, TOOLCHAIN_DIR, BPE_TOOLCHAIN_DIR
@@ -39,6 +40,7 @@ HOST_EXE=""
RUNTIME_BINS=()
SEARCH_DIRS=()
REQUIRE_CLI=1
OMIT_OPENBIOS=0
usage() {
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
@@ -59,6 +61,7 @@ while [[ $# -gt 0 ]]; do
--search-dir) SEARCH_DIRS+=("${2:?}"); shift 2 ;;
--require-cli) REQUIRE_CLI=1; shift ;;
--no-require-cli) REQUIRE_CLI=0; shift ;;
--omit-openbios) OMIT_OPENBIOS=1; shift ;;
*)
echo "error: unknown arg: $1" >&2
usage
@@ -209,7 +212,16 @@ else
echo "error: Python is required to check the staged BIOS policy" >&2
exit 1
fi
"${SDK_PYTHON}" "${SCRIPT_DIR}/check_setup_bios_assets.py" "${STAGE}"
bios_args=()
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
# Run after overlays so they cannot restore an unwanted BIOS surface.
for bios_root in "${STAGE}/bios" "${STAGE}/psxrecomp/bios"; do
rm -f "${bios_root}/openbios.bin" "${bios_root}/OpenBIOS.toml" \
"${bios_root}/OpenBIOS.LICENSE"
done
bios_args+=(--omit-openbios)
fi
"${SDK_PYTHON}" "${SCRIPT_DIR}/check_setup_bios_assets.py" "${STAGE}" "${bios_args[@]}"
if [[ "${REQUIRE_CLI}" -eq 1 && ! -f "${STAGE}/psxrecomp/psxrecomp_cli.py" ]]; then
echo "error: missing psxrecomp/psxrecomp_cli.py in staged tree" >&2
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Exercise the release ZIP auditor against actual synthetic archives."""
import importlib.util
import ast
import builtins
from pathlib import Path
import runpy
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
import warnings
import zipfile
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "tools/ci/audit_playtest_diagnostics_package.py"
spec = importlib.util.spec_from_file_location("package_audit", SCRIPT)
audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(audit)
DIAG = "fixture.diagnostics"
GAME = "SLUS-00000"
MANIFEST = "mods/bundled/{}/1.0.0/manifest.toml".format(DIAG)
SOURCE = "mods/preloaded/packages/{}/1.0.0/manifest.toml".format(DIAG)
TEXT = '''id = "fixture.diagnostics"
[[target]]
game_id = "SLUS-00000"
[[feature]]
id = "recording"
default_enabled = true
[[feature]]
id = "osd"
default_enabled = true
[[plugin]]
feature = "recording"
id = "fixture.diagnostics"
[[plugin]]
feature = "osd"
id = "fixture.diagnostics.osd"
'''
class ArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.zip = Path(self.tmp.name) / "release.zip"
self.files = {"game.toml": '[game]\nid="SLUS-00000"\n',
"Game.exe": b"MZfixture", "replay-diagnostics.py": "# helper\n",
MANIFEST: TEXT}
def write(self, prefix=""):
with zipfile.ZipFile(self.zip, "w") as archive:
for name, content in self.files.items():
archive.writestr(prefix + name, content)
def check(self):
self.write()
return audit.audit(self.zip, GAME, DIAG)
def test_valid_flat_and_one_folder_packages(self):
self.assertEqual(self.check(), 1)
self.write("Release/")
self.assertEqual(audit.audit(self.zip, GAME, DIAG), 1)
def test_valid_setup_sources_and_excluded_framework_hd_catalog(self):
self.files[SOURCE] = TEXT
self.files["psxrecomp/mods/builtin/packages/psx.enhancement.hd-textures/1.0.0/manifest.toml"] = 'id="psx.enhancement.hd-textures"\n'
self.files["psxrecomp/runtime/tests/fixtures/memcards/card1.mcd"] = b"synthetic"
self.assertEqual(self.check(), 1)
def test_cli_and_native_linux_executable(self):
del self.files["Game.exe"]
self.files["Game"] = b"\x7fELFfixture"
self.write()
result = subprocess.run([sys.executable, str(SCRIPT), str(self.zip),
"--game-id", GAME, "--diagnostics-id", DIAG],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("audit passed", result.stdout)
def test_python38_syntax_and_tomli_fallback(self):
original_import = builtins.__import__
def import_without_tomllib(name, *args, **kwargs):
if name == "tomllib":
raise ModuleNotFoundError("simulated Python 3.8")
if name == "tomli":
return audit.tomllib
return original_import(name, *args, **kwargs)
for script in (SCRIPT, ROOT / "tools/replay_diagnostics.py", ROOT / "tools/check_setup_bios_assets.py"):
with self.subTest(script=script.name):
ast.parse(script.read_text(encoding="utf-8"), feature_version=8)
with mock.patch("builtins.__import__", side_effect=import_without_tomllib):
namespace = runpy.run_path(str(script))
self.assertIs(namespace["tomllib"], audit.tomllib)
def test_each_default_and_plugin_is_required(self):
for bad in (TEXT.replace("default_enabled = true", "default_enabled = false", 1),
TEXT.replace('id = "osd"\ndefault_enabled = true', 'id = "osd"\ndefault_enabled = false'),
TEXT.replace('id = "fixture.diagnostics.osd"', 'id = "wrong.osd"'),
'channel="developer"\n' + TEXT):
with self.subTest(manifest=bad):
self.files[MANIFEST] = bad
with self.assertRaises(ValueError):
self.check()
def test_source_catalog_defaults_also_required(self):
self.files[SOURCE] = TEXT.replace("default_enabled = true", "default_enabled = false")
with self.assertRaisesRegex(ValueError, "enabled by default"):
self.check()
def test_missing_helper_executable_or_runtime_catalog(self):
for name in ("replay-diagnostics.py", "Game.exe", MANIFEST):
with self.subTest(file=name):
content = self.files.pop(name)
with self.assertRaises(ValueError):
self.check()
self.files[name] = content
def test_wrong_game_recipe_and_manifest_target(self):
self.files["game.toml"] = '[game]\nid="WRONG"\n'
with self.assertRaisesRegex(ValueError, "game ID"):
self.check()
self.files["game.toml"] = '[game]\nid="SLUS-00000"\n'
self.files[MANIFEST] = TEXT.replace(GAME, "WRONG")
with self.assertRaisesRegex(ValueError, "target"):
self.check()
def test_hd_package_plugin_and_control_surfaces_fail(self):
for name, content in (
("mods/bundled/psx.enhancement.hd-textures/1.0.0/manifest.toml", 'id="psx.enhancement.hd-textures"'),
(SOURCE, TEXT + '\n[[plugin]]\nid="psx.enhancement.hd-textures"\n'),
("mods/preloaded/packages/fixture/1.0.0/options.toml", 'texture_pack_mode="off"\n')):
with self.subTest(file=name):
self.files[name] = content
with self.assertRaisesRegex(ValueError, "HD texture"):
self.check()
del self.files[name]
def test_personal_state_save_and_retail_images_fail(self):
for name in ("mods/state.toml", "settings.toml", "saves/card1.mcd", "saves/diagnostics/inputs.csv",
"mods/installed/private.psxmod", "SCPH1001.BIN", "disc/game.cue",
"psxrecomp/bios/SCPH5552.rom", "psxrecomp/runtime/tests/private.mcd"):
with self.subTest(file=name):
self.files[name] = "private"
with self.assertRaises(ValueError):
self.check()
del self.files[name]
def test_retail_only_recipe_rejects_openbios(self):
self.files["game.toml"] += '[runtime]\nopenbios=false\n'
self.assertEqual(self.check(), 1)
self.files["psxrecomp/bios/openbios.bin"] = "synthetic"
with self.assertRaisesRegex(ValueError, "retail-BIOS-only"):
self.check()
def test_unsafe_and_duplicate_paths_fail(self):
self.files["../escape.txt"] = "bad"
with self.assertRaisesRegex(ValueError, "unsafe"):
self.check()
del self.files["../escape.txt"]
self.write()
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
with zipfile.ZipFile(self.zip, "a") as archive:
archive.writestr(MANIFEST, TEXT)
with self.assertRaisesRegex(ValueError, "duplicate"):
audit.audit(self.zip, GAME, DIAG)
if __name__ == "__main__":
unittest.main()