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:
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
|
||||
Reference in New Issue
Block a user