refactor: Update Performance Header Includes and Add Emulator Test Suite
- Refactored header includes for Performance Profiler and Dashboard to reflect new directory structure under `app/gfx/performance/`. - Introduced a new `EmulatorTestSuite` class to validate core emulator components, including APU, SPC700, and debugging features. - Enhanced test suite with detailed test cases for APU handshake, SPC700 cycle accuracy, breakpoint management, and audio backend functionality. - Removed outdated `test_dungeon_objects.cc` file from the test suite to streamline testing focus.
This commit is contained in:
361
src/app/test/emulator_test_suite.h
Normal file
361
src/app/test/emulator_test_suite.h
Normal file
@@ -0,0 +1,361 @@
|
||||
#ifndef YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|
||||
#define YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
#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/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<uint8_t> 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::milliseconds>(
|
||||
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::milliseconds>(
|
||||
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::milliseconds>(
|
||||
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::milliseconds>(
|
||||
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::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start_time);
|
||||
results.AddResult(result);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|
||||
Reference in New Issue
Block a user