Update CMake configuration and CI/CD workflows
- Upgraded CMake minimum version requirement to 3.16 and updated project version to 0.3.0. - Introduced new CMake presets for build configurations, including default, debug, and release options. - Added CI/CD workflows for continuous integration and release management, enhancing automated testing and deployment processes. - Integrated Asar assembler support with new wrapper classes and CLI commands for patching ROMs. - Implemented comprehensive tests for Asar integration, ensuring robust functionality and error handling. - Enhanced packaging configuration for cross-platform support, including Windows, macOS, and Linux. - Updated documentation and added test assets for improved clarity and usability.
This commit is contained in:
@@ -18,6 +18,7 @@ add_executable(
|
||||
rom_test.cc
|
||||
test_editor.cc
|
||||
hex_test.cc
|
||||
core/asar_wrapper_test.cc
|
||||
gfx/snes_tile_test.cc
|
||||
gfx/compression_test.cc
|
||||
gfx/snes_palette_test.cc
|
||||
@@ -37,6 +38,8 @@ add_executable(
|
||||
emu/audio/apu_test.cc
|
||||
emu/audio/ipl_handshake_test.cc
|
||||
integration/dungeon_editor_test.cc
|
||||
integration/asar_integration_test.cc
|
||||
integration/asar_rom_test.cc
|
||||
zelda3/object_parser_test.cc
|
||||
zelda3/object_parser_structs_test.cc
|
||||
zelda3/test_dungeon_objects.cc
|
||||
@@ -82,9 +85,10 @@ target_include_directories(
|
||||
app/
|
||||
lib/
|
||||
${CMAKE_SOURCE_DIR}/incl/
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src/
|
||||
${CMAKE_SOURCE_DIR}/test/
|
||||
${CMAKE_SOURCE_DIR}/src/lib/imgui_test_engine
|
||||
${ASAR_INCLUDE_DIR}
|
||||
${ASAR_INCLUDE_DIRS}
|
||||
${SDL2_INCLUDE_DIR}
|
||||
${PNG_INCLUDE_DIRS}
|
||||
${PROJECT_BINARY_DIR}
|
||||
@@ -106,9 +110,51 @@ target_link_libraries(
|
||||
gtest_main
|
||||
gtest
|
||||
)
|
||||
target_compile_definitions(yaze_test PRIVATE "linux")
|
||||
target_compile_definitions(yaze_test PRIVATE "stricmp=strcasecmp")
|
||||
# ROM Testing Configuration
|
||||
if(YAZE_ENABLE_ROM_TESTS)
|
||||
target_compile_definitions(yaze_test PRIVATE
|
||||
YAZE_ENABLE_ROM_TESTS=1
|
||||
YAZE_TEST_ROM_PATH="${YAZE_TEST_ROM_PATH}"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(yaze_test PRIVATE "IMGUI_ENABLE_TEST_ENGINE")
|
||||
|
||||
# Platform-specific definitions
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_definitions(yaze_test PRIVATE "linux" "stricmp=strcasecmp")
|
||||
elseif(APPLE)
|
||||
target_compile_definitions(yaze_test PRIVATE "MACOS" "stricmp=strcasecmp")
|
||||
elseif(WIN32)
|
||||
target_compile_definitions(yaze_test PRIVATE "WINDOWS")
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(yaze_test)
|
||||
|
||||
# Configure test discovery with labels
|
||||
gtest_discover_tests(yaze_test
|
||||
PROPERTIES
|
||||
LABELS "UNIT_TEST"
|
||||
)
|
||||
|
||||
# Add labels for ROM-dependent tests
|
||||
if(YAZE_ENABLE_ROM_TESTS)
|
||||
gtest_discover_tests(yaze_test
|
||||
TEST_FILTER "*AsarRomIntegrationTest*"
|
||||
PROPERTIES
|
||||
LABELS "ROM_DEPENDENT;INTEGRATION_TEST"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Add labels for other integration tests
|
||||
gtest_discover_tests(yaze_test
|
||||
TEST_FILTER "*AsarIntegrationTest*"
|
||||
PROPERTIES
|
||||
LABELS "INTEGRATION_TEST"
|
||||
)
|
||||
|
||||
gtest_discover_tests(yaze_test
|
||||
TEST_FILTER "*AsarWrapperTest*"
|
||||
PROPERTIES
|
||||
LABELS "UNIT_TEST"
|
||||
)
|
||||
82
test/assets/test_patch.asm
Normal file
82
test/assets/test_patch.asm
Normal file
@@ -0,0 +1,82 @@
|
||||
; Yaze Test Patch for Zelda3 ROM
|
||||
; This patch demonstrates Asar integration with a real ROM
|
||||
|
||||
; Test constants
|
||||
!test_ram_addr = $7E2000
|
||||
|
||||
; Simple code modification
|
||||
org $008000
|
||||
yaze_test_hook:
|
||||
; Save original context
|
||||
pha
|
||||
phx
|
||||
phy
|
||||
|
||||
; Set up processor state
|
||||
rep #$30 ; 16-bit A and X/Y
|
||||
|
||||
; Write test signature to RAM
|
||||
lda #$CAFE
|
||||
sta !test_ram_addr
|
||||
lda #$BEEF
|
||||
sta !test_ram_addr+2
|
||||
lda #$DEAD
|
||||
sta !test_ram_addr+4
|
||||
lda #$BABE
|
||||
sta !test_ram_addr+6
|
||||
|
||||
; Call test subroutine
|
||||
jsr yaze_test_function
|
||||
|
||||
; Restore context
|
||||
ply
|
||||
plx
|
||||
pla
|
||||
|
||||
; Continue with original code
|
||||
; (In a real patch, this would jump to the original code)
|
||||
rts
|
||||
|
||||
; Test function to verify Asar compilation
|
||||
yaze_test_function:
|
||||
pha
|
||||
|
||||
; Test arithmetic operations
|
||||
lda #$1000
|
||||
clc
|
||||
adc #$0234
|
||||
sta !test_ram_addr+8
|
||||
|
||||
; Test conditional logic
|
||||
cmp #$1234
|
||||
bne +
|
||||
lda #$600D
|
||||
sta !test_ram_addr+10
|
||||
+
|
||||
|
||||
pla
|
||||
rts
|
||||
|
||||
; Test data section
|
||||
yaze_test_data:
|
||||
db "YAZE", $00
|
||||
dw $1234, $5678, $9ABC, $DEF0
|
||||
|
||||
yaze_test_string:
|
||||
db "ASAR INTEGRATION TEST", $00
|
||||
|
||||
; Test lookup table
|
||||
yaze_test_table:
|
||||
dw yaze_test_hook
|
||||
dw yaze_test_function
|
||||
dw yaze_test_data
|
||||
dw yaze_test_string
|
||||
|
||||
; More advanced test - interrupt vector modification
|
||||
; org $00FFE4
|
||||
; dw yaze_test_hook ; COP vector (for testing)
|
||||
|
||||
print "Yaze Asar integration test patch compiled successfully!"
|
||||
print "Test hook at: ", hex(yaze_test_hook)
|
||||
print "Test function at: ", hex(yaze_test_function)
|
||||
print "Test data at: ", hex(yaze_test_data)
|
||||
322
test/core/asar_wrapper_test.cc
Normal file
322
test/core/asar_wrapper_test.cc
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace core {
|
||||
namespace {
|
||||
|
||||
class AsarWrapperTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
wrapper_ = std::make_unique<AsarWrapper>();
|
||||
CreateTestFiles();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
CleanupTestFiles();
|
||||
}
|
||||
|
||||
void CreateTestFiles() {
|
||||
// Create test directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_test";
|
||||
std::filesystem::create_directories(test_dir_);
|
||||
|
||||
// Create a simple test assembly file
|
||||
test_asm_path_ = test_dir_ / "test_patch.asm";
|
||||
std::ofstream asm_file(test_asm_path_);
|
||||
asm_file << R"(
|
||||
; Test assembly patch for yaze
|
||||
org $008000
|
||||
testlabel:
|
||||
LDA #$42
|
||||
STA $7E0000
|
||||
RTS
|
||||
|
||||
anotherlabel:
|
||||
LDA #$FF
|
||||
STA $7E0001
|
||||
RTL
|
||||
)";
|
||||
asm_file.close();
|
||||
|
||||
// Create invalid assembly file for error testing
|
||||
invalid_asm_path_ = test_dir_ / "invalid_patch.asm";
|
||||
std::ofstream invalid_file(invalid_asm_path_);
|
||||
invalid_file << R"(
|
||||
; Invalid assembly that should cause errors
|
||||
org $008000
|
||||
invalid_instruction_here
|
||||
LDA unknown_operand
|
||||
)";
|
||||
invalid_file.close();
|
||||
|
||||
// Create test ROM data using utility
|
||||
test_rom_ = yaze::test::TestRomManager::CreateMinimalTestRom(1024 * 1024);
|
||||
}
|
||||
|
||||
void CleanupTestFiles() {
|
||||
try {
|
||||
if (std::filesystem::exists(test_dir_)) {
|
||||
std::filesystem::remove_all(test_dir_);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Ignore cleanup errors in tests
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path test_asm_path_;
|
||||
std::filesystem::path invalid_asm_path_;
|
||||
std::vector<uint8_t> test_rom_;
|
||||
};
|
||||
|
||||
TEST_F(AsarWrapperTest, InitializationAndShutdown) {
|
||||
// Test initialization
|
||||
ASSERT_FALSE(wrapper_->IsInitialized());
|
||||
|
||||
auto status = wrapper_->Initialize();
|
||||
EXPECT_TRUE(status.ok()) << status.message();
|
||||
EXPECT_TRUE(wrapper_->IsInitialized());
|
||||
|
||||
// Test version info
|
||||
std::string version = wrapper_->GetVersion();
|
||||
EXPECT_FALSE(version.empty());
|
||||
EXPECT_NE(version, "Not initialized");
|
||||
|
||||
int api_version = wrapper_->GetApiVersion();
|
||||
EXPECT_GT(api_version, 0);
|
||||
|
||||
// Test shutdown
|
||||
wrapper_->Shutdown();
|
||||
EXPECT_FALSE(wrapper_->IsInitialized());
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, DoubleInitialization) {
|
||||
auto status1 = wrapper_->Initialize();
|
||||
EXPECT_TRUE(status1.ok());
|
||||
|
||||
auto status2 = wrapper_->Initialize();
|
||||
EXPECT_TRUE(status2.ok()); // Should not fail on double init
|
||||
EXPECT_TRUE(wrapper_->IsInitialized());
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, OperationsWithoutInitialization) {
|
||||
// Operations should fail when not initialized
|
||||
ASSERT_FALSE(wrapper_->IsInitialized());
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(test_asm_path_.string(), rom_copy);
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::HasSubstr("not initialized"));
|
||||
|
||||
auto symbols_result = wrapper_->ExtractSymbols(test_asm_path_.string());
|
||||
EXPECT_FALSE(symbols_result.ok());
|
||||
EXPECT_THAT(symbols_result.status().message(),
|
||||
testing::HasSubstr("not initialized"));
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, ValidPatchApplication) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
size_t original_size = rom_copy.size();
|
||||
|
||||
auto patch_result = wrapper_->ApplyPatch(test_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok()) << patch_result.status().message();
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
EXPECT_GT(result.rom_size, 0);
|
||||
EXPECT_EQ(rom_copy.size(), result.rom_size);
|
||||
|
||||
// Check that ROM was actually modified
|
||||
EXPECT_NE(rom_copy, test_rom_); // Should be different after patching
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, InvalidPatchHandling) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
|
||||
auto patch_result = wrapper_->ApplyPatch(invalid_asm_path_.string(), rom_copy);
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::HasSubstr("Patch failed"));
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, NonexistentPatchFile) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
std::string nonexistent_path = test_dir_.string() + "/nonexistent.asm";
|
||||
|
||||
auto patch_result = wrapper_->ApplyPatch(nonexistent_path, rom_copy);
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, SymbolExtraction) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
auto symbols_result = wrapper_->ExtractSymbols(test_asm_path_.string());
|
||||
ASSERT_TRUE(symbols_result.ok()) << symbols_result.status().message();
|
||||
|
||||
const auto& symbols = symbols_result.value();
|
||||
EXPECT_GT(symbols.size(), 0);
|
||||
|
||||
// Check for expected symbols from our test assembly
|
||||
bool found_testlabel = false;
|
||||
bool found_anotherlabel = false;
|
||||
|
||||
for (const auto& symbol : symbols) {
|
||||
EXPECT_FALSE(symbol.name.empty());
|
||||
EXPECT_GT(symbol.address, 0);
|
||||
|
||||
if (symbol.name == "testlabel") {
|
||||
found_testlabel = true;
|
||||
EXPECT_EQ(symbol.address, 0x008000); // Expected address from org directive
|
||||
} else if (symbol.name == "anotherlabel") {
|
||||
found_anotherlabel = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_testlabel) << "Expected 'testlabel' symbol not found";
|
||||
EXPECT_TRUE(found_anotherlabel) << "Expected 'anotherlabel' symbol not found";
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, SymbolTableOperations) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(test_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok());
|
||||
|
||||
// Test symbol table retrieval
|
||||
auto symbol_table = wrapper_->GetSymbolTable();
|
||||
EXPECT_GT(symbol_table.size(), 0);
|
||||
|
||||
// Test symbol lookup by name
|
||||
auto testlabel_symbol = wrapper_->FindSymbol("testlabel");
|
||||
EXPECT_TRUE(testlabel_symbol.has_value());
|
||||
if (testlabel_symbol) {
|
||||
EXPECT_EQ(testlabel_symbol->name, "testlabel");
|
||||
EXPECT_GT(testlabel_symbol->address, 0);
|
||||
}
|
||||
|
||||
// Test lookup of non-existent symbol
|
||||
auto nonexistent_symbol = wrapper_->FindSymbol("nonexistent_symbol");
|
||||
EXPECT_FALSE(nonexistent_symbol.has_value());
|
||||
|
||||
// Test symbols at address lookup
|
||||
if (testlabel_symbol) {
|
||||
auto symbols_at_addr = wrapper_->GetSymbolsAtAddress(testlabel_symbol->address);
|
||||
EXPECT_GT(symbols_at_addr.size(), 0);
|
||||
|
||||
bool found = false;
|
||||
for (const auto& symbol : symbols_at_addr) {
|
||||
if (symbol.name == "testlabel") {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, PatchFromString) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::string patch_content = R"(
|
||||
org $009000
|
||||
stringpatchlabel:
|
||||
LDA #$55
|
||||
STA $7E0002
|
||||
RTS
|
||||
)";
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatchFromString(
|
||||
patch_content, rom_copy, test_dir_.string());
|
||||
|
||||
ASSERT_TRUE(patch_result.ok()) << patch_result.status().message();
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_GT(result.symbols.size(), 0);
|
||||
|
||||
// Check for the symbol we defined
|
||||
bool found_symbol = false;
|
||||
for (const auto& symbol : result.symbols) {
|
||||
if (symbol.name == "stringpatchlabel") {
|
||||
found_symbol = true;
|
||||
EXPECT_EQ(symbol.address, 0x009000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_symbol);
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, AssemblyValidation) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Test valid assembly
|
||||
auto valid_status = wrapper_->ValidateAssembly(test_asm_path_.string());
|
||||
EXPECT_TRUE(valid_status.ok()) << valid_status.message();
|
||||
|
||||
// Test invalid assembly
|
||||
auto invalid_status = wrapper_->ValidateAssembly(invalid_asm_path_.string());
|
||||
EXPECT_FALSE(invalid_status.ok());
|
||||
EXPECT_THAT(invalid_status.message(),
|
||||
testing::HasSubstr("validation failed"));
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, ResetFunctionality) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Apply a patch to generate some state
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(test_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok());
|
||||
|
||||
// Verify we have symbols and potentially warnings/errors
|
||||
auto symbol_table_before = wrapper_->GetSymbolTable();
|
||||
EXPECT_GT(symbol_table_before.size(), 0);
|
||||
|
||||
// Reset and verify state is cleared
|
||||
wrapper_->Reset();
|
||||
|
||||
auto symbol_table_after = wrapper_->GetSymbolTable();
|
||||
EXPECT_EQ(symbol_table_after.size(), 0);
|
||||
|
||||
auto errors = wrapper_->GetLastErrors();
|
||||
auto warnings = wrapper_->GetLastWarnings();
|
||||
EXPECT_EQ(errors.size(), 0);
|
||||
EXPECT_EQ(warnings.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(AsarWrapperTest, CreatePatchNotImplemented) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::vector<uint8_t> original_rom = test_rom_;
|
||||
std::vector<uint8_t> modified_rom = test_rom_;
|
||||
modified_rom[100] = 0x42; // Make a small change
|
||||
|
||||
std::string patch_path = test_dir_.string() + "/generated.asm";
|
||||
auto status = wrapper_->CreatePatch(original_rom, modified_rom, patch_path);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_THAT(status.message(), testing::HasSubstr("not yet implemented"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace core
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "app/emu/memory/asm_parser.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "test/mocks/mock_memory.h"
|
||||
#include "mocks/mock_memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include "test/mocks/mock_memory.h"
|
||||
#include "mocks/mock_memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
#include "yaze.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
544
test/integration/asar_integration_test.cc
Normal file
544
test/integration/asar_integration_test.cc
Normal file
@@ -0,0 +1,544 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "app/rom.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
namespace integration {
|
||||
|
||||
class AsarIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
|
||||
// Create test directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_integration";
|
||||
std::filesystem::create_directories(test_dir_);
|
||||
|
||||
CreateTestRom();
|
||||
CreateTestAssemblyFiles();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
try {
|
||||
if (std::filesystem::exists(test_dir_)) {
|
||||
std::filesystem::remove_all(test_dir_);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
void CreateTestRom() {
|
||||
// Create a minimal SNES ROM structure
|
||||
test_rom_.resize(1024 * 1024, 0); // 1MB ROM
|
||||
|
||||
// Add SNES header at 0x7FC0 (LoROM)
|
||||
const uint32_t header_offset = 0x7FC0;
|
||||
|
||||
// ROM title (21 bytes)
|
||||
std::string title = "YAZE TEST ROM ";
|
||||
std::copy(title.begin(), title.end(), test_rom_.begin() + header_offset);
|
||||
|
||||
// Map mode (byte 21) - LoROM
|
||||
test_rom_[header_offset + 21] = 0x20;
|
||||
|
||||
// Cartridge type (byte 22)
|
||||
test_rom_[header_offset + 22] = 0x00;
|
||||
|
||||
// ROM size (byte 23) - 1MB
|
||||
test_rom_[header_offset + 23] = 0x0A;
|
||||
|
||||
// SRAM size (byte 24)
|
||||
test_rom_[header_offset + 24] = 0x00;
|
||||
|
||||
// Country code (byte 25)
|
||||
test_rom_[header_offset + 25] = 0x01;
|
||||
|
||||
// Developer ID (byte 26)
|
||||
test_rom_[header_offset + 26] = 0x00;
|
||||
|
||||
// Version (byte 27)
|
||||
test_rom_[header_offset + 27] = 0x00;
|
||||
|
||||
// Calculate and set checksum complement and checksum
|
||||
uint16_t checksum = 0;
|
||||
for (size_t i = 0; i < test_rom_.size(); ++i) {
|
||||
if (i != header_offset + 28 && i != header_offset + 29 &&
|
||||
i != header_offset + 30 && i != header_offset + 31) {
|
||||
checksum += test_rom_[i];
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t checksum_complement = checksum ^ 0xFFFF;
|
||||
test_rom_[header_offset + 28] = checksum_complement & 0xFF;
|
||||
test_rom_[header_offset + 29] = (checksum_complement >> 8) & 0xFF;
|
||||
test_rom_[header_offset + 30] = checksum & 0xFF;
|
||||
test_rom_[header_offset + 31] = (checksum >> 8) & 0xFF;
|
||||
|
||||
// Add some code at the reset vector location
|
||||
const uint32_t reset_vector_offset = 0x8000;
|
||||
test_rom_[reset_vector_offset] = 0x18; // CLC
|
||||
test_rom_[reset_vector_offset + 1] = 0xFB; // XCE
|
||||
test_rom_[reset_vector_offset + 2] = 0x4C; // JMP abs
|
||||
test_rom_[reset_vector_offset + 3] = 0x00; // $8000
|
||||
test_rom_[reset_vector_offset + 4] = 0x80;
|
||||
}
|
||||
|
||||
void CreateTestAssemblyFiles() {
|
||||
// Create comprehensive test assembly
|
||||
comprehensive_asm_path_ = test_dir_ / "comprehensive_test.asm";
|
||||
std::ofstream comp_file(comprehensive_asm_path_);
|
||||
comp_file << R"(
|
||||
; Comprehensive Asar test for Yaze integration
|
||||
!addr = $7E0000
|
||||
|
||||
; Test basic assembly
|
||||
org $008000
|
||||
main_entry:
|
||||
sei ; Disable interrupts
|
||||
clc ; Clear carry
|
||||
xce ; Switch to native mode
|
||||
|
||||
; Set up stack
|
||||
rep #$30 ; 16-bit A and X/Y
|
||||
ldx #$1FFF
|
||||
txs
|
||||
|
||||
; Test data writing
|
||||
lda #$1234
|
||||
sta !addr
|
||||
|
||||
; Call subroutines
|
||||
jsr init_graphics
|
||||
jsr init_sound
|
||||
|
||||
; Main loop
|
||||
main_loop:
|
||||
jsr update_game
|
||||
jsr wait_vblank
|
||||
bra main_loop
|
||||
|
||||
; Graphics initialization
|
||||
init_graphics:
|
||||
pha
|
||||
phx
|
||||
phy
|
||||
|
||||
; Set up PPU registers
|
||||
sep #$20 ; 8-bit A
|
||||
lda #$80
|
||||
sta $2100 ; Force blank
|
||||
|
||||
; Clear VRAM
|
||||
rep #$20 ; 16-bit A
|
||||
lda #$8000
|
||||
sta $2116 ; VRAM address
|
||||
|
||||
lda #$0000
|
||||
ldx #$8000
|
||||
clear_vram_loop:
|
||||
sta $2118 ; Write to VRAM
|
||||
dex
|
||||
bne clear_vram_loop
|
||||
|
||||
ply
|
||||
plx
|
||||
pla
|
||||
rts
|
||||
|
||||
; Sound initialization
|
||||
init_sound:
|
||||
pha
|
||||
|
||||
; Initialize APU
|
||||
sep #$20 ; 8-bit A
|
||||
lda #$00
|
||||
sta $2140 ; APU port 0
|
||||
sta $2141 ; APU port 1
|
||||
sta $2142 ; APU port 2
|
||||
sta $2143 ; APU port 3
|
||||
|
||||
pla
|
||||
rts
|
||||
|
||||
; Game update routine
|
||||
update_game:
|
||||
pha
|
||||
|
||||
; Read controller
|
||||
lda $4212 ; PPU status
|
||||
and #$01
|
||||
bne update_game ; Wait for vblank end
|
||||
|
||||
lda $4016 ; Controller 1
|
||||
; Process input here
|
||||
|
||||
pla
|
||||
rts
|
||||
|
||||
; Wait for vertical blank
|
||||
wait_vblank:
|
||||
pha
|
||||
wait_vb_loop:
|
||||
lda $4212 ; PPU status
|
||||
and #$80
|
||||
beq wait_vb_loop ; Wait for vblank
|
||||
pla
|
||||
rts
|
||||
|
||||
; Data tables
|
||||
org $00A000
|
||||
graphics_data:
|
||||
incbin "test_graphics.bin"
|
||||
|
||||
sound_data:
|
||||
db $00, $01, $02, $03, $04, $05, $06, $07
|
||||
db $08, $09, $0A, $0B, $0C, $0D, $0E, $0F
|
||||
|
||||
; String data for testing
|
||||
text_data:
|
||||
db "YAZE INTEGRATION TEST", $00
|
||||
|
||||
; Math functions
|
||||
calculate_distance:
|
||||
; Input: A = x1, X = y1, stack = x2, y2
|
||||
; Output: A = distance
|
||||
pha
|
||||
phx
|
||||
|
||||
; Calculate dx = x2 - x1
|
||||
pla ; Get x1
|
||||
pha ; Save it back
|
||||
; ... distance calculation here
|
||||
|
||||
plx
|
||||
pla
|
||||
rts
|
||||
|
||||
; Interrupt vectors
|
||||
org $00FFE0
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw main_entry ; RESET vector
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; Reserved
|
||||
dw $0000 ; NMI vector
|
||||
dw main_entry ; RESET vector
|
||||
dw $0000 ; IRQ vector
|
||||
)";
|
||||
comp_file.close();
|
||||
|
||||
// Create test graphics binary
|
||||
std::ofstream gfx_file(test_dir_ / "test_graphics.bin", std::ios::binary);
|
||||
std::vector<uint8_t> graphics_data(2048, 0x55); // Test pattern
|
||||
gfx_file.write(reinterpret_cast<char*>(graphics_data.data()), graphics_data.size());
|
||||
gfx_file.close();
|
||||
|
||||
// Create advanced assembly with macros and includes
|
||||
advanced_asm_path_ = test_dir_ / "advanced_test.asm";
|
||||
std::ofstream adv_file(advanced_asm_path_);
|
||||
adv_file << R"(
|
||||
; Advanced Asar features test
|
||||
!ram_addr = $7E1000
|
||||
|
||||
; Macro definitions
|
||||
macro move_block(source, dest, size)
|
||||
rep #$30
|
||||
ldx #<size>-1
|
||||
loop:
|
||||
lda <source>,x
|
||||
sta <dest>,x
|
||||
dex
|
||||
bpl loop
|
||||
endmacro
|
||||
|
||||
macro set_ppu_register(register, value)
|
||||
sep #$20
|
||||
lda #<value>
|
||||
sta <register>
|
||||
endmacro
|
||||
|
||||
; Test code with macros
|
||||
org $008000
|
||||
advanced_entry:
|
||||
%set_ppu_register($2100, $8F) ; Set forced blank
|
||||
|
||||
; Use block move macro
|
||||
%move_block($008100, !ram_addr, 256)
|
||||
|
||||
; Conditional assembly
|
||||
if !test_mode == 1
|
||||
jsr debug_routine
|
||||
endif
|
||||
|
||||
; Loop with labels
|
||||
ldx #$10
|
||||
test_loop:
|
||||
lda test_data,x
|
||||
sta !ram_addr,x
|
||||
dex
|
||||
bpl test_loop
|
||||
|
||||
rts
|
||||
|
||||
debug_routine:
|
||||
; Debug code
|
||||
rts
|
||||
|
||||
test_data:
|
||||
db $FF, $FE, $FD, $FC, $FB, $FA, $F9, $F8
|
||||
db $F7, $F6, $F5, $F4, $F3, $F2, $F1, $F0
|
||||
)";
|
||||
adv_file.close();
|
||||
|
||||
// Create error test assembly
|
||||
error_asm_path_ = test_dir_ / "error_test.asm";
|
||||
std::ofstream err_file(error_asm_path_);
|
||||
err_file << R"(
|
||||
; Assembly with intentional errors for testing error handling
|
||||
org $008000
|
||||
error_test:
|
||||
invalid_opcode ; This should cause an error
|
||||
lda unknown_label ; This should cause an error
|
||||
sta $999999 ; Invalid address
|
||||
bra ; Missing operand
|
||||
)";
|
||||
err_file.close();
|
||||
}
|
||||
|
||||
std::unique_ptr<app::core::AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path comprehensive_asm_path_;
|
||||
std::filesystem::path advanced_asm_path_;
|
||||
std::filesystem::path error_asm_path_;
|
||||
std::vector<uint8_t> test_rom_;
|
||||
};
|
||||
|
||||
TEST_F(AsarIntegrationTest, FullWorkflowIntegration) {
|
||||
// Initialize Asar
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Test ROM loading and patching workflow
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
size_t original_size = rom_copy.size();
|
||||
|
||||
// Apply comprehensive patch
|
||||
auto patch_result = wrapper_->ApplyPatch(comprehensive_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok()) << patch_result.status().message();
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Patch failed with errors: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
|
||||
// Verify ROM was modified correctly
|
||||
EXPECT_NE(rom_copy, test_rom_);
|
||||
EXPECT_GT(result.rom_size, 0);
|
||||
|
||||
// Verify symbols were extracted
|
||||
EXPECT_GT(result.symbols.size(), 0);
|
||||
|
||||
// Check for specific expected symbols
|
||||
bool found_main_entry = false;
|
||||
bool found_init_graphics = false;
|
||||
bool found_init_sound = false;
|
||||
|
||||
for (const auto& symbol : result.symbols) {
|
||||
if (symbol.name == "main_entry") {
|
||||
found_main_entry = true;
|
||||
EXPECT_EQ(symbol.address, 0x008000);
|
||||
} else if (symbol.name == "init_graphics") {
|
||||
found_init_graphics = true;
|
||||
} else if (symbol.name == "init_sound") {
|
||||
found_init_sound = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_main_entry) << "main_entry symbol not found";
|
||||
EXPECT_TRUE(found_init_graphics) << "init_graphics symbol not found";
|
||||
EXPECT_TRUE(found_init_sound) << "init_sound symbol not found";
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, AdvancedFeaturesIntegration) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Test advanced assembly features (macros, conditionals, etc.)
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
|
||||
auto patch_result = wrapper_->ApplyPatch(advanced_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok()) << patch_result.status().message();
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Advanced patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
|
||||
// Verify symbols from advanced assembly
|
||||
bool found_advanced_entry = false;
|
||||
bool found_test_loop = false;
|
||||
|
||||
for (const auto& symbol : result.symbols) {
|
||||
if (symbol.name == "advanced_entry") {
|
||||
found_advanced_entry = true;
|
||||
} else if (symbol.name == "test_loop") {
|
||||
found_test_loop = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_advanced_entry);
|
||||
EXPECT_TRUE(found_test_loop);
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, ErrorHandlingIntegration) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Test error handling with intentionally broken assembly
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
|
||||
auto patch_result = wrapper_->ApplyPatch(error_asm_path_.string(), rom_copy);
|
||||
|
||||
// Should fail due to errors in assembly
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
|
||||
// Verify error message contains useful information
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::AnyOf(
|
||||
testing::HasSubstr("invalid"),
|
||||
testing::HasSubstr("unknown"),
|
||||
testing::HasSubstr("error")));
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, SymbolExtractionWorkflow) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Extract symbols without applying patch
|
||||
auto symbols_result = wrapper_->ExtractSymbols(comprehensive_asm_path_.string());
|
||||
ASSERT_TRUE(symbols_result.ok()) << symbols_result.status().message();
|
||||
|
||||
const auto& symbols = symbols_result.value();
|
||||
EXPECT_GT(symbols.size(), 0);
|
||||
|
||||
// Test symbol table operations
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(comprehensive_asm_path_.string(), rom_copy);
|
||||
ASSERT_TRUE(patch_result.ok());
|
||||
|
||||
// Test symbol lookup by name
|
||||
auto main_symbol = wrapper_->FindSymbol("main_entry");
|
||||
ASSERT_TRUE(main_symbol.has_value());
|
||||
EXPECT_EQ(main_symbol->name, "main_entry");
|
||||
EXPECT_EQ(main_symbol->address, 0x008000);
|
||||
|
||||
// Test symbols at address lookup
|
||||
auto symbols_at_main = wrapper_->GetSymbolsAtAddress(0x008000);
|
||||
EXPECT_GT(symbols_at_main.size(), 0);
|
||||
|
||||
bool found_main_at_address = false;
|
||||
for (const auto& symbol : symbols_at_main) {
|
||||
if (symbol.name == "main_entry") {
|
||||
found_main_at_address = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_main_at_address);
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, MultipleOperationsIntegration) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Test multiple patch operations on the same wrapper instance
|
||||
std::vector<uint8_t> rom_copy1 = test_rom_;
|
||||
std::vector<uint8_t> rom_copy2 = test_rom_;
|
||||
|
||||
// First patch
|
||||
auto result1 = wrapper_->ApplyPatch(comprehensive_asm_path_.string(), rom_copy1);
|
||||
ASSERT_TRUE(result1.ok());
|
||||
EXPECT_TRUE(result1->success);
|
||||
|
||||
// Reset and apply different patch
|
||||
wrapper_->Reset();
|
||||
auto result2 = wrapper_->ApplyPatch(advanced_asm_path_.string(), rom_copy2);
|
||||
ASSERT_TRUE(result2.ok());
|
||||
EXPECT_TRUE(result2->success);
|
||||
|
||||
// Verify that symbol tables are different
|
||||
EXPECT_NE(result1->symbols.size(), result2->symbols.size());
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, PatchFromStringIntegration) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
std::string patch_content = R"(
|
||||
org $009000
|
||||
string_patch_test:
|
||||
lda #$42
|
||||
sta $7E2000
|
||||
jsr subroutine_test
|
||||
rts
|
||||
|
||||
subroutine_test:
|
||||
lda #$FF
|
||||
sta $7E2001
|
||||
rts
|
||||
)";
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto result = wrapper_->ApplyPatchFromString(patch_content, rom_copy, test_dir_.string());
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_TRUE(result->success);
|
||||
EXPECT_GT(result->symbols.size(), 0);
|
||||
|
||||
// Check for expected symbols
|
||||
bool found_string_patch = false;
|
||||
bool found_subroutine = false;
|
||||
|
||||
for (const auto& symbol : result->symbols) {
|
||||
if (symbol.name == "string_patch_test") {
|
||||
found_string_patch = true;
|
||||
EXPECT_EQ(symbol.address, 0x009000);
|
||||
} else if (symbol.name == "subroutine_test") {
|
||||
found_subroutine = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_string_patch);
|
||||
EXPECT_TRUE(found_subroutine);
|
||||
}
|
||||
|
||||
TEST_F(AsarIntegrationTest, LargeRomHandling) {
|
||||
ASSERT_TRUE(wrapper_->Initialize().ok());
|
||||
|
||||
// Create a larger ROM for testing
|
||||
std::vector<uint8_t> large_rom(4 * 1024 * 1024, 0); // 4MB ROM
|
||||
|
||||
// Set up basic SNES header
|
||||
const uint32_t header_offset = 0x7FC0;
|
||||
std::string title = "LARGE ROM TEST ";
|
||||
std::copy(title.begin(), title.end(), large_rom.begin() + header_offset);
|
||||
large_rom[header_offset + 21] = 0x20; // LoROM
|
||||
large_rom[header_offset + 23] = 0x0C; // 4MB
|
||||
|
||||
auto result = wrapper_->ApplyPatch(comprehensive_asm_path_.string(), large_rom);
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_TRUE(result->success);
|
||||
EXPECT_EQ(large_rom.size(), result->rom_size);
|
||||
}
|
||||
|
||||
} // namespace integration
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
412
test/integration/asar_rom_test.cc
Normal file
412
test/integration/asar_rom_test.cc
Normal file
@@ -0,0 +1,412 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "app/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
namespace integration {
|
||||
|
||||
/**
|
||||
* @brief Test class for Asar integration with real ROM files
|
||||
* These tests are only run when ROM testing is enabled
|
||||
*/
|
||||
class AsarRomIntegrationTest : public RomDependentTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
RomDependentTest::SetUp();
|
||||
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
ASSERT_OK(wrapper_->Initialize());
|
||||
|
||||
// Create test directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_rom_test";
|
||||
std::filesystem::create_directories(test_dir_);
|
||||
|
||||
CreateTestPatches();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
try {
|
||||
if (std::filesystem::exists(test_dir_)) {
|
||||
std::filesystem::remove_all(test_dir_);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
void CreateTestPatches() {
|
||||
// Create a simple test patch
|
||||
simple_patch_path_ = test_dir_ / "simple_test.asm";
|
||||
std::ofstream simple_file(simple_patch_path_);
|
||||
simple_file << R"(
|
||||
; Simple Asar patch for real ROM testing
|
||||
org $008000
|
||||
yaze_test_entry:
|
||||
sei ; Disable interrupts
|
||||
clc ; Clear carry
|
||||
xce ; Switch to native mode
|
||||
|
||||
rep #$30 ; 16-bit A and X/Y
|
||||
ldx #$1FFF
|
||||
txs ; Set stack pointer
|
||||
|
||||
; Test data writing
|
||||
lda #$CAFE
|
||||
sta $7E0000 ; Write test value to RAM
|
||||
|
||||
; Set a custom value that we can verify
|
||||
lda #$BEEF
|
||||
sta $7E0002
|
||||
|
||||
sep #$20 ; 8-bit A
|
||||
lda #$42
|
||||
sta $7E0004 ; Another test value
|
||||
|
||||
rep #$20 ; Back to 16-bit A
|
||||
rts
|
||||
|
||||
; Subroutine for testing
|
||||
yaze_test_subroutine:
|
||||
pha
|
||||
lda #$1337
|
||||
sta $7E0010
|
||||
pla
|
||||
rts
|
||||
|
||||
; Data for testing
|
||||
yaze_test_data:
|
||||
db "YAZE", $00
|
||||
dw $1234, $5678, $9ABC, $DEF0
|
||||
)";
|
||||
simple_file.close();
|
||||
|
||||
// Create a patch that modifies game behavior
|
||||
gameplay_patch_path_ = test_dir_ / "gameplay_test.asm";
|
||||
std::ofstream gameplay_file(gameplay_patch_path_);
|
||||
gameplay_file << R"(
|
||||
; Gameplay modification patch for testing
|
||||
; This modifies Link's starting health and magic
|
||||
|
||||
; Increase Link's maximum health
|
||||
org $7EF36C
|
||||
db $A0 ; 160/8 = 20 hearts (was usually $60 = 12 hearts)
|
||||
|
||||
; Increase Link's maximum magic
|
||||
org $7EF36E
|
||||
db $80 ; Full magic meter
|
||||
|
||||
; Custom routine for health restoration
|
||||
org $00C000
|
||||
yaze_health_restore:
|
||||
sep #$20 ; 8-bit A
|
||||
lda #$A0 ; Full health
|
||||
sta $7EF36C ; Current health
|
||||
|
||||
lda #$80 ; Full magic
|
||||
sta $7EF36E ; Current magic
|
||||
|
||||
rep #$20 ; 16-bit A
|
||||
rtl
|
||||
|
||||
; Hook into the game's main loop (example address)
|
||||
org $008012
|
||||
jsl yaze_health_restore
|
||||
nop ; Pad if needed
|
||||
)";
|
||||
gameplay_file.close();
|
||||
|
||||
// Create a symbol extraction test patch
|
||||
symbols_patch_path_ = test_dir_ / "symbols_test.asm";
|
||||
std::ofstream symbols_file(symbols_patch_path_);
|
||||
symbols_file << R"(
|
||||
; Comprehensive symbol test for Asar integration
|
||||
|
||||
; Define some constants
|
||||
!player_x = $7E0020
|
||||
!player_y = $7E0022
|
||||
!player_health = $7EF36C
|
||||
!player_magic = $7EF36E
|
||||
|
||||
; Main code section
|
||||
org $008000
|
||||
main_routine:
|
||||
jsr init_player
|
||||
jsr game_loop
|
||||
rts
|
||||
|
||||
; Player initialization
|
||||
init_player:
|
||||
rep #$30 ; 16-bit A and X/Y
|
||||
|
||||
; Set initial position
|
||||
lda #$0080
|
||||
sta !player_x
|
||||
lda #$0070
|
||||
sta !player_y
|
||||
|
||||
; Set initial stats
|
||||
sep #$20 ; 8-bit A
|
||||
lda #$A0
|
||||
sta !player_health
|
||||
lda #$80
|
||||
sta !player_magic
|
||||
|
||||
rep #$30 ; Back to 16-bit
|
||||
rts
|
||||
|
||||
; Main game loop
|
||||
game_loop:
|
||||
jsr update_player
|
||||
jsr update_enemies
|
||||
jsr update_graphics
|
||||
rts
|
||||
|
||||
; Player update routine
|
||||
update_player:
|
||||
; Read controller input
|
||||
sep #$20
|
||||
lda $4016 ; Controller 1
|
||||
|
||||
; Process movement
|
||||
bit #$08 ; Up
|
||||
beq +
|
||||
dec !player_y
|
||||
+ bit #$04 ; Down
|
||||
beq +
|
||||
inc !player_y
|
||||
+ bit #$02 ; Left
|
||||
beq +
|
||||
dec !player_x
|
||||
+ bit #$01 ; Right
|
||||
beq +
|
||||
inc !player_x
|
||||
+
|
||||
rep #$20
|
||||
rts
|
||||
|
||||
; Enemy update routine
|
||||
update_enemies:
|
||||
; Placeholder for enemy logic
|
||||
rts
|
||||
|
||||
; Graphics update routine
|
||||
update_graphics:
|
||||
; Placeholder for graphics updates
|
||||
rts
|
||||
|
||||
; Utility functions
|
||||
multiply_by_two:
|
||||
asl a
|
||||
rts
|
||||
|
||||
divide_by_two:
|
||||
lsr a
|
||||
rts
|
||||
|
||||
; Data tables
|
||||
enemy_data_table:
|
||||
dw enemy_goomba, enemy_koopa, enemy_shell
|
||||
dw $0000 ; End marker
|
||||
|
||||
enemy_goomba:
|
||||
dw $0010, $0020, $0001 ; x, y, type
|
||||
|
||||
enemy_koopa:
|
||||
dw $0050, $0030, $0002 ; x, y, type
|
||||
|
||||
enemy_shell:
|
||||
dw $0080, $0040, $0003 ; x, y, type
|
||||
)";
|
||||
symbols_file.close();
|
||||
}
|
||||
|
||||
std::unique_ptr<app::core::AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path simple_patch_path_;
|
||||
std::filesystem::path gameplay_patch_path_;
|
||||
std::filesystem::path symbols_patch_path_;
|
||||
};
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, SimplePatchOnRealRom) {
|
||||
// Make a copy of the ROM for testing
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
size_t original_size = rom_copy.size();
|
||||
|
||||
// Apply simple patch
|
||||
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
|
||||
// Verify ROM was modified
|
||||
EXPECT_NE(rom_copy, test_rom_); // Should be different
|
||||
EXPECT_GE(rom_copy.size(), original_size); // Size may have grown
|
||||
|
||||
// Check for expected symbols
|
||||
bool found_entry = false;
|
||||
bool found_subroutine = false;
|
||||
|
||||
for (const auto& symbol : result.symbols) {
|
||||
if (symbol.name == "yaze_test_entry") {
|
||||
found_entry = true;
|
||||
EXPECT_EQ(symbol.address, 0x008000);
|
||||
} else if (symbol.name == "yaze_test_subroutine") {
|
||||
found_subroutine = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_entry) << "yaze_test_entry symbol not found";
|
||||
EXPECT_TRUE(found_subroutine) << "yaze_test_subroutine symbol not found";
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, SymbolExtractionFromRealRom) {
|
||||
// Extract symbols from comprehensive test
|
||||
auto symbols_result = wrapper_->ExtractSymbols(symbols_patch_path_.string());
|
||||
ASSERT_OK(symbols_result.status());
|
||||
|
||||
const auto& symbols = symbols_result.value();
|
||||
EXPECT_GT(symbols.size(), 0);
|
||||
|
||||
// Check for specific symbols we expect
|
||||
std::vector<std::string> expected_symbols = {
|
||||
"main_routine", "init_player", "game_loop", "update_player",
|
||||
"update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"
|
||||
};
|
||||
|
||||
for (const auto& expected_symbol : expected_symbols) {
|
||||
bool found = false;
|
||||
for (const auto& symbol : symbols) {
|
||||
if (symbol.name == expected_symbol) {
|
||||
found = true;
|
||||
EXPECT_GT(symbol.address, 0) << "Symbol " << expected_symbol
|
||||
<< " has invalid address";
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found) << "Expected symbol not found: " << expected_symbol;
|
||||
}
|
||||
|
||||
// Test symbol lookup functionality
|
||||
auto symbol_table = wrapper_->GetSymbolTable();
|
||||
EXPECT_GT(symbol_table.size(), 0);
|
||||
|
||||
auto main_symbol = wrapper_->FindSymbol("main_routine");
|
||||
EXPECT_TRUE(main_symbol.has_value());
|
||||
if (main_symbol) {
|
||||
EXPECT_EQ(main_symbol->name, "main_routine");
|
||||
EXPECT_EQ(main_symbol->address, 0x008000);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, GameplayModificationPatch) {
|
||||
// Make a copy of the ROM
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
|
||||
// Apply gameplay modification patch
|
||||
auto patch_result = wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Gameplay patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
|
||||
// Verify specific memory locations were modified
|
||||
// Note: These addresses are based on the patch content
|
||||
|
||||
// Check health modification at 0x7EF36C -> ROM offset would need calculation
|
||||
// For a proper test, we'd need to convert SNES addresses to ROM offsets
|
||||
|
||||
// Check if custom routine was inserted at 0xC000 -> ROM offset 0x18000 (in LoROM)
|
||||
const uint32_t rom_offset = 0x18000; // Bank $00:C000 in LoROM
|
||||
if (rom_offset < rom_copy.size()) {
|
||||
// Check for SEP #$20 instruction (0xE2 0x20)
|
||||
EXPECT_EQ(rom_copy[rom_offset], 0xE2);
|
||||
EXPECT_EQ(rom_copy[rom_offset + 1], 0x20);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, LargeRomPatchingStability) {
|
||||
// Test with the actual ROM which might be larger
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
size_t original_size = rom_copy.size();
|
||||
|
||||
// Apply multiple patches in sequence
|
||||
auto result1 = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(result1.status());
|
||||
EXPECT_TRUE(result1->success);
|
||||
|
||||
// Reset and apply another patch
|
||||
wrapper_->Reset();
|
||||
auto result2 = wrapper_->ApplyPatch(symbols_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(result2.status());
|
||||
EXPECT_TRUE(result2->success);
|
||||
|
||||
// Verify stability
|
||||
EXPECT_GE(rom_copy.size(), original_size);
|
||||
EXPECT_GT(result2->symbols.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, ErrorHandlingWithRealRom) {
|
||||
// Create an intentionally broken patch
|
||||
auto broken_patch_path = test_dir_ / "broken_test.asm";
|
||||
std::ofstream broken_file(broken_patch_path);
|
||||
broken_file << R"(
|
||||
; Broken patch for error testing
|
||||
org $008000
|
||||
broken_routine:
|
||||
invalid_opcode ; This will cause an error
|
||||
lda unknown_symbol ; This will cause an error
|
||||
sta $FFFFFF ; Invalid address
|
||||
)";
|
||||
broken_file.close();
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy);
|
||||
|
||||
// Should fail with proper error messages
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::AnyOf(
|
||||
testing::HasSubstr("invalid"),
|
||||
testing::HasSubstr("unknown"),
|
||||
testing::HasSubstr("error")));
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, PatchValidationWorkflow) {
|
||||
// Test the complete workflow: validate -> patch -> verify
|
||||
|
||||
// Step 1: Validate assembly
|
||||
auto validation_result = wrapper_->ValidateAssembly(simple_patch_path_.string());
|
||||
EXPECT_OK(validation_result);
|
||||
|
||||
// Step 2: Apply patch
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
EXPECT_TRUE(patch_result->success);
|
||||
|
||||
// Step 3: Verify results
|
||||
EXPECT_GT(patch_result->symbols.size(), 0);
|
||||
EXPECT_GT(patch_result->rom_size, 0);
|
||||
|
||||
// Step 4: Test symbol operations
|
||||
auto entry_symbol = wrapper_->FindSymbol("yaze_test_entry");
|
||||
EXPECT_TRUE(entry_symbol.has_value());
|
||||
|
||||
if (entry_symbol) {
|
||||
auto symbols_at_address = wrapper_->GetSymbolsAtAddress(entry_symbol->address);
|
||||
EXPECT_GT(symbols_at_address.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace integration
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "test/integration/dungeon_editor_test.h"
|
||||
#include "integration/dungeon_editor_test.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include "app/rom.h"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
#include "app/transaction.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "test/test_editor.h"
|
||||
#include "test_editor.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
|
||||
156
test/test_utils.h
Normal file
156
test/test_utils.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#ifndef YAZE_TEST_TEST_UTILS_H
|
||||
#define YAZE_TEST_TEST_UTILS_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Utility class for handling test ROM files
|
||||
*/
|
||||
class TestRomManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Check if ROM testing is enabled and ROM file exists
|
||||
* @return True if ROM tests can be run
|
||||
*/
|
||||
static bool IsRomTestingEnabled() {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
return std::filesystem::exists(GetTestRomPath());
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the path to the test ROM file
|
||||
* @return Path to the test ROM
|
||||
*/
|
||||
static std::string GetTestRomPath() {
|
||||
#ifdef YAZE_TEST_ROM_PATH
|
||||
return YAZE_TEST_ROM_PATH;
|
||||
#else
|
||||
return "zelda3.sfc";
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load the test ROM file into memory
|
||||
* @return Vector containing ROM data, or empty if failed
|
||||
*/
|
||||
static std::vector<uint8_t> LoadTestRom() {
|
||||
if (!IsRomTestingEnabled()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string rom_path = GetTestRomPath();
|
||||
std::ifstream file(rom_path, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "Failed to open test ROM: " << rom_path << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get file size
|
||||
file.seekg(0, std::ios::end);
|
||||
size_t file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Read file
|
||||
std::vector<uint8_t> rom_data(file_size);
|
||||
file.read(reinterpret_cast<char*>(rom_data.data()), file_size);
|
||||
|
||||
if (!file) {
|
||||
std::cerr << "Failed to read test ROM: " << rom_path << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
return rom_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a minimal test ROM for unit testing
|
||||
* @param size Size of the ROM in bytes
|
||||
* @return Vector containing minimal ROM data
|
||||
*/
|
||||
static std::vector<uint8_t> CreateMinimalTestRom(size_t size = 1024 * 1024) {
|
||||
std::vector<uint8_t> rom_data(size, 0);
|
||||
|
||||
// Add minimal SNES header at 0x7FC0 (LoROM)
|
||||
const size_t header_offset = 0x7FC0;
|
||||
if (size > header_offset + 32) {
|
||||
// ROM title
|
||||
std::string title = "YAZE TEST ROM ";
|
||||
std::copy(title.begin(), title.end(), rom_data.begin() + header_offset);
|
||||
|
||||
// Map mode (LoROM)
|
||||
rom_data[header_offset + 21] = 0x20;
|
||||
|
||||
// ROM size (1MB)
|
||||
rom_data[header_offset + 23] = 0x0A;
|
||||
|
||||
// Calculate and set checksum
|
||||
uint16_t checksum = 0;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
if (i < header_offset + 28 || i > header_offset + 31) {
|
||||
checksum += rom_data[i];
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t checksum_complement = checksum ^ 0xFFFF;
|
||||
rom_data[header_offset + 28] = checksum_complement & 0xFF;
|
||||
rom_data[header_offset + 29] = (checksum_complement >> 8) & 0xFF;
|
||||
rom_data[header_offset + 30] = checksum & 0xFF;
|
||||
rom_data[header_offset + 31] = (checksum >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
return rom_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Skip test if ROM testing is not enabled
|
||||
* @param test_name Name of the test for logging
|
||||
*/
|
||||
static void SkipIfRomTestingDisabled(const std::string& test_name) {
|
||||
if (!IsRomTestingEnabled()) {
|
||||
GTEST_SKIP() << "ROM testing disabled or ROM file not found. "
|
||||
<< "Test: " << test_name << " requires: " << GetTestRomPath();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test macro for ROM-dependent tests
|
||||
*/
|
||||
#define YAZE_ROM_TEST(test_case_name, test_name) \
|
||||
TEST(test_case_name, test_name) { \
|
||||
yaze::test::TestRomManager::SkipIfRomTestingDisabled(#test_case_name "." #test_name); \
|
||||
YAZE_ROM_TEST_BODY_##test_case_name##_##test_name(); \
|
||||
} \
|
||||
void YAZE_ROM_TEST_BODY_##test_case_name##_##test_name()
|
||||
|
||||
/**
|
||||
* @brief Test fixture for ROM-dependent tests
|
||||
*/
|
||||
class RomDependentTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
TestRomManager::SkipIfRomTestingDisabled("RomDependentTest");
|
||||
test_rom_ = TestRomManager::LoadTestRom();
|
||||
ASSERT_FALSE(test_rom_.empty()) << "Failed to load test ROM";
|
||||
}
|
||||
|
||||
std::vector<uint8_t> test_rom_;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_TEST_TEST_UTILS_H
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "absl/debugging/failure_signal_handler.h"
|
||||
#include "absl/debugging/symbolize.h"
|
||||
#include "test/test_editor.h"
|
||||
#include "test_editor.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
absl::InitializeSymbolizer(argv[0]);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "app/editor/message/message_data.h"
|
||||
#include "app/editor/message/message_editor.h"
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "test/mocks/mock_rom.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "test_dungeon_objects.h"
|
||||
#include "test/mocks/mock_rom.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
#include "app/zelda3/dungeon/object_parser.h"
|
||||
#include "app/zelda3/dungeon/object_renderer.h"
|
||||
#include "app/zelda3/dungeon/room_object.h"
|
||||
#include "app/zelda3/dungeon/room_layout.h"
|
||||
#include "app/gfx/snes_color.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "test/testing.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "test/mocks/mock_rom.h"
|
||||
#include "test/testing.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
Reference in New Issue
Block a user