Files
psxrecomp/runtime/tests/test_setup_bios_assets.py
T

328 lines
17 KiB
Python

#!/usr/bin/env python3
"""Exercise the production SDK BIOS gate with synthetic files only."""
import importlib.util
import ast
import types
import os
import shutil
import subprocess
import tempfile
import unittest
from unittest import mock
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location("gate", ROOT / "tools/check_setup_bios_assets.py")
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
spec = importlib.util.spec_from_file_location("platform_copy", ROOT / "tools/ci/audit_setup_package_platform_copy.py")
platform_copy = importlib.util.module_from_spec(spec)
spec.loader.exec_module(platform_copy)
spec = importlib.util.spec_from_file_location("toolchain_pack", ROOT / "tools/toolchain_pack.py")
toolchain_pack = importlib.util.module_from_spec(spec)
spec.loader.exec_module(toolchain_pack)
class GateTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
(self.root / "psxrecomp/bios").mkdir(parents=True)
def recipe(self, text):
(self.root / "game.toml").write_text(text)
def asset(self, name):
(self.root / "psxrecomp/bios" / name).write_text("synthetic fixture")
def test_retail_profile_without_openbios(self):
self.recipe('[runtime]\nopenbios=false\n[recompiler]\nbios_config="psxrecomp/bios/SCPH5552.toml"\n')
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"):
gate.check(self.root)
def test_default_openbios_requires_all_assets(self):
self.recipe('[runtime]\n')
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
with self.assertRaises(ValueError):
gate.check(self.root)
self.asset(name)
self.assertEqual(len(gate.check(self.root)), 3)
def test_external_profile_fails(self):
self.recipe('[runtime]\nopenbios=false\n[recompiler]\nbios_config="../external.toml"\n')
with self.assertRaisesRegex(ValueError, "inside"):
gate.check(self.root)
def test_absent_recipe_fails(self):
with self.assertRaisesRegex(ValueError, "game.toml"):
gate.check(self.root)
def symlink(self, link, target, directory=False):
try:
link.symlink_to(target, target_is_directory=directory)
except (OSError, NotImplementedError) as error:
self.skipTest("symlink creation unavailable: " + str(error))
def outside(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
return Path(tmp.name)
def test_default_retail_rejects_external_profile_symlink(self):
self.recipe('[runtime]\nopenbios=false\n')
target = self.outside() / "SCPH1001.toml"
target.write_text("synthetic fixture")
self.symlink(self.root / "psxrecomp/bios/SCPH1001.toml", target)
with self.assertRaisesRegex(ValueError, "inside"):
gate.check(self.root)
def test_openbios_rejects_external_asset_symlinks(self):
self.recipe('[runtime]\n')
names = ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE")
for name in names:
self.asset(name)
target = self.outside() / "asset"
target.write_text("synthetic fixture")
for name in names:
with self.subTest(asset=name):
link = self.root / "psxrecomp/bios" / name
link.unlink()
self.symlink(link, target)
with self.assertRaisesRegex(ValueError, "inside"):
gate.check(self.root)
link.unlink()
self.asset(name)
def test_default_retail_rejects_external_bios_directory(self):
self.recipe('[runtime]\nopenbios=false\n')
target = self.outside()
(target / "SCPH1001.toml").write_text("synthetic fixture")
link = self.root / "psxrecomp/bios"
link.rmdir()
self.symlink(link, target, directory=True)
with self.assertRaisesRegex(ValueError, "inside"):
gate.check(self.root)
def test_openbios_rejects_external_framework_directory(self):
self.recipe('[runtime]\n')
target = self.outside()
(target / "bios").mkdir()
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
(target / "bios" / name).write_text("synthetic fixture")
(self.root / "psxrecomp/bios").rmdir()
link = self.root / "psxrecomp"
link.rmdir()
self.symlink(link, target, directory=True)
with self.assertRaisesRegex(ValueError, "inside"):
gate.check(self.root)
def test_internal_asset_symlinks_are_accepted(self):
self.recipe('[runtime]\n')
target = self.root / "shared-asset"
target.write_text("synthetic fixture")
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
self.symlink(self.root / "psxrecomp/bios" / name, target)
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"
"1. Native build tools\n"
"Install CMake, Ninja, Python 3, and either Clang or GCC\nCheck tools\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", "linux-x64",
"--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-linux-x64"
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()
self.assertIn("Install CMake, Ninja, Python 3", readme)
self.assertIn("native build tools", readme)
self.assertNotIn("cmake-clang-v1", readme)
self.assertEqual(platform_copy.audit(self.root / "dist/test-0.0.1-linux-x64.zip")["status"], "pass")
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)
class PlatformCopyTests(unittest.TestCase):
def test_gate_requires_real_platform_copy_and_controls(self):
with tempfile.TemporaryDirectory() as tmp:
for platform in ("windows-x64", "linux-x64", "macos-arm64", "macos-x64"):
windows = platform.startswith("windows")
host = "Fixture.exe" if windows else "Fixture"
readme = "1. Run {}.\n2. Provide a legal retail BIOS (required).\n".format(host)
if windows:
readme += "downloads cmake-clang-v1 toolchain pack\n"
controls = "Download latest portable toolchain Select toolchain zip cmake-clang-v1"
else:
readme += "Install CMake, Ninja, Python 3 and a C/C++ compiler; available on PATH; native build tools\n"
controls = "1. Native build tools Install CMake, Ninja, Python 3, and either Clang or GCC Check tools"
archive = Path(tmp) / ("setup-" + platform + ".zip")
def write(extra=""):
with zipfile.ZipFile(archive, "w") as output:
output.writestr("README-SETUP.txt", readme)
output.writestr(host, controls + extra)
write()
self.assertEqual(platform_copy.audit(archive)["status"], "pass")
if not windows:
write("1. Portable toolchain")
with self.assertRaisesRegex(ValueError, "forbidden"):
platform_copy.audit(archive)
class NativeToolchainTests(unittest.TestCase):
def test_cli_does_not_activate_cached_packs_on_posix(self):
tree = ast.parse((ROOT / "psxrecomp_cli.py").read_text(encoding="utf-8"))
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef)
and node.name == "activate_embedded_toolchain")
function.returns = None
for argument in function.args.args:
argument.annotation = None
namespace = {"sys": types.SimpleNamespace(platform="linux"),
"resolve_toolchain_bin": mock.Mock(side_effect=AssertionError("cached pack"))}
exec(compile(ast.Module(body=[function], type_ignores=[]), "cli_activation", "exec"), namespace)
self.assertFalse(namespace[function.name](Path(".")))
namespace["resolve_toolchain_bin"].assert_not_called()
def test_host_native_guards_precede_cache_and_download_paths(self):
source = (ROOT / "host/psxrecomp_codegen_host.c").read_text(encoding="utf-8")
for start, native, portable in (
("static void activate_toolchain_path(void)", "return;", "if (!resolve_toolchain_bin"),
("static int host_toolchain_is_ready(void)", "return host_system_toolchain_ready();", "migrate_legacy_psxrecomp_toolchain();"),
("static int host_ensure_toolchain_with_progress(", "if (host_system_toolchain_ready())", "migrate_legacy_psxrecomp_toolchain();")):
body = source[source.index(start):]
self.assertLess(body.index("#if !defined(_WIN32)"), body.index(native))
self.assertLess(body.index(native), body.index(portable))
body = source[source.index("static int host_system_toolchain_ready(void)"):
source.index("/* True when pack bin/ can compile+link")]
for tool in ("cmake", "ninja", "python3", "cc", "c++"):
self.assertIn('find_on_path("' + tool + '"', body)
self.assertIn("xcode-select -p", body)
def test_posix_uses_native_probes_and_never_touches_pack_cache_or_network(self):
for platform in ("linux", "darwin"):
with self.subTest(platform=platform), \
mock.patch.object(toolchain_pack.sys, "platform", platform), \
mock.patch.object(toolchain_pack.shutil, "which", side_effect=lambda name: "/native/bin/" + name), \
mock.patch.object(toolchain_pack.subprocess, "run", return_value=types.SimpleNamespace(returncode=0)) as probe, \
mock.patch.object(toolchain_pack, "migrate_legacy_psxrecomp_cache", side_effect=AssertionError("touched cache")):
self.assertEqual(toolchain_pack.ensure_toolchain(download=True), Path("/native/bin"))
self.assertEqual(len(probe.call_args_list), 5)
self.assertTrue(all(call.args[0][-1] == "--version" for call in probe.call_args_list))
def test_posix_missing_and_broken_tools_fail_without_download(self):
for broken in (False, True):
with self.subTest(broken=broken), \
mock.patch.object(toolchain_pack, "sys_platform_is_windows", return_value=False), \
mock.patch.object(toolchain_pack.shutil, "which", return_value="/native/cmake" if broken else None), \
mock.patch.object(toolchain_pack.subprocess, "run", return_value=types.SimpleNamespace(returncode=1)), \
mock.patch.object(toolchain_pack, "migrate_legacy_psxrecomp_cache", side_effect=AssertionError("touched cache")):
with self.assertRaisesRegex(RuntimeError, "package manager"):
toolchain_pack.ensure_toolchain(download=True)
def test_windows_retains_existing_portable_pack_activation(self):
binary = Path("fixture-pack/bin")
with mock.patch.object(toolchain_pack, "sys_platform_is_windows", return_value=True), \
mock.patch.object(toolchain_pack, "is_windows_store_python", return_value=False), \
mock.patch.object(toolchain_pack, "migrate_legacy_psxrecomp_cache"), \
mock.patch.object(toolchain_pack, "resolve_toolchain_bin", return_value=binary), \
mock.patch.object(toolchain_pack, "toolchain_bin_runs", return_value=True), \
mock.patch.object(toolchain_pack, "register_toolchain_user_env"), \
mock.patch.object(toolchain_pack, "activate_toolchain_bin") as activate:
self.assertEqual(toolchain_pack.ensure_toolchain(download=True), binary)
activate.assert_called_once_with(binary, log=None)
if __name__ == "__main__":
unittest.main()