diff --git a/.gitignore b/.gitignore index 32eb0523..f0a98020 100644 --- a/.gitignore +++ b/.gitignore @@ -111,8 +111,19 @@ games/*/* # Session-scratch debug scripts (hardcoded local paths; kept on disk, not tracked) tools/_options_session/ -# Compiled test binaries (build artifacts; the runtime/tests/*.c sources are tracked) +# Compiled test binaries (build artifacts; the runtime/tests/*.c sources are tracked). +# The *.exe rule alone missed extension-less Linux binaries, and three of them +# (test_netplay_snap_ring, test_netplay_hash_confirm, test_netplay_input_hist) +# were committed as a result — the snap-ring one from before boot_state grew its +# VRAM-incremental queries, so it could no longer be rebuilt from the tree it sat +# in. Ignore everything matching test_* here except the tracked sources/fixtures. runtime/tests/*.exe +runtime/tests/test_* +!runtime/tests/test_*.c +!runtime/tests/test_*.cpp +!runtime/tests/test_*.py +!runtime/tests/test_*.mcd +!runtime/tests/test_dir/ # transient split-gen build dirs recompiler/build-splitgen/ diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 8c53e25a..f8c4ab2b 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -309,29 +309,29 @@ if(BUILD_TESTING) lib/ELFIO ) target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer) - add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) - - add_executable(full_function_emitter_test - tests/full_function_emitter_test.cpp - src/ps1_exe_parser.cpp - src/mips_decoder.cpp - src/strict_translator.cpp - src/bios_address_model.cpp - src/function_discovery.cpp - src/full_function_emitter.cpp - src/config_loader.cpp - src/recompiler_patch.cpp - ) - target_include_directories(full_function_emitter_test PRIVATE - include - src - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include - lib/toml11 - ) - target_link_libraries(full_function_emitter_test PRIVATE fmt rabbitizer) - add_test(NAME full_function_emitter_test COMMAND full_function_emitter_test) + add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) + + add_executable(full_function_emitter_test + tests/full_function_emitter_test.cpp + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/strict_translator.cpp + src/bios_address_model.cpp + src/function_discovery.cpp + src/full_function_emitter.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + ) + target_include_directories(full_function_emitter_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + ) + target_link_libraries(full_function_emitter_test PRIVATE fmt rabbitizer) + add_test(NAME full_function_emitter_test COMMAND full_function_emitter_test) add_executable(recompiler_patch_cxx17_header_test tests/recompiler_patch_cxx17_header_test.cpp @@ -398,18 +398,18 @@ if(BUILD_TESTING) NAME overlay_shim_compile_contract COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_shim_compile.py) - add_test( - NAME dirty_text_continuation_guards - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_dirty_text_continuation_guards.py) - add_test( - NAME bios_backend_constant_codegen - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_bios_backend_constant_codegen.py - --recompiler $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - - # ---- codegen tests that were written but never registered ----------- + add_test( + NAME dirty_text_continuation_guards + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_dirty_text_continuation_guards.py) + add_test( + NAME bios_backend_constant_codegen + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_bios_backend_constant_codegen.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + + # ---- codegen tests that were written but never registered ----------- # These existed in recompiler/tests/ with no add_test() entry, so they # had no way to run and no way to fail. Each was executed manually # before being wired here; all pass. They drive psxrecomp-game, so they @@ -430,6 +430,24 @@ if(BUILD_TESTING) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) endforeach() + # test_aot_overlay_discovery.py had no add_test() entry at all. It runs + # far enough to exercise real discovery, then fails in + # check_real_batched_fragment_publication: that check reaches + # MOD.compile_fragment_batch, which spawns psxrecomp-game with cwd set + # to its own TemporaryDirectory, so BIOS-profile resolution (which + # probes bios/SCPH1001.toml relative to the working directory) finds + # nothing and returns "recompiler-error: no BIOS profile found". Every + # other subprocess in the same file passes cwd=str(ROOT) explicitly, so + # this looks like a missed cwd rather than a product defect — but it + # needs an owner, and DISABLED keeps it counted until then. + add_test( + NAME aot_overlay_discovery + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_aot_overlay_discovery.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + set_tests_properties(aot_overlay_discovery PROPERTIES DISABLED TRUE) + # ---- runtime source-invariant guards -------------------------------- # These live in runtime/tests/ but only READ runtime source, so they # need neither a generated BIOS nor a built runtime. Registering them @@ -438,21 +456,11 @@ if(BUILD_TESTING) # configure before regenerating a BIOS. Each was run manually first; # all pass. # - # Deliberately NOT registered, because they currently FAIL — see - # docs/TESTING.md for the detail. Wiring a known-failing test would - # recreate the exact problem that took CI off PRs (a red check nobody - # trusts), so these need an owner's decision first: - # test_interpreter_perf_guards.py - psx_devices_mmio_sync's - # recompute-deadline branch never clears g_psx_cycle_fast_limit, - # and the guard also still names the pre-rename - # s_next_service_cycle - # test_runtime_perf_diag_guards.py - asserts a substring no longer - # present in the runtime source - # test_overlay_pair_dedup_runtime.py - needs its companion harness - # built; not a source-only guard foreach(_t IN ITEMS + bios_selection_guards capture_history default_renderer_guards + mod_owned_display_controls fmv_quiet_guards fps_telemetry_opt_in frame_interpolation_display_rate @@ -479,6 +487,38 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) endforeach() + + # ---- known-failing guards: DISABLED, not de-registered --------------- + # These three were previously left out of the list above with a comment + # explaining that they FAIL and that wiring a red check would recreate + # the problem that took CI off PRs. That reasoning is right; the remedy + # was not. De-registering is precisely how a test stops being counted, + # and it is the mechanism that let 16 C/C++ tests rot in runtime/tests/. + # + # ctest's DISABLED property is the intended tool: the test stays + # registered, stays visible in `ctest -N`, and is reported as Disabled + # rather than silently absent — without turning the run red. Delete the + # matching set_tests_properties line to re-enable one once it is fixed. + # + # interpreter_perf_guards psx_devices_mmio_sync's recompute-deadline + # branch never clears g_psx_cycle_fast_limit, + # and the guard still names the pre-rename + # s_next_service_cycle + # runtime_perf_diag_guards asserts a substring no longer present in + # the runtime source + # overlay_pair_dedup_runtime needs its companion harness built; not a + # source-only guard + foreach(_t IN ITEMS + interpreter_perf_guards + runtime_perf_diag_guards + overlay_pair_dedup_runtime) + add_test( + NAME ${_t} + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + set_tests_properties(${_t} PROPERTIES DISABLED TRUE) + endforeach() endif() endif() @@ -495,6 +535,14 @@ target_include_directories(reachable_discovery_test PRIVATE ) target_link_libraries(reachable_discovery_test PRIVATE fmt) +# ---- test-registration completeness guard ----------------------------------- +# Reads both CMakeLists.txt files as text, so it catches an orphan in either +# project from whichever one is being configured. Placed before the collision +# guard, which must stay last because it needs every target declared; this one +# inspects files on disk and has no such ordering requirement. +include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/check_test_registration.cmake) +psxrecomp_check_all_tests_registered() + # ---- output-name collision guard (must stay LAST) --------------------------- # Fails configure if any two targets here would write the same file on a # case-insensitive filesystem. See the module header for the shipped-bug story. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 4c03f095..23246357 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -52,11 +52,22 @@ if(BUILD_TESTING) PSX_OVERLAY_DLL_BUILD=1) add_test(NAME psx_icache_fastpath_test COMMAND psx_icache_fastpath_test) + # Registered since before this change, but it did NOT LINK on origin/master: + # sio.c had grown psx_read_word / psx_get_cycle_count / i_mask / + # g_debug_last_store_pc references that this test's stub block never gained, + # so `ctest` in a runtime tree could not even build. The stubs are added now + # (a registered test that cannot compile is worse than an unregistered one — + # it breaks the whole suite's build), which exposes a real disagreement: + # 27 checks fail, starting with enter.id expected=0x73 (DualShock analog ID) + # actual=0xFF (no device). Constant-clock and monotonic-clock stubs give + # byte-identical results, so the cycle stub is not the discriminator — this + # is the SIO/peripheral axis, not the harness. DISABLED until owned. add_executable(sio_dualshock_rumble_test tests/test_sio_dualshock_rumble.c src/sio.c) target_include_directories(sio_dualshock_rumble_test PRIVATE include) add_test(NAME sio_dualshock_rumble_test COMMAND sio_dualshock_rumble_test) + set_tests_properties(sio_dualshock_rumble_test PROPERTIES DISABLED TRUE) add_executable(gte_register_access_test tests/test_gte_register_access.cpp @@ -99,9 +110,20 @@ if(BUILD_TESTING) # generator, volume sweeps / direct decode, capture buffers, and # savestate coverage of all the new state. Includes src/spu.c directly # (white-box) and stubs its externs. - add_executable(spu_fidelity_test tests/test_spu_fidelity.c) + # src/crc32.c is linked rather than stubbed: spu.c's capture/shadow paths + # feed real CRCs that this test checks, so a zero-returning stub would make + # those assertions vacuous. (This target did not link at all before — + # crc32_update was undefined on origin/master too.) + add_executable(spu_fidelity_test tests/test_spu_fidelity.c src/crc32.c) target_include_directories(spu_fidelity_test PRIVATE include) add_test(NAME spu_fidelity_test COMMAND spu_fidelity_test) + # Same story as sio_dualshock_rumble_test: registered but unlinkable on + # origin/master (crc32_update undefined), so it never ran. With crc32 linked + # it now runs 137 checks and fails 4, all in one ADSR sweep region + # (mid > 0, late > mid, fall_mid < 0x7FFE, fall_late < fall_mid). Rebuilding + # it against a zero-returning crc32 stub reproduces exactly those 4, so the + # crc32 linkage is not the cause — the envelope sweep genuinely disagrees. + set_tests_properties(spu_fidelity_test PROPERTIES DISABLED TRUE) add_executable(overlay_capture_retry_test tests/test_overlay_capture_retry.cpp @@ -201,8 +223,172 @@ if(BUILD_TESTING) add_test(NAME autocompile_degraded_test COMMAND autocompile_degraded_test) endif() + + # ---- previously-unregistered standalone tests ------------------------- + # These 16 existed in runtime/tests/ with no add_test() entry. Their only + # build instructions were a gcc command line in each file's header comment, + # which nothing executes — so when the runtime API moved under them they + # broke silently and stayed broken. Four had already rotted when they were + # collected here (2026-08-11): + # + # test_netplay_snap_ring.c duplicate TU in the documented command; + # no stubs for the VRAM-incremental + # boot_state queries (PR #134) + # test_psx_cycle_event_boundaries.c + # psx_cycles_resync_after_restore() grew a + # CPUState* parameter; psx_cycles.c grew + # three externs the stub block never did + # test_spu_end_without_repeat.c spu.c grew psx_irq_raise / crc32_update + # test_sio_card_protocol.c builds, but 147 of 309 checks fail — + # registered DISABLED below, see beads + # + # Registering them is only half the fix; check_test_registration.cmake at + # the end of this file is what stops the next one from going unnoticed. + # Each test's build recipe now lives HERE and nowhere else — the header + # comments were reduced to a `ctest -R ` pointer so there is exactly + # one description of how a test is built, and it is one the build executes. + + add_executable(cdrom_irq_mask_test tests/test_cdrom_irq_mask.c) + target_include_directories(cdrom_irq_mask_test PRIVATE src) + add_test(NAME cdrom_irq_mask_test COMMAND cdrom_irq_mask_test) + + # Mirrors func_00005A08 (BIOS card READ chain handler) against a mock CPU, + # so it pins the disassembly reading without linking the recompiled BIOS. + add_executable(chain_handler_check_test tests/test_chain_handler_check.c) + target_include_directories(chain_handler_check_test PRIVATE include src) + add_test(NAME chain_handler_check_test COMMAND chain_handler_check_test) + + # Bug B: the pacing decision must take ONE counter sample, so the unsigned + # subtraction cannot underflow into a ~24.7-day SDL_Delay. + add_executable(frame_pacing_test tests/test_frame_pacing.c) + target_include_directories(frame_pacing_test PRIVATE include) + add_test(NAME frame_pacing_test COMMAND frame_pacing_test) + + add_executable(gpu_primitive_reject_test tests/test_gpu_primitive_reject.c) + target_include_directories(gpu_primitive_reject_test PRIVATE include) + add_test(NAME gpu_primitive_reject_test COMMAND gpu_primitive_reject_test) + + add_executable(gpu_sw_edges_test tests/test_gpu_sw_edges.c) + target_include_directories(gpu_sw_edges_test PRIVATE src) + add_test(NAME gpu_sw_edges_test COMMAND gpu_sw_edges_test) + + add_executable(gpu_vk_upload_alignment_test + tests/test_gpu_vk_upload_alignment.c) + target_include_directories(gpu_vk_upload_alignment_test PRIVATE src) + add_test(NAME gpu_vk_upload_alignment_test + COMMAND gpu_vk_upload_alignment_test) + + add_executable(netplay_hash_confirm_test + tests/test_netplay_hash_confirm.c + src/netplay_hash_confirm.c) + target_include_directories(netplay_hash_confirm_test PRIVATE include) + add_test(NAME netplay_hash_confirm_test COMMAND netplay_hash_confirm_test) + + # Single TU: the test #includes src/netplay_snap_ring.c and stubs + # boot_state, so the .c must NOT also be listed here (that was PR #134's + # duplicate-symbol bug, in its documented command). + add_executable(netplay_snap_ring_test tests/test_netplay_snap_ring.c) + target_include_directories(netplay_snap_ring_test PRIVATE include) + target_compile_definitions(netplay_snap_ring_test PRIVATE + NETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE) + add_test(NAME netplay_snap_ring_test COMMAND netplay_snap_ring_test) + + add_executable(overlay_store_cycle_barrier_test + tests/test_overlay_store_cycle_barrier.c) + target_include_directories(overlay_store_cycle_barrier_test PRIVATE include) + add_test(NAME overlay_store_cycle_barrier_test + COMMAND overlay_store_cycle_barrier_test) + + add_executable(psx_cycle_event_boundaries_test + tests/test_psx_cycle_event_boundaries.c + src/psx_cycles.c) + target_include_directories(psx_cycle_event_boundaries_test PRIVATE include) + add_test(NAME psx_cycle_event_boundaries_test + COMMAND psx_cycle_event_boundaries_test) + + add_executable(rb_post_tip_filter_test tests/test_rb_post_tip_filter.c) + target_include_directories(rb_post_tip_filter_test PRIVATE include) + add_test(NAME rb_post_tip_filter_test COMMAND rb_post_tip_filter_test) + + add_executable(spu_end_without_repeat_test + tests/test_spu_end_without_repeat.c) + target_include_directories(spu_end_without_repeat_test PRIVATE include) + add_test(NAME spu_end_without_repeat_test + COMMAND spu_end_without_repeat_test) + + add_executable(spu_gaussian_test tests/test_spu_gaussian.c) + target_include_directories(spu_gaussian_test PRIVATE include) + add_test(NAME spu_gaussian_test COMMAND spu_gaussian_test) + + # DISABLED, not de-registered. It links and runs, but 147 of its 309 checks + # fail: the no-card path is clean (141/141) while the card-present READ + # reply stream comes back shifted by one and then stuck (idx[1] 0xFF for an + # expected 0x08 FLAG, then 0x08 for every subsequent byte). That is either a + # real sio.c protocol regression or a stale expectation in the test, and it + # needs an owner either way. + # + # De-registering a failing test is exactly the mechanism that let these 16 + # rot in the first place, and wiring it RED would recreate the red-check- + # nobody-trusts problem that took CI off PRs. DISABLED keeps it built, keeps + # it listed in `ctest -N`, and keeps it countable, without failing the run. + # Re-enable by deleting the set_tests_properties line once triaged. + add_executable(sio_card_protocol_test + tests/test_sio_card_protocol.c + src/sio.c + src/memcard.c) + target_include_directories(sio_card_protocol_test PRIVATE include src) + # Runs in a build-tree scratch dir, NOT in tests/: the test writes + # test_dir/card1.mcd relative to the working directory (self-provisioning it + # via mkdir if absent), and tests/test_dir/card1.mcd is a TRACKED fixture — + # running it from tests/ overwrites that file and leaves the repo dirty. + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/sio_card_protocol_scratch) + add_test(NAME sio_card_protocol_test COMMAND sio_card_protocol_test + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/sio_card_protocol_scratch) + set_tests_properties(sio_card_protocol_test PROPERTIES DISABLED TRUE) + + # POSIX-only: dlopen()s a fixture .so out of a synthetic cache tree, so it + # cannot link under MinGW/MSVC (-ldl). run_overlay_posix_test.sh already + # stages the cache tree, builds the fixture and the test, and runs it — so + # register THAT rather than reimplementing its fixture staging here. + if(UNIX) + find_program(BASH_EXECUTABLE bash) + if(BASH_EXECUTABLE) + add_test(NAME overlay_posix_test + COMMAND ${BASH_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/run_overlay_posix_test.sh) + else() + message(STATUS "overlay_posix_test: bash not found - skipped") + endif() + endif() + + # recomp-net is an optional submodule; the netplay input-history contract + # cannot be compiled without it. Conditional, not skipped silently: the + # registration guard is told about it via the same EXISTS check. + if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/include/recomp_net/rollback.h") + add_executable(netplay_input_hist_test + tests/test_netplay_input_hist.c + src/netplay_input_hist.c + ${PSXRECOMP_ROOT}/lib/recomp-net/src/input/rnet_input_contract.c) + target_include_directories(netplay_input_hist_test PRIVATE + include + ${PSXRECOMP_ROOT}/lib/recomp-net/include) + target_compile_definitions(netplay_input_hist_test PRIVATE + PSX_HAS_RECOMP_NET=1) + add_test(NAME netplay_input_hist_test COMMAND netplay_input_hist_test) + else() + message(STATUS + "netplay_input_hist_test: lib/recomp-net submodule absent - skipped") + endif() endif() +# ---- test-registration completeness guard (must stay LAST) ------------------ +# Fails configure if a file in runtime/tests/ or recompiler/tests/ is neither +# registered as a test nor listed as a deliberate exception. See the module +# header for the rot story this exists to prevent. +include(${CMAKE_CURRENT_SOURCE_DIR}/check_test_registration.cmake) +psxrecomp_check_all_tests_registered() + if(NOT MSVC) psxrecomp_add_runtime_target(psx-oracle ORACLE diff --git a/runtime/check_test_registration.cmake b/runtime/check_test_registration.cmake new file mode 100644 index 00000000..a26d87f3 --- /dev/null +++ b/runtime/check_test_registration.cmake @@ -0,0 +1,147 @@ +# Guard against test files that no build system knows about. +# +# WHY THIS EXISTS +# --------------- +# Until 2026-08-11, 16 of the 41 C/C++ tests in runtime/tests/ had no add_test() +# entry anywhere. Their only build instructions were a gcc command line written +# into each file's header comment — and a comment cannot fail. So when the +# runtime API moved under them, they broke and nobody found out: +# +# * test_netplay_snap_ring.c stopped linking when boot_state grew its +# VRAM-incremental queries. Its documented recipe ALSO listed a .c file the +# test already #includes, so the recipe produced duplicate symbols. Both +# defects sat there until someone tried to run it by hand (PR #134). +# * test_psx_cycle_event_boundaries.c called psx_cycles_resync_after_restore() +# after that function grew a CPUState* parameter. +# * test_spu_end_without_repeat.c never grew stubs for two externs spu.c +# picked up. +# * test_sio_card_protocol.c drifted until 147 of its 309 checks failed. +# +# Registering those 16 fixes the instance. It does not fix the class: the class +# is that adding an unregistered test is silent, and silence is the default. +# This check inverts that default. A new file under tests/ must either be +# registered or be listed below with a reason — there is no third option that +# configures successfully. +# +# HOW IT WORKS +# ------------ +# The two projects here (recompiler/ and runtime/) are separate project()s with +# separate BUILD_TESTING gates, and runtime/ additionally needs a generated BIOS +# to configure — so there is no single tree in which CMake can see every +# registered target. Rather than force them together (which would make the +# BIOS-free recompiler tree unconfigurable for newcomers), this reads both +# CMakeLists.txt files as TEXT and compares the test files they mention against +# the test files on disk. That works identically from either project, so both +# call it and each independently catches an orphan in the other. +# +# Usage: call at the END of a CMakeLists.txt. +# include(${CMAKE_CURRENT_SOURCE_DIR}/check_test_registration.cmake) +# psxrecomp_check_all_tests_registered() + +set(_PSXRECOMP_TESTREG_DIR "${CMAKE_CURRENT_LIST_DIR}") + +# Files under tests/ that are deliberately not registered as their own test. +# Every entry needs a reason. An entry with no reason is a silent orphan with +# extra steps, which is the thing this file exists to prevent. +set(PSXRECOMP_TESTS_NOT_REGISTERED + "test_overlay_posix.c|built and run by tests/run_overlay_posix_test.sh, which stages the dlopen fixture tree; that script is registered as overlay_posix_test on UNIX" +) + +function(psxrecomp_check_all_tests_registered) + set(_root "${_PSXRECOMP_TESTREG_DIR}/..") + + # ---- what the build systems say ------------------------------------- + set(_declared "") + foreach(_cml "${_root}/runtime/CMakeLists.txt" + "${_root}/recompiler/CMakeLists.txt") + if(NOT EXISTS "${_cml}") + continue() + endif() + file(READ "${_cml}" _text) + + # Direct references: tests/test_foo.c, ../runtime/tests/test_foo.py, + # tests/foo_test.cpp ... Matched narrowly (test_ prefix / _test suffix) + # so that ordinary sources on the same add_executable line cannot be + # mistaken for a registration. + string(REGEX MATCHALL "test_[A-Za-z0-9_]+\\.(c|cpp|py)" _hits "${_text}") + string(REGEX MATCHALL "[A-Za-z0-9_]+_test\\.(c|cpp)" _hits2 "${_text}") + foreach(_h IN LISTS _hits _hits2) + string(REGEX REPLACE "\\.(c|cpp|py)$" "" _stem "${_h}") + list(APPEND _declared "${_stem}") + endforeach() + + # foreach(_t IN ITEMS a b c) blocks register test_.py. Match only + # up to the first ')' so the list items are captured but the add_test() + # body inside the loop is not. + string(REGEX MATCHALL "foreach\\(_t IN ITEMS[^)]*\\)" _loops "${_text}") + foreach(_loop IN LISTS _loops) + string(REGEX REPLACE "foreach\\(_t IN ITEMS" "" _loop "${_loop}") + string(REGEX MATCHALL "[A-Za-z0-9_]+" _items "${_loop}") + foreach(_i IN LISTS _items) + list(APPEND _declared "test_${_i}") + endforeach() + endforeach() + endforeach() + list(REMOVE_DUPLICATES _declared) + + # ---- what is actually on disk --------------------------------------- + # CONFIGURE_DEPENDS matters more than usual here: without it, ADDING a test + # file would not re-run configure, so this check would sleep through the + # exact event it exists to catch. + file(GLOB _found CONFIGURE_DEPENDS + "${_root}/runtime/tests/test_*.c" + "${_root}/runtime/tests/test_*.cpp" + "${_root}/runtime/tests/test_*.py" + "${_root}/recompiler/tests/*_test.cpp" + "${_root}/recompiler/tests/test_*.py") + + # ---- diff ------------------------------------------------------------- + set(_orphans "") + foreach(_path IN LISTS _found) + get_filename_component(_file "${_path}" NAME) + get_filename_component(_stem "${_path}" NAME_WE) + + set(_excused FALSE) + foreach(_ex IN LISTS PSXRECOMP_TESTS_NOT_REGISTERED) + string(REGEX REPLACE "\\|.*$" "" _ex_file "${_ex}") + if(_ex_file STREQUAL _file) + set(_excused TRUE) + break() + endif() + endforeach() + if(_excused) + continue() + endif() + + list(FIND _declared "${_stem}" _idx) + if(_idx EQUAL -1) + file(RELATIVE_PATH _rel "${_root}" "${_path}") + list(APPEND _orphans "${_rel}") + endif() + endforeach() + + if(_orphans) + list(JOIN _orphans "\n " _pretty) + message(FATAL_ERROR + "Test file(s) present on disk but registered nowhere:\n" + " ${_pretty}\n\n" + "An unregistered test cannot run, cannot fail, and rots silently — " + "four of them had already broken against the runtime API before " + "anyone noticed (see the header of " + "runtime/check_test_registration.cmake).\n\n" + "Fix by EITHER:\n" + " * adding an add_test() entry in runtime/CMakeLists.txt or " + "recompiler/CMakeLists.txt — register it DISABLED via " + "set_tests_properties( PROPERTIES DISABLED TRUE) if it is " + "known-failing, so it stays visible in `ctest -N` instead of " + "vanishing; or\n" + " * adding '|' to " + "PSXRECOMP_TESTS_NOT_REGISTERED in " + "runtime/check_test_registration.cmake, if it is a fixture or is " + "driven by another test.") + endif() + + list(LENGTH _found _n_found) + message(STATUS + "test-registration guard: ${_n_found} test file(s), all registered") +endfunction() diff --git a/runtime/src/netplay_snap_ring.c b/runtime/src/netplay_snap_ring.c index a69fc39f..c057bef6 100644 --- a/runtime/src/netplay_snap_ring.c +++ b/runtime/src/netplay_snap_ring.c @@ -1,3 +1,13 @@ +/* clock_gettime/CLOCK_MONOTONIC are POSIX, not ISO C. Under a strict -std=cNN + * (no GNU extensions) glibc hides them unless _POSIX_C_SOURCE is set, so this + * TU fails to compile — loudly under -Werror, silently as an implicit + * declaration without it. The runtime build happens to use -std=gnu99, which + * is why this never surfaced there. Must precede the first libc header: + * glibc's latches the feature-test macros on first inclusion. */ +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 200809L +#endif + #include "netplay_snap_ring.h" #include "boot_state.h" diff --git a/runtime/tests/test_autocompile_degraded.c b/runtime/tests/test_autocompile_degraded.c index 259a55b0..9cdbe256 100644 --- a/runtime/tests/test_autocompile_degraded.c +++ b/runtime/tests/test_autocompile_degraded.c @@ -15,9 +15,7 @@ * not after three failed spawns - the point is to know before any compile is * attempted. * - * Build (standalone): - * gcc -I ../include -o test_autocompile_degraded \ - * test_autocompile_degraded.c ../src/autocompile.c + * Build/run: ctest -R autocompile_degraded_test */ #include "autocompile.h" #include "overlay_loader.h" diff --git a/runtime/tests/test_cdrom_irq_mask.c b/runtime/tests/test_cdrom_irq_mask.c index 645702eb..518928e0 100644 --- a/runtime/tests/test_cdrom_irq_mask.c +++ b/runtime/tests/test_cdrom_irq_mask.c @@ -1,9 +1,7 @@ /* * Validate PS1 CD-ROM interrupt mask matching. * - * Build: - * cc -std=c99 -Wall -Wextra -Werror -I../src \ - * -o test_cdrom_irq_mask test_cdrom_irq_mask.c + * Build/run: ctest -R cdrom_irq_mask_test */ #include "cdrom_irq.h" diff --git a/runtime/tests/test_netplay_hash_confirm b/runtime/tests/test_netplay_hash_confirm deleted file mode 100755 index 07e60f43..00000000 Binary files a/runtime/tests/test_netplay_hash_confirm and /dev/null differ diff --git a/runtime/tests/test_netplay_hash_confirm.c b/runtime/tests/test_netplay_hash_confirm.c index 82d6bdbb..fbb9e902 100644 --- a/runtime/tests/test_netplay_hash_confirm.c +++ b/runtime/tests/test_netplay_hash_confirm.c @@ -1,9 +1,7 @@ /* * test_netplay_hash_confirm.c — watermark advance / mismatch hold. * - * gcc -std=c11 -Wall -Wextra -Werror -I../include \ - * -o test_netplay_hash_confirm test_netplay_hash_confirm.c \ - * ../src/netplay_hash_confirm.c && ./test_netplay_hash_confirm + * Build/run: ctest -R netplay_hash_confirm_test */ #include "netplay_hash_confirm.h" diff --git a/runtime/tests/test_netplay_input_hist b/runtime/tests/test_netplay_input_hist deleted file mode 100755 index d8c935bc..00000000 Binary files a/runtime/tests/test_netplay_input_hist and /dev/null differ diff --git a/runtime/tests/test_netplay_input_hist.c b/runtime/tests/test_netplay_input_hist.c index b4b9209f..dfd81c81 100644 --- a/runtime/tests/test_netplay_input_hist.c +++ b/runtime/tests/test_netplay_input_hist.c @@ -1,12 +1,8 @@ /* * test_netplay_input_hist.c — pad map, invent hold-last, promote. * - * gcc -std=c11 -Wall -Wextra -Werror -DPSX_HAS_RECOMP_NET=1 \ - * -I../include -I../../lib/recomp-net/include \ - * -o test_netplay_input_hist test_netplay_input_hist.c \ - * ../src/netplay_input_hist.c \ - * ../../lib/recomp-net/src/input/rnet_input_contract.c && \ - * ./test_netplay_input_hist + * Build/run: ctest -R netplay_input_hist_test + * (needs the lib/recomp-net submodule; registration is conditional.) */ #define PSX_HAS_RECOMP_NET 1 #include "netplay_input_hist.h" diff --git a/runtime/tests/test_netplay_snap_ring b/runtime/tests/test_netplay_snap_ring deleted file mode 100755 index 868a6c04..00000000 Binary files a/runtime/tests/test_netplay_snap_ring and /dev/null differ diff --git a/runtime/tests/test_netplay_snap_ring.c b/runtime/tests/test_netplay_snap_ring.c index 2b912637..f081ed4a 100644 --- a/runtime/tests/test_netplay_snap_ring.c +++ b/runtime/tests/test_netplay_snap_ring.c @@ -4,11 +4,23 @@ * Stores synthetic blobs via netplay_snap_ring_store and checks has/peek, * same-tick overwrite, and oldest-tick eviction at capacity. * - * Build (from runtime/tests): - * gcc -std=c11 -Wall -Wextra -Werror -I../include \ - * -o test_netplay_snap_ring test_netplay_snap_ring.c \ - * -DNETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE + * Build/run: ctest -R netplay_snap_ring_test + * (registered in runtime/CMakeLists.txt, which owns the source list and the + * NETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE define. This file deliberately does + * not repeat a gcc recipe: an unexecuted recipe in a comment is what let this + * harness rot — it kept listing ../src/netplay_snap_ring.c, which the file + * already #includes, long after that became a duplicate-symbol error.) */ + +/* This TU pulls in ../src/netplay_snap_ring.c at the bottom, and that file + * calls clock_gettime/CLOCK_MONOTONIC on non-Windows. The equivalent define in + * netplay_snap_ring.c itself is too late here: below already latched + * glibc's , so under a strict -std=cNN the POSIX declarations stay + * hidden and the build dies on an implicit declaration. Must stay first. */ +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 200809L +#endif + #include #include #include diff --git a/runtime/tests/test_psx_cycle_event_boundaries.c b/runtime/tests/test_psx_cycle_event_boundaries.c index ce29ec05..1d5a6ad1 100644 --- a/runtime/tests/test_psx_cycle_event_boundaries.c +++ b/runtime/tests/test_psx_cycle_event_boundaries.c @@ -1,10 +1,7 @@ /* * Pin cross-device causality at a scheduler deadline. * - * Build from runtime/tests: - * gcc -std=c99 -Wall -Wextra -ffunction-sections -fdata-sections \ - * -Wl,--gc-sections -I../include -o test_psx_cycle_event_boundaries \ - * test_psx_cycle_event_boundaries.c ../src/psx_cycles.c + * Build/run: ctest -R psx_cycle_event_boundaries_test */ #include "psx_cycles.h" @@ -61,8 +58,14 @@ int psx_get_in_exception(void) { return 0; } void starvation_watchdog_check(void) {} void starvation_ring_pc_sample(void) {} +int psx_netplay_active(void) { return 0; } +int psx_selfcheck_enabled(void) { return 0; } +void dirty_ram_ld_delay_discard(void) {} + int main(void) { - psx_cycles_resync_after_restore(); + /* NULL cpu: this test pins the scheduler boundary, not the CPU-state + * rewind. psx_cycles_resync_after_restore guards that block on `if (cpu)`. */ + psx_cycles_resync_after_restore(NULL); psx_advance_cycles(5); if (psx_get_cycle_count() != 5) { diff --git a/runtime/tests/test_rb_post_tip_filter.c b/runtime/tests/test_rb_post_tip_filter.c index 0afa3d04..3ff90a8c 100644 --- a/runtime/tests/test_rb_post_tip_filter.c +++ b/runtime/tests/test_rb_post_tip_filter.c @@ -4,9 +4,7 @@ * Soak class (rb-diag 2026-07-31): both peers POST@1007 (matched), tip-extend * to 1008, both POST@1008 (matched); initiator aborted local@1008 vs peer@1007. * - * gcc -std=c11 -Wall -Wextra -Werror -I../include \ - * -o test_rb_post_tip_filter test_rb_post_tip_filter.c && \ - * ./test_rb_post_tip_filter + * Build/run: ctest -R rb_post_tip_filter_test */ #include "netplay_rb_post.h" diff --git a/runtime/tests/test_sio_card_protocol.c b/runtime/tests/test_sio_card_protocol.c index 607286dd..30ee00dd 100644 --- a/runtime/tests/test_sio_card_protocol.c +++ b/runtime/tests/test_sio_card_protocol.c @@ -38,9 +38,19 @@ /* ---- Stubs for sio.c's external dependencies ---- */ uint32_t i_stat = 0; +uint32_t i_mask = 0; uint32_t g_debug_current_func_addr = 0; +uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } +uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } +/* sio.c gates card-transfer deferral on `psx_get_cycle_count() < deadline` + * (s_card_ct_defer_until_cyc). A constant clock would defer forever and stall + * the state machine, so the stub advances monotonically. */ +uint64_t psx_get_cycle_count(void) { + static uint64_t t; + return t += 64; +} uint32_t memory_get_sr(void) { return 0; } void debug_server_poll(void) {} void debug_server_log_sio_write(uint32_t addr, uint32_t value, uint8_t width) { diff --git a/runtime/tests/test_sio_dualshock_rumble.c b/runtime/tests/test_sio_dualshock_rumble.c index d6b0ca59..6f913034 100644 --- a/runtime/tests/test_sio_dualshock_rumble.c +++ b/runtime/tests/test_sio_dualshock_rumble.c @@ -12,9 +12,19 @@ void sio_snapshot_write(uint8_t *p); int sio_snapshot_read(const uint8_t *p, uint32_t len); uint32_t i_stat = 0; +uint32_t i_mask = 0; uint32_t g_debug_current_func_addr = 0; +uint32_t g_debug_last_store_pc = 0; int psx_get_in_exception(void) { return 0; } uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; } +uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; } + +/* sio.c gates card-transfer deferral on `psx_get_cycle_count() < deadline`, so + * a constant clock would defer forever. Advance monotonically. */ +uint64_t psx_get_cycle_count(void) { + static uint64_t t; + return t += 64; +} uint32_t memory_get_sr(void) { return 0; } void debug_server_poll(void) {} void debug_server_log_sio_write(uint32_t a, uint32_t v, uint8_t w) { diff --git a/runtime/tests/test_spu_end_without_repeat.c b/runtime/tests/test_spu_end_without_repeat.c index e883276b..65d12a42 100644 --- a/runtime/tests/test_spu_end_without_repeat.c +++ b/runtime/tests/test_spu_end_without_repeat.c @@ -4,9 +4,7 @@ * The test includes spu.c so it can stage a voice exactly at the block * boundary, without exposing emulator internals in the runtime API. * - * Build from runtime/tests: - * gcc -std=c99 -I../include -o test_spu_end_without_repeat.exe \ - * test_spu_end_without_repeat.c + * Build/run: ctest -R spu_end_without_repeat_test */ #include @@ -32,6 +30,14 @@ void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) { (void)b; } +void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; } + +uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t len) { + (void)data; + (void)len; + return crc; +} + bool spu_shadow_enabled(void) { return false; } void spu_shadow_reset(void) {} void spu_shadow_process(int16_t *canon, int frames) {