Files
psxrecomp/runtime/tests/test_game_runtime_pgxp.py
T
Shokunin 2ddb2e0ec4 Let a title build its one runtime as the PGXP hook flavor
psxrecomp_add_game_runtime gains a PGXP option and forwards it to
psxrecomp_add_runtime_target, which already supports a PGXP primary
(-DPSX_PGXP=1, overlay flavor 2, published flavor file, no _pgxp suffix and no
clone). Until now a title could only get the hook flavor through
PSX_PGXP_VARIANT's side-by-side clone, or by leaving a bare PGXP in the
forwarded tail, where an earlier multi-value argument such as
CODEGEN_SETUP_SOURCES or GEN_FULL_GLOB swallowed it as a file name.

The generated C already carries the PGXP_*() hook sites, so a title's
committed generated/ does not change. Default unchanged: titles that do not
pass PGXP build the base flavor as before.

game_runtime_pgxp_test runs the real function body under cmake -P with the
runtime target stubbed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:56:05 -07:00

151 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""psxrecomp_add_game_runtime(... PGXP) builds the title's one runtime as the
PGXP hook flavor (docs/ENHANCEMENTS.md G1.11).
Runs the REAL psxrecomp_add_game_runtime body from runtime.cmake under
`cmake -P`, with psxrecomp_add_runtime_target and the target-scoped commands
stubbed to record what they receive. Checks that:
1. without PGXP nothing PGXP-related is forwarded (default unchanged);
2. PGXP is forwarded to psxrecomp_add_runtime_target, as the bare option
that selects the hook flavor for the primary (no PGXP_CLONE, so no _pgxp
suffix and no sibling);
3. PGXP written after CODEGEN_SETUP_SOURCES is still the option, not a
source file (it used to be swallowed by the multi-value argument);
4. the setup-host path (no generated C) forwards it the same way.
"""
import argparse
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
RUNTIME_CMAKE = REPO / "runtime" / "runtime.cmake"
def extract_function(text: str, name: str) -> str:
m = re.search(r"^function\(%s\b.*?^endfunction\(\)" % re.escape(name),
text, re.S | re.M)
if not m:
raise SystemExit("FAIL: %s not found in runtime.cmake" % name)
return m.group(0)
STUBS = r'''
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CURRENT_SOURCE_DIR "@SRC@")
set(PSXRECOMP_ROOT "@SRC@/psxrecomp")
set(CMAKE_BUILD_TYPE Release)
set(PSX_RECOMP_UI OFF)
function(psxrecomp_add_runtime_target target)
set_property(GLOBAL PROPERTY RT_TARGET "${target}")
set_property(GLOBAL PROPERTY RT_ARGS "${ARGN}")
endfunction()
function(psx_split_exclude_builtin_mods ids tail)
set(${ids} "${ARGN}" PARENT_SCOPE)
set(${tail} "" PARENT_SCOPE)
endfunction()
function(target_compile_definitions)
endfunction()
function(target_include_directories)
endfunction()
'''
REPORT = r'''
get_property(_t GLOBAL PROPERTY RT_TARGET)
get_property(_a GLOBAL PROPERTY RT_ARGS)
message(STATUS "RT_TARGET=${_t}")
message(STATUS "RT_ARGS=${_a}")
'''
def run_case(cmake: str, body: str, call: str, game_c: bool):
with tempfile.TemporaryDirectory() as td:
tmp = Path(td)
(tmp / "generated").mkdir()
if game_c:
(tmp / "generated" / "GAME_dispatch.c").write_text("/* stub */\n")
(tmp / "generated" / "GAME_full.c").write_text("/* stub */\n")
(tmp / "VERSION").write_text("1.2.3\n")
driver = tmp / "driver.cmake"
driver.write_text(STUBS.replace("@SRC@", tmp.as_posix()) + body +
"\n" + call + "\n" + REPORT)
r = subprocess.run([cmake, "-P", str(driver)], capture_output=True,
text=True)
out = r.stdout + r.stderr
if r.returncode != 0:
return None, None, out
target = re.search(r"RT_TARGET=(.*)", out).group(1).strip()
args = re.search(r"RT_ARGS=(.*)", out).group(1).strip().split(";")
return target, args, out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cmake", default=shutil.which("cmake") or "cmake")
args = ap.parse_args()
if shutil.which(args.cmake) is None and not Path(args.cmake).is_file():
print("SKIP: no cmake executable available (%s)" % args.cmake)
return 0
body = extract_function(RUNTIME_CMAKE.read_text(encoding="utf-8"),
"psxrecomp_add_game_runtime")
failures = 0
def check(ok, what, out=""):
nonlocal failures
print(("ok " if ok else "FAIL ") + what)
if not ok:
failures += 1
if out:
print(out)
base = ('psxrecomp_add_game_runtime(psx-runtime\n'
' WINDOW_TITLE "Probe"\n'
' GEN_MARKER "generated/GAME_dispatch.c"\n'
' GEN_FULL_GLOB "generated/GAME_full.c"\n'
' @EXTRA@)')
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", ""), True)
check(a is not None, "configure without PGXP runs", out)
if a is not None:
check(t == "psx-runtime", "the primary target is psx-runtime")
check("PGXP" not in a and "PGXP_CLONE" not in a,
"without PGXP nothing PGXP-related is forwarded")
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", "PGXP"), True)
check(a is not None, "configure with PGXP runs", out)
if a is not None:
check(t == "psx-runtime", "PGXP keeps the primary target name")
check(a.count("PGXP") == 1, "PGXP is forwarded once")
check("PGXP_CLONE" not in a,
"the primary is not marked as the _pgxp clone (no suffix)")
late = base.replace(
"@EXTRA@",
'CODEGEN_SETUP_SOURCES "codegen_setup.c"\n PGXP')
t, a, out = run_case(args.cmake, body, late, True)
check(a is not None, "configure with PGXP after CODEGEN_SETUP_SOURCES runs",
out)
if a is not None:
check("PGXP" in a,
"PGXP after a multi-value argument is still the option")
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", "PGXP"),
False)
check(a is not None, "setup-host configure with PGXP runs", out)
if a is not None:
check("PGXP" in a, "the setup host gets the same flavor")
if failures:
print("test_game_runtime_pgxp: %d FAILURES" % failures)
return 1
print("test_game_runtime_pgxp: all checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())