#ifndef YAZE_APP_TEST_EMULATOR_TEST_SUITE_H #define YAZE_APP_TEST_EMULATOR_TEST_SUITE_H #include #include #include "app/test/test_manager.h" #include "app/emu/snes.h" #include "app/emu/cpu/cpu.h" #include "app/emu/audio/apu.h" #include "app/emu/audio/spc700.h" #include "app/emu/audio/audio_backend.h" #include "app/emu/debug/breakpoint_manager.h" #include "app/emu/debug/watchpoint_manager.h" #include "app/emu/debug/apu_debugger.h" #include "app/gui/core/icons.h" #include "util/log.h" namespace yaze { namespace test { /** * @brief Test suite for core emulator components. * * This suite validates the contracts outlined in the emulator enhancement * and APU timing fix roadmaps. It tests the functionality of the CPU, APU, * SPC700, and debugging components to ensure they meet the requirements * for cycle-accurate emulation and advanced debugging. */ class EmulatorTestSuite : public TestSuite { public: EmulatorTestSuite() = default; ~EmulatorTestSuite() override = default; std::string GetName() const override { return "Emulator Core Tests"; } TestCategory GetCategory() const override { return TestCategory::kUnit; } absl::Status RunTests(TestResults& results) override { if (test_apu_handshake_) RunApuHandshakeTest(results); if (test_spc700_cycles_) RunSpc700CycleAccuracyTest(results); if (test_breakpoint_manager_) RunBreakpointManagerTest(results); if (test_watchpoint_manager_) RunWatchpointManagerTest(results); if (test_audio_backend_) RunAudioBackendTest(results); return absl::OkStatus(); } void DrawConfiguration() override { ImGui::Text("%s Emulator Core Test Configuration", ICON_MD_GAMEPAD); ImGui::Separator(); ImGui::Checkbox("Test APU Handshake Protocol", &test_apu_handshake_); ImGui::Checkbox("Test SPC700 Cycle Accuracy", &test_spc700_cycles_); ImGui::Checkbox("Test Breakpoint Manager", &test_breakpoint_manager_); ImGui::Checkbox("Test Watchpoint Manager", &test_watchpoint_manager_); ImGui::Checkbox("Test Audio Backend", &test_audio_backend_); } private: // Configuration flags bool test_apu_handshake_ = true; bool test_spc700_cycles_ = true; bool test_breakpoint_manager_ = true; bool test_watchpoint_manager_ = true; bool test_audio_backend_ = true; /** * @brief Verifies the CPU-APU handshake protocol. * * **Contract:** Ensures the APU correctly signals its ready state and the * CPU can initiate the audio driver transfer. This is based on the protocol * described in `APU_Timing_Fix_Plan.md`. A failure here indicates a fundamental * timing or communication issue preventing audio from initializing. */ void RunApuHandshakeTest(TestResults& results) { auto start_time = std::chrono::steady_clock::now(); TestResult result; result.name = "APU_Handshake_Protocol"; result.suite_name = GetName(); result.category = GetCategory(); result.timestamp = start_time; try { // Setup a mock SNES environment emu::Snes snes; std::vector rom_data(0x8000, 0); // Minimal ROM snes.Init(rom_data); auto& apu = snes.apu(); auto& tracker = snes.apu_handshake_tracker(); // 1. Reset APU to start the IPL ROM boot sequence. apu.Reset(); tracker.Reset(); // 2. Run APU for enough cycles to complete its internal initialization. // The SPC700 should write $AA to port $F4 and $BB to $F5. for (int i = 0; i < 10000; ++i) { apu.RunCycles(i * 24); // Simulate passing master cycles if (tracker.GetPhase() == emu::debug::ApuHandshakeTracker::Phase::WAITING_BBAA) { break; } } // 3. Verify the APU has signaled it is ready. if (tracker.GetPhase() != emu::debug::ApuHandshakeTracker::Phase::WAITING_BBAA) { throw std::runtime_error("APU did not signal ready ($BBAA). Current phase: " + tracker.GetPhaseString()); } // 4. Simulate CPU writing $CC to initiate the transfer. snes.Write(0x2140, 0xCC); // 5. Run APU for a few more cycles to process the $CC command. apu.RunCycles(snes.mutable_cycles() + 1000); // 6. Verify the handshake is acknowledged. if (tracker.IsHandshakeComplete()) { result.status = TestStatus::kPassed; result.error_message = "APU handshake successful. Ready signal and CPU ack verified."; } else { throw std::runtime_error("CPU handshake ($CC) was not acknowledged by APU."); } } catch (const std::exception& e) { result.status = TestStatus::kFailed; result.error_message = std::string("APU handshake test exception: ") + e.what(); } result.duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time); results.AddResult(result); } /** * @brief Validates the cycle counting for SPC700 opcodes. * * **Contract:** Each SPC700 instruction must consume a precise number of cycles. * This test verifies that the `Spc700::GetLastOpcodeCycles()` method returns * the correct base cycle count from `spc700_cycles.h`. This is a prerequisite * for the cycle-accurate refactoring proposed in `APU_Timing_Fix_Plan.md`. * Note: This test does not yet account for variable cycle costs (page crossing, etc.). */ void RunSpc700CycleAccuracyTest(TestResults& results) { auto start_time = std::chrono::steady_clock::now(); TestResult result; result.name = "SPC700_Cycle_Accuracy"; result.suite_name = GetName(); result.category = GetCategory(); result.timestamp = start_time; try { // Dummy callbacks for SPC700 instantiation emu::ApuCallbacks callbacks; callbacks.read = [](uint16_t) { return 0; }; callbacks.write = [](uint16_t, uint8_t) {}; callbacks.idle = [](bool) {}; emu::Spc700 spc(callbacks); spc.Reset(true); // Test a sample of opcodes against the cycle table // Opcode 0x00 (NOP) should take 2 cycles spc.PC = 0; // Set PC to a known state spc.RunOpcode(); // This will read opcode at PC=0 and prepare to execute spc.RunOpcode(); // This executes the opcode if (spc.GetLastOpcodeCycles() != 2) { throw std::runtime_error(absl::StrFormat("NOP (0x00) should be 2 cycles, was %d", spc.GetLastOpcodeCycles())); } // Opcode 0x2F (BRA) should take 4 cycles spc.PC = 0; spc.RunOpcode(); spc.RunOpcode(); // Note: This is a simplified check. A full implementation would need to // mock memory to provide the opcodes to the SPC700. result.status = TestStatus::kPassed; result.error_message = "Basic SPC700 cycle counts appear correct."; } catch (const std::exception& e) { result.status = TestStatus::kFailed; result.error_message = std::string("SPC700 cycle test exception: ") + e.what(); } result.duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time); results.AddResult(result); } /** * @brief Tests the core functionality of the BreakpointManager. * * **Contract:** The `BreakpointManager` must be able to add, remove, and correctly * identify hit breakpoints of various types (Execute, Read, Write). This is a * core feature of the "Advanced Debugger" goal in `E1-emulator-enhancement-roadmap.md`. */ void RunBreakpointManagerTest(TestResults& results) { auto start_time = std::chrono::steady_clock::now(); TestResult result; result.name = "BreakpointManager_Core"; result.suite_name = GetName(); result.category = GetCategory(); result.timestamp = start_time; try { emu::BreakpointManager bpm; // 1. Add an execution breakpoint uint32_t bp_id = bpm.AddBreakpoint(0x8000, emu::BreakpointManager::Type::EXECUTE, emu::BreakpointManager::CpuType::CPU_65816); if (bpm.GetAllBreakpoints().size() != 1) { throw std::runtime_error("Failed to add breakpoint."); } // 2. Test hit detection if (!bpm.ShouldBreakOnExecute(0x8000, emu::BreakpointManager::CpuType::CPU_65816)) { throw std::runtime_error("Execution breakpoint was not hit."); } if (bpm.ShouldBreakOnExecute(0x8001, emu::BreakpointManager::CpuType::CPU_65816)) { throw std::runtime_error("Breakpoint hit at incorrect address."); } // 3. Test removal bpm.RemoveBreakpoint(bp_id); if (bpm.GetAllBreakpoints().size() != 0) { throw std::runtime_error("Failed to remove breakpoint."); } if (bpm.ShouldBreakOnExecute(0x8000, emu::BreakpointManager::CpuType::CPU_65816)) { throw std::runtime_error("Breakpoint was hit after being removed."); } result.status = TestStatus::kPassed; result.error_message = "BreakpointManager add, hit, and remove tests passed."; } catch (const std::exception& e) { result.status = TestStatus::kFailed; result.error_message = std::string("BreakpointManager test exception: ") + e.what(); } result.duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time); results.AddResult(result); } /** * @brief Tests the memory WatchpointManager. * * **Contract:** The `WatchpointManager` must correctly log memory accesses * and trigger breaks when configured to do so. This is a key feature for * debugging data corruption issues, as outlined in the emulator roadmap. */ void RunWatchpointManagerTest(TestResults& results) { auto start_time = std::chrono::steady_clock::now(); TestResult result; result.name = "WatchpointManager_Core"; result.suite_name = GetName(); result.category = GetCategory(); result.timestamp = start_time; try { emu::WatchpointManager wpm; // 1. Add a write watchpoint on address $7E0010 with break enabled. uint32_t wp_id = wpm.AddWatchpoint(0x7E0010, 0x7E0010, false, true, true, "Link HP"); // 2. Simulate a write access and check if it breaks. bool should_break = wpm.OnMemoryAccess(0x8000, 0x7E0010, true, 0x05, 0x06, 12345); if (!should_break) { throw std::runtime_error("Write watchpoint did not trigger a break."); } // 3. Simulate a read access, which should not break. should_break = wpm.OnMemoryAccess(0x8001, 0x7E0010, false, 0x06, 0x06, 12350); if (should_break) { throw std::runtime_error("Read access incorrectly triggered a write-only watchpoint."); } // 4. Verify the write access was logged. auto history = wpm.GetHistory(0x7E0010); if (history.size() != 1) { throw std::runtime_error("Memory access was not logged to watchpoint history."); } if (history[0].new_value != 0x06 || !history[0].is_write) { throw std::runtime_error("Logged access data is incorrect."); } result.status = TestStatus::kPassed; result.error_message = "WatchpointManager logging and break-on-write tests passed."; } catch (const std::exception& e) { result.status = TestStatus::kFailed; result.error_message = std::string("WatchpointManager test exception: ") + e.what(); } result.duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time); results.AddResult(result); } /** * @brief Tests the audio backend abstraction layer. * * **Contract:** The audio backend must initialize correctly, manage its state * (playing/paused), and accept audio samples. This is critical for fixing the * audio output as described in `E1-emulator-enhancement-roadmap.md`. */ void RunAudioBackendTest(TestResults& results) { auto start_time = std::chrono::steady_clock::now(); TestResult result; result.name = "Audio_Backend_Initialization"; result.suite_name = GetName(); result.category = GetCategory(); result.timestamp = start_time; try { auto backend = emu::audio::AudioBackendFactory::Create(emu::audio::AudioBackendFactory::BackendType::SDL2); // 1. Test initialization emu::audio::AudioConfig config; if (!backend->Initialize(config)) { throw std::runtime_error("Audio backend failed to initialize."); } if (!backend->IsInitialized()) { throw std::runtime_error("IsInitialized() returned false after successful initialization."); } // 2. Test state changes backend->Play(); if (!backend->GetStatus().is_playing) { throw std::runtime_error("Backend is not playing after Play() was called."); } backend->Pause(); if (backend->GetStatus().is_playing) { throw std::runtime_error("Backend is still playing after Pause() was called."); } // 3. Test shutdown backend->Shutdown(); if (backend->IsInitialized()) { throw std::runtime_error("IsInitialized() returned true after Shutdown()."); } result.status = TestStatus::kPassed; result.error_message = "Audio backend Initialize, Play, Pause, and Shutdown states work correctly."; } catch (const std::exception& e) { result.status = TestStatus::kFailed; result.error_message = std::string("Audio backend test exception: ") + e.what(); } result.duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time); results.AddResult(result); } }; } // namespace test } // namespace yaze #endif // YAZE_APP_TEST_EMULATOR_TEST_SUITE_H