Files
psxrecomp/runtime/tests/test_spu_end_without_repeat.c
Matthew Stanley fe22c12f7b tests: make unregistered tests impossible, and repair the ones that rotted
PR #134 fixed test_netplay_snap_ring.c's harness. This addresses why it broke:
its only build instructions were a gcc command line in a header comment, and a
comment cannot fail. 16 of the 41 C/C++ tests in runtime/tests/ were in that
state, plus 3 python guards. Four had already rotted against the runtime API.

Worse, two tests that WERE registered did not link on origin/master --
sio_dualshock_rumble_test (psx_read_word / psx_get_cycle_count / i_mask /
g_debug_last_store_pc) and spu_fidelity_test (crc32_update) -- so `ctest` in a
runtime tree did not build at all.

Registering them fixes the instance. check_test_registration.cmake fixes the
class: it reads both CMakeLists.txt files as text and fails configure on any
test file that is neither registered nor listed with a reason. Reading them as
text (rather than adding a top-level CMakeLists.txt) keeps the BIOS-free
recompiler tree configurable, which is the only tree a newcomer can configure;
both projects call the guard, so each catches orphans in the other.
CONFIGURE_DEPENDS matters: without it, ADDING a test would not re-run configure.

Also incorporates TechnicallyComputers' review of #134. -Werror turns a
pre-existing Linux warning into a hard failure: clock_gettime/CLOCK_MONOTONIC
need _POSIX_C_SOURCE under a strict -std=cNN. Both halves are needed -- the
define in netplay_snap_ring.c fixes a direct compile of that TU, but not the
harness, which includes <stdio.h> first and so has already latched glibc's
<features.h>. Verified on Ubuntu 24.04 / GCC 13.3.

Test-file changes:
  * repaired 4 rotted tests (stubs / a changed parameter list), no logic edits
  * build recipes replaced with `ctest -R <name>`, so exactly one description
    of how a test builds exists and the build executes it
  * known-failing tests registered DISABLED, not de-registered -- de-registering
    is the mechanism that hid these in the first place; DISABLED keeps them in
    `ctest -N` and countable without turning the run red
  * sio_card_protocol_test runs in a build-tree scratch dir; it writes
    test_dir/card1.mcd, and that is a TRACKED fixture
  * removed three committed test binaries and widened the .gitignore rule that
    missed them (it only covered *.exe, and these were Linux builds)

Disabled, each with a bead: sio_card_protocol_test (147/309),
sio_dualshock_rumble_test (27), spu_fidelity_test (4 ADSR), aot_overlay_discovery
(cwd), and the 3 pre-existing perf guards.

Verified:
  * adding an unregistered test file fails the build with an actionable message;
    removing it passes
  * runtime ctest 32/32 pass, 3 disabled
  * recompiler ctest: same 5 failures as a pristine origin/master baseline
  * Tomba, Tomba 2 and MMX6 build against this framework and were each driven
    240s of attract + randomised pad fuzz (~195 injections, 47+ combos):
    59.6 fps sustained, zero stalls, all three reached real gameplay

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:34:12 -07:00

138 lines
4.7 KiB
C

/*
* Regression test for an ADPCM END block without REPEAT.
*
* 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/run: ctest -R spu_end_without_repeat_test
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "../src/spu.c"
/* Dependencies owned by the surrounding runtime are inert in this unit test. */
uint64_t s_frame_count;
uint64_t psx_get_cycle_count(void) { return 0; }
void audio_trace_pcm(int tap, const int16_t *stereo, int frames) {
(void)tap;
(void)stereo;
(void)frames;
}
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) {
(void)kind;
(void)a;
(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) {
(void)canon;
(void)frames;
}
static int failures;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL: %s\n", message); \
failures++; \
} else { \
printf("ok: %s\n", message); \
} \
} while (0)
static void fill_block(uint32_t addr, uint8_t flags, uint8_t packed) {
spu_ram[addr] = 0x00; /* filter 0, shift 0 */
spu_ram[addr + 1u] = flags;
memset(spu_ram + addr + 2u, packed, 14u);
}
static SpuVoice *stage_end_boundary(uint8_t flags, uint32_t repeat_addr) {
SpuVoice *v = &voices[0];
memset(v, 0, sizeof(*v));
v->active = 1;
v->cur_addr = 0x310u; /* the block after the terminator */
v->repeat_addr = repeat_addr;
v->sample_idx = SPU_BLOCK_SAMPLES;
v->env_level = 0x6000u;
v->adsr_phase = ADSR_SUSTAIN;
v->flags = flags;
spu_regs[2] = 0x1000u; /* one decoded sample per output sample */
return v;
}
static void test_end_without_repeat_is_silent(void) {
const uint32_t repeat_addr = 0x120u;
spu_init();
/* Distinguish the required repeat-address decode from an incorrect
* forward decode. Both blocks are deliberately non-silent: the envelope,
* rather than convenient zero sample data, must make the result silent. */
fill_block(repeat_addr, 0x04u, 0x11u);
fill_block(0x310u, 0x00u, 0x77u);
SpuVoice *v = stage_end_boundary(0x01u, repeat_addr);
int16_t sample = voice_next_sample(0);
CHECK(sample == 0, "END without REPEAT emits silence at the boundary");
CHECK(v->env_level == 0, "END without REPEAT forces the envelope to zero");
CHECK(v->adsr_phase == ADSR_RELEASE,
"END without REPEAT enters Release");
CHECK(!v->active, "zero-level Release deactivates the voice");
CHECK(v->samples[0] == 4096,
"decoder resumes at the latched repeat address, not forward RAM");
CHECK(spu_event_total() == 1, "END stop records exactly one event");
SpuEvent event;
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_STOP,
"END stop event has the stop kind");
CHECK(event.addr == repeat_addr,
"END stop event reports the redirected repeat address");
(void)voice_next_sample(0);
CHECK(spu_event_total() == 1,
"an inactive one-shot does not retrigger its END transition");
}
static void test_end_with_repeat_keeps_playing(void) {
const uint32_t repeat_addr = 0x180u;
spu_init();
fill_block(repeat_addr, 0x00u, 0x11u);
SpuVoice *v = stage_end_boundary(0x03u, repeat_addr);
int16_t sample = voice_next_sample(0);
CHECK(sample != 0, "END with REPEAT remains audible");
CHECK(v->env_level != 0, "END with REPEAT preserves the envelope");
CHECK(v->adsr_phase == ADSR_SUSTAIN,
"END with REPEAT preserves the ADSR phase");
CHECK(v->active, "END with REPEAT keeps the voice active");
SpuEvent event;
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_LOOP,
"END with REPEAT records a loop event");
}
int main(void) {
test_end_without_repeat_is_silent();
test_end_with_repeat_keeps_playing();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}