- 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.
545 lines
14 KiB
C++
545 lines
14 KiB
C++
#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
|