Files
yaze/test/unit/emu/step_controller_test.cc

269 lines
8.3 KiB
C++

/**
* @file step_controller_test.cc
* @brief Unit tests for the 65816 step controller (call stack tracking)
*
* Tests the StepOver and StepOut functionality that enables AI-assisted
* debugging with proper subroutine tracking.
*/
#include "app/emu/debug/step_controller.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <functional>
#include <vector>
namespace yaze {
namespace emu {
namespace debug {
namespace {
class StepControllerTest : public ::testing::Test {
protected:
void SetUp() override {
// Reset program state
pc_ = 0;
instruction_count_ = 0;
}
// Simulates a simple memory with program code
void SetupProgram(const std::vector<uint8_t>& code, uint32_t base = 0) {
memory_ = code;
base_address_ = base;
pc_ = base;
controller_.SetMemoryReader([this](uint32_t addr) -> uint8_t {
uint32_t offset = addr - base_address_;
if (offset < memory_.size()) {
return memory_[offset];
}
return 0;
});
controller_.SetPcGetter([this]() -> uint32_t { return pc_; });
controller_.SetSingleStepper([this]() {
// Simulate executing one instruction by advancing PC
// This is a simplified simulation - real stepping would be more complex
if (pc_ >= base_address_ && pc_ < base_address_ + memory_.size()) {
uint8_t opcode = memory_[pc_ - base_address_];
uint8_t size = GetSimulatedInstructionSize(opcode);
pc_ += size;
instruction_count_++;
}
});
}
// Simplified instruction size for testing
uint8_t GetSimulatedInstructionSize(uint8_t opcode) {
switch (opcode) {
// Implied (1 byte)
case 0xEA: // NOP
case 0x60: // RTS
case 0x6B: // RTL
case 0x40: // RTI
case 0x18: // CLC
case 0x38: // SEC
case 0x78: // SEI
return 1;
// Branch (2 bytes)
case 0xD0: // BNE
case 0xF0: // BEQ
case 0x80: // BRA
case 0xA9: // LDA #imm (8-bit)
return 2;
// Absolute (3 bytes)
case 0x20: // JSR
case 0x4C: // JMP
case 0xAD: // LDA abs
case 0x8D: // STA abs
return 3;
// Long (4 bytes)
case 0x22: // JSL
case 0x5C: // JMP long
return 4;
default:
return 1;
}
}
StepController controller_;
std::vector<uint8_t> memory_;
uint32_t base_address_ = 0;
uint32_t pc_ = 0;
uint32_t instruction_count_ = 0;
};
// --- Basic Classification Tests ---
TEST_F(StepControllerTest, ClassifyCallInstructions) {
EXPECT_TRUE(StepController::IsCallInstruction(0x20)); // JSR
EXPECT_TRUE(StepController::IsCallInstruction(0x22)); // JSL
EXPECT_TRUE(StepController::IsCallInstruction(0xFC)); // JSR (abs,X)
EXPECT_FALSE(StepController::IsCallInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsCallInstruction(0x4C)); // JMP
EXPECT_FALSE(StepController::IsCallInstruction(0x60)); // RTS
}
TEST_F(StepControllerTest, ClassifyReturnInstructions) {
EXPECT_TRUE(StepController::IsReturnInstruction(0x60)); // RTS
EXPECT_TRUE(StepController::IsReturnInstruction(0x6B)); // RTL
EXPECT_TRUE(StepController::IsReturnInstruction(0x40)); // RTI
EXPECT_FALSE(StepController::IsReturnInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsReturnInstruction(0x20)); // JSR
EXPECT_FALSE(StepController::IsReturnInstruction(0x4C)); // JMP
}
TEST_F(StepControllerTest, ClassifyBranchInstructions) {
EXPECT_TRUE(StepController::IsBranchInstruction(0x80)); // BRA
EXPECT_TRUE(StepController::IsBranchInstruction(0xD0)); // BNE
EXPECT_TRUE(StepController::IsBranchInstruction(0xF0)); // BEQ
EXPECT_TRUE(StepController::IsBranchInstruction(0x4C)); // JMP abs
EXPECT_TRUE(StepController::IsBranchInstruction(0x5C)); // JMP long
EXPECT_FALSE(StepController::IsBranchInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsBranchInstruction(0x20)); // JSR
EXPECT_FALSE(StepController::IsBranchInstruction(0x60)); // RTS
}
// --- StepInto Tests ---
TEST_F(StepControllerTest, StepIntoSimpleInstruction) {
// Simple program: NOP NOP NOP
SetupProgram({0xEA, 0xEA, 0xEA});
auto result = controller_.StepInto();
EXPECT_TRUE(result.success);
EXPECT_EQ(result.instructions_executed, 1u);
EXPECT_EQ(result.new_pc, 1u); // PC advanced by 1 (NOP size)
EXPECT_FALSE(result.call.has_value());
EXPECT_FALSE(result.ret.has_value());
}
TEST_F(StepControllerTest, StepIntoTracksCallStack) {
// Program: JSR $0010 at address 0
// JSR opcode (0x20) + 2-byte address = 3 bytes
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
auto result = controller_.StepInto();
EXPECT_TRUE(result.success);
EXPECT_TRUE(result.call.has_value());
EXPECT_EQ(result.call->target_address, 0x0010u);
EXPECT_EQ(controller_.GetCallDepth(), 1u);
}
// --- Call Stack Management Tests ---
TEST_F(StepControllerTest, CallStackPushesOnJSR) {
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
EXPECT_EQ(controller_.GetCallDepth(), 0u);
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
const auto& stack = controller_.GetCallStack();
EXPECT_EQ(stack.back().target_address, 0x0010u);
EXPECT_FALSE(stack.back().is_long);
}
TEST_F(StepControllerTest, CallStackPushesOnJSL) {
SetupProgram({0x22, 0x00, 0x80, 0x01}); // JSL $018000
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
const auto& stack = controller_.GetCallStack();
EXPECT_EQ(stack.back().target_address, 0x018000u);
EXPECT_TRUE(stack.back().is_long); // JSL is a long call
}
TEST_F(StepControllerTest, ClearCallStackWorks) {
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
controller_.ClearCallStack();
EXPECT_EQ(controller_.GetCallDepth(), 0u);
}
// --- GetInstructionSize Tests ---
TEST_F(StepControllerTest, InstructionSizeImplied) {
// Implied addressing (1 byte)
EXPECT_EQ(StepController::GetInstructionSize(0xEA, true, true), 1u); // NOP
EXPECT_EQ(StepController::GetInstructionSize(0x60, true, true), 1u); // RTS
EXPECT_EQ(StepController::GetInstructionSize(0x6B, true, true), 1u); // RTL
EXPECT_EQ(StepController::GetInstructionSize(0x40, true, true), 1u); // RTI
EXPECT_EQ(StepController::GetInstructionSize(0x18, true, true), 1u); // CLC
EXPECT_EQ(StepController::GetInstructionSize(0xFB, true, true), 1u); // XCE
}
TEST_F(StepControllerTest, InstructionSizeBranch) {
// Relative branch (2 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x80, true, true), 2u); // BRA
EXPECT_EQ(StepController::GetInstructionSize(0xD0, true, true), 2u); // BNE
EXPECT_EQ(StepController::GetInstructionSize(0xF0, true, true), 2u); // BEQ
EXPECT_EQ(StepController::GetInstructionSize(0x10, true, true), 2u); // BPL
// Relative long (3 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x82, true, true), 3u); // BRL
}
TEST_F(StepControllerTest, InstructionSizeJumpCall) {
// JSR/JMP absolute (3 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x20, true, true), 3u); // JSR
EXPECT_EQ(StepController::GetInstructionSize(0x4C, true, true), 3u); // JMP abs
EXPECT_EQ(StepController::GetInstructionSize(0xFC, true, true), 3u); // JSR (abs,X)
// Long (4 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x22, true, true), 4u); // JSL
EXPECT_EQ(StepController::GetInstructionSize(0x5C, true, true), 4u); // JMP long
}
// --- Error Handling Tests ---
TEST_F(StepControllerTest, StepIntoFailsWithoutConfiguration) {
// Don't call SetupProgram - controller is unconfigured
auto result = controller_.StepInto();
EXPECT_FALSE(result.success);
EXPECT_EQ(result.instructions_executed, 0u);
}
TEST_F(StepControllerTest, StepOutFailsWithEmptyCallStack) {
SetupProgram({0xEA, 0xEA, 0xEA}); // Just NOPs
// Don't execute any calls, so stack is empty
auto result = controller_.StepOut(100);
EXPECT_FALSE(result.success);
EXPECT_TRUE(result.message.find("empty") != std::string::npos);
}
// --- StepOver Non-Call Instruction ---
TEST_F(StepControllerTest, StepOverNonCallIsSameAsStepInto) {
// Program: NOP NOP
SetupProgram({0xEA, 0xEA});
auto result = controller_.StepOver(1000);
EXPECT_TRUE(result.success);
EXPECT_EQ(result.instructions_executed, 1u);
EXPECT_EQ(result.new_pc, 1u);
}
} // namespace
} // namespace debug
} // namespace emu
} // namespace yaze