Files
psxrecomp/runtime/tests/test_wtrace_dump.py
Matthew Stanley 4c6b3748ca debug: segment-mask wtrace PC filter; DMA entries never inherit CPU store PC
Also: wtrace_dump_test skips (exit 77) without a C compiler, supports cl, brace-matched extraction.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:42:45 -07:00

187 lines
7.6 KiB
Python

"""Execute the production trace-query handler against a wrapped fixture ring.
Compile only the handler and its real JSON helpers, replacing the transport
with stdout. This needs a C compiler, but no game image, SDL or live server.
Exits 77 (ctest SKIP_RETURN_CODE) when no C compiler is available.
"""
import json
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
SKIP_EXIT = 77
def matching_brace(source, open_index):
"""Index of the brace closing the one at open_index (skips strings/comments)."""
depth, i, n = 0, open_index, len(source)
while i < n:
c = source[i]
if source.startswith("/*", i):
i = source.index("*/", i + 2) + 1
elif source.startswith("//", i):
i = source.index("\n", i)
elif c in "\"'":
i += 1
while source[i] != c:
i += 2 if source[i] == "\\" else 1
elif c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return i
i += 1
raise AssertionError("unbalanced braces")
def find_compiler():
"""argv prefix for the C compiler named by CC (default cc), or None."""
setting = os.environ.get("CC", "cc")
if shutil.which(setting):
return [setting]
parts = shlex.split(setting, posix=os.name != "nt")
return parts if parts and shutil.which(parts[0]) else None
class WriteTraceQuery(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = find_compiler()
if compiler is None:
raise unittest.SkipTest("no C compiler (set CC)")
cls.temp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.temp.cleanup)
source = (Path(__file__).parents[1] / "src/debug_server.c").read_text(encoding="utf-8")
def function(name):
match = re.search(r"static [^;{}]+\b" + name + r"\([^;{}]*\)\s*\{", source)
if not match:
raise AssertionError(f"production function missing: {name}")
return source[match.start():matching_brace(source, match.end() - 1) + 1]
end = source.index("} WriteTraceEntry;") + len("} WriteTraceEntry;")
start = source.rfind("typedef struct {", 0, end)
harness = """#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define WRITE_TRACE_CAP 8
""" + source[start:end] + """
static WriteTraceEntry ring[WRITE_TRACE_CAP], *s_wtrace=ring;
static uint64_t s_wtrace_seq=10;
static uint32_t s_wtrace_head=2;
typedef struct { uint32_t pc; uint32_t gpr[32]; } CPUState;
static CPUState *debug_cpu_ptr = NULL;
static uint32_t s_frame_count = 0;
uint32_t g_debug_current_func_addr = 0;
uint32_t g_debug_last_store_pc = 0;
int g_dma_exec_depth = 0, g_dma_cur_ch = -1;
uint32_t g_dma_initiator_pc = 0;
static void debug_server_send_line(const char *line) { puts(line); }
static void send_err(int id, const char *error) { (void)id; (void)error; abort(); }
"""
for name in ("json_get_str", "json_get_int", "hex_to_u32", "wtrace_fill_entry",
"handle_wtrace_dump"):
harness += function(name) + "\n"
# seq 7 is a DMA entry with an unknown (0) initiator PC.
harness += """int main(int argc, char **argv) {
if (argc > 4 && !strcmp(argv[1], "fill")) { /* fill <dma_depth> <initiator> <cpu_store_pc> */
WriteTraceEntry e; memset(&e, 0, sizeof e);
g_dma_exec_depth = atoi(argv[2]); g_dma_cur_ch = 2;
g_dma_initiator_pc = (uint32_t)strtoul(argv[3], 0, 16);
g_debug_last_store_pc = (uint32_t)strtoul(argv[4], 0, 16);
wtrace_fill_entry(&e, 1, 0x1000, 0, 1, 4);
printf("{\\"pc\\":%u,\\"dma_ch\\":%d}\\n", e.pc, e.dma_ch);
return 0;
}
const uint32_t pcs[8]={0x80001000,0x80001004,0xA0001000,0x80001008,
0x8000100C,0x00000000,0x80001004,0x00001004};
for (int seq=2; seq<10; ++seq) {
WriteTraceEntry *e=&ring[seq%WRITE_TRACE_CAP];
e->seq=seq; e->pc=pcs[seq-2]; e->addr=0x1000+(seq-2)*4;
e->frame=10+(seq-2)/2; e->width=4; e->dma_ch=seq==5?2:(seq==7?3:-1);
}
handle_wtrace_dump(7,argc>1?argv[1]:"{}");
return 0;
}
"""
path = Path(cls.temp.name)
(path / "query.c").write_text(harness, encoding="utf-8", newline="\n")
cls.exe = path / ("query.exe" if os.name == "nt" else "query")
if Path(compiler[0]).stem.lower() in ("cl", "clang-cl"):
out = ["/nologo", "/Fe:" + str(cls.exe), "/Fo:" + str(path) + os.sep]
else:
out = ["-o", str(cls.exe)]
subprocess.run([*compiler, str(path / "query.c"), *out], check=True, cwd=path)
def query(self, **params):
result = subprocess.check_output([str(self.exe), json.dumps(params)], text=True)
reply = json.loads(result)
self.assertEqual(reply["emitted"], len(reply["entries"]))
return reply
def sequences(self, **params):
return [e["seq"] for e in self.query(**params)["entries"]]
def fill(self, depth, initiator, store_pc):
out = subprocess.check_output(
[str(self.exe), "fill", str(depth), initiator, store_pc], text=True)
return json.loads(out)
def test_fill_entry_producer_pc(self):
self.assertEqual(self.fill(0, "0", "80001234"), {"pc": 0x80001234, "dma_ch": -1})
self.assertEqual(self.fill(1, "80005000", "80001234"), {"pc": 0x80005000, "dma_ch": 2})
# Unknown initiator: the DMA entry must not inherit the stale CPU store PC.
self.assertEqual(self.fill(1, "0", "80001234"), {"pc": 0, "dma_ch": 2})
def test_default_wrapped_order(self):
self.assertEqual(self.sequences(), list(range(2, 10)))
def test_pc_range_exclusive_upper(self):
# seq 9 has KUSEG pc 0x1004, which aliases 0x80001004.
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C"), [3, 5, 8, 9])
def test_segments_alias(self):
self.assertEqual(self.sequences(pc_lo="A0001000", pc_hi="A0001004"), [2, 4])
self.assertEqual(self.sequences(pc_lo="00001000", pc_hi="00001004"), [2, 4])
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001008"), [3, 8, 9])
def test_unknown_dma_initiator_never_matches_pc_filter(self):
self.assertIn(7, self.sequences())
self.assertNotIn(7, self.sequences(pc_lo="0", pc_hi="FFFFFFFF"))
self.assertEqual(self.sequences(pc_hi="80001000"), [])
def test_newest_and_count_after_filter(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", newest=1, count=1), [9])
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", count=1), [3])
def test_combined_address_frame_filters(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C",
addr_lo="80001008", addr_hi="80001018",
frame_lo=11, frame_hi=12), [5])
def test_empty_ranges(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001004"), [])
self.assertEqual(self.sequences(pc_hi="0"), [])
def test_dma_recorded_initiator_and_response_bounds(self):
reply = self.query(pc_lo="80001008", pc_hi="8000100C")
self.assertEqual(reply["pc_lo"], "0x80001008")
self.assertEqual(reply["pc_hi"], "0x8000100C")
self.assertEqual(reply["entries"][0]["dma_ch"], 2)
self.assertEqual(reply["entries"][0]["seq"], 5)
if __name__ == "__main__":
if find_compiler() is None:
print("SKIP: no C compiler (set CC)")
sys.exit(SKIP_EXIT)
unittest.main()