mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
fa83aeb064
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
"""psxrecomp_cli.py rebuild: host portability and configure-flag precedence.
|
|
|
|
No cmake, compiler or game runs: configure/build are replaced by recorders, so
|
|
these cases pin what the CLI asks CMake for, and that the pre-build mtime clamp
|
|
works on hosts without lutimes (Windows CPython).
|
|
"""
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path[:0] = [str(ROOT), str(ROOT / 'tools')]
|
|
import psxrecomp_cli as cli # noqa: E402
|
|
|
|
|
|
class ClampFutureMtimesTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
self.future = time.time() + 3600
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def touch_future(self, rel):
|
|
path = self.root / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text('x')
|
|
os.utime(path, (self.future, self.future))
|
|
return path
|
|
|
|
def test_clamps_without_lutimes(self):
|
|
# Windows CPython: os.utime is absent from supports_follow_symlinks and
|
|
# follow_symlinks=False raises NotImplementedError, not OSError.
|
|
log = self.touch_future('build-release.log')
|
|
with mock.patch.object(cli, '_UTIME_NOFOLLOW', False):
|
|
self.assertEqual(cli.clamp_future_mtimes(self.root), 1)
|
|
self.assertLessEqual(log.stat().st_mtime, time.time())
|
|
|
|
def test_skips_build_dir(self):
|
|
self.touch_future('build-release/out.o')
|
|
src = self.touch_future('src/a.c')
|
|
self.assertEqual(
|
|
cli.clamp_future_mtimes(self.root, skip=self.root / 'build-release'), 1)
|
|
self.assertLessEqual(src.stat().st_mtime, time.time())
|
|
|
|
def test_never_retimes_symlink_target_without_lutimes(self):
|
|
outside = tempfile.TemporaryDirectory()
|
|
self.addCleanup(outside.cleanup)
|
|
target = Path(outside.name) / 'target.txt'
|
|
target.write_text('x')
|
|
os.utime(target, (self.future, self.future))
|
|
try:
|
|
os.symlink(target, self.root / 'link.txt')
|
|
except (OSError, NotImplementedError):
|
|
self.skipTest('symlinks unavailable on this host')
|
|
with mock.patch.object(cli, '_UTIME_NOFOLLOW', False):
|
|
self.assertEqual(cli.clamp_future_mtimes(self.root), 0)
|
|
self.assertGreater(target.stat().st_mtime, time.time())
|
|
|
|
|
|
class RebuildConfigureFlagsTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
(self.root / 'game.toml').write_text('[game]\nid = "TEST-00001"\n')
|
|
self.configures = []
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def rebuild(self, extra, pgo=False):
|
|
args = cli.build_parser().parse_args(
|
|
['rebuild', '--config', str(self.root / 'game.toml'),
|
|
'--project-root', str(self.root), '--build-dir', 'build-x',
|
|
*(['--force-pgo', '--disc', str(self.root / 'game.toml')] if pgo
|
|
else ['--no-pgo']),
|
|
*[f'--cmake-extra={e}' for e in extra]])
|
|
exe = self.root / 'build-x' / 'psx-runtime.exe'
|
|
|
|
def configure(root, build_dir, pgo, extra, progress):
|
|
self.configures.append((pgo, list(extra)))
|
|
|
|
with mock.patch.object(cli, 'activate_embedded_toolchain', return_value=True), \
|
|
mock.patch.object(cli, '_cmake_configure', side_effect=configure), \
|
|
mock.patch.object(cli, '_cmake_build'), \
|
|
mock.patch.object(cli, 'run_pgo_train'), \
|
|
mock.patch.object(cli, '_resolve_runtime_exe', return_value=(exe, '')), \
|
|
mock.patch.object(cli, 'stage_overlay_toolchain_for_product'):
|
|
self.assertEqual(cli.cmd_rebuild(args, mock.Mock()), cli.EXIT_OK)
|
|
|
|
@staticmethod
|
|
def debug_tools(extra):
|
|
values = [e.split('=', 1)[1] for e in extra if e.startswith('-DPSX_DEBUG_TOOLS=')]
|
|
return values[-1] # CMake applies the last -D for a name
|
|
|
|
def test_shipping_build_defaults_debug_tools_off(self):
|
|
self.rebuild([])
|
|
self.assertEqual([self.debug_tools(e) for _, e in self.configures], ['OFF'])
|
|
|
|
def test_cmake_extra_overrides_debug_tools(self):
|
|
self.rebuild(['-DPSX_DEBUG_TOOLS=ON'])
|
|
self.assertEqual([self.debug_tools(e) for _, e in self.configures], ['ON'])
|
|
|
|
def test_pgo_instrument_stage_keeps_debug_tools_on(self):
|
|
# PGO training drives the debug server; a user OFF must not break it,
|
|
# while the optimized stage still honours the user's choice.
|
|
self.rebuild(['-DPSX_DEBUG_TOOLS=OFF'], pgo=True)
|
|
self.assertEqual([(p, self.debug_tools(e)) for p, e in self.configures],
|
|
[('generate', 'ON'), ('use', 'OFF')])
|
|
|
|
|
|
class CmakeBuildToolTest(unittest.TestCase):
|
|
"""Build must run the cmake that configured the tree, never a PATH cmake.
|
|
|
|
On Windows a PATH ``cmake`` is often MSYS2's, which rewrites the Windows
|
|
build directory as ``/f/.../F:/...`` and fails.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.build = Path(self.tmp.name) / 'build-release'
|
|
self.build.mkdir()
|
|
self.configured = Path(self.tmp.name) / 'toolchain' / 'bin' / 'cmake.exe'
|
|
self.configured.parent.mkdir(parents=True)
|
|
self.configured.write_text('')
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def build_cmd(self):
|
|
calls = []
|
|
|
|
def run(cmd, **_):
|
|
calls.append(cmd)
|
|
return mock.Mock(returncode=0, stdout='', stderr='')
|
|
|
|
with mock.patch.object(cli.subprocess, 'run', side_effect=run):
|
|
cli._cmake_build(self.build, 'psx-runtime', mock.Mock())
|
|
return calls[0]
|
|
|
|
def test_uses_cmake_command_from_cache(self):
|
|
(self.build / 'CMakeCache.txt').write_text(
|
|
'CMAKE_GENERATOR:INTERNAL=Ninja\n'
|
|
f'CMAKE_COMMAND:INTERNAL={self.configured}\n')
|
|
with mock.patch.object(cli, '_which_tool', return_value=Path('/msys/usr/bin/cmake')):
|
|
self.assertEqual(self.build_cmd()[0], str(self.configured))
|
|
|
|
def test_falls_back_to_path_without_cache(self):
|
|
with mock.patch.object(cli, '_which_tool', return_value=Path('/usr/bin/cmake')):
|
|
self.assertEqual(self.build_cmd()[0], str(Path('/usr/bin/cmake')))
|
|
|
|
def test_stale_cache_entry_falls_back_to_path(self):
|
|
(self.build / 'CMakeCache.txt').write_text(
|
|
f'CMAKE_COMMAND:INTERNAL={self.configured.parent / "gone.exe"}\n')
|
|
with mock.patch.object(cli, '_which_tool', return_value=Path('/usr/bin/cmake')):
|
|
self.assertEqual(self.build_cmd()[0], str(Path('/usr/bin/cmake')))
|
|
|
|
def test_no_cmake_anywhere_is_an_error(self):
|
|
with mock.patch.object(cli, '_which_tool', return_value=None):
|
|
with self.assertRaises(RuntimeError):
|
|
cli._cmake_build(self.build, 'psx-runtime', mock.Mock())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|