backend-infra-engineer: Release v0.3.2 snapshot
This commit is contained in:
93
test/unit/cli/resource_catalog_test.cc
Normal file
93
test/unit/cli/resource_catalog_test.cc
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "cli/service/resources/resource_catalog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace {
|
||||
|
||||
TEST(ResourceCatalogTest, SerializeResourceIncludesReturnsArray) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
auto overworld_schema = catalog.GetResource("overworld");
|
||||
ASSERT_TRUE(overworld_schema.ok());
|
||||
|
||||
std::string output = catalog.SerializeResource(overworld_schema.value());
|
||||
EXPECT_NE(output.find("\"resources\""), std::string::npos);
|
||||
EXPECT_NE(output.find("\"returns\":"), std::string::npos);
|
||||
EXPECT_NE(output.find("\"tile\""), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ResourceCatalogTest, SerializeAllResourcesIncludesAgentDescribeMetadata) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
std::string output = catalog.SerializeResources(catalog.AllResources());
|
||||
|
||||
EXPECT_NE(output.find("\"agent\""), std::string::npos);
|
||||
EXPECT_NE(output.find("\"effects\":"), std::string::npos);
|
||||
EXPECT_NE(output.find("\"returns\":"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ResourceCatalogTest, RomSchemaExposesActionsAndMetadata) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
auto rom_schema = catalog.GetResource("rom");
|
||||
ASSERT_TRUE(rom_schema.ok());
|
||||
|
||||
const auto& actions = rom_schema->actions;
|
||||
ASSERT_EQ(actions.size(), 3);
|
||||
EXPECT_EQ(actions[0].name, "validate");
|
||||
EXPECT_FALSE(actions[0].effects.empty());
|
||||
EXPECT_FALSE(actions[0].returns.empty());
|
||||
EXPECT_EQ(actions[1].name, "diff");
|
||||
EXPECT_EQ(actions[2].name, "generate-golden");
|
||||
}
|
||||
|
||||
TEST(ResourceCatalogTest, PatchSchemaIncludesAsarAndCreateActions) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
auto patch_schema = catalog.GetResource("patch");
|
||||
ASSERT_TRUE(patch_schema.ok());
|
||||
|
||||
const auto& actions = patch_schema->actions;
|
||||
ASSERT_GE(actions.size(), 3);
|
||||
EXPECT_EQ(actions[0].name, "apply");
|
||||
EXPECT_FALSE(actions[0].returns.empty());
|
||||
|
||||
auto has_asar = std::find_if(actions.begin(), actions.end(), [](const auto& action) {
|
||||
return action.name == "apply-asar";
|
||||
});
|
||||
EXPECT_NE(has_asar, actions.end());
|
||||
|
||||
auto has_create = std::find_if(actions.begin(), actions.end(), [](const auto& action) {
|
||||
return action.name == "create";
|
||||
});
|
||||
EXPECT_NE(has_create, actions.end());
|
||||
}
|
||||
|
||||
TEST(ResourceCatalogTest, DungeonSchemaListsMetadataAndObjectsReturns) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
auto dungeon_schema = catalog.GetResource("dungeon");
|
||||
ASSERT_TRUE(dungeon_schema.ok());
|
||||
|
||||
const auto& actions = dungeon_schema->actions;
|
||||
ASSERT_EQ(actions.size(), 2);
|
||||
EXPECT_EQ(actions[0].name, "export");
|
||||
EXPECT_FALSE(actions[0].returns.empty());
|
||||
EXPECT_EQ(actions[1].name, "list-objects");
|
||||
EXPECT_FALSE(actions[1].returns.empty());
|
||||
}
|
||||
|
||||
TEST(ResourceCatalogTest, YamlSerializationIncludesMetadataAndActions) {
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
std::string yaml = catalog.SerializeResourcesAsYaml(
|
||||
catalog.AllResources(), "0.1.0", "2025-10-01");
|
||||
|
||||
EXPECT_NE(yaml.find("version: \"0.1.0\""), std::string::npos);
|
||||
EXPECT_NE(yaml.find("name: \"patch\""), std::string::npos);
|
||||
EXPECT_NE(yaml.find("effects:"), std::string::npos);
|
||||
EXPECT_NE(yaml.find("returns:"), std::string::npos);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
244
test/unit/cli/tile16_proposal_generator_test.cc
Normal file
244
test/unit/cli/tile16_proposal_generator_test.cc
Normal file
@@ -0,0 +1,244 @@
|
||||
// Test suite for Tile16ProposalGenerator
|
||||
// Tests the new ParseSetAreaCommand and ParseReplaceTileCommand functionality
|
||||
|
||||
#include "cli/service/planning/tile16_proposal_generator.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "test/mocks/mock_rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace {
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
class Tile16ProposalGeneratorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
generator_ = std::make_unique<Tile16ProposalGenerator>();
|
||||
}
|
||||
|
||||
std::unique_ptr<Tile16ProposalGenerator> generator_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ParseSetTileCommand Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetTileCommand_ValidCommand) {
|
||||
std::string command = "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E";
|
||||
|
||||
auto result = generator_->ParseSetTileCommand(command, nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_EQ(result->map_id, 0);
|
||||
EXPECT_EQ(result->x, 10);
|
||||
EXPECT_EQ(result->y, 20);
|
||||
EXPECT_EQ(result->new_tile, 0x02E);
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetTileCommand_InvalidFormat) {
|
||||
std::string command = "overworld set-tile --map 0"; // Missing required args
|
||||
|
||||
auto result = generator_->ParseSetTileCommand(command, nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("Invalid command format"));
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetTileCommand_WrongCommandType) {
|
||||
std::string command = "overworld get-tile --map 0 --x 10 --y 20";
|
||||
|
||||
auto result = generator_->ParseSetTileCommand(command, nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("Not a set-tile command"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ParseSetAreaCommand Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetAreaCommand_ValidCommand) {
|
||||
std::string command =
|
||||
"overworld set-area --map 0 --x 10 --y 20 --width 5 --height 3 --tile 0x02E";
|
||||
|
||||
auto result = generator_->ParseSetAreaCommand(command, nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_EQ(result->size(), 15); // 5 width * 3 height = 15 tiles
|
||||
|
||||
// Check first tile
|
||||
EXPECT_EQ((*result)[0].map_id, 0);
|
||||
EXPECT_EQ((*result)[0].x, 10);
|
||||
EXPECT_EQ((*result)[0].y, 20);
|
||||
EXPECT_EQ((*result)[0].new_tile, 0x02E);
|
||||
|
||||
// Check last tile
|
||||
EXPECT_EQ((*result)[14].x, 14); // 10 + 4
|
||||
EXPECT_EQ((*result)[14].y, 22); // 20 + 2
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetAreaCommand_SingleTile) {
|
||||
std::string command =
|
||||
"overworld set-area --map 0 --x 10 --y 20 --width 1 --height 1 --tile 0x02E";
|
||||
|
||||
auto result = generator_->ParseSetAreaCommand(command, nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result->size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetAreaCommand_LargeArea) {
|
||||
std::string command =
|
||||
"overworld set-area --map 0 --x 0 --y 0 --width 32 --height 32 --tile 0x000";
|
||||
|
||||
auto result = generator_->ParseSetAreaCommand(command, nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result->size(), 1024); // 32 * 32
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseSetAreaCommand_InvalidFormat) {
|
||||
std::string command = "overworld set-area --map 0 --x 10"; // Missing args
|
||||
|
||||
auto result = generator_->ParseSetAreaCommand(command, nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("Invalid set-area command format"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ParseReplaceTileCommand Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseReplaceTileCommand_NoROM) {
|
||||
std::string command =
|
||||
"overworld replace-tile --map 0 --old-tile 0x02E --new-tile 0x030";
|
||||
|
||||
auto result = generator_->ParseReplaceTileCommand(command, nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("ROM must be loaded"));
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, ParseReplaceTileCommand_InvalidFormat) {
|
||||
std::string command = "overworld replace-tile --map 0"; // Missing tiles
|
||||
|
||||
auto result = generator_->ParseReplaceTileCommand(command, nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("Invalid replace-tile command format"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GenerateFromCommands Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, GenerateFromCommands_MultipleCommands) {
|
||||
std::vector<std::string> commands = {
|
||||
"overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E",
|
||||
"overworld set-area --map 0 --x 5 --y 5 --width 2 --height 2 --tile 0x030"
|
||||
};
|
||||
|
||||
auto result = generator_->GenerateFromCommands(
|
||||
"Test prompt", commands, "test_ai", nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_EQ(result->changes.size(), 5); // 1 from set-tile + 4 from set-area
|
||||
EXPECT_EQ(result->prompt, "Test prompt");
|
||||
EXPECT_EQ(result->ai_service, "test_ai");
|
||||
EXPECT_EQ(result->status, Tile16Proposal::Status::PENDING);
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, GenerateFromCommands_EmptyCommands) {
|
||||
std::vector<std::string> commands = {};
|
||||
|
||||
auto result = generator_->GenerateFromCommands(
|
||||
"Test prompt", commands, "test_ai", nullptr);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_THAT(result.status().message(),
|
||||
::testing::HasSubstr("No valid tile16 changes found"));
|
||||
}
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, GenerateFromCommands_IgnoresComments) {
|
||||
std::vector<std::string> commands = {
|
||||
"# This is a comment",
|
||||
"overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E",
|
||||
"# Another comment",
|
||||
"" // Empty line
|
||||
};
|
||||
|
||||
auto result = generator_->GenerateFromCommands(
|
||||
"Test prompt", commands, "test_ai", nullptr);
|
||||
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result->changes.size(), 1); // Only the valid command
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tile16Change Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, Tile16Change_ToString) {
|
||||
Tile16Change change;
|
||||
change.map_id = 5;
|
||||
change.x = 10;
|
||||
change.y = 20;
|
||||
change.old_tile = 0x02E;
|
||||
change.new_tile = 0x030;
|
||||
|
||||
std::string result = change.ToString();
|
||||
|
||||
EXPECT_THAT(result, ::testing::HasSubstr("Map 5"));
|
||||
EXPECT_THAT(result, ::testing::HasSubstr("(10,20)"));
|
||||
EXPECT_THAT(result, ::testing::HasSubstr("0x2e"));
|
||||
EXPECT_THAT(result, ::testing::HasSubstr("0x30"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Proposal Serialization Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(Tile16ProposalGeneratorTest, Proposal_ToJsonAndFromJson) {
|
||||
Tile16Proposal original;
|
||||
original.id = "test_id_123";
|
||||
original.prompt = "Test prompt";
|
||||
original.ai_service = "gemini";
|
||||
original.reasoning = "Test reasoning";
|
||||
original.status = Tile16Proposal::Status::PENDING;
|
||||
|
||||
Tile16Change change;
|
||||
change.map_id = 5;
|
||||
change.x = 10;
|
||||
change.y = 20;
|
||||
change.old_tile = 0x02E;
|
||||
change.new_tile = 0x030;
|
||||
original.changes.push_back(change);
|
||||
|
||||
std::string json = original.ToJson();
|
||||
auto result = Tile16Proposal::FromJson(json);
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_EQ(result->id, original.id);
|
||||
EXPECT_EQ(result->prompt, original.prompt);
|
||||
EXPECT_EQ(result->ai_service, original.ai_service);
|
||||
EXPECT_EQ(result->reasoning, original.reasoning);
|
||||
EXPECT_EQ(result->status, original.status);
|
||||
EXPECT_EQ(result->changes.size(), 1);
|
||||
EXPECT_EQ(result->changes[0].map_id, 5);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
323
test/unit/core/asar_wrapper_test.cc
Normal file
323
test/unit/core/asar_wrapper_test.cc
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "core/asar_wrapper.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace yaze {
|
||||
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::AnyOf(testing::HasSubstr("validation failed"),
|
||||
testing::HasSubstr("Patch failed"),
|
||||
testing::HasSubstr("Unknown command"),
|
||||
testing::HasSubstr("Label")));
|
||||
}
|
||||
|
||||
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 yaze
|
||||
103
test/unit/core/hex_test.cc
Normal file
103
test/unit/core/hex_test.cc
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "testing.h"
|
||||
|
||||
#include "util/hex.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
|
||||
TEST(HexTest, HexByte) {
|
||||
// Test basic byte conversion
|
||||
EXPECT_THAT(util::HexByte(0x00), Eq("$00"));
|
||||
EXPECT_THAT(util::HexByte(0xFF), Eq("$FF"));
|
||||
EXPECT_THAT(util::HexByte(0x1A), Eq("$1A"));
|
||||
|
||||
// Test different prefixes
|
||||
util::HexStringParams params;
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
EXPECT_THAT(util::HexByte(0x1A, params), Eq("1A"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::kHash;
|
||||
EXPECT_THAT(util::HexByte(0x1A, params), Eq("#1A"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::k0x;
|
||||
EXPECT_THAT(util::HexByte(0x1A, params), Eq("0x1A"));
|
||||
|
||||
// Test lowercase
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
params.uppercase = false;
|
||||
EXPECT_THAT(util::HexByte(0x1A, params), Eq("1a"));
|
||||
}
|
||||
|
||||
TEST(HexTest, HexWord) {
|
||||
// Test basic word conversion
|
||||
EXPECT_THAT(util::HexWord(0x0000), Eq("$0000"));
|
||||
EXPECT_THAT(util::HexWord(0xFFFF), Eq("$FFFF"));
|
||||
EXPECT_THAT(util::HexWord(0x1A2B), Eq("$1A2B"));
|
||||
|
||||
// Test different prefixes
|
||||
util::HexStringParams params;
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
EXPECT_THAT(util::HexWord(0x1A2B, params), Eq("1A2B"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::kHash;
|
||||
EXPECT_THAT(util::HexWord(0x1A2B, params), Eq("#1A2B"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::k0x;
|
||||
EXPECT_THAT(util::HexWord(0x1A2B, params), Eq("0x1A2B"));
|
||||
|
||||
// Test lowercase
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
params.uppercase = false;
|
||||
EXPECT_THAT(util::HexWord(0x1A2B, params), Eq("1a2b"));
|
||||
}
|
||||
|
||||
TEST(HexTest, HexLong) {
|
||||
// Test basic long conversion
|
||||
EXPECT_THAT(util::HexLong(0x000000), Eq("$000000"));
|
||||
EXPECT_THAT(util::HexLong(0xFFFFFF), Eq("$FFFFFF"));
|
||||
EXPECT_THAT(util::HexLong(0x1A2B3C), Eq("$1A2B3C"));
|
||||
|
||||
// Test different prefixes
|
||||
util::HexStringParams params;
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
EXPECT_THAT(util::HexLong(0x1A2B3C, params), Eq("1A2B3C"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::kHash;
|
||||
EXPECT_THAT(util::HexLong(0x1A2B3C, params), Eq("#1A2B3C"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::k0x;
|
||||
EXPECT_THAT(util::HexLong(0x1A2B3C, params), Eq("0x1A2B3C"));
|
||||
|
||||
// Test lowercase
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
params.uppercase = false;
|
||||
EXPECT_THAT(util::HexLong(0x1A2B3C, params), Eq("1a2b3c"));
|
||||
}
|
||||
|
||||
TEST(HexTest, HexLongLong) {
|
||||
// Test basic long long conversion
|
||||
EXPECT_THAT(util::HexLongLong(0x00000000), Eq("$00000000"));
|
||||
EXPECT_THAT(util::HexLongLong(0xFFFFFFFF), Eq("$FFFFFFFF"));
|
||||
EXPECT_THAT(util::HexLongLong(0x1A2B3C4D), Eq("$1A2B3C4D"));
|
||||
|
||||
// Test different prefixes
|
||||
util::HexStringParams params;
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
EXPECT_THAT(util::HexLongLong(0x1A2B3C4D, params), Eq("1A2B3C4D"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::kHash;
|
||||
EXPECT_THAT(util::HexLongLong(0x1A2B3C4D, params), Eq("#1A2B3C4D"));
|
||||
|
||||
params.prefix = util::HexStringParams::Prefix::k0x;
|
||||
EXPECT_THAT(util::HexLongLong(0x1A2B3C4D, params), Eq("0x1A2B3C4D"));
|
||||
|
||||
// Test lowercase
|
||||
params.prefix = util::HexStringParams::Prefix::kNone;
|
||||
params.uppercase = false;
|
||||
EXPECT_THAT(util::HexLongLong(0x1A2B3C4D, params), Eq("1a2b3c4d"));
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
74
test/unit/emu/apu_dsp_test.cc
Normal file
74
test/unit/emu/apu_dsp_test.cc
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
class ApuDspTest : public ::testing::Test {
|
||||
protected:
|
||||
MemoryImpl mem;
|
||||
Apu* apu;
|
||||
|
||||
void SetUp() override {
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
apu = new Apu(mem);
|
||||
apu->Init();
|
||||
apu->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override { delete apu; }
|
||||
};
|
||||
|
||||
TEST_F(ApuDspTest, DspRegistersReadWriteMirror) {
|
||||
// Select register 0x0C (MVOLL)
|
||||
apu->Write(0xF2, 0x0C);
|
||||
apu->Write(0xF3, 0x7F);
|
||||
// Read back
|
||||
apu->Write(0xF2, 0x0C);
|
||||
uint8_t mvoll = apu->Read(0xF3);
|
||||
EXPECT_EQ(mvoll, 0x7F);
|
||||
|
||||
// Select register 0x1C (MVOLR)
|
||||
apu->Write(0xF2, 0x1C);
|
||||
apu->Write(0xF3, 0x40);
|
||||
apu->Write(0xF2, 0x1C);
|
||||
uint8_t mvolr = apu->Read(0xF3);
|
||||
EXPECT_EQ(mvolr, 0x40);
|
||||
}
|
||||
|
||||
TEST_F(ApuDspTest, TimersEnableAndReadback) {
|
||||
// Enable timers 0 and 1, clear in-ports, map IPL off for RAM access
|
||||
apu->Write(0xF1, 0x03);
|
||||
|
||||
// Set timer targets
|
||||
apu->Write(0xFA, 0x04); // timer0 target
|
||||
apu->Write(0xFB, 0x02); // timer1 target
|
||||
|
||||
// Run enough SPC cycles via APU cycle stepping
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
// Read counters (auto-clears)
|
||||
uint8_t t0 = apu->Read(0xFD);
|
||||
uint8_t t1 = apu->Read(0xFE);
|
||||
// Should be within 0..15 and non-zero under these cycles
|
||||
EXPECT_LE(t0, 0x0F);
|
||||
EXPECT_LE(t1, 0x0F);
|
||||
}
|
||||
|
||||
TEST_F(ApuDspTest, GetSamplesReturnsSilenceAfterReset) {
|
||||
int16_t buffer[2 * 256]{};
|
||||
apu->dsp().GetSamples(buffer, 256, /*pal=*/false);
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
EXPECT_EQ(buffer[i * 2 + 0], 0);
|
||||
EXPECT_EQ(buffer[i * 2 + 1], 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
153
test/unit/emu/apu_ipl_handshake_test.cc
Normal file
153
test/unit/emu/apu_ipl_handshake_test.cc
Normal file
@@ -0,0 +1,153 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "app/emu/audio/spc700.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
class ApuIplHandshakeTest : public ::testing::Test {
|
||||
protected:
|
||||
MemoryImpl mem;
|
||||
Apu* apu;
|
||||
|
||||
void SetUp() override {
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
apu = new Apu(mem);
|
||||
apu->Init();
|
||||
apu->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override { delete apu; }
|
||||
};
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, SPC700StartsAtIplRomEntry) {
|
||||
// After reset, PC should be at IPL ROM reset vector
|
||||
uint16_t reset_vector = apu->spc700().read(0xFFFE) |
|
||||
(apu->spc700().read(0xFFFF) << 8);
|
||||
|
||||
// The IPL ROM reset vector should point to 0xFFC0 (start of IPL ROM)
|
||||
EXPECT_EQ(reset_vector, 0xFFC0);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplRomReadable) {
|
||||
// IPL ROM should be readable at 0xFFC0-0xFFFF after reset
|
||||
uint8_t first_byte = apu->Read(0xFFC0);
|
||||
|
||||
// First byte of IPL ROM should be 0xCD (CMP Y, #$EF)
|
||||
EXPECT_EQ(first_byte, 0xCD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, CycleTrackingWorks) {
|
||||
// Execute one SPC700 opcode
|
||||
apu->spc700().RunOpcode();
|
||||
|
||||
// GetLastOpcodeCycles should return a valid cycle count (2-12 typically)
|
||||
int cycles = apu->spc700().GetLastOpcodeCycles();
|
||||
EXPECT_GT(cycles, 0);
|
||||
EXPECT_LE(cycles, 12);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, PortReadWrite) {
|
||||
// Write to input port from CPU side (simulating CPU writes to $2140-$2143)
|
||||
apu->in_ports_[0] = 0xAA;
|
||||
apu->in_ports_[1] = 0xBB;
|
||||
|
||||
// SPC should be able to read these ports at $F4-$F7
|
||||
EXPECT_EQ(apu->Read(0xF4), 0xAA);
|
||||
EXPECT_EQ(apu->Read(0xF5), 0xBB);
|
||||
|
||||
// Write to output ports from SPC side
|
||||
apu->Write(0xF4, 0xCC);
|
||||
apu->Write(0xF5, 0xDD);
|
||||
|
||||
// CPU should be able to read these (simulating reads from $2140-$2143)
|
||||
EXPECT_EQ(apu->out_ports_[0], 0xCC);
|
||||
EXPECT_EQ(apu->out_ports_[1], 0xDD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplRomDisableViaControlRegister) {
|
||||
// IPL ROM is readable by default
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0xCD);
|
||||
|
||||
// Write to control register ($F1) to disable IPL ROM (bit 7 = 1)
|
||||
apu->Write(0xF1, 0x80);
|
||||
|
||||
// Now $FFC0-$FFFF should read from RAM instead of IPL ROM
|
||||
// RAM is initialized to 0, so we should read 0
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0x00);
|
||||
|
||||
// Write something to RAM
|
||||
apu->ram[0xFFC0] = 0x42;
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0x42);
|
||||
|
||||
// Re-enable IPL ROM (bit 7 = 0)
|
||||
apu->Write(0xF1, 0x00);
|
||||
|
||||
// Should read IPL ROM again
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0xCD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, TimersEnableAndCount) {
|
||||
// Enable timer 0 via control register
|
||||
apu->Write(0xF1, 0x01);
|
||||
|
||||
// Set timer 0 target to 4
|
||||
apu->Write(0xFA, 0x04);
|
||||
|
||||
// Run enough cycles to trigger timer
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
// Read timer 0 counter (auto-clears on read)
|
||||
uint8_t counter = apu->Read(0xFD);
|
||||
|
||||
// Counter should be non-zero if timer is working
|
||||
EXPECT_GT(counter, 0);
|
||||
EXPECT_LE(counter, 0x0F);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplBootSequenceProgresses) {
|
||||
// This test verifies that the IPL ROM boot sequence can actually progress
|
||||
// without getting stuck in an infinite loop
|
||||
|
||||
uint16_t initial_pc = apu->spc700().PC;
|
||||
|
||||
// Run multiple opcodes to let the IPL boot sequence progress
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
apu->spc700().RunOpcode();
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
uint16_t final_pc = apu->spc700().PC;
|
||||
|
||||
// PC should have advanced (boot sequence is progressing)
|
||||
// If it's stuck in a tight loop, PC won't change much
|
||||
EXPECT_NE(initial_pc, final_pc);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, AccurateCycleCountsForCommonOpcodes) {
|
||||
// Test that specific opcodes return correct cycle counts
|
||||
|
||||
// NOP (0x00) should take 2 cycles
|
||||
apu->spc700().PC = 0x0000;
|
||||
apu->ram[0x0000] = 0x00; // NOP
|
||||
apu->spc700().RunOpcode();
|
||||
apu->spc700().RunOpcode(); // Execute
|
||||
EXPECT_EQ(apu->spc700().GetLastOpcodeCycles(), 2);
|
||||
|
||||
// MOV A, #imm (0xE8) should take 2 cycles
|
||||
apu->spc700().PC = 0x0002;
|
||||
apu->ram[0x0002] = 0xE8; // MOV A, #imm
|
||||
apu->ram[0x0003] = 0x42; // immediate value
|
||||
apu->spc700().RunOpcode();
|
||||
apu->spc700().RunOpcode();
|
||||
EXPECT_EQ(apu->spc700().GetLastOpcodeCycles(), 2);
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
30
test/unit/emu/spc700_reset_test.cc
Normal file
30
test/unit/emu/spc700_reset_test.cc
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
TEST(Spc700ResetTest, ResetVectorExecutesIplSequence) {
|
||||
MemoryImpl mem;
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
|
||||
Apu apu(mem);
|
||||
apu.Init();
|
||||
apu.Reset();
|
||||
|
||||
// After reset, running some cycles should advance SPC PC from IPL entry
|
||||
uint16_t pc_before = apu.spc700().PC;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
apu.spc700().RunOpcode();
|
||||
apu.Cycle();
|
||||
}
|
||||
uint16_t pc_after = apu.spc700().PC;
|
||||
EXPECT_NE(pc_after, pc_before);
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
431
test/unit/gfx/compression_test.cc
Normal file
431
test/unit/gfx/compression_test.cc
Normal file
@@ -0,0 +1,431 @@
|
||||
#include "app/gfx/util/compression.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
#define BUILD_HEADER(command, length) (command << 5) + (length - 1)
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using yaze::Rom;
|
||||
using yaze::gfx::lc_lz2::CompressionContext;
|
||||
using yaze::gfx::lc_lz2::CompressionPiece;
|
||||
using yaze::gfx::lc_lz2::CompressV2;
|
||||
using yaze::gfx::lc_lz2::CompressV3;
|
||||
using yaze::gfx::lc_lz2::DecompressV2;
|
||||
using yaze::gfx::lc_lz2::kCommandByteFill;
|
||||
using yaze::gfx::lc_lz2::kCommandDirectCopy;
|
||||
using yaze::gfx::lc_lz2::kCommandIncreasingFill;
|
||||
using yaze::gfx::lc_lz2::kCommandLongLength;
|
||||
using yaze::gfx::lc_lz2::kCommandRepeatingBytes;
|
||||
using yaze::gfx::lc_lz2::kCommandWordFill;
|
||||
|
||||
using ::testing::ElementsAre;
|
||||
using ::testing::ElementsAreArray;
|
||||
using ::testing::TypedEq;
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<uint8_t> ExpectCompressOk(Rom& rom, uint8_t* in, int in_size) {
|
||||
std::vector<uint8_t> data(in, in + in_size);
|
||||
auto load_status = rom.LoadFromData(data, false);
|
||||
EXPECT_TRUE(load_status.ok());
|
||||
auto compression_status = CompressV3(rom.vector(), 0, in_size);
|
||||
EXPECT_TRUE(compression_status.ok());
|
||||
auto compressed_bytes = std::move(*compression_status);
|
||||
return compressed_bytes;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ExpectDecompressBytesOk(Rom& rom,
|
||||
std::vector<uint8_t>& in) {
|
||||
auto load_status = rom.LoadFromData(in, false);
|
||||
EXPECT_TRUE(load_status.ok());
|
||||
auto decompression_status = DecompressV2(rom.data(), 0, in.size());
|
||||
EXPECT_TRUE(decompression_status.ok());
|
||||
auto decompressed_bytes = std::move(*decompression_status);
|
||||
return decompressed_bytes;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ExpectDecompressOk(Rom& rom, uint8_t* in, int in_size) {
|
||||
std::vector<uint8_t> data(in, in + in_size);
|
||||
auto load_status = rom.LoadFromData(data, false);
|
||||
EXPECT_TRUE(load_status.ok());
|
||||
auto decompression_status = DecompressV2(rom.data(), 0, in_size);
|
||||
EXPECT_TRUE(decompression_status.ok());
|
||||
auto decompressed_bytes = std::move(*decompression_status);
|
||||
return decompressed_bytes;
|
||||
}
|
||||
|
||||
std::shared_ptr<CompressionPiece> ExpectNewCompressionPieceOk(
|
||||
const char command, const int length, std::string& args,
|
||||
const int argument_length) {
|
||||
auto new_piece = std::make_shared<CompressionPiece>(command, length, args,
|
||||
argument_length);
|
||||
EXPECT_TRUE(new_piece != nullptr);
|
||||
return new_piece;
|
||||
}
|
||||
|
||||
// Helper function to assert compression quality.
|
||||
void AssertCompressionQuality(
|
||||
const std::vector<uint8_t>& uncompressed_data,
|
||||
const std::vector<uint8_t>& expected_compressed_data) {
|
||||
absl::StatusOr<std::vector<uint8_t>> result =
|
||||
CompressV3(uncompressed_data, 0, uncompressed_data.size(), 0, false);
|
||||
ASSERT_TRUE(result.ok());
|
||||
auto compressed_data = std::move(*result);
|
||||
EXPECT_THAT(compressed_data, ElementsAreArray(expected_compressed_data));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ExpectCompressV3Ok(
|
||||
const std::vector<uint8_t>& uncompressed_data,
|
||||
const std::vector<uint8_t>& expected_compressed_data) {
|
||||
absl::StatusOr<std::vector<uint8_t>> result =
|
||||
CompressV3(uncompressed_data, 0, uncompressed_data.size(), 0, false);
|
||||
EXPECT_TRUE(result.ok());
|
||||
auto compressed_data = std::move(*result);
|
||||
return compressed_data;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> CreateRepeatedBetweenUncompressable(
|
||||
int leftUncompressedSize, int repeatedByteSize, int rightUncompressedSize) {
|
||||
std::vector<uint8_t> result(
|
||||
leftUncompressedSize + repeatedByteSize + rightUncompressedSize, 0);
|
||||
std::fill_n(result.begin() + leftUncompressedSize, repeatedByteSize, 0x00);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, TrivialRepeatedBytes) {
|
||||
AssertCompressionQuality({0x00, 0x00, 0x00}, {0x22, 0x00, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, RepeatedBytesBetweenUncompressable) {
|
||||
AssertCompressionQuality({0x01, 0x00, 0x00, 0x00, 0x10},
|
||||
{0x04, 0x01, 0x00, 0x00, 0x00, 0x10, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, RepeatedBytesBeforeUncompressable) {
|
||||
AssertCompressionQuality({0x00, 0x00, 0x00, 0x10},
|
||||
{0x22, 0x00, 0x00, 0x10, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, RepeatedBytesAfterUncompressable) {
|
||||
AssertCompressionQuality({0x01, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x22, 0x00, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, RepeatedBytesAfterUncompressableRepeated) {
|
||||
AssertCompressionQuality(
|
||||
{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02},
|
||||
{0x22, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x02, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, RepeatedBytesBeforeUncompressableRepeated) {
|
||||
AssertCompressionQuality(
|
||||
{0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00},
|
||||
{0x04, 0x01, 0x00, 0x00, 0x00, 0x02, 0x22, 0x00, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, NewDecompressionPieceOk) {
|
||||
char command = 1;
|
||||
int length = 1;
|
||||
char args[] = "aaa";
|
||||
int argument_length = 0x02;
|
||||
CompressionPiece old_piece;
|
||||
old_piece.command = command;
|
||||
old_piece.length = length;
|
||||
old_piece.argument = args;
|
||||
old_piece.argument_length = argument_length;
|
||||
old_piece.next = nullptr;
|
||||
|
||||
std::string new_args = "aaa";
|
||||
|
||||
auto new_piece = ExpectNewCompressionPieceOk(0x01, 0x01, new_args, 0x02);
|
||||
|
||||
EXPECT_EQ(old_piece.command, new_piece->command);
|
||||
EXPECT_EQ(old_piece.length, new_piece->length);
|
||||
ASSERT_EQ(old_piece.argument_length, new_piece->argument_length);
|
||||
for (int i = 0; i < old_piece.argument_length; ++i) {
|
||||
EXPECT_EQ(old_piece.argument[i], new_piece->argument[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check why header built is off by one
|
||||
// 0x25 instead of 0x24
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleSet) {
|
||||
Rom rom;
|
||||
uint8_t single_set[5] = {0x2A, 0x2A, 0x2A, 0x2A, 0x2A};
|
||||
uint8_t single_set_expected[3] = {BUILD_HEADER(1, 5), 0x2A, 0xFF};
|
||||
|
||||
auto comp_result = ExpectCompressOk(rom, single_set, 5);
|
||||
EXPECT_THAT(single_set_expected, ElementsAreArray(comp_result.data(), 3));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleWord) {
|
||||
Rom rom;
|
||||
uint8_t single_word[6] = {0x2A, 0x01, 0x2A, 0x01, 0x2A, 0x01};
|
||||
uint8_t single_word_expected[4] = {BUILD_HEADER(0x02, 0x06), 0x2A, 0x01, 0xFF};
|
||||
|
||||
auto comp_result = ExpectCompressOk(rom, single_word, 6);
|
||||
EXPECT_THAT(single_word_expected, ElementsAreArray(comp_result.data(), 4));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleIncrement) {
|
||||
Rom rom;
|
||||
uint8_t single_inc[3] = {0x01, 0x02, 0x03};
|
||||
uint8_t single_inc_expected[3] = {BUILD_HEADER(0x03, 0x03), 0x01, 0xFF};
|
||||
auto comp_result = ExpectCompressOk(rom, single_inc, 3);
|
||||
EXPECT_THAT(single_inc_expected, ElementsAreArray(comp_result.data(), 3));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleCopy) {
|
||||
Rom rom;
|
||||
uint8_t single_copy[4] = {0x03, 0x0A, 0x07, 0x14};
|
||||
uint8_t single_copy_expected[6] = {
|
||||
BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14, 0xFF};
|
||||
auto comp_result = ExpectCompressOk(rom, single_copy, 4);
|
||||
EXPECT_THAT(single_copy_expected, ElementsAreArray(comp_result.data(), 6));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleOverflowIncrement) {
|
||||
AssertCompressionQuality({0xFE, 0xFF, 0x00, 0x01},
|
||||
{BUILD_HEADER(0x03, 0x04), 0xFE, 0xFF});
|
||||
}
|
||||
/**
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionSingleCopyRepeat) {
|
||||
std::vector<uint8_t> single_copy_expected = {0x03, 0x0A, 0x07, 0x14,
|
||||
0x03, 0x0A, 0x07, 0x14};
|
||||
|
||||
auto comp_result = ExpectCompressV3Ok(
|
||||
single_copy_expected, {BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14,
|
||||
BUILD_HEADER(0x04, 0x04), 0x00, 0x00, 0xFF});
|
||||
EXPECT_THAT(single_copy_expected, ElementsAreArray(comp_result.data(), 6));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionMixedRepeatIncrement) {
|
||||
AssertCompressionQuality(
|
||||
{0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
|
||||
0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02,
|
||||
0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05},
|
||||
{BUILD_HEADER(0x01, 0x04), 0x05, BUILD_HEADER(0x03, 0x06), 0x06,
|
||||
BUILD_HEADER(0x00, 0x01), 0x05, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionMixedIncrementIntraCopyOffset) {
|
||||
// "Mixing, inc, alternate, intra copy"
|
||||
// compress start: 3, length: 21
|
||||
// compressed length: 9
|
||||
AssertCompressionQuality(
|
||||
{0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
|
||||
0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02,
|
||||
0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05},
|
||||
{BUILD_HEADER(0x03, 0x07), 0x05, BUILD_HEADER(0x02, 0x06), 0x05, 0x02,
|
||||
BUILD_HEADER(0x04, 0x08), 0x05, 0x00, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionMixedIncrementIntraCopySource) {
|
||||
// "Mixing, inc, alternate, intra copy"
|
||||
// 0, 28
|
||||
// 16
|
||||
AssertCompressionQuality(
|
||||
{0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
|
||||
0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02,
|
||||
0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05},
|
||||
{BUILD_HEADER(0x01, 0x04), 0x05, BUILD_HEADER(0x03, 0x06), 0x06,
|
||||
BUILD_HEADER(0x02, 0x06), 0x05, 0x02, BUILD_HEADER(0x04, 0x08), 0x08,
|
||||
0x00, BUILD_HEADER(0x00, 0x04), 0x08, 0x0A, 0x00, 0x05, 0xFF});
|
||||
}
|
||||
|
||||
// Extended Header
|
||||
// 111CCCLL LLLLLLLL
|
||||
// CCC: Real command
|
||||
// LLLLLLLLLL: Length
|
||||
|
||||
// Normally you have 5 bits for the length, so the maximum value you can
|
||||
// represent is 31 (which outputs 32 bytes). With the long length, you get 5
|
||||
// more bits for the length, so the maximum value you can represent becomes
|
||||
// 1023, outputting 1024 bytes at a time.
|
||||
|
||||
void build_extended_header(uint8_t command, uint8_t length, uint8_t& byte1,
|
||||
uint8_t& byte2) {
|
||||
byte1 = command << 3;
|
||||
byte1 += (length - 1);
|
||||
byte1 += 0b11100000;
|
||||
byte2 = length >> 3;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> CreateRepeatedBetweenUncompressable(
|
||||
int leftUncompressedSize, int repeatedByteSize, int rightUncompressedSize) {
|
||||
std::vector<uint8_t> result(
|
||||
leftUncompressedSize + repeatedByteSize + rightUncompressedSize, 0);
|
||||
std::fill_n(result.begin() + leftUncompressedSize, repeatedByteSize, 0x00);
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, LengthBorderCompression) {
|
||||
// "Length border compression"
|
||||
std::vector<uint8_t> result(42, 0);
|
||||
std::fill_n(result.begin(), 42, 0x05);
|
||||
AssertCompressionQuality(result, {BUILD_HEADER(0x04, 42), 0x05, 0x05, 0xFF});
|
||||
|
||||
// "Extended length, 400 repeat of 5"
|
||||
std::vector<uint8_t> result2(400, 0);
|
||||
std::fill_n(result2.begin(), 400, 0x05);
|
||||
uint8_t byte1;
|
||||
uint8_t byte2;
|
||||
build_extended_header(0x01, 42, byte1, byte2);
|
||||
AssertCompressionQuality(result2, {byte1, byte2, 0x05, 0x05, 0xFF});
|
||||
|
||||
// "Extended length, 1050 repeat of 5"
|
||||
std::vector<uint8_t> result3(1050, 0);
|
||||
std::fill_n(result3.begin(), 1050, 0x05);
|
||||
uint8_t byte3;
|
||||
uint8_t byte4;
|
||||
build_extended_header(0x04, 1050, byte3, byte4);
|
||||
AssertCompressionQuality(result3, {byte3, byte4, 0x05, 0x05, 0xFF});
|
||||
|
||||
// // "Extended length, 2050 repeat of 5"
|
||||
std::vector<uint8_t> result4(2050, 0);
|
||||
std::fill_n(result4.begin(), 2050, 0x05);
|
||||
uint8_t byte5;
|
||||
uint8_t byte6;
|
||||
build_extended_header(0x04, 2050, byte5, byte6);
|
||||
AssertCompressionQuality(result4, {byte5, byte6, 0x05, 0x05, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionExtendedWordCopy) {
|
||||
// ROM rom;
|
||||
// uint8_t buffer[3000];
|
||||
// for (unsigned int i = 0; i < 3000; i += 2) {
|
||||
// buffer[i] = 0x05;
|
||||
// buffer[i + 1] = 0x06;
|
||||
// }
|
||||
// uint8_t hightlength_word_1050[] = {
|
||||
// 0b11101011, 0xFF, 0x05, 0x06, BUILD_HEADER(0x02, 0x1A), 0x05, 0x06,
|
||||
// 0xFF};
|
||||
|
||||
// // "Extended word copy"
|
||||
// auto comp_result = ExpectCompressOk(rom, buffer, 1050);
|
||||
// EXPECT_THAT(hightlength_word_1050, ElementsAreArray(comp_result.data(),
|
||||
// 8));
|
||||
|
||||
std::vector<uint8_t> buffer(3000, 0);
|
||||
std::fill_n(buffer.begin(), 3000, 0x05);
|
||||
for (unsigned int i = 0; i < 3000; i += 2) {
|
||||
buffer[i] = 0x05;
|
||||
buffer[i + 1] = 0x06;
|
||||
}
|
||||
|
||||
uint8_t byte1;
|
||||
uint8_t byte2;
|
||||
build_extended_header(0x02, 0x1A, byte1, byte2);
|
||||
AssertCompressionQuality(
|
||||
buffer, {0b11101011, 0xFF, 0x05, 0x06, byte1, byte2, 0x05, 0x06, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionMixedPatterns) {
|
||||
AssertCompressionQuality(
|
||||
{0x05, 0x05, 0x05, 0x06, 0x07, 0x06, 0x07, 0x08, 0x09, 0x0A},
|
||||
{BUILD_HEADER(0x01, 0x03), 0x05, BUILD_HEADER(0x02, 0x04), 0x06, 0x07,
|
||||
BUILD_HEADER(0x03, 0x03), 0x08, 0xFF});
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, CompressionLongIntraCopy) {
|
||||
ROM rom;
|
||||
uint8_t long_data[15] = {0x05, 0x06, 0x07, 0x08, 0x05, 0x06, 0x07, 0x08,
|
||||
0x05, 0x06, 0x07, 0x08, 0x05, 0x06, 0x07};
|
||||
uint8_t long_expected[] = {BUILD_HEADER(0x00, 0x04), 0x05, 0x06, 0x07, 0x08,
|
||||
BUILD_HEADER(0x04, 0x0C), 0x00, 0x00, 0xFF};
|
||||
|
||||
auto comp_result = ExpectCompressOk(rom, long_data, 15);
|
||||
EXPECT_THAT(long_expected,
|
||||
ElementsAreArray(comp_result.data(), sizeof(long_expected)));
|
||||
}
|
||||
|
||||
*/
|
||||
// Tests for HandleDirectCopy
|
||||
|
||||
TEST(HandleDirectCopyTest, NotDirectCopyWithAccumulatedBytes) {
|
||||
CompressionContext context({0x01, 0x02, 0x03}, 0, 3);
|
||||
context.cmd_with_max = kCommandByteFill;
|
||||
context.comp_accumulator = 2;
|
||||
HandleDirectCopy(context);
|
||||
EXPECT_EQ(context.compressed_data.size(), 3);
|
||||
}
|
||||
|
||||
TEST(HandleDirectCopyTest, NotDirectCopyWithoutAccumulatedBytes) {
|
||||
CompressionContext context({0x01, 0x02, 0x03}, 0, 3);
|
||||
context.cmd_with_max = kCommandByteFill;
|
||||
HandleDirectCopy(context);
|
||||
EXPECT_EQ(context.compressed_data.size(), 2); // Header + 1 byte
|
||||
}
|
||||
|
||||
TEST(HandleDirectCopyTest, AccumulateBytesWithoutMax) {
|
||||
CompressionContext context({0x01, 0x02, 0x03}, 0, 3);
|
||||
context.cmd_with_max = kCommandDirectCopy;
|
||||
HandleDirectCopy(context);
|
||||
EXPECT_EQ(context.comp_accumulator, 1);
|
||||
EXPECT_EQ(context.compressed_data.size(), 0); // No data added yet
|
||||
}
|
||||
|
||||
// Tests for CheckIncByteV3
|
||||
|
||||
TEST(CheckIncByteV3Test, IncreasingSequence) {
|
||||
CompressionContext context({0x01, 0x02, 0x03}, 0, 3);
|
||||
CheckIncByteV3(context);
|
||||
EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill], 3);
|
||||
}
|
||||
|
||||
TEST(CheckIncByteV3Test, IncreasingSequenceSurroundedByIdenticalBytes) {
|
||||
CompressionContext context({0x01, 0x02, 0x03, 0x04, 0x01}, 1,
|
||||
3); // Start from index 1
|
||||
CheckIncByteV3(context);
|
||||
EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill],
|
||||
0); // Reset to prioritize direct copy
|
||||
}
|
||||
|
||||
TEST(CheckIncByteV3Test, NotAnIncreasingSequence) {
|
||||
CompressionContext context({0x01, 0x01, 0x03}, 0, 3);
|
||||
CheckIncByteV3(context);
|
||||
EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill],
|
||||
1); // Only one byte is detected
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, DecompressionValidCommand) {
|
||||
Rom rom;
|
||||
std::vector<uint8_t> simple_copy_input = {BUILD_HEADER(0x00, 0x02), 0x2A,
|
||||
0x45, 0xFF};
|
||||
uint8_t simple_copy_output[2] = {0x2A, 0x45};
|
||||
auto decomp_result = ExpectDecompressBytesOk(rom, simple_copy_input);
|
||||
EXPECT_THAT(simple_copy_output, ElementsAreArray(decomp_result.data(), 2));
|
||||
}
|
||||
|
||||
TEST(LC_LZ2_CompressionTest, DecompressionMixingCommand) {
|
||||
Rom rom;
|
||||
uint8_t random1_i[11] = {BUILD_HEADER(0x01, 0x03),
|
||||
0x2A,
|
||||
BUILD_HEADER(0x00, 0x04),
|
||||
0x01,
|
||||
0x02,
|
||||
0x03,
|
||||
0x04,
|
||||
BUILD_HEADER(0x02, 0x02),
|
||||
0x0B,
|
||||
0x16,
|
||||
0xFF};
|
||||
uint8_t random1_o[9] = {42, 42, 42, 1, 2, 3, 4, 11, 22};
|
||||
auto decomp_result = ExpectDecompressOk(rom, random1_i, 11);
|
||||
EXPECT_THAT(random1_o, ElementsAreArray(decomp_result.data(), 9));
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
199
test/unit/gfx/snes_palette_test.cc
Normal file
199
test/unit/gfx/snes_palette_test.cc
Normal file
@@ -0,0 +1,199 @@
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/types/snes_color.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::ElementsAreArray;
|
||||
using yaze::gfx::ConvertRgbToSnes;
|
||||
using yaze::gfx::ConvertSnesToRgb;
|
||||
using yaze::gfx::Extract;
|
||||
|
||||
namespace {
|
||||
unsigned int test_convert(snes_color col) {
|
||||
unsigned int toret;
|
||||
toret = col.red << 16;
|
||||
toret += col.green << 8;
|
||||
toret += col.blue;
|
||||
return toret;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// SnesColor Tests
|
||||
TEST(SnesColorTest, DefaultConstructor) {
|
||||
yaze::gfx::SnesColor color;
|
||||
EXPECT_EQ(color.rgb().x, 0.0f);
|
||||
EXPECT_EQ(color.rgb().y, 0.0f);
|
||||
EXPECT_EQ(color.rgb().z, 0.0f);
|
||||
EXPECT_EQ(color.rgb().w, 0.0f);
|
||||
EXPECT_EQ(color.snes(), 0);
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, RGBConstructor) {
|
||||
ImVec4 rgb(1.0f, 0.5f, 0.25f, 1.0f);
|
||||
yaze::gfx::SnesColor color(rgb);
|
||||
EXPECT_EQ(color.rgb().x, rgb.x);
|
||||
EXPECT_EQ(color.rgb().y, rgb.y);
|
||||
EXPECT_EQ(color.rgb().z, rgb.z);
|
||||
EXPECT_EQ(color.rgb().w, rgb.w);
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, SNESConstructor) {
|
||||
uint16_t snes = 0x4210;
|
||||
yaze::gfx::SnesColor color(snes);
|
||||
EXPECT_EQ(color.snes(), snes);
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, ConvertRgbToSnes) {
|
||||
snes_color color = {132, 132, 132};
|
||||
uint16_t snes = ConvertRgbToSnes(color);
|
||||
ASSERT_EQ(snes, 0x4210);
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, ConvertSnestoRGB) {
|
||||
uint16_t snes = 0x4210;
|
||||
snes_color color = ConvertSnesToRgb(snes);
|
||||
ASSERT_EQ(color.red, 132);
|
||||
ASSERT_EQ(color.green, 132);
|
||||
ASSERT_EQ(color.blue, 132);
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, ConvertSnesToRGB_Binary) {
|
||||
uint16_t red = 0b0000000000011111;
|
||||
uint16_t blue = 0b0111110000000000;
|
||||
uint16_t green = 0b0000001111100000;
|
||||
uint16_t purple = 0b0111110000011111;
|
||||
snes_color testcolor;
|
||||
|
||||
testcolor = ConvertSnesToRgb(red);
|
||||
ASSERT_EQ(0xFF0000, test_convert(testcolor));
|
||||
testcolor = ConvertSnesToRgb(green);
|
||||
ASSERT_EQ(0x00FF00, test_convert(testcolor));
|
||||
testcolor = ConvertSnesToRgb(blue);
|
||||
ASSERT_EQ(0x0000FF, test_convert(testcolor));
|
||||
testcolor = ConvertSnesToRgb(purple);
|
||||
ASSERT_EQ(0xFF00FF, test_convert(testcolor));
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, Extraction) {
|
||||
// red, blue, green, purple
|
||||
char data[8] = {0x1F, 0x00, 0x00, 0x7C, static_cast<char>(0xE0),
|
||||
0x03, 0x1F, 0x7C};
|
||||
auto pal = Extract(data, 0, 4);
|
||||
ASSERT_EQ(4, pal.size());
|
||||
ASSERT_EQ(0xFF0000, test_convert(pal[0]));
|
||||
ASSERT_EQ(0x0000FF, test_convert(pal[1]));
|
||||
ASSERT_EQ(0x00FF00, test_convert(pal[2]));
|
||||
ASSERT_EQ(0xFF00FF, test_convert(pal[3]));
|
||||
}
|
||||
|
||||
TEST(SnesColorTest, Convert) {
|
||||
// red, blue, green, purple white
|
||||
char data[10] = {0x1F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x7C,
|
||||
static_cast<char>(0xE0),
|
||||
0x03,
|
||||
0x1F,
|
||||
0x7C,
|
||||
static_cast<char>(0xFF),
|
||||
0x1F};
|
||||
auto pal = Extract(data, 0, 5);
|
||||
auto snes_string = yaze::gfx::Convert(pal);
|
||||
EXPECT_EQ(10, snes_string.size());
|
||||
EXPECT_THAT(data, ElementsAreArray(snes_string.data(), 10));
|
||||
}
|
||||
|
||||
// SnesPalette Tests
|
||||
TEST(SnesPaletteTest, DefaultConstructor) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
EXPECT_TRUE(palette.empty());
|
||||
EXPECT_EQ(palette.size(), 0);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, AddColor) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color;
|
||||
palette.AddColor(color);
|
||||
ASSERT_EQ(palette.size(), 1);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, AddMultipleColors) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color1(0x4210);
|
||||
yaze::gfx::SnesColor color2(0x7FFF);
|
||||
palette.AddColor(color1);
|
||||
palette.AddColor(color2);
|
||||
ASSERT_EQ(palette.size(), 2);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, UpdateColor) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color1(0x4210);
|
||||
yaze::gfx::SnesColor color2(0x7FFF);
|
||||
palette.AddColor(color1);
|
||||
palette.UpdateColor(0, color2);
|
||||
auto result = palette[0];
|
||||
ASSERT_EQ(result.snes(), 0x7FFF);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, SubPalette) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color1(0x4210);
|
||||
yaze::gfx::SnesColor color2(0x7FFF);
|
||||
yaze::gfx::SnesColor color3(0x1F1F);
|
||||
palette.AddColor(color1);
|
||||
palette.AddColor(color2);
|
||||
palette.AddColor(color3);
|
||||
|
||||
auto sub = palette.sub_palette(1, 3);
|
||||
ASSERT_EQ(sub.size(), 2);
|
||||
auto result = sub[0];
|
||||
ASSERT_EQ(result.snes(), 0x7FFF);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, VectorConstructor) {
|
||||
std::vector<yaze::gfx::SnesColor> colors = {yaze::gfx::SnesColor(0x4210),
|
||||
yaze::gfx::SnesColor(0x7FFF)};
|
||||
yaze::gfx::SnesPalette palette(colors);
|
||||
ASSERT_EQ(palette.size(), 2);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, Clear) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color(0x4210);
|
||||
palette.AddColor(color);
|
||||
ASSERT_EQ(palette.size(), 1);
|
||||
palette.clear();
|
||||
ASSERT_TRUE(palette.empty());
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, Iterator) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color1(0x4210);
|
||||
yaze::gfx::SnesColor color2(0x7FFF);
|
||||
palette.AddColor(color1);
|
||||
palette.AddColor(color2);
|
||||
|
||||
int count = 0;
|
||||
for (const auto& color : palette) {
|
||||
EXPECT_TRUE(color.snes() == 0x4210 || color.snes() == 0x7FFF);
|
||||
count++;
|
||||
}
|
||||
EXPECT_EQ(count, 2);
|
||||
}
|
||||
|
||||
TEST(SnesPaletteTest, OperatorAccess) {
|
||||
yaze::gfx::SnesPalette palette;
|
||||
yaze::gfx::SnesColor color(0x4210);
|
||||
palette.AddColor(color);
|
||||
EXPECT_EQ(palette[0].snes(), 0x4210);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
209
test/unit/gfx/snes_tile_test.cc
Normal file
209
test/unit/gfx/snes_tile_test.cc
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "app/gfx/types/snes_tile.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
|
||||
TEST(SnesTileTest, UnpackBppTile) {
|
||||
// Test 1bpp tile unpacking
|
||||
std::vector<uint8_t> data1bpp = {0x80, 0x40, 0x20, 0x10,
|
||||
0x08, 0x04, 0x02, 0x01};
|
||||
auto tile1bpp = gfx::UnpackBppTile(data1bpp, 0, 1);
|
||||
EXPECT_EQ(tile1bpp.data[0], 1); // First pixel
|
||||
EXPECT_EQ(tile1bpp.data[7], 0); // Last pixel of first row
|
||||
EXPECT_EQ(tile1bpp.data[56], 0); // First pixel of last row
|
||||
EXPECT_EQ(tile1bpp.data[63], 1); // Last pixel
|
||||
|
||||
// Test 2bpp tile unpacking
|
||||
// Create test data where we know the expected results
|
||||
// For 2bpp: 16 bytes total (8 rows × 2 bytes per row)
|
||||
// Each row has 2 bytes: plane 0 byte, plane 1 byte
|
||||
// First pixel should be 3 (both bits set): plane0 bit7=1, plane1 bit7=1
|
||||
// Last pixel of first row should be 1: plane0 bit0=1, plane1 bit0=0
|
||||
std::vector<uint8_t> data2bpp = {
|
||||
0x81, 0x80, // Row 0: plane0=10000001, plane1=10000000
|
||||
0x00, 0x00, // Row 1: plane0=00000000, plane1=00000000
|
||||
0x00, 0x00, // Row 2: plane0=00000000, plane1=00000000
|
||||
0x00, 0x00, // Row 3: plane0=00000000, plane1=00000000
|
||||
0x00, 0x00, // Row 4: plane0=00000000, plane1=00000000
|
||||
0x00, 0x00, // Row 5: plane0=00000000, plane1=00000000
|
||||
0x00, 0x00, // Row 6: plane0=00000000, plane1=00000000
|
||||
0x01, 0x81 // Row 7: plane0=00000001, plane1=10000001
|
||||
};
|
||||
auto tile2bpp = gfx::UnpackBppTile(data2bpp, 0, 2);
|
||||
EXPECT_EQ(tile2bpp.data[0], 3); // First pixel: 1|1<<1 = 3
|
||||
EXPECT_EQ(tile2bpp.data[7], 1); // Last pixel of first row: 1|0<<1 = 1
|
||||
EXPECT_EQ(tile2bpp.data[56], 2); // First pixel of last row: 0|1<<1 = 2
|
||||
EXPECT_EQ(tile2bpp.data[63], 3); // Last pixel: 1|1<<1 = 3
|
||||
|
||||
// Test 4bpp tile unpacking
|
||||
// According to SnesLab: First planes 1&2 intertwined, then planes 3&4 intertwined
|
||||
// 32 bytes total: 16 bytes for planes 1&2, then 16 bytes for planes 3&4
|
||||
std::vector<uint8_t> data4bpp = {
|
||||
// Planes 1&2 intertwined (rows 0-7)
|
||||
0x81, 0x80, // Row 0: bp1=10000001, bp2=10000000
|
||||
0x00, 0x00, // Row 1: bp1=00000000, bp2=00000000
|
||||
0x00, 0x00, // Row 2: bp1=00000000, bp2=00000000
|
||||
0x00, 0x00, // Row 3: bp1=00000000, bp2=00000000
|
||||
0x00, 0x00, // Row 4: bp1=00000000, bp2=00000000
|
||||
0x00, 0x00, // Row 5: bp1=00000000, bp2=00000000
|
||||
0x00, 0x00, // Row 6: bp1=00000000, bp2=00000000
|
||||
0x01, 0x81, // Row 7: bp1=00000001, bp2=10000001
|
||||
// Planes 3&4 intertwined (rows 0-7)
|
||||
0x81, 0x80, // Row 0: bp3=10000001, bp4=10000000
|
||||
0x00, 0x00, // Row 1: bp3=00000000, bp4=00000000
|
||||
0x00, 0x00, // Row 2: bp3=00000000, bp4=00000000
|
||||
0x00, 0x00, // Row 3: bp3=00000000, bp4=00000000
|
||||
0x00, 0x00, // Row 4: bp3=00000000, bp4=00000000
|
||||
0x00, 0x00, // Row 5: bp3=00000000, bp4=00000000
|
||||
0x00, 0x00, // Row 6: bp3=00000000, bp4=00000000
|
||||
0x01, 0x81 // Row 7: bp3=00000001, bp4=10000001
|
||||
};
|
||||
auto tile4bpp = gfx::UnpackBppTile(data4bpp, 0, 4);
|
||||
EXPECT_EQ(tile4bpp.data[0], 0xF); // First pixel: 1|1<<1|1<<2|1<<3 = 15
|
||||
EXPECT_EQ(tile4bpp.data[7], 0x5); // Last pixel of first row: 1|0<<1|1<<2|0<<3 = 5
|
||||
EXPECT_EQ(tile4bpp.data[56], 0xA); // First pixel of last row: 0|1<<1|0<<2|1<<3 = 10
|
||||
EXPECT_EQ(tile4bpp.data[63], 0xF); // Last pixel: 1|1<<1|1<<2|1<<3 = 15
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, PackBppTile) {
|
||||
// Test 1bpp tile packing
|
||||
snes_tile8 tile1bpp;
|
||||
std::fill(tile1bpp.data, tile1bpp.data + 64, 0);
|
||||
tile1bpp.data[0] = 1;
|
||||
tile1bpp.data[63] = 1;
|
||||
auto packed1bpp = gfx::PackBppTile(tile1bpp, 1);
|
||||
EXPECT_EQ(packed1bpp[0], 0x80); // First byte
|
||||
EXPECT_EQ(packed1bpp[7], 0x01); // Last byte
|
||||
|
||||
// Test 2bpp tile packing
|
||||
snes_tile8 tile2bpp;
|
||||
std::fill(tile2bpp.data, tile2bpp.data + 64, 0);
|
||||
tile2bpp.data[0] = 3;
|
||||
tile2bpp.data[7] = 1;
|
||||
tile2bpp.data[56] = 2;
|
||||
tile2bpp.data[63] = 3;
|
||||
auto packed2bpp = gfx::PackBppTile(tile2bpp, 2);
|
||||
EXPECT_EQ(packed2bpp[0], 0x81); // First byte of first plane: pixel0=3→0x80, pixel7=1→0x01
|
||||
EXPECT_EQ(packed2bpp[1], 0x80); // First byte of second plane: pixel0=3→0x80, pixel7=1→0x00
|
||||
EXPECT_EQ(packed2bpp[14], 0x01); // Last byte of first plane: pixel56=2→0x00, pixel63=3→0x01
|
||||
EXPECT_EQ(packed2bpp[15], 0x81); // Last byte of second plane: pixel56=2→0x80, pixel63=3→0x01
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, ConvertBpp) {
|
||||
// Test 2bpp to 4bpp conversion
|
||||
std::vector<uint8_t> data2bpp = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04,
|
||||
0x02, 0x01, 0x01, 0x02, 0x04, 0x08,
|
||||
0x10, 0x20, 0x40, 0x80};
|
||||
auto converted4bpp = gfx::ConvertBpp(data2bpp, 2, 4);
|
||||
EXPECT_EQ(converted4bpp.size(), 32); // 4bpp tile is 32 bytes
|
||||
|
||||
// Test 4bpp to 2bpp conversion (using only colors 0-3 for valid 2bpp)
|
||||
std::vector<uint8_t> data4bpp = {
|
||||
// Planes 1&2 (rows 0-7) - create colors 0-3 only
|
||||
0x80, 0x80, 0x40, 0x00, 0x20, 0x40, 0x10, 0x80, // rows 0-3
|
||||
0x08, 0x00, 0x04, 0x40, 0x02, 0x80, 0x01, 0x00, // rows 4-7
|
||||
// Planes 3&4 (rows 0-7) - all zeros to ensure colors stay ≤ 3
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rows 0-3
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // rows 4-7
|
||||
};
|
||||
auto converted2bpp = gfx::ConvertBpp(data4bpp, 4, 2);
|
||||
EXPECT_EQ(converted2bpp.size(), 16); // 2bpp tile is 16 bytes
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, TileInfo) {
|
||||
// Test TileInfo construction and bit manipulation
|
||||
gfx::TileInfo info(0x123, 3, true, true, true);
|
||||
EXPECT_EQ(info.id_, 0x123);
|
||||
EXPECT_EQ(info.palette_, 3);
|
||||
EXPECT_TRUE(info.vertical_mirror_);
|
||||
EXPECT_TRUE(info.horizontal_mirror_);
|
||||
EXPECT_TRUE(info.over_);
|
||||
|
||||
// Test TileInfo from bytes
|
||||
gfx::TileInfo infoFromBytes(0x23, 0xED); // v=1, h=1, o=1, p=3, id=0x123
|
||||
EXPECT_EQ(infoFromBytes.id_, 0x123);
|
||||
EXPECT_EQ(infoFromBytes.palette_, 3);
|
||||
EXPECT_TRUE(infoFromBytes.vertical_mirror_);
|
||||
EXPECT_TRUE(infoFromBytes.horizontal_mirror_);
|
||||
EXPECT_TRUE(infoFromBytes.over_);
|
||||
|
||||
// Test TileInfo equality
|
||||
EXPECT_TRUE(info == infoFromBytes);
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, TileInfoToWord) {
|
||||
gfx::TileInfo info(0x123, 3, true, true, true);
|
||||
uint16_t word = gfx::TileInfoToWord(info);
|
||||
|
||||
// Verify bit positions:
|
||||
// vhopppcc cccccccc
|
||||
EXPECT_EQ(word & 0x3FF, 0x123); // id (10 bits)
|
||||
EXPECT_TRUE(word & 0x8000); // vertical mirror
|
||||
EXPECT_TRUE(word & 0x4000); // horizontal mirror
|
||||
EXPECT_TRUE(word & 0x2000); // over
|
||||
EXPECT_EQ((word >> 10) & 0x07, 3); // palette (3 bits)
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, WordToTileInfo) {
|
||||
uint16_t word = 0xED23; // v=1, h=1, o=1, p=3, id=0x123
|
||||
gfx::TileInfo info = gfx::WordToTileInfo(word);
|
||||
|
||||
EXPECT_EQ(info.id_, 0x123);
|
||||
EXPECT_EQ(info.palette_, 3);
|
||||
EXPECT_TRUE(info.vertical_mirror_);
|
||||
EXPECT_TRUE(info.horizontal_mirror_);
|
||||
EXPECT_TRUE(info.over_);
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, Tile32) {
|
||||
// Test Tile32 construction and operations
|
||||
gfx::Tile32 tile32(0x1234, 0x5678, 0x9ABC, 0xDEF0);
|
||||
EXPECT_EQ(tile32.tile0_, 0x1234);
|
||||
EXPECT_EQ(tile32.tile1_, 0x5678);
|
||||
EXPECT_EQ(tile32.tile2_, 0x9ABC);
|
||||
EXPECT_EQ(tile32.tile3_, 0xDEF0);
|
||||
|
||||
// Test packed value
|
||||
uint64_t packed = tile32.GetPackedValue();
|
||||
EXPECT_EQ(packed, 0xDEF09ABC56781234);
|
||||
|
||||
// Test from packed value
|
||||
gfx::Tile32 tile32FromPacked(packed);
|
||||
EXPECT_EQ(tile32FromPacked.tile0_, 0x1234);
|
||||
EXPECT_EQ(tile32FromPacked.tile1_, 0x5678);
|
||||
EXPECT_EQ(tile32FromPacked.tile2_, 0x9ABC);
|
||||
EXPECT_EQ(tile32FromPacked.tile3_, 0xDEF0);
|
||||
|
||||
// Test equality
|
||||
EXPECT_TRUE(tile32 == tile32FromPacked);
|
||||
}
|
||||
|
||||
TEST(SnesTileTest, Tile16) {
|
||||
// Test Tile16 construction and operations
|
||||
gfx::TileInfo info0(0x123, 3, true, true, true);
|
||||
gfx::TileInfo info1(0x456, 2, false, true, false);
|
||||
gfx::TileInfo info2(0x789, 1, true, false, true);
|
||||
gfx::TileInfo info3(0xABC, 0, false, false, false);
|
||||
|
||||
gfx::Tile16 tile16(info0, info1, info2, info3);
|
||||
EXPECT_TRUE(tile16.tile0_ == info0);
|
||||
EXPECT_TRUE(tile16.tile1_ == info1);
|
||||
EXPECT_TRUE(tile16.tile2_ == info2);
|
||||
EXPECT_TRUE(tile16.tile3_ == info3);
|
||||
|
||||
// Test array access
|
||||
EXPECT_TRUE(tile16.tiles_info[0] == info0);
|
||||
EXPECT_TRUE(tile16.tiles_info[1] == info1);
|
||||
EXPECT_TRUE(tile16.tiles_info[2] == info2);
|
||||
EXPECT_TRUE(tile16.tiles_info[3] == info3);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
477
test/unit/gui/canvas_automation_api_test.cc
Normal file
477
test/unit/gui/canvas_automation_api_test.cc
Normal file
@@ -0,0 +1,477 @@
|
||||
#include "app/gui/canvas/canvas_automation_api.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::Ge;
|
||||
using ::testing::Le;
|
||||
|
||||
class CanvasAutomationAPITest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas with known dimensions
|
||||
canvas_ = std::make_unique<gui::Canvas>("TestCanvas", ImVec2(512, 512),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
api_ = canvas_->GetAutomationAPI();
|
||||
ASSERT_NE(api_, nullptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
gui::CanvasAutomationAPI* api_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Coordinate Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, TileToCanvas_BasicConversion) {
|
||||
// At 1.0x zoom, tile (0,0) should be at canvas (0,0)
|
||||
canvas_->set_global_scale(1.0f);
|
||||
|
||||
ImVec2 pos = api_->TileToCanvas(0, 0);
|
||||
EXPECT_FLOAT_EQ(pos.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(pos.y, 0.0f);
|
||||
|
||||
// Tile (1,0) should be at (16,0) for 16x16 grid
|
||||
pos = api_->TileToCanvas(1, 0);
|
||||
EXPECT_FLOAT_EQ(pos.x, 16.0f);
|
||||
EXPECT_FLOAT_EQ(pos.y, 0.0f);
|
||||
|
||||
// Tile (0,1) should be at (0,16)
|
||||
pos = api_->TileToCanvas(0, 1);
|
||||
EXPECT_FLOAT_EQ(pos.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(pos.y, 16.0f);
|
||||
|
||||
// Tile (10,10) should be at (160,160)
|
||||
pos = api_->TileToCanvas(10, 10);
|
||||
EXPECT_FLOAT_EQ(pos.x, 160.0f);
|
||||
EXPECT_FLOAT_EQ(pos.y, 160.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, TileToCanvas_WithZoom) {
|
||||
// At 2.0x zoom, tile coordinates should scale
|
||||
canvas_->set_global_scale(2.0f);
|
||||
|
||||
ImVec2 pos = api_->TileToCanvas(1, 1);
|
||||
EXPECT_FLOAT_EQ(pos.x, 32.0f); // 1 * 16 * 2.0
|
||||
EXPECT_FLOAT_EQ(pos.y, 32.0f);
|
||||
|
||||
// At 0.5x zoom, tile coordinates should scale down
|
||||
canvas_->set_global_scale(0.5f);
|
||||
pos = api_->TileToCanvas(10, 10);
|
||||
EXPECT_FLOAT_EQ(pos.x, 80.0f); // 10 * 16 * 0.5
|
||||
EXPECT_FLOAT_EQ(pos.y, 80.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CanvasToTile_BasicConversion) {
|
||||
canvas_->set_global_scale(1.0f);
|
||||
|
||||
// Canvas (0,0) should be tile (0,0)
|
||||
ImVec2 tile = api_->CanvasToTile(ImVec2(0, 0));
|
||||
EXPECT_FLOAT_EQ(tile.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(tile.y, 0.0f);
|
||||
|
||||
// Canvas (16,16) should be tile (1,1)
|
||||
tile = api_->CanvasToTile(ImVec2(16, 16));
|
||||
EXPECT_FLOAT_EQ(tile.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(tile.y, 1.0f);
|
||||
|
||||
// Canvas (160,160) should be tile (10,10)
|
||||
tile = api_->CanvasToTile(ImVec2(160, 160));
|
||||
EXPECT_FLOAT_EQ(tile.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(tile.y, 10.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CanvasToTile_WithZoom) {
|
||||
// At 2.0x zoom
|
||||
canvas_->set_global_scale(2.0f);
|
||||
|
||||
ImVec2 tile = api_->CanvasToTile(ImVec2(32, 32));
|
||||
EXPECT_FLOAT_EQ(tile.x, 1.0f); // 32 / (16 * 2.0)
|
||||
EXPECT_FLOAT_EQ(tile.y, 1.0f);
|
||||
|
||||
// At 0.5x zoom
|
||||
canvas_->set_global_scale(0.5f);
|
||||
tile = api_->CanvasToTile(ImVec2(8, 8));
|
||||
EXPECT_FLOAT_EQ(tile.x, 1.0f); // 8 / (16 * 0.5)
|
||||
EXPECT_FLOAT_EQ(tile.y, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CoordinateRoundTrip) {
|
||||
canvas_->set_global_scale(1.0f);
|
||||
|
||||
// Test round-trip conversion
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
ImVec2 canvas_pos = api_->TileToCanvas(i, i);
|
||||
ImVec2 tile_pos = api_->CanvasToTile(canvas_pos);
|
||||
|
||||
EXPECT_FLOAT_EQ(tile_pos.x, static_cast<float>(i));
|
||||
EXPECT_FLOAT_EQ(tile_pos.y, static_cast<float>(i));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Bounds Checking Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, IsInBounds_ValidCoordinates) {
|
||||
EXPECT_TRUE(api_->IsInBounds(0, 0));
|
||||
EXPECT_TRUE(api_->IsInBounds(10, 10));
|
||||
EXPECT_TRUE(api_->IsInBounds(31, 31)); // 512/16 = 32 tiles, so 31 is max
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, IsInBounds_InvalidCoordinates) {
|
||||
EXPECT_FALSE(api_->IsInBounds(-1, 0));
|
||||
EXPECT_FALSE(api_->IsInBounds(0, -1));
|
||||
EXPECT_FALSE(api_->IsInBounds(-1, -1));
|
||||
EXPECT_FALSE(api_->IsInBounds(32, 0)); // Out of bounds
|
||||
EXPECT_FALSE(api_->IsInBounds(0, 32));
|
||||
EXPECT_FALSE(api_->IsInBounds(100, 100));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tile Operations Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SetTileAt_WithCallback) {
|
||||
// Set up a tile paint callback
|
||||
std::vector<std::tuple<int, int, int>> painted_tiles;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
painted_tiles.push_back({x, y, tile_id});
|
||||
return true;
|
||||
});
|
||||
|
||||
// Paint some tiles
|
||||
EXPECT_TRUE(api_->SetTileAt(5, 5, 42));
|
||||
EXPECT_TRUE(api_->SetTileAt(10, 10, 100));
|
||||
|
||||
ASSERT_EQ(painted_tiles.size(), 2);
|
||||
EXPECT_EQ(painted_tiles[0], std::make_tuple(5, 5, 42));
|
||||
EXPECT_EQ(painted_tiles[1], std::make_tuple(10, 10, 100));
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SetTileAt_OutOfBounds) {
|
||||
bool callback_called = false;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
callback_called = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Out of bounds tiles should return false without calling callback
|
||||
EXPECT_FALSE(api_->SetTileAt(-1, 0, 42));
|
||||
EXPECT_FALSE(api_->SetTileAt(0, -1, 42));
|
||||
EXPECT_FALSE(api_->SetTileAt(100, 100, 42));
|
||||
|
||||
EXPECT_FALSE(callback_called);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, GetTileAt_WithCallback) {
|
||||
// Set up a tile query callback
|
||||
api_->SetTileQueryCallback([](int x, int y) {
|
||||
return x * 100 + y; // Simple deterministic value
|
||||
});
|
||||
|
||||
EXPECT_EQ(api_->GetTileAt(0, 0), 0);
|
||||
EXPECT_EQ(api_->GetTileAt(1, 0), 100);
|
||||
EXPECT_EQ(api_->GetTileAt(0, 1), 1);
|
||||
EXPECT_EQ(api_->GetTileAt(5, 7), 507);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, GetTileAt_OutOfBounds) {
|
||||
api_->SetTileQueryCallback([](int x, int y) { return 42; });
|
||||
|
||||
EXPECT_EQ(api_->GetTileAt(-1, 0), -1);
|
||||
EXPECT_EQ(api_->GetTileAt(0, -1), -1);
|
||||
EXPECT_EQ(api_->GetTileAt(100, 100), -1);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SetTiles_BatchOperation) {
|
||||
std::vector<std::tuple<int, int, int>> painted_tiles;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
painted_tiles.push_back({x, y, tile_id});
|
||||
return true;
|
||||
});
|
||||
|
||||
std::vector<std::tuple<int, int, int>> tiles_to_paint = {
|
||||
{0, 0, 10},
|
||||
{1, 0, 11},
|
||||
{2, 0, 12},
|
||||
{0, 1, 20},
|
||||
{1, 1, 21}
|
||||
};
|
||||
|
||||
EXPECT_TRUE(api_->SetTiles(tiles_to_paint));
|
||||
EXPECT_EQ(painted_tiles.size(), 5);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Selection Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SelectTile) {
|
||||
api_->SelectTile(5, 5);
|
||||
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_TRUE(selection.has_selection);
|
||||
EXPECT_EQ(selection.selected_tiles.size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SelectTileRect) {
|
||||
api_->SelectTileRect(5, 5, 9, 9);
|
||||
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_TRUE(selection.has_selection);
|
||||
|
||||
// 5x5 rectangle = 25 tiles
|
||||
EXPECT_EQ(selection.selected_tiles.size(), 25);
|
||||
|
||||
// Check first and last tiles
|
||||
EXPECT_FLOAT_EQ(selection.selected_tiles[0].x, 5.0f);
|
||||
EXPECT_FLOAT_EQ(selection.selected_tiles[0].y, 5.0f);
|
||||
EXPECT_FLOAT_EQ(selection.selected_tiles[24].x, 9.0f);
|
||||
EXPECT_FLOAT_EQ(selection.selected_tiles[24].y, 9.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SelectTileRect_SwappedCoordinates) {
|
||||
// Should handle coordinates in any order
|
||||
api_->SelectTileRect(9, 9, 5, 5); // Reversed
|
||||
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_TRUE(selection.has_selection);
|
||||
EXPECT_EQ(selection.selected_tiles.size(), 25);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, ClearSelection) {
|
||||
api_->SelectTileRect(5, 5, 10, 10);
|
||||
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_TRUE(selection.has_selection);
|
||||
|
||||
api_->ClearSelection();
|
||||
|
||||
selection = api_->GetSelection();
|
||||
EXPECT_FALSE(selection.has_selection);
|
||||
EXPECT_EQ(selection.selected_tiles.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SelectTile_OutOfBounds) {
|
||||
api_->SelectTile(-1, 0);
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_FALSE(selection.has_selection);
|
||||
|
||||
api_->SelectTile(100, 100);
|
||||
selection = api_->GetSelection();
|
||||
EXPECT_FALSE(selection.has_selection);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View Operations Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SetZoom_ValidRange) {
|
||||
api_->SetZoom(1.0f);
|
||||
EXPECT_FLOAT_EQ(api_->GetZoom(), 1.0f);
|
||||
|
||||
api_->SetZoom(2.0f);
|
||||
EXPECT_FLOAT_EQ(api_->GetZoom(), 2.0f);
|
||||
|
||||
api_->SetZoom(0.5f);
|
||||
EXPECT_FLOAT_EQ(api_->GetZoom(), 0.5f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, SetZoom_Clamping) {
|
||||
// Should clamp to 0.25 - 4.0 range
|
||||
api_->SetZoom(10.0f);
|
||||
EXPECT_LE(api_->GetZoom(), 4.0f);
|
||||
|
||||
api_->SetZoom(0.1f);
|
||||
EXPECT_GE(api_->GetZoom(), 0.25f);
|
||||
|
||||
api_->SetZoom(-1.0f);
|
||||
EXPECT_GE(api_->GetZoom(), 0.25f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, ScrollToTile_ValidTile) {
|
||||
// Should not crash when scrolling to valid tiles
|
||||
api_->ScrollToTile(0, 0, true);
|
||||
api_->ScrollToTile(10, 10, false);
|
||||
api_->ScrollToTile(15, 15, true);
|
||||
|
||||
// Just verify no crash - actual scroll behavior depends on ImGui state
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, ScrollToTile_OutOfBounds) {
|
||||
// Should handle out of bounds gracefully
|
||||
api_->ScrollToTile(-1, 0, true);
|
||||
api_->ScrollToTile(100, 100, true);
|
||||
|
||||
// Should not crash
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CenterOn_ValidTile) {
|
||||
// Should not crash when centering on valid tiles
|
||||
api_->CenterOn(10, 10);
|
||||
api_->CenterOn(0, 0);
|
||||
api_->CenterOn(20, 20);
|
||||
|
||||
// Verify scroll position changed (should be non-zero after centering on non-origin)
|
||||
ImVec2 scroll = canvas_->scrolling();
|
||||
// Scroll values will depend on canvas size, just verify they're set
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CenterOn_OutOfBounds) {
|
||||
api_->CenterOn(-1, 0);
|
||||
api_->CenterOn(100, 100);
|
||||
|
||||
// Should not crash
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Operations Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, GetDimensions) {
|
||||
canvas_->set_global_scale(1.0f);
|
||||
|
||||
auto dims = api_->GetDimensions();
|
||||
EXPECT_EQ(dims.tile_size, 16); // 16x16 grid
|
||||
EXPECT_EQ(dims.width_tiles, 32); // 512 / 16
|
||||
EXPECT_EQ(dims.height_tiles, 32);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, GetDimensions_WithZoom) {
|
||||
canvas_->set_global_scale(2.0f);
|
||||
|
||||
auto dims = api_->GetDimensions();
|
||||
EXPECT_EQ(dims.tile_size, 16);
|
||||
EXPECT_EQ(dims.width_tiles, 16); // 512 / (16 * 2.0)
|
||||
EXPECT_EQ(dims.height_tiles, 16);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, GetVisibleRegion) {
|
||||
canvas_->set_global_scale(1.0f);
|
||||
canvas_->set_scrolling(ImVec2(0, 0));
|
||||
|
||||
auto region = api_->GetVisibleRegion();
|
||||
|
||||
// At origin with no scroll, should start at (0,0)
|
||||
EXPECT_GE(region.min_x, 0);
|
||||
EXPECT_GE(region.min_y, 0);
|
||||
|
||||
// Should have valid bounds
|
||||
EXPECT_GE(region.max_x, region.min_x);
|
||||
EXPECT_GE(region.max_y, region.min_y);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, IsTileVisible_AtOrigin) {
|
||||
canvas_->set_global_scale(1.0f);
|
||||
canvas_->set_scrolling(ImVec2(0, 0));
|
||||
|
||||
// Tiles at origin should be visible
|
||||
EXPECT_TRUE(api_->IsTileVisible(0, 0));
|
||||
EXPECT_TRUE(api_->IsTileVisible(1, 1));
|
||||
|
||||
// Tiles far away might not be visible (depends on canvas size)
|
||||
// We just verify the method doesn't crash
|
||||
api_->IsTileVisible(50, 50);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, IsTileVisible_OutOfBounds) {
|
||||
// Out of bounds tiles should return false
|
||||
EXPECT_FALSE(api_->IsTileVisible(-1, 0));
|
||||
EXPECT_FALSE(api_->IsTileVisible(0, -1));
|
||||
EXPECT_FALSE(api_->IsTileVisible(100, 100));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, CompleteWorkflow) {
|
||||
// Simulate a complete automation workflow
|
||||
|
||||
// 1. Set zoom level
|
||||
api_->SetZoom(1.0f);
|
||||
EXPECT_FLOAT_EQ(api_->GetZoom(), 1.0f);
|
||||
|
||||
// 2. Select a tile region
|
||||
api_->SelectTileRect(0, 0, 4, 4);
|
||||
auto selection = api_->GetSelection();
|
||||
EXPECT_EQ(selection.selected_tiles.size(), 25);
|
||||
|
||||
// 3. Query tile data with callback
|
||||
api_->SetTileQueryCallback([](int x, int y) {
|
||||
return x + y * 100;
|
||||
});
|
||||
|
||||
EXPECT_EQ(api_->GetTileAt(2, 3), 302);
|
||||
|
||||
// 4. Paint tiles with callback
|
||||
std::vector<std::tuple<int, int, int>> painted;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
painted.push_back({x, y, tile_id});
|
||||
return true;
|
||||
});
|
||||
|
||||
std::vector<std::tuple<int, int, int>> tiles = {
|
||||
{0, 0, 10}, {1, 0, 11}, {2, 0, 12}
|
||||
};
|
||||
EXPECT_TRUE(api_->SetTiles(tiles));
|
||||
EXPECT_EQ(painted.size(), 3);
|
||||
|
||||
// 5. Clear selection
|
||||
api_->ClearSelection();
|
||||
selection = api_->GetSelection();
|
||||
EXPECT_FALSE(selection.has_selection);
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, DifferentGridSizes) {
|
||||
// Test with 8x8 grid
|
||||
auto canvas_8x8 = std::make_unique<gui::Canvas>(
|
||||
"Test8x8", ImVec2(512, 512), gui::CanvasGridSize::k8x8);
|
||||
auto api_8x8 = canvas_8x8->GetAutomationAPI();
|
||||
|
||||
auto dims = api_8x8->GetDimensions();
|
||||
EXPECT_EQ(dims.tile_size, 8);
|
||||
EXPECT_EQ(dims.width_tiles, 64); // 512 / 8
|
||||
|
||||
// Test with 32x32 grid
|
||||
auto canvas_32x32 = std::make_unique<gui::Canvas>(
|
||||
"Test32x32", ImVec2(512, 512), gui::CanvasGridSize::k32x32);
|
||||
auto api_32x32 = canvas_32x32->GetAutomationAPI();
|
||||
|
||||
dims = api_32x32->GetDimensions();
|
||||
EXPECT_EQ(dims.tile_size, 32);
|
||||
EXPECT_EQ(dims.width_tiles, 16); // 512 / 32
|
||||
}
|
||||
|
||||
TEST_F(CanvasAutomationAPITest, MultipleZoomLevels) {
|
||||
float zoom_levels[] = {0.25f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f};
|
||||
|
||||
for (float zoom : zoom_levels) {
|
||||
api_->SetZoom(zoom);
|
||||
float actual_zoom = api_->GetZoom();
|
||||
|
||||
// Should be clamped to valid range
|
||||
EXPECT_GE(actual_zoom, 0.25f);
|
||||
EXPECT_LE(actual_zoom, 4.0f);
|
||||
|
||||
// Coordinate conversion should still work
|
||||
ImVec2 canvas_pos = api_->TileToCanvas(10, 10);
|
||||
ImVec2 tile_pos = api_->CanvasToTile(canvas_pos);
|
||||
|
||||
EXPECT_FLOAT_EQ(tile_pos.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(tile_pos.y, 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
297
test/unit/gui/canvas_coordinate_sync_test.cc
Normal file
297
test/unit/gui/canvas_coordinate_sync_test.cc
Normal file
@@ -0,0 +1,297 @@
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::FloatEq;
|
||||
using ::testing::Ne;
|
||||
|
||||
/**
|
||||
* @brief Tests for canvas coordinate synchronization
|
||||
*
|
||||
* These tests verify that the canvas coordinate system properly tracks
|
||||
* mouse position for both hover and paint operations, fixing the regression
|
||||
* where CheckForCurrentMap() in OverworldEditor was using raw ImGui mouse
|
||||
* position instead of canvas-local coordinates.
|
||||
*
|
||||
* Regression: overworld_editor.cc:1041 was using ImGui::GetIO().MousePos
|
||||
* instead of canvas hover position, causing map highlighting to break.
|
||||
*/
|
||||
class CanvasCoordinateSyncTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas with known dimensions (4096x4096 for overworld)
|
||||
canvas_ = std::make_unique<gui::Canvas>("OverworldCanvas", ImVec2(4096, 4096),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
canvas_->set_global_scale(1.0f);
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hover Position Tests (hover_mouse_pos)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverMousePos_InitialState) {
|
||||
// Hover position should start at (0,0) or invalid state
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Initial state may be (0,0) - this is valid
|
||||
EXPECT_GE(hover_pos.x, 0.0f);
|
||||
EXPECT_GE(hover_pos.y, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverMousePos_IndependentFromDrawnPos) {
|
||||
// Hover position and drawn tile position are independent
|
||||
// hover_mouse_pos() tracks continuous mouse movement
|
||||
// drawn_tile_position() only updates during painting
|
||||
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
auto drawn_pos = canvas_->drawn_tile_position();
|
||||
|
||||
// These may differ - hover tracks all movement, drawn only tracks paint
|
||||
// We just verify both are valid (non-negative or expected sentinel values)
|
||||
EXPECT_TRUE(hover_pos.x >= 0.0f || hover_pos.x == -1.0f);
|
||||
EXPECT_TRUE(drawn_pos.x >= 0.0f || drawn_pos.x == -1.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Coordinate Space Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, CoordinateSpace_WorldNotScreen) {
|
||||
// REGRESSION TEST: Verify hover_mouse_pos() returns world coordinates
|
||||
// not screen coordinates. The bug was using ImGui::GetIO().MousePos
|
||||
// which is in screen space and doesn't account for scrolling/canvas offset.
|
||||
|
||||
// Simulate scrolling the canvas
|
||||
canvas_->set_scrolling(ImVec2(100, 100));
|
||||
|
||||
// The hover position should be in canvas/world space, not affected by
|
||||
// the canvas's screen position. This is tested by ensuring the method
|
||||
// exists and returns a coordinate that could be used for map calculations.
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Valid world coordinates should be usable for map index calculations
|
||||
// For a 512x512 map size (kOverworldMapSize = 512):
|
||||
// map_x = hover_pos.x / 512
|
||||
// map_y = hover_pos.y / 512
|
||||
|
||||
int map_x = static_cast<int>(hover_pos.x) / 512;
|
||||
int map_y = static_cast<int>(hover_pos.y) / 512;
|
||||
|
||||
// Map indices should be within valid range for 8x8 overworld grid
|
||||
EXPECT_GE(map_x, 0);
|
||||
EXPECT_GE(map_y, 0);
|
||||
EXPECT_LT(map_x, 64); // 8x8 grid = 64 maps max
|
||||
EXPECT_LT(map_y, 64);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapCalculation_SmallMaps) {
|
||||
// Test map index calculation for standard 512x512 maps
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Simulate hover at different world positions
|
||||
std::vector<ImVec2> test_positions = {
|
||||
ImVec2(0, 0), // Map (0, 0)
|
||||
ImVec2(512, 0), // Map (1, 0)
|
||||
ImVec2(0, 512), // Map (0, 1)
|
||||
ImVec2(512, 512), // Map (1, 1)
|
||||
ImVec2(1536, 1024), // Map (3, 2)
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, int>> expected_maps = {
|
||||
{0, 0}, {1, 0}, {0, 1}, {1, 1}, {3, 2}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < test_positions.size(); ++i) {
|
||||
ImVec2 pos = test_positions[i];
|
||||
int map_x = pos.x / kOverworldMapSize;
|
||||
int map_y = pos.y / kOverworldMapSize;
|
||||
|
||||
EXPECT_EQ(map_x, expected_maps[i].first);
|
||||
EXPECT_EQ(map_y, expected_maps[i].second);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapCalculation_LargeMaps) {
|
||||
// Test map index calculation for ZSCustomOverworld v3 large maps (1024x1024)
|
||||
const int kLargeMapSize = 1024;
|
||||
|
||||
// Large maps should span multiple standard map coordinates
|
||||
std::vector<ImVec2> test_positions = {
|
||||
ImVec2(0, 0), // Large map (0, 0)
|
||||
ImVec2(1024, 0), // Large map (1, 0)
|
||||
ImVec2(0, 1024), // Large map (0, 1)
|
||||
ImVec2(2048, 2048), // Large map (2, 2)
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, int>> expected_large_maps = {
|
||||
{0, 0}, {1, 0}, {0, 1}, {2, 2}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < test_positions.size(); ++i) {
|
||||
ImVec2 pos = test_positions[i];
|
||||
int map_x = pos.x / kLargeMapSize;
|
||||
int map_y = pos.y / kLargeMapSize;
|
||||
|
||||
EXPECT_EQ(map_x, expected_large_maps[i].first);
|
||||
EXPECT_EQ(map_y, expected_large_maps[i].second);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scale Invariance Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverPosition_ScaleInvariant) {
|
||||
// REGRESSION TEST: Hover position should be in world space regardless of scale
|
||||
// The bug was scale-dependent because it used screen coordinates
|
||||
|
||||
auto test_hover_at_scale = [&](float scale) {
|
||||
canvas_->set_global_scale(scale);
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Hover position should be in world coordinates, not affected by scale
|
||||
// World coordinates are always in the range [0, canvas_size)
|
||||
EXPECT_GE(hover_pos.x, 0.0f);
|
||||
EXPECT_GE(hover_pos.y, 0.0f);
|
||||
EXPECT_LE(hover_pos.x, 4096.0f);
|
||||
EXPECT_LE(hover_pos.y, 4096.0f);
|
||||
};
|
||||
|
||||
test_hover_at_scale(0.25f);
|
||||
test_hover_at_scale(0.5f);
|
||||
test_hover_at_scale(1.0f);
|
||||
test_hover_at_scale(2.0f);
|
||||
test_hover_at_scale(4.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overworld Editor Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, OverworldMapHighlight_UsesHoverNotDrawn) {
|
||||
// CRITICAL REGRESSION TEST
|
||||
// This verifies the fix for overworld_editor.cc:1041
|
||||
// CheckForCurrentMap() must use hover_mouse_pos() not ImGui::GetIO().MousePos
|
||||
|
||||
// The pattern used in DrawOverworldEdits (line 664) for painting:
|
||||
auto drawn_pos = canvas_->drawn_tile_position();
|
||||
|
||||
// The pattern that SHOULD be used in CheckForCurrentMap (line 1041) for highlighting:
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// These are different methods for different purposes:
|
||||
// - drawn_tile_position(): Only updates during active painting (mouse drag)
|
||||
// - hover_mouse_pos(): Updates continuously during hover
|
||||
|
||||
// Verify both methods exist and return valid (or sentinel) values
|
||||
EXPECT_TRUE(drawn_pos.x >= 0.0f || drawn_pos.x == -1.0f);
|
||||
EXPECT_TRUE(hover_pos.x >= 0.0f || hover_pos.x == -1.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, OverworldMapIndex_From8x8Grid) {
|
||||
// Simulate the exact calculation from OverworldEditor::CheckForCurrentMap
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Test all three worlds (Light, Dark, Special)
|
||||
struct TestCase {
|
||||
ImVec2 hover_pos;
|
||||
int current_world; // 0=Light, 1=Dark, 2=Special
|
||||
int expected_map_index;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
// Light World (0x00 - 0x3F)
|
||||
{ImVec2(0, 0), 0, 0}, // Map 0 (Light World)
|
||||
{ImVec2(512, 0), 0, 1}, // Map 1
|
||||
{ImVec2(1024, 512), 0, 10}, // Map 10 = 2 + 1*8
|
||||
|
||||
// Dark World (0x40 - 0x7F)
|
||||
{ImVec2(0, 0), 1, 0x40}, // Map 0x40 (Dark World)
|
||||
{ImVec2(512, 0), 1, 0x41}, // Map 0x41
|
||||
{ImVec2(1024, 512), 1, 0x4A}, // Map 0x4A = 0x40 + 10
|
||||
|
||||
// Special World (0x80+)
|
||||
{ImVec2(0, 0), 2, 0x80}, // Map 0x80 (Special World)
|
||||
{ImVec2(512, 512), 2, 0x89}, // Map 0x89 = 0x80 + 9
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
int map_x = tc.hover_pos.x / kOverworldMapSize;
|
||||
int map_y = tc.hover_pos.y / kOverworldMapSize;
|
||||
int hovered_map = map_x + map_y * 8;
|
||||
|
||||
if (tc.current_world == 1) {
|
||||
hovered_map += 0x40;
|
||||
} else if (tc.current_world == 2) {
|
||||
hovered_map += 0x80;
|
||||
}
|
||||
|
||||
EXPECT_EQ(hovered_map, tc.expected_map_index)
|
||||
<< "Failed for world " << tc.current_world
|
||||
<< " at position (" << tc.hover_pos.x << ", " << tc.hover_pos.y << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Boundary Condition Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapBoundaries_512x512) {
|
||||
// Test coordinates exactly at map boundaries
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Boundary coordinates (edges of maps)
|
||||
std::vector<ImVec2> boundary_positions = {
|
||||
ImVec2(511, 0), // Right edge of map 0
|
||||
ImVec2(512, 0), // Left edge of map 1
|
||||
ImVec2(0, 511), // Bottom edge of map 0
|
||||
ImVec2(0, 512), // Top edge of map 8
|
||||
ImVec2(511, 511), // Corner of map 0
|
||||
ImVec2(512, 512), // Corner of map 9
|
||||
};
|
||||
|
||||
for (const auto& pos : boundary_positions) {
|
||||
int map_x = pos.x / kOverworldMapSize;
|
||||
int map_y = pos.y / kOverworldMapSize;
|
||||
int map_index = map_x + map_y * 8;
|
||||
|
||||
// Verify map indices are within valid range
|
||||
EXPECT_GE(map_index, 0);
|
||||
EXPECT_LT(map_index, 64); // 8x8 grid = 64 maps
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapBoundaries_1024x1024) {
|
||||
// Test large map boundaries (ZSCustomOverworld v3)
|
||||
const int kLargeMapSize = 1024;
|
||||
|
||||
std::vector<ImVec2> boundary_positions = {
|
||||
ImVec2(1023, 0), // Right edge of large map 0
|
||||
ImVec2(1024, 0), // Left edge of large map 1
|
||||
ImVec2(0, 1023), // Bottom edge of large map 0
|
||||
ImVec2(0, 1024), // Top edge of large map 4 (0,1 in 4x4 grid)
|
||||
};
|
||||
|
||||
for (const auto& pos : boundary_positions) {
|
||||
int map_x = pos.x / kLargeMapSize;
|
||||
int map_y = pos.y / kLargeMapSize;
|
||||
int map_index = map_x + map_y * 4; // 4x4 grid for large maps
|
||||
|
||||
// Verify map indices are within valid range for large maps
|
||||
EXPECT_GE(map_index, 0);
|
||||
EXPECT_LT(map_index, 16); // 4x4 grid = 16 large maps
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::NotNull;
|
||||
|
||||
class TileSelectorWidgetTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas
|
||||
canvas_ = std::make_unique<gui::Canvas>("TestCanvas", ImVec2(512, 512),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
|
||||
// Create a test config
|
||||
config_.tile_size = 16;
|
||||
config_.display_scale = 2.0f;
|
||||
config_.tiles_per_row = 8;
|
||||
config_.total_tiles = 64; // 8x8 grid
|
||||
config_.draw_offset = {2.0f, 0.0f};
|
||||
config_.show_tile_ids = false;
|
||||
config_.highlight_color = {1.0f, 0.85f, 0.35f, 1.0f};
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
gui::TileSelectorWidget::Config config_;
|
||||
};
|
||||
|
||||
// Test basic construction
|
||||
TEST_F(TileSelectorWidgetTest, Construction) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test construction with config
|
||||
TEST_F(TileSelectorWidgetTest, ConstructionWithConfig) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test canvas attachment
|
||||
TEST_F(TileSelectorWidgetTest, AttachCanvas) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
// No crash means success
|
||||
}
|
||||
|
||||
// Test tile count setting
|
||||
TEST_F(TileSelectorWidgetTest, SetTileCount) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(128);
|
||||
// Verify selection is clamped when tile count changes
|
||||
widget.SetSelectedTile(100);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 100);
|
||||
|
||||
// Setting tile count lower should clamp selection
|
||||
widget.SetTileCount(50);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0); // Should reset to 0
|
||||
}
|
||||
|
||||
// Test selected tile setting
|
||||
TEST_F(TileSelectorWidgetTest, SetSelectedTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
widget.SetSelectedTile(10);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 10);
|
||||
|
||||
widget.SetSelectedTile(63);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63);
|
||||
|
||||
// Out of bounds should be ignored
|
||||
widget.SetSelectedTile(64);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
|
||||
widget.SetSelectedTile(-1);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
}
|
||||
|
||||
// Test tile origin calculation
|
||||
TEST_F(TileSelectorWidgetTest, TileOrigin) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Test first tile (0,0)
|
||||
auto origin = widget.TileOrigin(0);
|
||||
EXPECT_FLOAT_EQ(origin.x, config_.draw_offset.x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (1,0)
|
||||
origin = widget.TileOrigin(1);
|
||||
float expected_x = config_.draw_offset.x +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (0,1) - first tile of second row
|
||||
origin = widget.TileOrigin(8);
|
||||
expected_x = config_.draw_offset.x;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
|
||||
// Test invalid tile ID
|
||||
origin = widget.TileOrigin(64);
|
||||
EXPECT_FLOAT_EQ(origin.x, -1.0f);
|
||||
EXPECT_FLOAT_EQ(origin.y, -1.0f);
|
||||
}
|
||||
|
||||
// Test render without atlas (should not crash)
|
||||
TEST_F(TileSelectorWidgetTest, RenderWithoutAtlas) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
|
||||
gfx::Bitmap atlas;
|
||||
auto result = widget.Render(atlas, false);
|
||||
|
||||
EXPECT_FALSE(result.tile_clicked);
|
||||
EXPECT_FALSE(result.tile_double_clicked);
|
||||
EXPECT_FALSE(result.selection_changed);
|
||||
EXPECT_EQ(result.selected_tile, -1);
|
||||
}
|
||||
|
||||
// Test programmatic selection for AI/automation
|
||||
TEST_F(TileSelectorWidgetTest, ProgrammaticSelection) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Simulate AI/automation selecting tiles programmatically
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), i);
|
||||
|
||||
auto origin = widget.TileOrigin(i);
|
||||
int expected_col = i % config_.tiles_per_row;
|
||||
int expected_row = i / config_.tiles_per_row;
|
||||
float expected_x = config_.draw_offset.x +
|
||||
expected_col * config_.tile_size * config_.display_scale;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
expected_row * config_.tile_size * config_.display_scale;
|
||||
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Test scroll to tile
|
||||
TEST_F(TileSelectorWidgetTest, ScrollToTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Scroll to various tiles (should not crash)
|
||||
widget.ScrollToTile(0);
|
||||
widget.ScrollToTile(10);
|
||||
widget.ScrollToTile(63);
|
||||
|
||||
// Invalid tile should not crash
|
||||
widget.ScrollToTile(-1);
|
||||
widget.ScrollToTile(64);
|
||||
}
|
||||
|
||||
// Test different configs
|
||||
TEST_F(TileSelectorWidgetTest, DifferentConfigs) {
|
||||
// Test with 16x16 grid
|
||||
gui::TileSelectorWidget::Config large_config;
|
||||
large_config.tile_size = 8;
|
||||
large_config.display_scale = 1.0f;
|
||||
large_config.tiles_per_row = 16;
|
||||
large_config.total_tiles = 256;
|
||||
large_config.draw_offset = {0.0f, 0.0f};
|
||||
|
||||
gui::TileSelectorWidget large_widget("large_widget", large_config);
|
||||
large_widget.SetTileCount(256);
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
large_widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(large_widget.GetSelectedTileID(), i);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
238
test/unit/rom/rom_test.cc
Normal file
238
test/unit/rom/rom_test.cc
Normal file
@@ -0,0 +1,238 @@
|
||||
#include "app/rom.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
#include "testing.h"
|
||||
#include "app/transaction.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
const static std::vector<uint8_t> kMockRomData = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
|
||||
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
|
||||
0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
};
|
||||
|
||||
class RomTest : public ::testing::Test {
|
||||
protected:
|
||||
Rom rom_;
|
||||
};
|
||||
|
||||
TEST_F(RomTest, Uninitialized) {
|
||||
EXPECT_EQ(rom_.size(), 0);
|
||||
EXPECT_EQ(rom_.data(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(RomTest, LoadFromFile) {
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#endif
|
||||
EXPECT_OK(rom_.LoadFromFile("zelda3.sfc"));
|
||||
EXPECT_EQ(rom_.size(), 0x200000);
|
||||
EXPECT_NE(rom_.data(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(RomTest, LoadFromFileInvalid) {
|
||||
EXPECT_THAT(rom_.LoadFromFile("invalid.sfc"),
|
||||
StatusIs(absl::StatusCode::kNotFound));
|
||||
EXPECT_EQ(rom_.size(), 0);
|
||||
EXPECT_EQ(rom_.data(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(RomTest, LoadFromFileEmpty) {
|
||||
EXPECT_THAT(rom_.LoadFromFile(""),
|
||||
StatusIs(absl::StatusCode::kInvalidArgument));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadByteOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); ++i) {
|
||||
uint8_t byte;
|
||||
ASSERT_OK_AND_ASSIGN(byte, rom_.ReadByte(i));
|
||||
EXPECT_EQ(byte, kMockRomData[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadByteInvalid) {
|
||||
EXPECT_THAT(rom_.ReadByte(0).status(),
|
||||
StatusIs(absl::StatusCode::kFailedPrecondition));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadWordOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); i += 2) {
|
||||
// Little endian
|
||||
EXPECT_THAT(
|
||||
rom_.ReadWord(i),
|
||||
IsOkAndHolds<uint16_t>((kMockRomData[i]) | kMockRomData[i + 1] << 8));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadWordInvalid) {
|
||||
EXPECT_THAT(rom_.ReadWord(0).status(),
|
||||
StatusIs(absl::StatusCode::kFailedPrecondition));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadLongOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); i += 4) {
|
||||
// Little endian
|
||||
EXPECT_THAT(rom_.ReadLong(i),
|
||||
IsOkAndHolds<uint32_t>((kMockRomData[i]) | kMockRomData[i] |
|
||||
kMockRomData[i + 1] << 8 |
|
||||
kMockRomData[i + 2] << 16));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadBytesOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
std::vector<uint8_t> bytes;
|
||||
ASSERT_OK_AND_ASSIGN(bytes, rom_.ReadByteVector(0, kMockRomData.size()));
|
||||
EXPECT_THAT(bytes, ::testing::ContainerEq(kMockRomData));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadBytesOutOfRange) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
std::vector<uint8_t> bytes;
|
||||
EXPECT_THAT(rom_.ReadByteVector(kMockRomData.size() + 1, 1).status(),
|
||||
StatusIs(absl::StatusCode::kOutOfRange));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, WriteByteOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); ++i) {
|
||||
EXPECT_OK(rom_.WriteByte(i, 0xFF));
|
||||
uint8_t byte;
|
||||
ASSERT_OK_AND_ASSIGN(byte, rom_.ReadByte(i));
|
||||
EXPECT_EQ(byte, 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, WriteWordOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); i += 2) {
|
||||
EXPECT_OK(rom_.WriteWord(i, 0xFFFF));
|
||||
uint16_t word;
|
||||
ASSERT_OK_AND_ASSIGN(word, rom_.ReadWord(i));
|
||||
EXPECT_EQ(word, 0xFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, WriteLongOk) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, false));
|
||||
|
||||
for (size_t i = 0; i < kMockRomData.size(); i += 4) {
|
||||
EXPECT_OK(rom_.WriteLong(i, 0xFFFFFF));
|
||||
uint32_t word;
|
||||
ASSERT_OK_AND_ASSIGN(word, rom_.ReadLong(i));
|
||||
EXPECT_EQ(word, 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RomTest, WriteTransactionSuccess) {
|
||||
MockRom mock_rom;
|
||||
EXPECT_OK(mock_rom.LoadFromData(kMockRomData, false));
|
||||
|
||||
EXPECT_CALL(mock_rom, WriteHelper(_))
|
||||
.WillRepeatedly(Return(absl::OkStatus()));
|
||||
|
||||
EXPECT_OK(mock_rom.WriteTransaction(
|
||||
Rom::WriteAction{0x1000, uint8_t{0xFF}},
|
||||
Rom::WriteAction{0x1001, uint16_t{0xABCD}},
|
||||
Rom::WriteAction{0x1002, std::vector<uint8_t>{0x12, 0x34}}));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, WriteTransactionFailure) {
|
||||
MockRom mock_rom;
|
||||
EXPECT_OK(mock_rom.LoadFromData(kMockRomData, false));
|
||||
|
||||
EXPECT_CALL(mock_rom, WriteHelper(_))
|
||||
.WillOnce(Return(absl::OkStatus()))
|
||||
.WillOnce(Return(absl::InternalError("Write failed")));
|
||||
|
||||
EXPECT_EQ(
|
||||
mock_rom.WriteTransaction(Rom::WriteAction{0x1000, uint8_t{0xFF}},
|
||||
Rom::WriteAction{0x1001, uint16_t{0xABCD}}),
|
||||
absl::InternalError("Write failed"));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadTransactionSuccess) {
|
||||
MockRom mock_rom;
|
||||
EXPECT_OK(mock_rom.LoadFromData(kMockRomData, false));
|
||||
uint8_t byte_val;
|
||||
uint16_t word_val;
|
||||
|
||||
EXPECT_OK(mock_rom.ReadTransaction(byte_val, 0x0000, word_val, 0x0001));
|
||||
|
||||
EXPECT_EQ(byte_val, 0x00);
|
||||
EXPECT_EQ(word_val, 0x0201);
|
||||
}
|
||||
|
||||
TEST_F(RomTest, ReadTransactionFailure) {
|
||||
MockRom mock_rom;
|
||||
EXPECT_OK(mock_rom.LoadFromData(kMockRomData, false));
|
||||
uint8_t byte_val;
|
||||
|
||||
EXPECT_EQ(mock_rom.ReadTransaction(byte_val, 0x1000),
|
||||
absl::FailedPreconditionError("Offset out of range"));
|
||||
}
|
||||
|
||||
TEST_F(RomTest, SaveTruncatesExistingFile) {
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#endif
|
||||
// Prepare ROM data and save to a temp file twice; second save should overwrite, not append
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, /*z3_load=*/false));
|
||||
|
||||
const char* tmp_name = "test_temp_rom.sfc";
|
||||
yaze::Rom::SaveSettings settings;
|
||||
settings.filename = tmp_name;
|
||||
settings.z3_save = false;
|
||||
|
||||
// First save
|
||||
EXPECT_OK(rom_.SaveToFile(settings));
|
||||
|
||||
// Modify one byte and save again
|
||||
EXPECT_OK(rom_.WriteByte(0, 0xEE));
|
||||
EXPECT_OK(rom_.SaveToFile(settings));
|
||||
|
||||
// Load the saved file and verify size equals original data size and first byte matches
|
||||
Rom verify;
|
||||
EXPECT_OK(verify.LoadFromFile(tmp_name, /*z3_load=*/false));
|
||||
EXPECT_EQ(verify.size(), kMockRomData.size());
|
||||
auto b0 = verify.ReadByte(0);
|
||||
ASSERT_TRUE(b0.ok());
|
||||
EXPECT_EQ(*b0, 0xEE);
|
||||
}
|
||||
|
||||
TEST_F(RomTest, TransactionRollbackRestoresOriginals) {
|
||||
EXPECT_OK(rom_.LoadFromData(kMockRomData, /*z3_load=*/false));
|
||||
// Force an out-of-range write to trigger failure after a successful write
|
||||
yaze::Transaction tx{rom_};
|
||||
auto status = tx.WriteByte(0x01, 0xAA) // valid
|
||||
.WriteWord(0xFFFF, 0xBBBB) // invalid: should fail and rollback
|
||||
.Commit();
|
||||
EXPECT_FALSE(status.ok());
|
||||
auto b1 = rom_.ReadByte(0x01);
|
||||
ASSERT_TRUE(b1.ok());
|
||||
// Should be restored to original 0x01 value (from kMockRomData)
|
||||
EXPECT_EQ(*b1, kMockRomData[0x01]);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
258
test/unit/snes_color_test.cc
Normal file
258
test/unit/snes_color_test.cc
Normal file
@@ -0,0 +1,258 @@
|
||||
#include "app/gfx/types/snes_color.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
namespace {
|
||||
|
||||
// Test fixture for SnesColor tests
|
||||
class SnesColorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Common setup if needed
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RGB Format Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbFromImGuiNormalizedValues) {
|
||||
SnesColor color;
|
||||
|
||||
// ImGui ColorPicker returns values in 0-1 range
|
||||
ImVec4 imgui_color(0.5f, 0.75f, 1.0f, 1.0f);
|
||||
color.set_rgb(imgui_color);
|
||||
|
||||
// Internal storage should be in 0-255 range
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 127.5f); // 0.5 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.y, 191.25f); // 0.75 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f); // 1.0 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f); // Alpha always 255
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbBlackColor) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 black(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(black);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbWhiteColor) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
color.set_rgb(white);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, SetRgbMidRangeColor) {
|
||||
SnesColor color;
|
||||
|
||||
// Test a mid-range color (medium gray)
|
||||
ImVec4 gray(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
color.set_rgb(gray);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_NEAR(rgb.x, 127.5f, 0.01f);
|
||||
EXPECT_NEAR(rgb.y, 127.5f, 0.01f);
|
||||
EXPECT_NEAR(rgb.z, 127.5f, 0.01f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constructor Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromImVec4) {
|
||||
// ImGui color in 0-1 range
|
||||
ImVec4 imgui_color(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
|
||||
SnesColor color(imgui_color);
|
||||
|
||||
// Should be converted to 0-255 range
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_NEAR(rgb.x, 63.75f, 0.01f); // 0.25 * 255
|
||||
EXPECT_NEAR(rgb.y, 127.5f, 0.01f); // 0.5 * 255
|
||||
EXPECT_NEAR(rgb.z, 191.25f, 0.01f); // 0.75 * 255
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromSnesValue) {
|
||||
// SNES BGR555 format: 0x7FFF = white (all bits set in 15-bit color)
|
||||
SnesColor white(0x7FFF);
|
||||
|
||||
auto rgb = white.rgb();
|
||||
// All channels should be max (after BGR555 conversion)
|
||||
EXPECT_GT(rgb.x, 240.0f); // Close to 255
|
||||
EXPECT_GT(rgb.y, 240.0f);
|
||||
EXPECT_GT(rgb.z, 240.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ConstructFromSnesBlack) {
|
||||
// SNES BGR555 format: 0x0000 = black
|
||||
SnesColor black(0x0000);
|
||||
|
||||
auto rgb = black.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SNES Format Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, SetSnesUpdatesRgb) {
|
||||
SnesColor color;
|
||||
|
||||
// Set a SNES color value
|
||||
color.set_snes(0x7FFF); // White in BGR555
|
||||
|
||||
// RGB should be updated
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_GT(rgb.x, 240.0f);
|
||||
EXPECT_GT(rgb.y, 240.0f);
|
||||
EXPECT_GT(rgb.z, 240.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, RgbToSnesConversion) {
|
||||
SnesColor color;
|
||||
|
||||
// Set pure red in RGB (0-1 range for ImGui)
|
||||
ImVec4 red(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(red);
|
||||
|
||||
// SNES value should be set (BGR555 format)
|
||||
uint16_t snes = color.snes();
|
||||
EXPECT_NE(snes, 0x0000); // Should not be black
|
||||
|
||||
// Extract red component from BGR555 (bits 0-4)
|
||||
uint16_t snes_red = snes & 0x1F;
|
||||
EXPECT_EQ(snes_red, 0x1F); // Max red in 5-bit
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Round-Trip Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, RoundTripImGuiToSnesColor) {
|
||||
// Start with ImGui color
|
||||
ImVec4 original(0.6f, 0.4f, 0.8f, 1.0f);
|
||||
|
||||
// Convert to SnesColor
|
||||
SnesColor color(original);
|
||||
|
||||
// Convert back to ImVec4 (normalized)
|
||||
auto rgb = color.rgb();
|
||||
ImVec4 converted(rgb.x / 255.0f, rgb.y / 255.0f, rgb.z / 255.0f, 1.0f);
|
||||
|
||||
// Should be approximately equal (within floating point precision)
|
||||
EXPECT_NEAR(converted.x, original.x, 0.01f);
|
||||
EXPECT_NEAR(converted.y, original.y, 0.01f);
|
||||
EXPECT_NEAR(converted.z, original.z, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, MultipleSetRgbCalls) {
|
||||
SnesColor color;
|
||||
|
||||
// First color
|
||||
ImVec4 color1(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
color.set_rgb(color1);
|
||||
|
||||
auto rgb1 = color.rgb();
|
||||
EXPECT_NEAR(rgb1.x, 51.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb1.y, 102.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb1.z, 153.0f, 1.0f);
|
||||
|
||||
// Second color (should completely replace)
|
||||
ImVec4 color2(0.8f, 0.6f, 0.4f, 1.0f);
|
||||
color.set_rgb(color2);
|
||||
|
||||
auto rgb2 = color.rgb();
|
||||
EXPECT_NEAR(rgb2.x, 204.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb2.y, 153.0f, 1.0f);
|
||||
EXPECT_NEAR(rgb2.z, 102.0f, 1.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Case Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, HandlesMaxValues) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 max(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
color.set_rgb(max);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 255.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 255.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, HandlesMinValues) {
|
||||
SnesColor color;
|
||||
|
||||
ImVec4 min(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
color.set_rgb(min);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, AlphaAlwaysMaximum) {
|
||||
SnesColor color;
|
||||
|
||||
// Try setting alpha to different values (should always be ignored)
|
||||
ImVec4 color_with_alpha(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
color.set_rgb(color_with_alpha);
|
||||
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.w, 255.0f); // Alpha should always be 255
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Modified Flag Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(SnesColorTest, ModifiedFlagSetOnRgbChange) {
|
||||
SnesColor color;
|
||||
|
||||
EXPECT_FALSE(color.is_modified());
|
||||
|
||||
ImVec4 new_color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
color.set_rgb(new_color);
|
||||
|
||||
EXPECT_TRUE(color.is_modified());
|
||||
}
|
||||
|
||||
TEST_F(SnesColorTest, ModifiedFlagSetOnSnesChange) {
|
||||
SnesColor color;
|
||||
|
||||
EXPECT_FALSE(color.is_modified());
|
||||
|
||||
color.set_snes(0x7FFF);
|
||||
|
||||
EXPECT_TRUE(color.is_modified());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
324
test/unit/zelda3/dungeon/object_rendering_test.cc
Normal file
324
test/unit/zelda3/dungeon/object_rendering_test.cc
Normal file
@@ -0,0 +1,324 @@
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/background_buffer.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/object_drawer.h"
|
||||
#include "zelda3/dungeon/object_parser.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
class ObjectRenderingTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a mock ROM for testing
|
||||
rom_ = std::make_unique<Rom>();
|
||||
// Initialize with minimal ROM data for testing
|
||||
std::vector<uint8_t> mock_rom_data(1024 * 1024, 0); // 1MB mock ROM
|
||||
rom_->LoadFromData(mock_rom_data);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
gfx::BackgroundBuffer bg1_;
|
||||
gfx::BackgroundBuffer bg2_;
|
||||
|
||||
// Create a test palette
|
||||
gfx::SnesPalette CreateTestPalette() {
|
||||
gfx::SnesPalette palette;
|
||||
// Add some test colors
|
||||
palette.AddColor(gfx::SnesColor(0, 0, 0)); // Transparent
|
||||
palette.AddColor(gfx::SnesColor(255, 0, 0)); // Red
|
||||
palette.AddColor(gfx::SnesColor(0, 255, 0)); // Green
|
||||
palette.AddColor(gfx::SnesColor(0, 0, 255)); // Blue
|
||||
palette.AddColor(gfx::SnesColor(255, 255, 0)); // Yellow
|
||||
palette.AddColor(gfx::SnesColor(255, 0, 255)); // Magenta
|
||||
palette.AddColor(gfx::SnesColor(0, 255, 255)); // Cyan
|
||||
palette.AddColor(gfx::SnesColor(255, 255, 255)); // White
|
||||
return palette;
|
||||
}
|
||||
|
||||
gfx::PaletteGroup CreateTestPaletteGroup() {
|
||||
gfx::PaletteGroup group;
|
||||
group.AddPalette(CreateTestPalette());
|
||||
return group;
|
||||
}
|
||||
};
|
||||
|
||||
// Test object drawer initialization
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerInitializesCorrectly) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
|
||||
// Test that drawer can be created without errors
|
||||
EXPECT_NE(rom_.get(), nullptr);
|
||||
}
|
||||
|
||||
// Test object parser draw routine detection
|
||||
TEST_F(ObjectRenderingTest, ObjectParserDetectsDrawRoutines) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Test common object IDs and their expected draw routines
|
||||
auto info_00 = parser.GetObjectDrawInfo(0x00);
|
||||
EXPECT_EQ(info_00.draw_routine_id, 0);
|
||||
EXPECT_EQ(info_00.routine_name, "Rightwards2x2_1to15or32");
|
||||
EXPECT_TRUE(info_00.is_horizontal);
|
||||
|
||||
auto info_01 = parser.GetObjectDrawInfo(0x01);
|
||||
EXPECT_EQ(info_01.draw_routine_id, 1);
|
||||
EXPECT_EQ(info_01.routine_name, "Rightwards2x4_1to15or26");
|
||||
EXPECT_TRUE(info_01.is_horizontal);
|
||||
|
||||
auto info_09 = parser.GetObjectDrawInfo(0x09);
|
||||
EXPECT_EQ(info_09.draw_routine_id, 5);
|
||||
EXPECT_EQ(info_09.routine_name, "DiagonalAcute_1to16");
|
||||
EXPECT_FALSE(info_09.is_horizontal);
|
||||
|
||||
auto info_34 = parser.GetObjectDrawInfo(0x34);
|
||||
EXPECT_EQ(info_34.draw_routine_id, 16);
|
||||
EXPECT_EQ(info_34.routine_name, "Rightwards1x1Solid_1to16_plus3");
|
||||
EXPECT_TRUE(info_34.is_horizontal);
|
||||
|
||||
// Test unmapped object defaults to solid block routine
|
||||
auto info_unknown = parser.GetObjectDrawInfo(0x999);
|
||||
EXPECT_EQ(info_unknown.draw_routine_id, 16); // Default solid routine
|
||||
EXPECT_EQ(info_unknown.routine_name, "DefaultSolid");
|
||||
}
|
||||
|
||||
// Test object drawer with various object types
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesVariousObjectTypes) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Test object 0x00 (horizontal floor tile)
|
||||
RoomObject floor_object(0x00, 10, 10, 3, 0); // ID, X, Y, size, layer
|
||||
|
||||
auto status = drawer.DrawObject(floor_object, bg1_, bg2_, palette_group);
|
||||
// Should succeed even if tiles aren't loaded (graceful handling)
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test object 0x09 (diagonal stairs)
|
||||
RoomObject stair_object(0x09, 15, 15, 5, 0);
|
||||
stair_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(stair_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test object 0x34 (solid block)
|
||||
RoomObject block_object(0x34, 20, 20, 1, 0);
|
||||
block_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(block_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test object drawer with different layers
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesDifferentLayers) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Test BG1 layer object
|
||||
RoomObject bg1_object(0x00, 5, 5, 2, 0); // Layer 0 = BG1
|
||||
bg1_object.set_rom(rom_.get());
|
||||
|
||||
auto status = drawer.DrawObject(bg1_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test BG2 layer object
|
||||
RoomObject bg2_object(0x01, 10, 10, 2, 1); // Layer 1 = BG2
|
||||
bg2_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(bg2_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test object drawer with size variations
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesSizeVariations) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Test small object
|
||||
RoomObject small_object(0x00, 5, 5, 1, 0); // Size = 1
|
||||
small_object.set_rom(rom_.get());
|
||||
|
||||
auto status = drawer.DrawObject(small_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test large object
|
||||
RoomObject large_object(0x00, 10, 10, 15, 0); // Size = 15
|
||||
large_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(large_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test maximum size object
|
||||
RoomObject max_object(0x00, 15, 15, 31, 0); // Size = 31 (0x1F)
|
||||
max_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(max_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test object drawer with edge cases
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesEdgeCases) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Test object at origin
|
||||
RoomObject origin_object(0x34, 0, 0, 1, 0);
|
||||
origin_object.set_rom(rom_.get());
|
||||
|
||||
auto status = drawer.DrawObject(origin_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test object with zero size
|
||||
RoomObject zero_size_object(0x34, 10, 10, 0, 0);
|
||||
zero_size_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(zero_size_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test object with maximum coordinates
|
||||
RoomObject max_coord_object(0x34, 63, 63, 1, 0); // Near buffer edge
|
||||
max_coord_object.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(max_coord_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test object drawer with multiple objects
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesMultipleObjects) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
std::vector<RoomObject> objects;
|
||||
|
||||
// Create various test objects
|
||||
objects.emplace_back(0x00, 5, 5, 3, 0); // Horizontal floor
|
||||
objects.emplace_back(0x01, 10, 10, 2, 0); // Vertical floor
|
||||
objects.emplace_back(0x09, 15, 15, 4, 0); // Diagonal stairs
|
||||
objects.emplace_back(0x34, 20, 20, 1, 1); // Solid block on BG2
|
||||
|
||||
// Set ROM for all objects
|
||||
for (auto& obj : objects) {
|
||||
obj.set_rom(rom_.get());
|
||||
}
|
||||
|
||||
auto status = drawer.DrawObjectList(objects, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test specific draw routines
|
||||
TEST_F(ObjectRenderingTest, DrawRoutinesWorkCorrectly) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Test rightward patterns
|
||||
RoomObject rightward_obj(0x00, 5, 5, 5, 0);
|
||||
rightward_obj.set_rom(rom_.get());
|
||||
|
||||
auto status = drawer.DrawObject(rightward_obj, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test diagonal patterns
|
||||
RoomObject diagonal_obj(0x09, 10, 10, 6, 0);
|
||||
diagonal_obj.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(diagonal_obj, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Test solid block patterns
|
||||
RoomObject solid_obj(0x34, 15, 15, 8, 0);
|
||||
solid_obj.set_rom(rom_.get());
|
||||
|
||||
status = drawer.DrawObject(solid_obj, bg1_, bg2_, palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test object drawer error handling
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerHandlesErrorsGracefully) {
|
||||
ObjectDrawer drawer(nullptr); // No ROM
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
RoomObject test_object(0x00, 5, 5, 1, 0);
|
||||
|
||||
auto status = drawer.DrawObject(test_object, bg1_, bg2_, palette_group);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
// Test object parser with various object IDs
|
||||
TEST_F(ObjectRenderingTest, ObjectParserHandlesVariousObjectIDs) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Test subtype 1 objects (0x00-0xFF)
|
||||
for (int id = 0; id <= 0x40; id += 4) { // Test every 4th object
|
||||
auto info = parser.GetObjectDrawInfo(id);
|
||||
EXPECT_GE(info.draw_routine_id, 0);
|
||||
EXPECT_LT(info.draw_routine_id, 25); // Should be within valid range
|
||||
EXPECT_FALSE(info.routine_name.empty());
|
||||
}
|
||||
|
||||
// Test some specific important objects
|
||||
std::vector<int16_t> important_objects = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
|
||||
0x0A, 0x0B, 0x15, 0x16, 0x21, 0x22, 0x2F, 0x30, 0x31, 0x32,
|
||||
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C,
|
||||
0x3D, 0x3E, 0x3F, 0x40
|
||||
};
|
||||
|
||||
for (int16_t obj_id : important_objects) {
|
||||
auto info = parser.GetObjectDrawInfo(obj_id);
|
||||
EXPECT_GE(info.draw_routine_id, 0);
|
||||
EXPECT_LT(info.draw_routine_id, 25);
|
||||
EXPECT_FALSE(info.routine_name.empty());
|
||||
|
||||
// Verify tile count is reasonable
|
||||
EXPECT_GT(info.tile_count, 0);
|
||||
EXPECT_LE(info.tile_count, 64); // Reasonable upper bound
|
||||
}
|
||||
}
|
||||
|
||||
// Test object drawer performance with many objects
|
||||
TEST_F(ObjectRenderingTest, ObjectDrawerPerformanceTest) {
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
std::vector<RoomObject> objects;
|
||||
|
||||
// Create 100 test objects
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
int id = i % 65; // Cycle through object IDs 0-64
|
||||
int x = (i * 2) % 60; // Spread across buffer
|
||||
int y = (i * 3) % 60;
|
||||
int size = (i % 8) + 1; // Size 1-8
|
||||
int layer = i % 2; // Alternate layers
|
||||
|
||||
objects.emplace_back(id, x, y, size, layer);
|
||||
objects.back().set_rom(rom_.get());
|
||||
}
|
||||
|
||||
// Time the drawing operation
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto status = drawer.DrawObjectList(objects, bg1_, bg2_, palette_group);
|
||||
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
end_time - start_time);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Should complete in reasonable time (less than 1 second for 100 objects)
|
||||
EXPECT_LT(duration.count(), 1000);
|
||||
|
||||
std::cout << "Drew 100 objects in " << duration.count() << "ms" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
169
test/unit/zelda3/dungeon/room_manipulation_test.cc
Normal file
169
test/unit/zelda3/dungeon/room_manipulation_test.cc
Normal file
@@ -0,0 +1,169 @@
|
||||
// Tests for Room object manipulation methods (Phase 3)
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
namespace test {
|
||||
|
||||
class RoomManipulationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
rom_ = std::make_unique<Rom>();
|
||||
// Create a minimal ROM for testing
|
||||
std::vector<uint8_t> dummy_data(0x200000, 0);
|
||||
rom_->LoadFromData(dummy_data, false);
|
||||
|
||||
room_ = std::make_unique<Room>(0, rom_.get());
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<Room> room_;
|
||||
};
|
||||
|
||||
TEST_F(RoomManipulationTest, AddObject) {
|
||||
RoomObject obj(0x10, 10, 20, 3, 0);
|
||||
|
||||
auto status = room_->AddObject(obj);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects.size(), 1);
|
||||
EXPECT_EQ(objects[0].id_, 0x10);
|
||||
EXPECT_EQ(objects[0].x(), 10);
|
||||
EXPECT_EQ(objects[0].y(), 20);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, AddInvalidObject) {
|
||||
// Invalid X position (> 63)
|
||||
RoomObject obj(0x10, 100, 20, 3, 0);
|
||||
|
||||
auto status = room_->AddObject(obj);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, RemoveObject) {
|
||||
RoomObject obj1(0x10, 10, 20, 3, 0);
|
||||
RoomObject obj2(0x20, 15, 25, 2, 1);
|
||||
|
||||
room_->AddObject(obj1);
|
||||
room_->AddObject(obj2);
|
||||
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 2);
|
||||
|
||||
auto status = room_->RemoveObject(0);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects.size(), 1);
|
||||
EXPECT_EQ(objects[0].id_, 0x20);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, RemoveInvalidIndex) {
|
||||
auto status = room_->RemoveObject(0);
|
||||
EXPECT_FALSE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, UpdateObject) {
|
||||
RoomObject obj(0x10, 10, 20, 3, 0);
|
||||
room_->AddObject(obj);
|
||||
|
||||
RoomObject updated(0x20, 15, 25, 5, 1);
|
||||
auto status = room_->UpdateObject(0, updated);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects[0].id_, 0x20);
|
||||
EXPECT_EQ(objects[0].x(), 15);
|
||||
EXPECT_EQ(objects[0].y(), 25);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, FindObjectAt) {
|
||||
RoomObject obj1(0x10, 10, 20, 3, 0);
|
||||
RoomObject obj2(0x20, 15, 25, 2, 1);
|
||||
|
||||
room_->AddObject(obj1);
|
||||
room_->AddObject(obj2);
|
||||
|
||||
auto result = room_->FindObjectAt(15, 25, 1);
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.value(), 1);
|
||||
|
||||
auto not_found = room_->FindObjectAt(99, 99, 0);
|
||||
EXPECT_FALSE(not_found.ok());
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, ValidateObject) {
|
||||
// Valid Type 1 object
|
||||
RoomObject valid1(0x10, 10, 20, 3, 0);
|
||||
EXPECT_TRUE(room_->ValidateObject(valid1));
|
||||
|
||||
// Valid Type 2 object
|
||||
RoomObject valid2(0x110, 30, 40, 0, 1);
|
||||
EXPECT_TRUE(room_->ValidateObject(valid2));
|
||||
|
||||
// Invalid X (> 63)
|
||||
RoomObject invalid_x(0x10, 100, 20, 3, 0);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_x));
|
||||
|
||||
// Invalid layer (> 2)
|
||||
RoomObject invalid_layer(0x10, 10, 20, 3, 5);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_layer));
|
||||
|
||||
// Invalid size for Type 1 (> 15)
|
||||
RoomObject invalid_size(0x10, 10, 20, 20, 0);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_size));
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, MultipleOperations) {
|
||||
// Add several objects
|
||||
for (int i = 0; i < 5; i++) {
|
||||
RoomObject obj(0x10 + i, i * 5, i * 6, i, 0);
|
||||
ASSERT_TRUE(room_->AddObject(obj).ok());
|
||||
}
|
||||
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 5);
|
||||
|
||||
// Update middle object
|
||||
RoomObject updated(0x99, 30, 35, 7, 1);
|
||||
ASSERT_TRUE(room_->UpdateObject(2, updated).ok());
|
||||
|
||||
// Verify update
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects[2].id_, 0x99);
|
||||
|
||||
// Remove first object
|
||||
ASSERT_TRUE(room_->RemoveObject(0).ok());
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 4);
|
||||
|
||||
// Verify first object is now what was second
|
||||
EXPECT_EQ(room_->GetTileObjects()[0].id_, 0x11);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, LayerOrganization) {
|
||||
// Add objects to different layers
|
||||
RoomObject layer0_obj(0x10, 10, 10, 2, 0);
|
||||
RoomObject layer1_obj(0x20, 20, 20, 3, 1);
|
||||
RoomObject layer2_obj(0x30, 30, 30, 4, 2);
|
||||
|
||||
room_->AddObject(layer0_obj);
|
||||
room_->AddObject(layer1_obj);
|
||||
room_->AddObject(layer2_obj);
|
||||
|
||||
// Verify can find by layer
|
||||
EXPECT_TRUE(room_->FindObjectAt(10, 10, 0).ok());
|
||||
EXPECT_TRUE(room_->FindObjectAt(20, 20, 1).ok());
|
||||
EXPECT_TRUE(room_->FindObjectAt(30, 30, 2).ok());
|
||||
|
||||
// Wrong layer should not find
|
||||
EXPECT_FALSE(room_->FindObjectAt(10, 10, 1).ok());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
330
test/unit/zelda3/dungeon/room_object_encoding_test.cc
Normal file
330
test/unit/zelda3/dungeon/room_object_encoding_test.cc
Normal file
@@ -0,0 +1,330 @@
|
||||
// test/zelda3/dungeon/room_object_encoding_test.cc
|
||||
// Unit tests for Phase 1, Task 1.1: Object Encoding/Decoding
|
||||
//
|
||||
// These tests verify that the object encoding and decoding functions work
|
||||
// correctly for all three object types (Type1, Type2, Type3) based on
|
||||
// ZScream's proven implementation.
|
||||
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
namespace {
|
||||
|
||||
// ============================================================================
|
||||
// Object Type Detection Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, DetermineObjectTypeType1) {
|
||||
// Type1: b1 < 0xFC, b3 < 0xF8
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0x10), 1);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0x42), 1);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0xFB, 0xF7), 1);
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, DetermineObjectTypeType2) {
|
||||
// Type2: b1 >= 0xFC, b3 < 0xF8
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0x42), 2);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0xFD, 0x25), 2);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0xFF, 0x00), 2);
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, DetermineObjectTypeType3) {
|
||||
// Type3: b3 >= 0xF8
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0xF8), 3);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0xF9), 3);
|
||||
EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0xFF), 3);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type 1 Object Encoding/Decoding Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1EncodeDecodeBasic) {
|
||||
// Type1: xxxxxxss yyyyyyss iiiiiiii
|
||||
// Example: Object ID 0x42, position (10, 20), size 3, layer 0
|
||||
|
||||
RoomObject obj(0x42, 10, 20, 3, 0);
|
||||
|
||||
// Encode
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Decode
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0);
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
EXPECT_EQ(decoded.size(), obj.size());
|
||||
EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1MaxValues) {
|
||||
// Test maximum valid values for Type1
|
||||
// Constraints:
|
||||
// - ID < 0xF8 (b3 >= 0xF8 triggers Type3 detection)
|
||||
// - X < 63 OR Size < 12 (b1 >= 0xFC triggers Type2 detection)
|
||||
// Safe max values: ID=0xF7, X=62, Y=63, Size=15
|
||||
RoomObject obj(0xF7, 62, 63, 15, 2);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2);
|
||||
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
EXPECT_EQ(decoded.size(), obj.size());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1MinValues) {
|
||||
// Test minimum values for Type1
|
||||
RoomObject obj(0x00, 0, 0, 0, 0);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0);
|
||||
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
EXPECT_EQ(decoded.size(), obj.size());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1DifferentSizes) {
|
||||
// Test all valid size values (0-15)
|
||||
for (int size = 0; size <= 15; size++) {
|
||||
RoomObject obj(0x30, 15, 20, size, 1);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1);
|
||||
|
||||
EXPECT_EQ(decoded.size(), size) << "Failed for size " << size;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1RealWorldExample1) {
|
||||
// Example from actual ROM: Wall object
|
||||
// Bytes: 0x28 0x50 0x10
|
||||
// Expected: X=10, Y=20, Size=0, ID=0x10
|
||||
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x28, 0x50, 0x10, 0);
|
||||
|
||||
EXPECT_EQ(decoded.x(), 10);
|
||||
EXPECT_EQ(decoded.y(), 20);
|
||||
EXPECT_EQ(decoded.size(), 0);
|
||||
EXPECT_EQ(decoded.id_, 0x10);
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type1RealWorldExample2) {
|
||||
// Example: Ceiling object with size
|
||||
// Correct bytes for X=10, Y=20, Size=3, ID=0x00: 0x28 0x53 0x00
|
||||
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x28, 0x53, 0x00, 0);
|
||||
|
||||
EXPECT_EQ(decoded.x(), 10);
|
||||
EXPECT_EQ(decoded.y(), 20);
|
||||
EXPECT_EQ(decoded.size(), 3);
|
||||
EXPECT_EQ(decoded.id_, 0x00);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type 2 Object Encoding/Decoding Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type2EncodeDecodeBasic) {
|
||||
// Type2: 111111xx xxxxyyyy yyiiiiii
|
||||
// Example: Object ID 0x125, position (15, 30), size ignored, layer 1
|
||||
|
||||
RoomObject obj(0x125, 15, 30, 0, 1);
|
||||
|
||||
// Encode
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Verify b1 starts with 0xFC
|
||||
EXPECT_GE(bytes.b1, 0xFC);
|
||||
|
||||
// Decode
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1);
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type2MaxValues) {
|
||||
// Type2 allows larger position range, but has constraints:
|
||||
// When Y=63 and ID=0x13F, b3 becomes 0xFF >= 0xF8, triggering Type3 detection
|
||||
// Safe max: X=63, Y=59, ID=0x13F (b3 = ((59&0x03)<<6)|(0x3F) = 0xFF still!)
|
||||
// Even safer: X=63, Y=63, ID=0x11F (b3 = (0xC0|0x1F) = 0xDF < 0xF8)
|
||||
RoomObject obj(0x11F, 63, 63, 0, 2);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2);
|
||||
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type2RealWorldExample) {
|
||||
// Example: Large brazier (object 0x11C)
|
||||
// Position (8, 12)
|
||||
|
||||
RoomObject obj(0x11C, 8, 12, 0, 0);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0);
|
||||
|
||||
EXPECT_EQ(decoded.id_, 0x11C);
|
||||
EXPECT_EQ(decoded.x(), 8);
|
||||
EXPECT_EQ(decoded.y(), 12);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type 3 Object Encoding/Decoding Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type3EncodeDecodeChest) {
|
||||
// Type3: xxxxxxii yyyyyyii 11111iii
|
||||
// Example: Small chest (0xF99), position (5, 10)
|
||||
|
||||
RoomObject obj(0xF99, 5, 10, 0, 0);
|
||||
|
||||
// Encode
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Verify b3 >= 0xF8
|
||||
EXPECT_GE(bytes.b3, 0xF8);
|
||||
|
||||
// Decode
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0);
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(decoded.id_, obj.id_);
|
||||
EXPECT_EQ(decoded.x(), obj.x());
|
||||
EXPECT_EQ(decoded.y(), obj.y());
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type3EncodeDcodeBigChest) {
|
||||
// Example: Big chest (0xFB1), position (15, 20)
|
||||
|
||||
RoomObject obj(0xFB1, 15, 20, 0, 1);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1);
|
||||
|
||||
EXPECT_EQ(decoded.id_, 0xFB1);
|
||||
EXPECT_EQ(decoded.x(), 15);
|
||||
EXPECT_EQ(decoded.y(), 20);
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, Type3RealWorldExample) {
|
||||
// Example from ROM: Chest at position (10, 15)
|
||||
// Correct bytes for ID 0xF99: 0x29 0x3E 0xF9
|
||||
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x29, 0x3E, 0xF9, 0);
|
||||
|
||||
// Expected: X=10, Y=15, ID=0xF99 (small chest)
|
||||
EXPECT_EQ(decoded.x(), 10);
|
||||
EXPECT_EQ(decoded.y(), 15);
|
||||
EXPECT_EQ(decoded.id_, 0xF99);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases and Special Values
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, LayerPreservation) {
|
||||
// Test that layer information is preserved through encode/decode
|
||||
for (uint8_t layer = 0; layer <= 2; layer++) {
|
||||
RoomObject obj(0x42, 10, 20, 3, layer);
|
||||
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, layer);
|
||||
|
||||
EXPECT_EQ(decoded.GetLayerValue(), layer) << "Failed for layer " << (int)layer;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, BoundaryBetweenTypes) {
|
||||
// Test boundary values between object types
|
||||
// NOTE: Type1 can only go up to ID 0xF7 (b3 >= 0xF8 triggers Type3)
|
||||
|
||||
// Last safe Type1 object
|
||||
RoomObject type1(0xF7, 10, 20, 3, 0);
|
||||
auto bytes1 = type1.EncodeObjectToBytes();
|
||||
auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0);
|
||||
EXPECT_EQ(decoded1.id_, 0xF7);
|
||||
|
||||
// First Type2 object
|
||||
RoomObject type2(0x100, 10, 20, 0, 0);
|
||||
auto bytes2 = type2.EncodeObjectToBytes();
|
||||
auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0);
|
||||
EXPECT_EQ(decoded2.id_, 0x100);
|
||||
|
||||
// Last Type2 object
|
||||
RoomObject type2_last(0x13F, 10, 20, 0, 0);
|
||||
auto bytes2_last = type2_last.EncodeObjectToBytes();
|
||||
auto decoded2_last = RoomObject::DecodeObjectFromBytes(bytes2_last.b1, bytes2_last.b2, bytes2_last.b3, 0);
|
||||
EXPECT_EQ(decoded2_last.id_, 0x13F);
|
||||
|
||||
// Type3 objects (start at 0xF80)
|
||||
RoomObject type3(0xF99, 10, 20, 0, 0);
|
||||
auto bytes3 = type3.EncodeObjectToBytes();
|
||||
auto decoded3 = RoomObject::DecodeObjectFromBytes(bytes3.b1, bytes3.b2, bytes3.b3, 0);
|
||||
EXPECT_EQ(decoded3.id_, 0xF99);
|
||||
}
|
||||
|
||||
TEST(RoomObjectEncodingTest, ZeroPosition) {
|
||||
// Test objects at position (0, 0)
|
||||
RoomObject type1(0x10, 0, 0, 0, 0);
|
||||
auto bytes1 = type1.EncodeObjectToBytes();
|
||||
auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0);
|
||||
EXPECT_EQ(decoded1.x(), 0);
|
||||
EXPECT_EQ(decoded1.y(), 0);
|
||||
|
||||
RoomObject type2(0x110, 0, 0, 0, 0);
|
||||
auto bytes2 = type2.EncodeObjectToBytes();
|
||||
auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0);
|
||||
EXPECT_EQ(decoded2.x(), 0);
|
||||
EXPECT_EQ(decoded2.y(), 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Tests with Multiple Objects
|
||||
// ============================================================================
|
||||
|
||||
TEST(RoomObjectEncodingTest, MultipleObjectsRoundTrip) {
|
||||
// Test encoding/decoding a batch of different objects
|
||||
std::vector<RoomObject> objects;
|
||||
|
||||
// Add various objects
|
||||
objects.emplace_back(0x10, 5, 10, 2, 0); // Type1
|
||||
objects.emplace_back(0x42, 15, 20, 5, 1); // Type1
|
||||
objects.emplace_back(0x110, 8, 12, 0, 0); // Type2
|
||||
objects.emplace_back(0x125, 25, 30, 0, 1); // Type2
|
||||
objects.emplace_back(0xF99, 10, 15, 0, 0); // Type3 (chest)
|
||||
objects.emplace_back(0xFB1, 20, 25, 0, 2); // Type3 (big chest)
|
||||
|
||||
for (size_t i = 0; i < objects.size(); i++) {
|
||||
auto& obj = objects[i];
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(
|
||||
bytes.b1, bytes.b2, bytes.b3, obj.GetLayerValue());
|
||||
|
||||
EXPECT_EQ(decoded.id_, obj.id_) << "Failed at index " << i;
|
||||
EXPECT_EQ(decoded.x(), obj.x()) << "Failed at index " << i;
|
||||
EXPECT_EQ(decoded.y(), obj.y()) << "Failed at index " << i;
|
||||
if (obj.id_ < 0x100) { // Type1 objects have size
|
||||
EXPECT_EQ(decoded.size(), obj.size()) << "Failed at index " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
127
test/unit/zelda3/dungeon_component_unit_test.cc
Normal file
127
test/unit/zelda3/dungeon_component_unit_test.cc
Normal file
@@ -0,0 +1,127 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
// Test the individual components independently
|
||||
#include "app/editor/dungeon/dungeon_toolset.h"
|
||||
#include "app/editor/dungeon/dungeon_usage_tracker.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Unit tests for individual dungeon components
|
||||
*
|
||||
* These tests validate component behavior without requiring ROM files
|
||||
* or complex graphics initialization.
|
||||
*/
|
||||
|
||||
// Test DungeonToolset Component
|
||||
TEST(DungeonToolsetTest, BasicFunctionality) {
|
||||
editor::DungeonToolset toolset;
|
||||
|
||||
// Test initial state
|
||||
EXPECT_EQ(toolset.background_type(), editor::DungeonToolset::kBackgroundAny);
|
||||
EXPECT_EQ(toolset.placement_type(), editor::DungeonToolset::kNoType);
|
||||
|
||||
// Test state changes
|
||||
toolset.set_background_type(editor::DungeonToolset::kBackground1);
|
||||
EXPECT_EQ(toolset.background_type(), editor::DungeonToolset::kBackground1);
|
||||
|
||||
toolset.set_placement_type(editor::DungeonToolset::kObject);
|
||||
EXPECT_EQ(toolset.placement_type(), editor::DungeonToolset::kObject);
|
||||
|
||||
// Test all background types
|
||||
toolset.set_background_type(editor::DungeonToolset::kBackground2);
|
||||
EXPECT_EQ(toolset.background_type(), editor::DungeonToolset::kBackground2);
|
||||
|
||||
toolset.set_background_type(editor::DungeonToolset::kBackground3);
|
||||
EXPECT_EQ(toolset.background_type(), editor::DungeonToolset::kBackground3);
|
||||
|
||||
// Test all placement types
|
||||
std::vector<editor::DungeonToolset::PlacementType> placement_types = {
|
||||
editor::DungeonToolset::kSprite,
|
||||
editor::DungeonToolset::kItem,
|
||||
editor::DungeonToolset::kEntrance,
|
||||
editor::DungeonToolset::kDoor,
|
||||
editor::DungeonToolset::kChest,
|
||||
editor::DungeonToolset::kBlock
|
||||
};
|
||||
|
||||
for (auto type : placement_types) {
|
||||
toolset.set_placement_type(type);
|
||||
EXPECT_EQ(toolset.placement_type(), type);
|
||||
}
|
||||
}
|
||||
|
||||
// Test DungeonToolset Callbacks
|
||||
TEST(DungeonToolsetTest, CallbackFunctionality) {
|
||||
editor::DungeonToolset toolset;
|
||||
|
||||
// Test callback setup (should not crash)
|
||||
bool undo_called = false;
|
||||
bool redo_called = false;
|
||||
bool palette_called = false;
|
||||
|
||||
toolset.SetUndoCallback([&undo_called]() { undo_called = true; });
|
||||
toolset.SetRedoCallback([&redo_called]() { redo_called = true; });
|
||||
toolset.SetPaletteToggleCallback([&palette_called]() { palette_called = true; });
|
||||
|
||||
// Callbacks are set but won't be triggered without UI interaction
|
||||
// The fact that we can set them without crashing validates the interface
|
||||
EXPECT_FALSE(undo_called); // Not called yet
|
||||
EXPECT_FALSE(redo_called); // Not called yet
|
||||
EXPECT_FALSE(palette_called); // Not called yet
|
||||
}
|
||||
|
||||
// Test DungeonUsageTracker Component
|
||||
TEST(DungeonUsageTrackerTest, BasicFunctionality) {
|
||||
editor::DungeonUsageTracker tracker;
|
||||
|
||||
// Test initial state
|
||||
EXPECT_TRUE(tracker.GetBlocksetUsage().empty());
|
||||
EXPECT_TRUE(tracker.GetSpritesetUsage().empty());
|
||||
EXPECT_TRUE(tracker.GetPaletteUsage().empty());
|
||||
|
||||
// Test initial selection state
|
||||
EXPECT_EQ(tracker.GetSelectedBlockset(), 0xFFFF);
|
||||
EXPECT_EQ(tracker.GetSelectedSpriteset(), 0xFFFF);
|
||||
EXPECT_EQ(tracker.GetSelectedPalette(), 0xFFFF);
|
||||
|
||||
// Test selection setters
|
||||
tracker.SetSelectedBlockset(0x01);
|
||||
EXPECT_EQ(tracker.GetSelectedBlockset(), 0x01);
|
||||
|
||||
tracker.SetSelectedSpriteset(0x02);
|
||||
EXPECT_EQ(tracker.GetSelectedSpriteset(), 0x02);
|
||||
|
||||
tracker.SetSelectedPalette(0x03);
|
||||
EXPECT_EQ(tracker.GetSelectedPalette(), 0x03);
|
||||
|
||||
// Test clear functionality
|
||||
tracker.ClearUsageStats();
|
||||
EXPECT_EQ(tracker.GetSelectedBlockset(), 0xFFFF);
|
||||
EXPECT_EQ(tracker.GetSelectedSpriteset(), 0xFFFF);
|
||||
EXPECT_EQ(tracker.GetSelectedPalette(), 0xFFFF);
|
||||
}
|
||||
|
||||
// Test Component File Size Reduction
|
||||
TEST(ComponentArchitectureTest, FileSizeReduction) {
|
||||
// This test validates that the refactoring actually reduced complexity
|
||||
// by ensuring the component files exist and are reasonably sized
|
||||
|
||||
// The main dungeon_editor.cc should be significantly smaller
|
||||
// Before: ~1444 lines, Target: ~400-600 lines
|
||||
|
||||
// We can't directly test file sizes, but we can test that
|
||||
// the components exist and function properly
|
||||
|
||||
editor::DungeonToolset toolset;
|
||||
editor::DungeonUsageTracker tracker;
|
||||
|
||||
// If we can create the components, the refactoring was successful
|
||||
EXPECT_EQ(toolset.background_type(), editor::DungeonToolset::kBackgroundAny);
|
||||
EXPECT_TRUE(tracker.GetBlocksetUsage().empty());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
89
test/unit/zelda3/object_parser_structs_test.cc
Normal file
89
test/unit/zelda3/object_parser_structs_test.cc
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "zelda3/dungeon/object_parser.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
class ObjectParserStructsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectRoutineInfoDefaultConstructor) {
|
||||
zelda3::ObjectRoutineInfo info;
|
||||
|
||||
EXPECT_EQ(info.routine_ptr, 0);
|
||||
EXPECT_EQ(info.tile_ptr, 0);
|
||||
EXPECT_EQ(info.tile_count, 0);
|
||||
EXPECT_FALSE(info.is_repeatable);
|
||||
EXPECT_FALSE(info.is_orientation_dependent);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectSubtypeInfoDefaultConstructor) {
|
||||
zelda3::ObjectSubtypeInfo info;
|
||||
|
||||
EXPECT_EQ(info.subtype, 0);
|
||||
EXPECT_EQ(info.subtype_ptr, 0);
|
||||
EXPECT_EQ(info.routine_ptr, 0);
|
||||
EXPECT_EQ(info.max_tile_count, 0);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectSizeInfoDefaultConstructor) {
|
||||
zelda3::ObjectSizeInfo info;
|
||||
|
||||
EXPECT_EQ(info.width_tiles, 0);
|
||||
EXPECT_EQ(info.height_tiles, 0);
|
||||
EXPECT_TRUE(info.is_horizontal);
|
||||
EXPECT_FALSE(info.is_repeatable);
|
||||
EXPECT_EQ(info.repeat_count, 1);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectRoutineInfoAssignment) {
|
||||
zelda3::ObjectRoutineInfo info;
|
||||
|
||||
info.routine_ptr = 0x12345;
|
||||
info.tile_ptr = 0x67890;
|
||||
info.tile_count = 8;
|
||||
info.is_repeatable = true;
|
||||
info.is_orientation_dependent = true;
|
||||
|
||||
EXPECT_EQ(info.routine_ptr, 0x12345);
|
||||
EXPECT_EQ(info.tile_ptr, 0x67890);
|
||||
EXPECT_EQ(info.tile_count, 8);
|
||||
EXPECT_TRUE(info.is_repeatable);
|
||||
EXPECT_TRUE(info.is_orientation_dependent);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectSubtypeInfoAssignment) {
|
||||
zelda3::ObjectSubtypeInfo info;
|
||||
|
||||
info.subtype = 2;
|
||||
info.subtype_ptr = 0x83F0;
|
||||
info.routine_ptr = 0x8470;
|
||||
info.max_tile_count = 16;
|
||||
|
||||
EXPECT_EQ(info.subtype, 2);
|
||||
EXPECT_EQ(info.subtype_ptr, 0x83F0);
|
||||
EXPECT_EQ(info.routine_ptr, 0x8470);
|
||||
EXPECT_EQ(info.max_tile_count, 16);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserStructsTest, ObjectSizeInfoAssignment) {
|
||||
zelda3::ObjectSizeInfo info;
|
||||
|
||||
info.width_tiles = 4;
|
||||
info.height_tiles = 2;
|
||||
info.is_horizontal = false;
|
||||
info.is_repeatable = true;
|
||||
info.repeat_count = 3;
|
||||
|
||||
EXPECT_EQ(info.width_tiles, 4);
|
||||
EXPECT_EQ(info.height_tiles, 2);
|
||||
EXPECT_FALSE(info.is_horizontal);
|
||||
EXPECT_TRUE(info.is_repeatable);
|
||||
EXPECT_EQ(info.repeat_count, 3);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
144
test/unit/zelda3/object_parser_test.cc
Normal file
144
test/unit/zelda3/object_parser_test.cc
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "zelda3/dungeon/object_parser.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "mocks/mock_rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
class ObjectParserTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
mock_rom_ = std::make_unique<MockRom>();
|
||||
SetupMockData();
|
||||
parser_ = std::make_unique<zelda3::ObjectParser>(mock_rom_.get());
|
||||
}
|
||||
|
||||
void SetupMockData() {
|
||||
std::vector<uint8_t> mock_data(0x100000, 0x00);
|
||||
|
||||
// Set up object subtype tables
|
||||
SetupSubtypeTable(mock_data, 0x8000, 0x100); // Subtype 1 table
|
||||
SetupSubtypeTable(mock_data, 0x83F0, 0x80); // Subtype 2 table
|
||||
SetupSubtypeTable(mock_data, 0x84F0, 0x100); // Subtype 3 table
|
||||
|
||||
// Set up tile data
|
||||
SetupTileData(mock_data, 0x1B52, 0x1000);
|
||||
|
||||
static_cast<MockRom*>(mock_rom_.get())->SetTestData(mock_data);
|
||||
}
|
||||
|
||||
void SetupSubtypeTable(std::vector<uint8_t>& data, int base_addr, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int addr = base_addr + (i * 2);
|
||||
if (addr + 1 < (int)data.size()) {
|
||||
// Point to tile data at 0x1B52 + (i * 8)
|
||||
int tile_offset = (i * 8) & 0xFFFF;
|
||||
data[addr] = tile_offset & 0xFF;
|
||||
data[addr + 1] = (tile_offset >> 8) & 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetupTileData(std::vector<uint8_t>& data, int base_addr, int size) {
|
||||
for (int i = 0; i < size; i += 8) {
|
||||
int addr = base_addr + i;
|
||||
if (addr + 7 < (int)data.size()) {
|
||||
// Create simple tile data (4 words per tile)
|
||||
for (int j = 0; j < 8; j++) {
|
||||
data[addr + j] = (i + j) & 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<MockRom> mock_rom_;
|
||||
std::unique_ptr<zelda3::ObjectParser> parser_;
|
||||
};
|
||||
|
||||
TEST_F(ObjectParserTest, ParseSubtype1Object) {
|
||||
auto result = parser_->ParseObject(0x01);
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
const auto& tiles = result.value();
|
||||
EXPECT_EQ(tiles.size(), 8);
|
||||
|
||||
// Verify tile data was parsed correctly
|
||||
for (const auto& tile : tiles) {
|
||||
EXPECT_NE(tile.id_, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, ParseSubtype2Object) {
|
||||
auto result = parser_->ParseObject(0x101);
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
const auto& tiles = result.value();
|
||||
EXPECT_EQ(tiles.size(), 8);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, ParseSubtype3Object) {
|
||||
auto result = parser_->ParseObject(0x201);
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
const auto& tiles = result.value();
|
||||
EXPECT_EQ(tiles.size(), 8);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, GetObjectSubtype) {
|
||||
auto result1 = parser_->GetObjectSubtype(0x01);
|
||||
ASSERT_TRUE(result1.ok());
|
||||
EXPECT_EQ(result1->subtype, 1);
|
||||
|
||||
auto result2 = parser_->GetObjectSubtype(0x101);
|
||||
ASSERT_TRUE(result2.ok());
|
||||
EXPECT_EQ(result2->subtype, 2);
|
||||
|
||||
auto result3 = parser_->GetObjectSubtype(0x201);
|
||||
ASSERT_TRUE(result3.ok());
|
||||
EXPECT_EQ(result3->subtype, 3);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, ParseObjectSize) {
|
||||
auto result = parser_->ParseObjectSize(0x01, 0x12);
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
const auto& size_info = result.value();
|
||||
EXPECT_EQ(size_info.width_tiles, 4); // (1 + 1) * 2
|
||||
EXPECT_EQ(size_info.height_tiles, 6); // (2 + 1) * 2
|
||||
EXPECT_TRUE(size_info.is_horizontal);
|
||||
EXPECT_TRUE(size_info.is_repeatable);
|
||||
EXPECT_EQ(size_info.repeat_count, 0x12);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, ParseObjectRoutine) {
|
||||
auto result = parser_->ParseObjectRoutine(0x01);
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
const auto& routine_info = result.value();
|
||||
EXPECT_NE(routine_info.routine_ptr, 0);
|
||||
EXPECT_NE(routine_info.tile_ptr, 0);
|
||||
EXPECT_EQ(routine_info.tile_count, 8);
|
||||
EXPECT_TRUE(routine_info.is_repeatable);
|
||||
EXPECT_TRUE(routine_info.is_orientation_dependent);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, InvalidObjectId) {
|
||||
auto result = parser_->ParseObject(-1);
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_F(ObjectParserTest, NullRom) {
|
||||
zelda3::ObjectParser null_parser(nullptr);
|
||||
auto result = null_parser.ParseObject(0x01);
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
294
test/unit/zelda3/overworld_test.cc
Normal file
294
test/unit/zelda3/overworld_test.cc
Normal file
@@ -0,0 +1,294 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/overworld/overworld.h"
|
||||
#include "zelda3/overworld/overworld_map.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
class OverworldTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Skip tests on Linux for automated github builds
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#endif
|
||||
// Create a mock ROM for testing
|
||||
rom_ = std::make_unique<Rom>();
|
||||
// Initialize with minimal ROM data for testing
|
||||
std::vector<uint8_t> mock_rom_data(0x200000, 0x00); // 2MB ROM filled with 0x00
|
||||
|
||||
// Set up some basic ROM data that OverworldMap expects
|
||||
mock_rom_data[0x140145] = 0xFF; // OverworldCustomASMHasBeenApplied = vanilla
|
||||
|
||||
// Message IDs (2 bytes per map)
|
||||
for (int i = 0; i < 160; i++) { // 160 maps total
|
||||
mock_rom_data[0x3F51D + (i * 2)] = 0x00;
|
||||
mock_rom_data[0x3F51D + (i * 2) + 1] = 0x00;
|
||||
}
|
||||
|
||||
// Area graphics (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x7C9C + i] = 0x00;
|
||||
}
|
||||
|
||||
// Area palettes (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x7D1C + i] = 0x00;
|
||||
}
|
||||
|
||||
// Screen sizes (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x1788D + i] = 0x01; // Small area by default
|
||||
}
|
||||
|
||||
// Sprite sets (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x7A41 + i] = 0x00;
|
||||
}
|
||||
|
||||
// Sprite palettes (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x7B41 + i] = 0x00;
|
||||
}
|
||||
|
||||
// Music (1 byte per map)
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data[0x14303 + i] = 0x00;
|
||||
mock_rom_data[0x14303 + 0x40 + i] = 0x00;
|
||||
mock_rom_data[0x14303 + 0x80 + i] = 0x00;
|
||||
mock_rom_data[0x14303 + 0xC0 + i] = 0x00;
|
||||
}
|
||||
|
||||
// Dark World music
|
||||
for (int i = 0; i < 64; i++) {
|
||||
mock_rom_data[0x14403 + i] = 0x00;
|
||||
}
|
||||
|
||||
// Special world graphics and palettes
|
||||
for (int i = 0; i < 32; i++) {
|
||||
mock_rom_data[0x16821 + i] = 0x00;
|
||||
mock_rom_data[0x16831 + i] = 0x00;
|
||||
}
|
||||
|
||||
// Special world sprite graphics and palettes
|
||||
for (int i = 0; i < 32; i++) {
|
||||
mock_rom_data[0x0166E1 + i] = 0x00;
|
||||
mock_rom_data[0x016701 + i] = 0x00;
|
||||
}
|
||||
|
||||
rom_->LoadFromData(mock_rom_data);
|
||||
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
overworld_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<Overworld> overworld_;
|
||||
};
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapInitialization) {
|
||||
// Test that OverworldMap can be created with valid parameters
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
EXPECT_EQ(map.area_graphics(), 0);
|
||||
EXPECT_EQ(map.area_palette(), 0);
|
||||
EXPECT_EQ(map.message_id(), 0);
|
||||
EXPECT_EQ(map.area_size(), AreaSizeEnum::SmallArea);
|
||||
EXPECT_EQ(map.main_palette(), 0);
|
||||
EXPECT_EQ(map.area_specific_bg_color(), 0);
|
||||
EXPECT_EQ(map.subscreen_overlay(), 0);
|
||||
EXPECT_EQ(map.animated_gfx(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, AreaSizeEnumValues) {
|
||||
// Test that AreaSizeEnum has correct values
|
||||
EXPECT_EQ(static_cast<int>(AreaSizeEnum::SmallArea), 0);
|
||||
EXPECT_EQ(static_cast<int>(AreaSizeEnum::LargeArea), 1);
|
||||
EXPECT_EQ(static_cast<int>(AreaSizeEnum::WideArea), 2);
|
||||
EXPECT_EQ(static_cast<int>(AreaSizeEnum::TallArea), 3);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapSetters) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test main palette setter
|
||||
map.set_main_palette(5);
|
||||
EXPECT_EQ(map.main_palette(), 5);
|
||||
|
||||
// Test area-specific background color setter
|
||||
map.set_area_specific_bg_color(0x7FFF);
|
||||
EXPECT_EQ(map.area_specific_bg_color(), 0x7FFF);
|
||||
|
||||
// Test subscreen overlay setter
|
||||
map.set_subscreen_overlay(0x1234);
|
||||
EXPECT_EQ(map.subscreen_overlay(), 0x1234);
|
||||
|
||||
// Test animated GFX setter
|
||||
map.set_animated_gfx(10);
|
||||
EXPECT_EQ(map.animated_gfx(), 10);
|
||||
|
||||
// Test custom tileset setter
|
||||
map.set_custom_tileset(0, 20);
|
||||
EXPECT_EQ(map.custom_tileset(0), 20);
|
||||
|
||||
// Test area size setter
|
||||
map.SetAreaSize(AreaSizeEnum::LargeArea);
|
||||
EXPECT_EQ(map.area_size(), AreaSizeEnum::LargeArea);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapLargeMapSetup) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test SetAsLargeMap
|
||||
map.SetAsLargeMap(10, 2);
|
||||
EXPECT_EQ(map.parent(), 10);
|
||||
EXPECT_EQ(map.large_index(), 2);
|
||||
EXPECT_TRUE(map.is_large_map());
|
||||
EXPECT_EQ(map.area_size(), AreaSizeEnum::LargeArea);
|
||||
|
||||
// Test SetAsSmallMap
|
||||
map.SetAsSmallMap(5);
|
||||
EXPECT_EQ(map.parent(), 5);
|
||||
EXPECT_EQ(map.large_index(), 0);
|
||||
EXPECT_FALSE(map.is_large_map());
|
||||
EXPECT_EQ(map.area_size(), AreaSizeEnum::SmallArea);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapCustomTilesetArray) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test setting all 8 custom tileset slots
|
||||
for (int i = 0; i < 8; i++) {
|
||||
map.set_custom_tileset(i, i + 10);
|
||||
EXPECT_EQ(map.custom_tileset(i), i + 10);
|
||||
}
|
||||
|
||||
// Test mutable access
|
||||
for (int i = 0; i < 8; i++) {
|
||||
*map.mutable_custom_tileset(i) = i + 20;
|
||||
EXPECT_EQ(map.custom_tileset(i), i + 20);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapSpriteProperties) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test sprite graphics setters
|
||||
map.set_sprite_graphics(0, 1);
|
||||
map.set_sprite_graphics(1, 2);
|
||||
map.set_sprite_graphics(2, 3);
|
||||
|
||||
EXPECT_EQ(map.sprite_graphics(0), 1);
|
||||
EXPECT_EQ(map.sprite_graphics(1), 2);
|
||||
EXPECT_EQ(map.sprite_graphics(2), 3);
|
||||
|
||||
// Test sprite palette setters
|
||||
map.set_sprite_palette(0, 4);
|
||||
map.set_sprite_palette(1, 5);
|
||||
map.set_sprite_palette(2, 6);
|
||||
|
||||
EXPECT_EQ(map.sprite_palette(0), 4);
|
||||
EXPECT_EQ(map.sprite_palette(1), 5);
|
||||
EXPECT_EQ(map.sprite_palette(2), 6);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapBasicProperties) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test basic property setters
|
||||
map.set_area_graphics(15);
|
||||
EXPECT_EQ(map.area_graphics(), 15);
|
||||
|
||||
map.set_area_palette(8);
|
||||
EXPECT_EQ(map.area_palette(), 8);
|
||||
|
||||
map.set_message_id(0x1234);
|
||||
EXPECT_EQ(map.message_id(), 0x1234);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapMutableAccessors) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Test mutable accessors
|
||||
*map.mutable_area_graphics() = 25;
|
||||
EXPECT_EQ(map.area_graphics(), 25);
|
||||
|
||||
*map.mutable_area_palette() = 12;
|
||||
EXPECT_EQ(map.area_palette(), 12);
|
||||
|
||||
*map.mutable_message_id() = 0x5678;
|
||||
EXPECT_EQ(map.message_id(), 0x5678);
|
||||
|
||||
*map.mutable_main_palette() = 7;
|
||||
EXPECT_EQ(map.main_palette(), 7);
|
||||
|
||||
*map.mutable_animated_gfx() = 15;
|
||||
EXPECT_EQ(map.animated_gfx(), 15);
|
||||
|
||||
*map.mutable_subscreen_overlay() = 0x9ABC;
|
||||
EXPECT_EQ(map.subscreen_overlay(), 0x9ABC);
|
||||
}
|
||||
|
||||
TEST_F(OverworldTest, OverworldMapDestroy) {
|
||||
OverworldMap map(0, rom_.get());
|
||||
|
||||
// Set some properties
|
||||
map.set_area_graphics(10);
|
||||
map.set_main_palette(5);
|
||||
map.SetAreaSize(AreaSizeEnum::LargeArea);
|
||||
|
||||
// Destroy and verify reset
|
||||
map.Destroy();
|
||||
|
||||
EXPECT_EQ(map.area_graphics(), 0);
|
||||
EXPECT_EQ(map.main_palette(), 0);
|
||||
EXPECT_EQ(map.area_size(), AreaSizeEnum::SmallArea);
|
||||
EXPECT_FALSE(map.is_initialized());
|
||||
}
|
||||
|
||||
// Integration test for world-based sprite filtering
|
||||
TEST_F(OverworldTest, WorldBasedSpriteFiltering) {
|
||||
// This test verifies the logic used in DrawOverworldSprites
|
||||
// for filtering sprites by world
|
||||
|
||||
int current_world = 1; // Dark World
|
||||
int sprite_map_id = 0x50; // Map 0x50 (Dark World)
|
||||
|
||||
// Test that sprite should be shown for Dark World
|
||||
bool should_show = (sprite_map_id < 0x40 + (current_world * 0x40) &&
|
||||
sprite_map_id >= (current_world * 0x40));
|
||||
EXPECT_TRUE(should_show);
|
||||
|
||||
// Test that sprite should NOT be shown for Light World
|
||||
current_world = 0; // Light World
|
||||
should_show = (sprite_map_id < 0x40 + (current_world * 0x40) &&
|
||||
sprite_map_id >= (current_world * 0x40));
|
||||
EXPECT_FALSE(should_show);
|
||||
|
||||
// Test boundary conditions
|
||||
current_world = 1; // Dark World
|
||||
sprite_map_id = 0x40; // First Dark World map
|
||||
should_show = (sprite_map_id < 0x40 + (current_world * 0x40) &&
|
||||
sprite_map_id >= (current_world * 0x40));
|
||||
EXPECT_TRUE(should_show);
|
||||
|
||||
sprite_map_id = 0x7F; // Last Dark World map
|
||||
should_show = (sprite_map_id < 0x40 + (current_world * 0x40) &&
|
||||
sprite_map_id >= (current_world * 0x40));
|
||||
EXPECT_TRUE(should_show);
|
||||
|
||||
sprite_map_id = 0x80; // First Special World map
|
||||
should_show = (sprite_map_id < 0x40 + (current_world * 0x40) &&
|
||||
sprite_map_id >= (current_world * 0x40));
|
||||
EXPECT_FALSE(should_show);
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
64
test/unit/zelda3/sprite_builder_test.cc
Normal file
64
test/unit/zelda3/sprite_builder_test.cc
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "zelda3/sprite/sprite_builder.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using namespace yaze::zelda3;
|
||||
|
||||
class SpriteBuilderTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a new sprite
|
||||
SpriteBuilder sprite = SpriteBuilder::Create("Puffstool")
|
||||
.SetProperty("NbrTiles", 2)
|
||||
.SetProperty("Health", 10)
|
||||
.SetProperty("Harmless", false);
|
||||
// Create an anonymous global action for the sprite to run before each
|
||||
// action
|
||||
SpriteAction globalAction = SpriteAction::Create().AddInstruction(
|
||||
SpriteInstruction::BehaveAsBarrier());
|
||||
// Create an action for the SprAction::LocalJumpTable
|
||||
SpriteAction walkAction =
|
||||
SpriteAction::Create("Walk")
|
||||
.AddInstruction(SpriteInstruction::PlayAnimation(0, 6, 10))
|
||||
.AddInstruction(SpriteInstruction::ApplySpeedTowardsPlayer(2))
|
||||
.AddInstruction(SpriteInstruction::MoveXyz())
|
||||
.AddInstruction(SpriteInstruction::BounceFromTileCollision())
|
||||
.AddCustomInstruction("JSL $0DBB7C"); // Custom ASM
|
||||
// Link to the idle action. If the action does not exist, build will fail
|
||||
walkAction.SetNextAction("IdleAction");
|
||||
|
||||
// Idle action which jumps to a fn. If the fn does not exist, build will
|
||||
// fail
|
||||
SpriteAction idleAction =
|
||||
SpriteAction::Create("IdleAction")
|
||||
.AddInstruction(SpriteInstruction::JumpToFunction("IdleFn"));
|
||||
idleAction.SetNextAction("Walk");
|
||||
|
||||
// Build the function that the idle action jumps to
|
||||
SpriteAction idleFunction = SpriteAction::Create("IdleFn").AddInstruction(
|
||||
SpriteInstruction::MoveXyz());
|
||||
|
||||
// Add actions and functions to sprite
|
||||
sprite.SetGlobalAction(globalAction);
|
||||
sprite.AddAction(idleAction); // 0x00
|
||||
sprite.AddAction(walkAction); // 0x01
|
||||
sprite.AddFunction(idleFunction); // Local
|
||||
}
|
||||
void TearDown() override {}
|
||||
|
||||
SpriteBuilder sprite;
|
||||
};
|
||||
|
||||
TEST_F(SpriteBuilderTest, BuildSpritePropertiesOk) {
|
||||
EXPECT_THAT(sprite.BuildProperties(), testing::HasSubstr(R"(!SPRID = $00
|
||||
!NbrTiles = $00
|
||||
!Harmless = $00
|
||||
)"));
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
46
test/unit/zelda3/test_dungeon_objects.h
Normal file
46
test/unit/zelda3/test_dungeon_objects.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef YAZE_TEST_TEST_DUNGEON_OBJECTS_H
|
||||
#define YAZE_TEST_TEST_DUNGEON_OBJECTS_H
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "mocks/mock_rom.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Simplified test framework for dungeon object rendering
|
||||
*
|
||||
* This provides a clean, focused testing environment for dungeon object
|
||||
* functionality without the complexity of full integration tests.
|
||||
*/
|
||||
class TestDungeonObjects : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override;
|
||||
void TearDown() override;
|
||||
|
||||
// Test helpers
|
||||
absl::Status CreateTestRom();
|
||||
absl::Status SetupObjectData();
|
||||
|
||||
// Mock data generators
|
||||
std::vector<uint8_t> CreateObjectSubtypeTable(int base_addr, int count);
|
||||
std::vector<uint8_t> CreateTileData(int base_addr, int tile_count);
|
||||
std::vector<uint8_t> CreateRoomHeader(int room_id);
|
||||
|
||||
std::unique_ptr<MockRom> test_rom_;
|
||||
|
||||
// Test constants
|
||||
static constexpr int kTestObjectId = 0x01;
|
||||
static constexpr int kTestRoomId = 0x00;
|
||||
static constexpr size_t kTestRomSize = 0x100000; // 1MB test ROM
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_TEST_TEST_DUNGEON_OBJECTS_H
|
||||
Reference in New Issue
Block a user