Files
psxrecomp/tools/test_compile_overlays_static_split.py

308 lines
13 KiB
Python

"""Static-mode output split + worker contract for compile_overlays.py.
python tools/test_compile_overlays_static_split.py
"""
import os
import pickle
import shutil
import subprocess
import sys
import tempfile
import textwrap
import time
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import compile_overlays # noqa: E402
def part(ns, body, symbols):
return {
'src': f'#include "psx_runtime.h"\n{body}\n',
'namespace': ns,
'variants': [{'addr': 0x80100000 + i * 4, 'symbol': s, 'crc': 1,
'ranges': ((0x00100000, 8),)} for i, s in enumerate(symbols)],
'func_addrs': set(), 'symbols': {}, 'ids_by_addr': {},
'continuation_owners': {},
}
class StaticSplitTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.out = os.path.join(self.tmp.name, 'overlays_static.c')
def tearDown(self):
self.tmp.cleanup()
def test_split_writes_dispatcher_prototypes_and_parts(self):
parts = [part('ov_a', 'void ov_a_func_80100000(CPUState *cpu) {}',
['ov_a_func_80100000']),
part('ov_b', 'void ov_b_func_80100004(CPUState *cpu) {}',
['ov_b_func_80100004'])]
variants = [v for p in parts for v in p['variants']]
written = compile_overlays.write_static_outputs(
self.out, parts, variants, '/* dispatch */\n')
self.assertEqual([os.path.basename(w) for w in written],
['overlays_static.c', 'overlays_static_0000.c',
'overlays_static_0001.c'])
main = open(self.out, encoding='utf-8').read()
self.assertIn('#include "psx_runtime.h"', main)
self.assertIn('void ov_a_func_80100000(CPUState *cpu);', main)
self.assertIn('void ov_b_func_80100004(CPUState *cpu);', main)
self.assertIn('/* dispatch */', main)
self.assertNotIn('ov_a_func_80100000(CPUState *cpu) {}', main)
p0 = open(written[1], encoding='utf-8').read()
self.assertIn('ov_a_func_80100000(CPUState *cpu) {}', p0)
self.assertIn('#include "psx_runtime.h"', p0)
self.assertNotIn('ov_b_func', p0)
def test_unchanged_part_keeps_mtime_and_stale_parts_are_removed(self):
parts = [part('ov_a', 'int a;', ['ov_a_func_80100000']),
part('ov_b', 'int b;', ['ov_b_func_80100004']),
part('ov_c', 'int c;', ['ov_c_func_80100008'])]
variants = [v for p in parts for v in p['variants']]
first = compile_overlays.write_static_outputs(
self.out, parts, variants, '/* d */\n')
self.assertEqual(len(first), 4)
old_mtime = os.stat(first[1]).st_mtime_ns
os.utime(first[1], ns=(old_mtime - 5_000_000_000,
old_mtime - 5_000_000_000))
old_mtime = os.stat(first[1]).st_mtime_ns
time.sleep(0.01)
# Drop the last part; part 0 unchanged, part 1 changed.
parts[1] = part('ov_b', 'int b2;', ['ov_b_func_80100004'])
parts = parts[:2]
variants = [v for p in parts for v in p['variants']]
second = compile_overlays.write_static_outputs(
self.out, parts, variants, '/* d */\n')
self.assertEqual(len(second), 3)
self.assertEqual(os.stat(second[1]).st_mtime_ns, old_mtime,
'unchanged part must not be rewritten')
self.assertIn('int b2;', open(second[2], encoding='utf-8').read())
self.assertFalse(os.path.exists(first[3]), 'stale part must be deleted')
self.assertEqual(sorted(os.listdir(self.tmp.name)),
['overlays_static.c', 'overlays_static_0000.c',
'overlays_static_0001.c'])
def test_single_file_mode_is_monolithic_and_cleans_parts(self):
parts = [part('ov_a', 'int a;', ['ov_a_func_80100000'])]
variants = parts[0]['variants']
compile_overlays.write_static_outputs(self.out, parts, variants, '/* d */\n')
self.assertTrue(os.path.exists(os.path.join(self.tmp.name,
'overlays_static_0000.c')))
written = compile_overlays.write_static_outputs(
self.out, parts, variants, '/* d */\n', single_file=True)
self.assertEqual(written, [self.out])
self.assertEqual(os.listdir(self.tmp.name), ['overlays_static.c'])
main = open(self.out, encoding='utf-8').read()
self.assertIn('int a;', main)
self.assertIn('/* d */', main)
self.assertNotIn('(CPUState *cpu);', main)
def test_static_part_paths_only_matches_numbered_siblings(self):
for name in ('overlays_static_0000.c', 'overlays_static_0001.c',
'overlays_static.c', 'overlays_static_x.c',
'other_0000.c'):
open(os.path.join(self.tmp.name, name), 'w').close()
found = [os.path.basename(p)
for p in compile_overlays.static_part_paths(self.out)]
self.assertEqual(found, ['overlays_static_0000.c',
'overlays_static_0001.c'])
class StaticDispatchApiTests(unittest.TestCase):
def test_empty_dispatcher_exports_can_dispatch_api(self):
dispatch = compile_overlays.generate_overlay_dispatch([])
self.assertIn('int psx_overlay_static_can_dispatch(uint32_t addr)', dispatch)
self.assertIn('int psx_overlay_dispatch(CPUState *cpu, uint32_t addr)', dispatch)
self.assertIn('psx_overlay_static_find_variant(addr)', dispatch)
def test_can_dispatch_uses_crc_lookup_and_does_not_invoke_cpu(self):
cc = shutil.which('gcc') or shutil.which('clang')
if cc is None:
self.skipTest('no C compiler available')
variants = [
{'addr': 0x80100000, 'symbol': 'ov_func_a', 'crc': 0x11111111,
'ranges': ((0x00100000, 8),)},
{'addr': 0x80100000, 'symbol': 'ov_func_b', 'crc': 0x22222222,
'ranges': ((0x00100000, 8),)},
]
dispatch = compile_overlays.generate_overlay_dispatch(variants)
with tempfile.TemporaryDirectory() as tmp:
Path(tmp, 'psx_runtime.h').write_text(
'#include <stdint.h>\n'
'typedef struct CPUState { uint32_t pc; } CPUState;\n',
encoding='utf-8')
exe = os.path.join(tmp, 'dispatch_api.exe' if os.name == 'nt'
else 'dispatch_api')
src = os.path.join(tmp, 'dispatch_api.c')
Path(src).write_text(textwrap.dedent(f'''
#include "psx_runtime.h"
#include <stdio.h>
void ov_func_a(CPUState *cpu);
void ov_func_b(CPUState *cpu);
{dispatch}
static uint32_t resident_crc;
static int match_checks;
static int calls_a;
static int calls_b;
int psx_overlay_static_code_matches(
const uint32_t *lo_len_pairs, uint32_t count,
uint32_t expected_crc)
{{
(void)lo_len_pairs;
(void)count;
match_checks++;
return expected_crc == resident_crc;
}}
void ov_func_a(CPUState *cpu) {{ (void)cpu; calls_a++; }}
void ov_func_b(CPUState *cpu) {{ (void)cpu; calls_b++; }}
int main(void)
{{
CPUState cpu = {{0}};
uint64_t checks = 0, hits = 0, vm = 0, am = 0;
resident_crc = 0x22222222u;
if (!psx_overlay_static_can_dispatch(0x80100000u)) return 1;
if (calls_a != 0 || calls_b != 0) return 2;
psx_overlay_static_get_stats(&checks, &hits, &vm, &am);
if (checks != 2 || hits != 0 || vm != 1 || am != 0) {{
fprintf(stderr, "stats after can: %llu %llu %llu %llu\\n",
(unsigned long long)checks,
(unsigned long long)hits,
(unsigned long long)vm,
(unsigned long long)am);
return 3;
}}
if (!psx_overlay_dispatch(&cpu, 0x80100000u)) return 4;
if (calls_a != 0 || calls_b != 1) return 5;
if (match_checks != 3) return 6;
resident_crc = 0x33333333u;
if (psx_overlay_static_can_dispatch(0x80100000u)) return 7;
if (calls_a != 0 || calls_b != 1) return 8;
if (psx_overlay_static_can_dispatch(0x80100004u)) return 9;
if (calls_a != 0 || calls_b != 1) return 10;
return 0;
}}
'''), encoding='utf-8')
subprocess.run([cc, src, '-I', tmp, '-std=c99', '-Wall', '-Werror',
'-o', exe], check=True)
subprocess.run([exe], check=True)
class StaticWorkerContractTests(unittest.TestCase):
def test_worker_is_picklable_for_a_process_pool(self):
# ProcessPoolExecutor pickles the callable by qualified name; a nested
# closure would fail here. This is the guard for that regression.
fn = pickle.loads(pickle.dumps(compile_overlays.static_capture_job))
self.assertIs(fn, compile_overlays.static_capture_job)
def test_data_only_capture_reports_skip_with_captured_log(self):
import argparse
import base64
cap = {
'schema': 'psxrecomp overlay capture v2',
'load_addr': '0x80100000',
'size': 16,
'bytes_b64': base64.b64encode(b'\0' * 16).decode('ascii'),
'executed_pcs': [], 'dispatch_entry_pcs': [],
'function_entry_pcs': [],
}
args = argparse.Namespace(recompiler='unused', game_toml='unused',
cps=False, project_root=None)
res = compile_overlays.static_capture_job(cap, args, {}, set(),
'overlays_static.c')
self.assertEqual(res['outcome'], 'skip')
self.assertIsNone(res['part'])
self.assertIn('SKIP: no walk-root seeds', res['log'])
self.assertEqual(res['requested_entries'], set())
HOST_SRC = """#include "psx_runtime.h"
void ov_x_func_80100000(CPUState* cpu);
void ov_x_func_80100000(CPUState* cpu)
{
debug_server_log_call_entry(0x80100000u);
block_80100010:
cpu->pc = 1;
block_80100020:
cpu->pc = 2;
block_80100030:
cpu->pc = 3;
}
void ov_x_func_80100100(CPUState* cpu)
{
if (cpu->pc != 0u) {
uint32_t _cont = cpu->pc; cpu->pc = 0;
switch (_cont) {
case 0x80100100u: break;
default: cpu->pc = _cont; psx_native_bad_entry(cpu, 0x80100100u, _cont); return;
}
}
debug_server_log_call_entry(0x80100100u);
block_80100110:
cpu->pc = 4;
block_80100120:
cpu->pc = 5;
}
"""
class ResumeCaseBatchTests(unittest.TestCase):
"""add_cps_resume_cases must be byte-identical to looping
add_cps_resume_case entry by entry (the pre-batch implementation)."""
def _sequential(self, src, requests):
ok = {}
for host_symbol, host, entries in requests:
for entry in entries:
src, good = compile_overlays.add_cps_resume_case(
src, host_symbol, host, entry)
if good:
ok.setdefault(host_symbol, set()).add(entry)
return src, ok
def test_batch_matches_sequential_fresh_switch_and_existing_switch(self):
requests = [
('ov_x_func_80100000', 0x80100000,
[0x80100010, 0x80100020, 0x80100030, 0x80100999]), # no such block
('ov_x_func_80100100', 0x80100100, [0x80100110, 0x80100120]),
('ov_x_func_missing', 0x80100200, [0x80100210]), # no definition
]
seq_src, seq_ok = self._sequential(HOST_SRC, requests)
bat_src, bat_ok = compile_overlays.add_cps_resume_cases(HOST_SRC, requests)
self.assertEqual(bat_src, seq_src)
self.assertEqual({k: v for k, v in bat_ok.items() if v}, seq_ok)
self.assertIn('case 0x80100010u: goto block_80100010;', bat_src)
self.assertIn('case 0x80100120u: goto block_80100120;', bat_src)
self.assertNotIn('80100999', bat_src)
# Re-applying is a no-op (arms already present).
again_src, again_ok = compile_overlays.add_cps_resume_cases(bat_src, requests)
self.assertEqual(again_src, bat_src)
self.assertEqual(again_ok['ov_x_func_80100000'],
{0x80100010, 0x80100020, 0x80100030})
def test_batch_edits_are_confined_to_each_host_segment(self):
requests = [('ov_x_func_80100100', 0x80100100, [0x80100110])]
bat_src, _ = compile_overlays.add_cps_resume_cases(HOST_SRC, requests)
marker = 'void ov_x_func_80100100(CPUState* cpu)\n{'
head = bat_src[:bat_src.index(marker)]
self.assertEqual(head, HOST_SRC[:HOST_SRC.index(marker)])
if __name__ == '__main__':
unittest.main()