mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
ab5d63e1f1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
187 lines
7.7 KiB
Python
187 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for cursor_writers' attribution of cursor writes."""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
import cursor_writers as cw
|
|
|
|
|
|
def w(addr, old, new, pc, seq=0, width=1, frame=0, func="0x1000"):
|
|
return {"seq": seq, "addr": "0x%08X" % (addr & 0x1FFFFFFF),
|
|
"old": "0x%08X" % old, "new": "0x%08X" % new,
|
|
"pc": pc, "func": func, "ra": "0x2000", "w": width, "frame": frame}
|
|
|
|
|
|
class PhysTest(unittest.TestCase):
|
|
def test_kseg0_maps_to_physical(self):
|
|
self.assertEqual(cw.phys(0x8004A5F5), 0x0004A5F5)
|
|
|
|
def test_physical_unchanged(self):
|
|
self.assertEqual(cw.phys(0x0004A5F5), 0x0004A5F5)
|
|
|
|
|
|
class DeltaTest(unittest.TestCase):
|
|
def test_simple_decrement(self):
|
|
self.assertEqual(cw.delta(w(0x8004A5F5, 5, 4, "0x1")), -1)
|
|
|
|
def test_double_step(self):
|
|
self.assertEqual(cw.delta(w(0x8004A5F5, 5, 3, "0x1")), -2)
|
|
|
|
def test_byte_wrap_reads_as_small_negative(self):
|
|
"""0 -> 255 at byte width is -1, not +255."""
|
|
self.assertEqual(cw.delta(w(0x8004A5F5, 0, 255, "0x1", width=1)), -1)
|
|
|
|
def test_word_width_increment(self):
|
|
self.assertEqual(cw.delta(w(0x8004A5F5, 7, 8, "0x1", width=4)), 1)
|
|
|
|
|
|
class RangeTest(unittest.TestCase):
|
|
def test_filters_and_sorts(self):
|
|
entries = [w(0x8004A5F5, 5, 4, "0x1", seq=9),
|
|
w(0x8004B000, 1, 2, "0x2", seq=8),
|
|
w(0x8004A50D, 7, 6, "0x3", seq=7)]
|
|
got = cw.writes_in_range(entries, 0x8004A500, 0x8004A600)
|
|
self.assertEqual([e["seq"] for e in got], [7, 9])
|
|
|
|
def test_upper_bound_is_exclusive(self):
|
|
entries = [w(0x8004A600, 1, 2, "0x1")]
|
|
self.assertEqual(cw.writes_in_range(entries, 0x8004A500, 0x8004A600), [])
|
|
|
|
|
|
class VerdictTest(unittest.TestCase):
|
|
def test_no_writes(self):
|
|
self.assertEqual(cw.verdict([])[0], "no_writes")
|
|
|
|
def test_single_write_double_step(self):
|
|
"""One write of -2: the step calculation doubled, not the input."""
|
|
got = cw.verdict([w(0x8004A5F5, 5, 3, "0x800684C0")], 0x8004A5F5)
|
|
self.assertEqual(got[0], "single_write_double_step")
|
|
|
|
def test_single_write_single_step(self):
|
|
got = cw.verdict([w(0x8004A5F5, 5, 4, "0x800684C0")], 0x8004A5F5)
|
|
# Capture-level verdict: nothing doubled anywhere.
|
|
self.assertEqual(got[0], "no_double_step")
|
|
# The event itself is still classified as a clean single step.
|
|
ev = cw.describe_event([w(0x8004A5F5, 5, 4, "0x800684C0")], 0x8004A5F5)
|
|
self.assertEqual(ev["kind"], "single_write_single_step")
|
|
self.assertEqual(ev["total"], -1)
|
|
|
|
def test_two_distinct_pcs_is_two_paths(self):
|
|
got = cw.verdict([w(0x8004A5F5, 5, 4, "0x11110000", seq=1),
|
|
w(0x8004A5F5, 4, 3, "0x22220000", seq=2)],
|
|
0x8004A5F5)
|
|
self.assertEqual(got[0], "two_paths")
|
|
|
|
def test_same_pc_twice_is_one_path(self):
|
|
got = cw.verdict([w(0x8004A5F5, 5, 4, "0x11110000", seq=1),
|
|
w(0x8004A5F5, 4, 3, "0x11110000", seq=2)],
|
|
0x8004A5F5)
|
|
self.assertEqual(got[0], "one_path_twice")
|
|
|
|
|
|
class SummarizeTest(unittest.TestCase):
|
|
def test_groups_by_address_and_collects_pcs(self):
|
|
writes = [w(0x8004A5F5, 5, 4, "0xA", seq=1),
|
|
w(0x8004A5F5, 4, 3, "0xB", seq=2),
|
|
w(0x8004A50D, 7, 6, "0xC", seq=3)]
|
|
by_addr, focus = cw.summarize(writes, 0x8004A5F5)
|
|
self.assertEqual(len(by_addr), 2)
|
|
self.assertEqual(by_addr[0x0004A5F5]["pcs"], ["0xA", "0xB"])
|
|
self.assertEqual(len(focus), 2)
|
|
|
|
|
|
class WidthCoverageTest(unittest.TestCase):
|
|
"""A wide store changes bytes that never appear as its address."""
|
|
|
|
def test_halfword_write_covers_high_byte(self):
|
|
e = w(0x8004A5F4, 0x05F9, 0x0366, "0x80024E3C", width=2)
|
|
self.assertTrue(cw.covers(e, 0x8004A5F5))
|
|
|
|
def test_write_does_not_cover_beyond_its_width(self):
|
|
e = w(0x8004A5F4, 0, 1, "0x1", width=2)
|
|
self.assertFalse(cw.covers(e, 0x8004A5F6))
|
|
|
|
def test_byte_value_extracts_the_right_lane(self):
|
|
e = w(0x8004A5F4, 0x05F9, 0x0366, "0x1", width=2)
|
|
self.assertEqual(cw.byte_value(e, "old", 0x8004A5F5), 5)
|
|
self.assertEqual(cw.byte_value(e, "new", 0x8004A5F5), 3)
|
|
self.assertEqual(cw.byte_value(e, "old", 0x8004A5F4), 0xF9)
|
|
|
|
def test_byte_delta_of_covered_byte(self):
|
|
e = w(0x8004A5F4, 0x05F9, 0x0366, "0x1", width=2)
|
|
self.assertEqual(cw.byte_delta(e, 0x8004A5F5), -2)
|
|
|
|
def test_wide_write_leaving_the_byte_alone_is_not_a_move(self):
|
|
"""Covering the byte is not the same as changing it."""
|
|
e = w(0x8004A5F4, 0x0500, 0x05FF, "0x1", width=2)
|
|
self.assertEqual(cw.byte_delta(e, 0x8004A5F5), 0)
|
|
self.assertEqual(cw.touching_writes([e], 0x8004A5F5), [])
|
|
|
|
def test_touching_writes_finds_wide_stores(self):
|
|
es = [w(0x8004A5F4, 0x05F9, 0x0366, "0x1", width=2),
|
|
w(0x8004A5F4, 0x0366, 0x0823, "0x1", width=2)]
|
|
self.assertEqual(len(cw.touching_writes(es, 0x8004A5F5)), 2)
|
|
|
|
def test_verdict_uses_byte_delta_not_word_delta(self):
|
|
"""-2 on the byte, though the halfword moved by -659."""
|
|
e = w(0x8004A5F4, 0x05F9, 0x0366, "0x1", width=2)
|
|
kind, _ = cw.verdict([e], 0x8004A5F5)
|
|
self.assertEqual(kind, "single_write_double_step")
|
|
|
|
|
|
class GroupEventsTest(unittest.TestCase):
|
|
"""Several presses in one capture must not be read as one event."""
|
|
|
|
def test_writes_in_the_same_frame_are_one_event(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0xA", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0xB", seq=2, frame=100)]
|
|
self.assertEqual(len(cw.group_events(ws, 0x8004A5F5)), 1)
|
|
|
|
def test_writes_seconds_apart_are_separate_events(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0xA", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0xA", seq=2, frame=250)]
|
|
self.assertEqual(len(cw.group_events(ws, 0x8004A5F5)), 2)
|
|
|
|
def test_adjacent_frames_stay_together(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0xA", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0xB", seq=2, frame=102)]
|
|
self.assertEqual(len(cw.group_events(ws, 0x8004A5F5)), 1)
|
|
|
|
|
|
class PerPressVerdictTest(unittest.TestCase):
|
|
def test_two_paths_on_a_doubled_press(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0x11110000", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0x22220000", seq=2, frame=100)]
|
|
self.assertEqual(cw.verdict(ws, 0x8004A5F5)[0], "two_paths")
|
|
|
|
def test_one_path_twice_on_a_doubled_press(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0x11110000", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0x11110000", seq=2, frame=101)]
|
|
self.assertEqual(cw.verdict(ws, 0x8004A5F5)[0], "one_path_twice")
|
|
|
|
def test_single_write_of_two_steps(self):
|
|
ws = [w(0x8004A5F5, 5, 3, "0x11110000", seq=1, frame=100)]
|
|
self.assertEqual(cw.verdict(ws, 0x8004A5F5)[0],
|
|
"single_write_double_step")
|
|
|
|
def test_clean_presses_report_no_doubling(self):
|
|
ws = [w(0x8004A5F5, 5, 4, "0xA", seq=1, frame=100),
|
|
w(0x8004A5F5, 4, 3, "0xA", seq=2, frame=300)]
|
|
self.assertEqual(cw.verdict(ws, 0x8004A5F5)[0], "no_double_step")
|
|
|
|
def test_mixed_capture_reports_the_doubled_presses(self):
|
|
"""Single-step presses in the same capture must not mask the bug."""
|
|
ws = [w(0x8004A5F5, 9, 8, "0xA", seq=1, frame=100),
|
|
w(0x8004A5F5, 8, 7, "0xA", seq=2, frame=300),
|
|
w(0x8004A5F5, 7, 6, "0xB", seq=3, frame=500),
|
|
w(0x8004A5F5, 6, 5, "0xC", seq=4, frame=500)]
|
|
self.assertEqual(cw.verdict(ws, 0x8004A5F5)[0], "two_paths")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|