backend-infra-engineer: Release v0.3.2 snapshot

This commit is contained in:
scawful
2025-10-17 12:10:25 -04:00
parent 4371618a9b
commit 3d71417f62
857 changed files with 174954 additions and 45626 deletions

View File

@@ -0,0 +1,334 @@
// Integration tests for AIGUIController
// Tests the gRPC GUI automation with vision feedback
#include "cli/service/ai/ai_gui_controller.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "cli/service/ai/gemini_ai_service.h"
#include "cli/service/gui/gui_automation_client.h"
namespace yaze {
namespace cli {
namespace ai {
namespace {
using ::testing::_;
using ::testing::Return;
// Mock GuiAutomationClient for testing without actual GUI
class MockGuiAutomationClient : public GuiAutomationClient {
public:
MockGuiAutomationClient() : GuiAutomationClient("localhost:50052") {}
MOCK_METHOD(absl::Status, Connect, ());
MOCK_METHOD(absl::StatusOr<AutomationResult>, Ping, (const std::string&));
MOCK_METHOD(absl::StatusOr<AutomationResult>, Click,
(const std::string&, ClickType));
MOCK_METHOD(absl::StatusOr<AutomationResult>, Type,
(const std::string&, const std::string&, bool));
MOCK_METHOD(absl::StatusOr<AutomationResult>, Wait,
(const std::string&, int, int));
MOCK_METHOD(absl::StatusOr<AutomationResult>, Assert,
(const std::string&));
};
class AIGUIControllerTest : public ::testing::Test {
protected:
void SetUp() override {
// Create mock services
GeminiConfig config;
config.api_key = "test_key";
config.model = "gemini-2.5-flash";
gemini_service_ = std::make_unique<GeminiAIService>(config);
gui_client_ = std::make_unique<MockGuiAutomationClient>();
controller_ = std::make_unique<AIGUIController>(
gemini_service_.get(), gui_client_.get());
ControlLoopConfig loop_config;
loop_config.max_iterations = 5;
loop_config.enable_vision_verification = false; // Disable for unit tests
loop_config.enable_iterative_refinement = false;
controller_->Initialize(loop_config);
}
std::unique_ptr<GeminiAIService> gemini_service_;
std::unique_ptr<MockGuiAutomationClient> gui_client_;
std::unique_ptr<AIGUIController> controller_;
};
// ============================================================================
// Basic Action Execution Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteClickAction_Success) {
AIAction action(AIActionType::kClickButton);
action.parameters["target"] = "button:Test";
action.parameters["click_type"] = "left";
AutomationResult result;
result.success = true;
result.message = "Click successful";
EXPECT_CALL(*gui_client_, Click("button:Test", ClickType::kLeft))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
ASSERT_TRUE(status.ok()) << status.status().message();
EXPECT_TRUE(status->action_successful);
}
TEST_F(AIGUIControllerTest, ExecuteClickAction_Failure) {
AIAction action(AIActionType::kClickButton);
action.parameters["target"] = "button:NonExistent";
AutomationResult result;
result.success = false;
result.message = "Button not found";
EXPECT_CALL(*gui_client_, Click("button:NonExistent", ClickType::kLeft))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
EXPECT_FALSE(status.ok());
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("Click action failed"));
}
// ============================================================================
// Type Action Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteTypeAction_Success) {
AIAction action(AIActionType::kSelectTile); // Using SelectTile as a type action
action.parameters["target"] = "input:TileID";
action.parameters["text"] = "0x42";
action.parameters["clear_first"] = "true";
AutomationResult result;
result.success = true;
result.message = "Text entered";
EXPECT_CALL(*gui_client_, Type("input:TileID", "0x42", true))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
ASSERT_TRUE(status.ok());
EXPECT_TRUE(status->action_successful);
}
// ============================================================================
// Wait Action Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteWaitAction_Success) {
AIAction action(AIActionType::kWait);
action.parameters["condition"] = "window:OverworldEditor";
action.parameters["timeout_ms"] = "2000";
AutomationResult result;
result.success = true;
result.message = "Condition met";
EXPECT_CALL(*gui_client_, Wait("window:OverworldEditor", 2000, 100))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
ASSERT_TRUE(status.ok());
EXPECT_TRUE(status->action_successful);
}
TEST_F(AIGUIControllerTest, ExecuteWaitAction_Timeout) {
AIAction action(AIActionType::kWait);
action.parameters["condition"] = "window:NonExistentWindow";
action.parameters["timeout_ms"] = "100";
AutomationResult result;
result.success = false;
result.message = "Timeout waiting for condition";
EXPECT_CALL(*gui_client_, Wait("window:NonExistentWindow", 100, 100))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
EXPECT_FALSE(status.ok());
}
// ============================================================================
// Verify/Assert Action Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteVerifyAction_Success) {
AIAction action(AIActionType::kVerifyTile);
action.parameters["condition"] = "tile_placed";
AutomationResult result;
result.success = true;
result.message = "Assertion passed";
result.expected_value = "0x42";
result.actual_value = "0x42";
EXPECT_CALL(*gui_client_, Assert("tile_placed"))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
ASSERT_TRUE(status.ok());
EXPECT_TRUE(status->action_successful);
}
TEST_F(AIGUIControllerTest, ExecuteVerifyAction_Failure) {
AIAction action(AIActionType::kVerifyTile);
action.parameters["condition"] = "tile_placed";
AutomationResult result;
result.success = false;
result.message = "Assertion failed";
result.expected_value = "0x42";
result.actual_value = "0x00";
EXPECT_CALL(*gui_client_, Assert("tile_placed"))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
EXPECT_FALSE(status.ok());
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("Assert action failed"));
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("expected: 0x42"));
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("actual: 0x00"));
}
// ============================================================================
// Complex Tile Placement Action Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecutePlaceTileAction_CompleteFlow) {
AIAction action(AIActionType::kPlaceTile);
action.parameters["map_id"] = "5";
action.parameters["x"] = "10";
action.parameters["y"] = "20";
action.parameters["tile"] = "0x42";
AutomationResult result;
result.success = true;
// Expect sequence: open menu, wait for window, set map ID, click position
testing::InSequence seq;
EXPECT_CALL(*gui_client_, Click("menu:Overworld", ClickType::kLeft))
.WillOnce(Return(result));
EXPECT_CALL(*gui_client_, Wait("window:Overworld Editor", 2000, 100))
.WillOnce(Return(result));
EXPECT_CALL(*gui_client_, Type("input:Map ID", "5", true))
.WillOnce(Return(result));
EXPECT_CALL(*gui_client_, Click(::testing::_, ClickType::kLeft))
.WillOnce(Return(result));
auto status = controller_->ExecuteSingleAction(action, false);
ASSERT_TRUE(status.ok()) << status.status().message();
EXPECT_TRUE(status->action_successful);
}
// ============================================================================
// Multiple Actions Execution Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteActions_MultipleActionsSuccess) {
std::vector<AIAction> actions;
AIAction action1(AIActionType::kClickButton);
action1.parameters["target"] = "button:Overworld";
actions.push_back(action1);
AIAction action2(AIActionType::kWait);
action2.parameters["condition"] = "window:OverworldEditor";
actions.push_back(action2);
AutomationResult success_result;
success_result.success = true;
EXPECT_CALL(*gui_client_, Click("button:Overworld", ClickType::kLeft))
.WillOnce(Return(success_result));
EXPECT_CALL(*gui_client_, Wait("window:OverworldEditor", 5000, 100))
.WillOnce(Return(success_result));
auto result = controller_->ExecuteActions(actions);
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_TRUE(result->success);
EXPECT_EQ(result->actions_executed.size(), 2);
}
TEST_F(AIGUIControllerTest, ExecuteActions_StopsOnFirstFailure) {
std::vector<AIAction> actions;
AIAction action1(AIActionType::kClickButton);
action1.parameters["target"] = "button:Test";
actions.push_back(action1);
AIAction action2(AIActionType::kClickButton);
action2.parameters["target"] = "button:NeverReached";
actions.push_back(action2);
AutomationResult failure_result;
failure_result.success = false;
failure_result.message = "First action failed";
EXPECT_CALL(*gui_client_, Click("button:Test", ClickType::kLeft))
.WillOnce(Return(failure_result));
// Second action should never be called
EXPECT_CALL(*gui_client_, Click("button:NeverReached", _))
.Times(0);
auto result = controller_->ExecuteActions(actions);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result->actions_executed.size(), 1);
}
// ============================================================================
// Error Handling Tests
// ============================================================================
TEST_F(AIGUIControllerTest, ExecuteAction_InvalidActionType) {
AIAction action(AIActionType::kInvalidAction);
auto status = controller_->ExecuteSingleAction(action, false);
EXPECT_FALSE(status.ok());
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("Action type not implemented"));
}
TEST_F(AIGUIControllerTest, ExecutePlaceTileAction_MissingParameters) {
AIAction action(AIActionType::kPlaceTile);
// Missing required parameters
auto status = controller_->ExecuteSingleAction(action, false);
EXPECT_FALSE(status.ok());
EXPECT_THAT(status.status().message(),
::testing::HasSubstr("requires map_id, x, y, and tile"));
}
} // namespace
} // namespace ai
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,211 @@
#include "gtest/gtest.h"
#include "absl/strings/str_format.h"
#include "cli/service/ai/ai_action_parser.h"
#include "cli/service/ai/vision_action_refiner.h"
#include "cli/service/ai/ai_gui_controller.h"
#ifdef YAZE_WITH_GRPC
#include "cli/service/gui/gui_automation_client.h"
#include "cli/service/ai/gemini_ai_service.h"
#endif
namespace yaze {
namespace test {
/**
* @brief Integration tests for AI-controlled tile placement
*
* These tests verify the complete pipeline:
* 1. Parse natural language commands
* 2. Execute actions via gRPC
* 3. Verify success with vision analysis
* 4. Refine and retry on failure
*/
class AITilePlacementTest : public ::testing::Test {
protected:
void SetUp() override {
// These tests require YAZE GUI to be running with gRPC test harness
// Skip if not available
}
};
TEST_F(AITilePlacementTest, ParsePlaceTileCommand) {
using namespace cli::ai;
// Test basic tile placement command
auto result = AIActionParser::ParseCommand(
"Place tile 0x42 at overworld position (5, 7)");
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_EQ(result->size(), 3); // Select, Place, Save
// Check first action (Select)
EXPECT_EQ(result->at(0).type, AIActionType::kSelectTile);
EXPECT_EQ(result->at(0).parameters.at("tile_id"), "66"); // 0x42 = 66
// Check second action (Place)
EXPECT_EQ(result->at(1).type, AIActionType::kPlaceTile);
EXPECT_EQ(result->at(1).parameters.at("x"), "5");
EXPECT_EQ(result->at(1).parameters.at("y"), "7");
EXPECT_EQ(result->at(1).parameters.at("map_id"), "0");
// Check third action (Save)
EXPECT_EQ(result->at(2).type, AIActionType::kSaveTile);
}
TEST_F(AITilePlacementTest, ParseSelectTileCommand) {
using namespace cli::ai;
auto result = AIActionParser::ParseCommand("Select tile 100");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->size(), 1);
EXPECT_EQ(result->at(0).type, AIActionType::kSelectTile);
EXPECT_EQ(result->at(0).parameters.at("tile_id"), "100");
}
TEST_F(AITilePlacementTest, ParseOpenEditorCommand) {
using namespace cli::ai;
auto result = AIActionParser::ParseCommand("Open the overworld editor");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->size(), 1);
EXPECT_EQ(result->at(0).type, AIActionType::kOpenEditor);
EXPECT_EQ(result->at(0).parameters.at("editor"), "overworld");
}
TEST_F(AITilePlacementTest, ActionToStringRoundtrip) {
using namespace cli::ai;
AIAction action(AIActionType::kPlaceTile, {
{"x", "5"},
{"y", "7"},
{"tile_id", "42"}
});
std::string str = AIActionParser::ActionToString(action);
EXPECT_FALSE(str.empty());
EXPECT_TRUE(str.find("5") != std::string::npos);
EXPECT_TRUE(str.find("7") != std::string::npos);
}
#ifdef YAZE_WITH_GRPC
TEST_F(AITilePlacementTest, DISABLED_VisionAnalysisBasic) {
// This test requires Gemini API key
const char* api_key = std::getenv("GEMINI_API_KEY");
if (!api_key || std::string(api_key).empty()) {
GTEST_SKIP() << "GEMINI_API_KEY not set";
}
cli::GeminiConfig config;
config.api_key = api_key;
config.model = "gemini-2.5-flash";
cli::GeminiAIService gemini_service(config);
cli::ai::VisionActionRefiner refiner(&gemini_service);
// Would need actual screenshots for real test
// This is a structure test
EXPECT_TRUE(true);
}
TEST_F(AITilePlacementTest, DISABLED_FullAIControlLoop) {
// This test requires:
// 1. YAZE GUI running with gRPC test harness
// 2. Gemini API key for vision
// 3. Test ROM loaded
const char* api_key = std::getenv("GEMINI_API_KEY");
if (!api_key || std::string(api_key).empty()) {
GTEST_SKIP() << "GEMINI_API_KEY not set";
}
// Initialize services
cli::GeminiConfig gemini_config;
gemini_config.api_key = api_key;
cli::GeminiAIService gemini_service(gemini_config);
cli::GuiAutomationClient gui_client("localhost:50051");
auto connect_status = gui_client.Connect();
if (!connect_status.ok()) {
GTEST_SKIP() << "GUI test harness not available: "
<< connect_status.message();
}
// Create AI controller
cli::ai::AIGUIController controller(&gemini_service, &gui_client);
cli::ai::ControlLoopConfig config;
config.max_iterations = 5;
config.enable_vision_verification = true;
controller.Initialize(config);
// Execute command
auto result = controller.ExecuteCommand(
"Place tile 0x42 at overworld position (5, 7)");
if (result.ok()) {
EXPECT_TRUE(result->success);
EXPECT_GT(result->iterations_performed, 0);
}
}
#endif // YAZE_WITH_GRPC
TEST_F(AITilePlacementTest, ActionRefinement) {
using namespace cli::ai;
// Test refinement logic with a failed action
VisionAnalysisResult analysis;
analysis.action_successful = false;
analysis.error_message = "Element not found";
AIAction original_action(AIActionType::kClickButton, {
{"button", "save"}
});
// Would need VisionActionRefiner for real test
// This verifies the structure compiles
EXPECT_TRUE(true);
}
TEST_F(AITilePlacementTest, MultipleCommandsParsing) {
using namespace cli::ai;
// Test that we can parse multiple commands in sequence
std::vector<std::string> commands = {
"Open overworld editor",
"Select tile 0x42",
"Place tile at position (5, 7)",
"Save changes"
};
for (const auto& cmd : commands) {
auto result = AIActionParser::ParseCommand(cmd);
// At least some should parse successfully
if (result.ok()) {
EXPECT_FALSE(result->empty());
}
}
}
TEST_F(AITilePlacementTest, HexAndDecimalParsing) {
using namespace cli::ai;
// Test hex notation
auto hex_result = AIActionParser::ParseCommand("Select tile 0xFF");
if (hex_result.ok() && !hex_result->empty()) {
EXPECT_EQ(hex_result->at(0).parameters.at("tile_id"), "255");
}
// Test decimal notation
auto dec_result = AIActionParser::ParseCommand("Select tile 255");
if (dec_result.ok() && !dec_result->empty()) {
EXPECT_EQ(dec_result->at(0).parameters.at("tile_id"), "255");
}
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,243 @@
#include <filesystem>
#include <fstream>
#include "gtest/gtest.h"
#include "absl/strings/str_cat.h"
#include "cli/service/ai/gemini_ai_service.h"
#ifdef YAZE_WITH_GRPC
#include "app/service/screenshot_utils.h"
#endif
namespace yaze {
namespace test {
class GeminiVisionTest : public ::testing::Test {
protected:
void SetUp() override {
// Check if GEMINI_API_KEY is set
const char* api_key = std::getenv("GEMINI_API_KEY");
if (!api_key || std::string(api_key).empty()) {
GTEST_SKIP() << "GEMINI_API_KEY not set. Skipping multimodal tests.";
}
api_key_ = api_key;
// Create test data directory
test_dir_ = std::filesystem::temp_directory_path() / "yaze_multimodal_test";
std::filesystem::create_directories(test_dir_);
}
void TearDown() override {
// Clean up test directory
if (std::filesystem::exists(test_dir_)) {
std::filesystem::remove_all(test_dir_);
}
}
// Helper: Create a simple test image (16x16 PNG)
std::filesystem::path CreateTestImage() {
auto image_path = test_dir_ / "test_image.png";
// Create a minimal PNG file (16x16 red square)
// PNG signature + IHDR + IDAT + IEND
const unsigned char png_data[] = {
// PNG signature
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
// IHDR chunk
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
0x36,
// IDAT chunk (minimal data)
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54,
0x08, 0x99, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00,
0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB4,
// IEND chunk
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
0xAE, 0x42, 0x60, 0x82
};
std::ofstream file(image_path, std::ios::binary);
file.write(reinterpret_cast<const char*>(png_data), sizeof(png_data));
file.close();
return image_path;
}
std::string api_key_;
std::filesystem::path test_dir_;
};
TEST_F(GeminiVisionTest, BasicImageAnalysis) {
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash"; // Vision-capable model
config.verbose = false;
cli::GeminiAIService service(config);
// Create test image
auto image_path = CreateTestImage();
ASSERT_TRUE(std::filesystem::exists(image_path));
// Send multimodal request
auto response = service.GenerateMultimodalResponse(
image_path.string(),
"Describe this image in one sentence."
);
ASSERT_TRUE(response.ok()) << response.status().message();
EXPECT_FALSE(response->text_response.empty());
std::cout << "Vision API response: " << response->text_response << std::endl;
}
TEST_F(GeminiVisionTest, ImageWithSpecificPrompt) {
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash";
config.verbose = false;
cli::GeminiAIService service(config);
auto image_path = CreateTestImage();
// Ask specific question about the image
auto response = service.GenerateMultimodalResponse(
image_path.string(),
"What color is the dominant color in this image? Answer with just the color name."
);
ASSERT_TRUE(response.ok()) << response.status().message();
EXPECT_FALSE(response->text_response.empty());
// Response should mention "red" since we created a red square
std::string response_lower = response->text_response;
std::transform(response_lower.begin(), response_lower.end(),
response_lower.begin(), ::tolower);
EXPECT_TRUE(response_lower.find("red") != std::string::npos ||
response_lower.find("pink") != std::string::npos)
<< "Expected color 'red' or 'pink' in response: " << response->text_response;
}
TEST_F(GeminiVisionTest, InvalidImagePath) {
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash";
cli::GeminiAIService service(config);
// Try with non-existent image
auto response = service.GenerateMultimodalResponse(
"/nonexistent/image.png",
"Describe this image."
);
EXPECT_FALSE(response.ok());
EXPECT_TRUE(absl::IsNotFound(response.status()) ||
absl::IsInternal(response.status()));
}
#ifdef YAZE_WITH_GRPC
// Integration test with screenshot capture
TEST_F(GeminiVisionTest, ScreenshotCaptureIntegration) {
// Note: This test requires a running YAZE instance with gRPC test harness
// Skip if we can't connect
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash";
config.verbose = false;
cli::GeminiAIService service(config);
// Attempt to capture a screenshot
auto screenshot_result = yaze::test::CaptureHarnessScreenshot(
(test_dir_ / "screenshot.png").string());
if (!screenshot_result.ok()) {
GTEST_SKIP() << "Screenshot capture failed (YAZE may not be running): "
<< screenshot_result.status().message();
}
// Analyze the captured screenshot
auto response = service.GenerateMultimodalResponse(
screenshot_result->file_path,
"What UI elements are visible in this screenshot? List them."
);
ASSERT_TRUE(response.ok()) << response.status().message();
EXPECT_FALSE(response->text_response.empty());
std::cout << "Screenshot analysis: " << response->text_response << std::endl;
}
#endif
// Performance test
TEST_F(GeminiVisionTest, MultipleRequestsSequential) {
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash";
config.verbose = false;
cli::GeminiAIService service(config);
auto image_path = CreateTestImage();
// Make 3 sequential requests
const int num_requests = 3;
for (int i = 0; i < num_requests; ++i) {
auto response = service.GenerateMultimodalResponse(
image_path.string(),
absl::StrCat("Request ", i + 1, ": Describe this image briefly.")
);
ASSERT_TRUE(response.ok()) << "Request " << i + 1 << " failed: "
<< response.status().message();
EXPECT_FALSE(response->text_response.empty());
}
}
// Rate limiting test (should handle gracefully)
TEST_F(GeminiVisionTest, RateLimitHandling) {
cli::GeminiConfig config;
config.api_key = api_key_;
config.model = "gemini-2.5-flash";
config.verbose = false;
cli::GeminiAIService service(config);
auto image_path = CreateTestImage();
// Make many rapid requests (may hit rate limit)
int successful = 0;
int rate_limited = 0;
for (int i = 0; i < 10; ++i) {
auto response = service.GenerateMultimodalResponse(
image_path.string(),
"Describe this image."
);
if (response.ok()) {
successful++;
} else if (absl::IsResourceExhausted(response.status()) ||
response.status().message().find("429") != std::string::npos) {
rate_limited++;
}
}
// At least some requests should succeed
EXPECT_GT(successful, 0) << "No successful requests out of 10";
// If we hit rate limits, that's expected behavior (not a failure)
if (rate_limited > 0) {
std::cout << "Note: Hit rate limit on " << rate_limited << " out of 10 requests (expected)" << std::endl;
}
}
} // namespace test
} // namespace yaze
// Note: main() is provided by yaze_test.cc for the unified test runner

View File

@@ -2,7 +2,7 @@
#include <filesystem>
#include <fstream>
#include "app/core/asar_wrapper.h"
#include "core/asar_wrapper.h"
#include "app/rom.h"
#include "absl/status/status.h"
#include "testing.h"
@@ -17,7 +17,7 @@ namespace integration {
class AsarIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
wrapper_ = std::make_unique<app::core::AsarWrapper>();
wrapper_ = std::make_unique<core::AsarWrapper>();
// Create test directory
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_integration";
@@ -322,7 +322,7 @@ error_test:
err_file.close();
}
std::unique_ptr<app::core::AsarWrapper> wrapper_;
std::unique_ptr<core::AsarWrapper> wrapper_;
std::filesystem::path test_dir_;
std::filesystem::path comprehensive_asm_path_;
std::filesystem::path advanced_asm_path_;

View File

@@ -1,8 +1,13 @@
// Must define before any ImGui includes
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include "app/core/asar_wrapper.h"
#include "core/asar_wrapper.h"
#include "app/rom.h"
#include "test_utils.h"
#include "testing.h"
@@ -19,14 +24,14 @@ class AsarRomIntegrationTest : public RomDependentTest {
protected:
void SetUp() override {
RomDependentTest::SetUp();
wrapper_ = std::make_unique<app::core::AsarWrapper>();
wrapper_ = std::make_unique<core::AsarWrapper>();
ASSERT_OK(wrapper_->Initialize());
// Create test directory
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_rom_test";
std::filesystem::create_directories(test_dir_);
CreateTestPatches();
}
@@ -226,7 +231,7 @@ enemy_shell:
symbols_file.close();
}
std::unique_ptr<app::core::AsarWrapper> wrapper_;
std::unique_ptr<core::AsarWrapper> wrapper_;
std::filesystem::path test_dir_;
std::filesystem::path simple_patch_path_;
std::filesystem::path gameplay_patch_path_;
@@ -239,15 +244,16 @@ TEST_F(AsarRomIntegrationTest, SimplePatchOnRealRom) {
size_t original_size = rom_copy.size();
// Apply simple patch
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
auto patch_result =
wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
ASSERT_OK(patch_result.status());
const auto& result = patch_result.value();
EXPECT_TRUE(result.success) << "Patch failed: "
<< testing::PrintToString(result.errors);
EXPECT_TRUE(result.success)
<< "Patch failed: " << testing::PrintToString(result.errors);
// Verify ROM was modified
EXPECT_NE(rom_copy, test_rom_); // Should be different
EXPECT_NE(rom_copy, test_rom_); // Should be different
EXPECT_GE(rom_copy.size(), original_size); // Size may have grown
// Check for expected symbols
@@ -277,17 +283,16 @@ TEST_F(AsarRomIntegrationTest, SymbolExtractionFromRealRom) {
// Check for specific symbols we expect
std::vector<std::string> expected_symbols = {
"main_routine", "init_player", "game_loop", "update_player",
"update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"
};
"main_routine", "init_player", "game_loop", "update_player",
"update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"};
for (const auto& expected_symbol : expected_symbols) {
bool found = false;
for (const auto& symbol : symbols) {
if (symbol.name == expected_symbol) {
found = true;
EXPECT_GT(symbol.address, 0) << "Symbol " << expected_symbol
<< " has invalid address";
EXPECT_GT(symbol.address, 0)
<< "Symbol " << expected_symbol << " has invalid address";
break;
}
}
@@ -311,19 +316,20 @@ TEST_F(AsarRomIntegrationTest, GameplayModificationPatch) {
std::vector<uint8_t> rom_copy = test_rom_;
// Apply gameplay modification patch
auto patch_result = wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy);
auto patch_result =
wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy);
ASSERT_OK(patch_result.status());
const auto& result = patch_result.value();
EXPECT_TRUE(result.success) << "Gameplay patch failed: "
<< testing::PrintToString(result.errors);
EXPECT_TRUE(result.success)
<< "Gameplay patch failed: " << testing::PrintToString(result.errors);
// Verify specific memory locations were modified
// Note: These addresses are based on the patch content
// Check health modification at 0x7EF36C -> ROM offset would need calculation
// For a proper test, we'd need to convert SNES addresses to ROM offsets
// Check if custom routine was inserted at 0xC000 -> ROM offset 0x18000 (in LoROM)
const uint32_t rom_offset = 0x18000; // Bank $00:C000 in LoROM
if (rom_offset < rom_copy.size()) {
@@ -369,40 +375,43 @@ broken_routine:
broken_file.close();
std::vector<uint8_t> rom_copy = test_rom_;
auto patch_result = wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy);
auto patch_result =
wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy);
// Should fail with proper error messages
EXPECT_FALSE(patch_result.ok());
EXPECT_THAT(patch_result.status().message(),
testing::AnyOf(
testing::HasSubstr("invalid"),
testing::HasSubstr("unknown"),
testing::HasSubstr("error")));
EXPECT_THAT(patch_result.status().message(),
testing::AnyOf(testing::HasSubstr("invalid"),
testing::HasSubstr("unknown"),
testing::HasSubstr("error")));
}
TEST_F(AsarRomIntegrationTest, PatchValidationWorkflow) {
// Test the complete workflow: validate -> patch -> verify
// Step 1: Validate assembly
auto validation_result = wrapper_->ValidateAssembly(simple_patch_path_.string());
auto validation_result =
wrapper_->ValidateAssembly(simple_patch_path_.string());
EXPECT_OK(validation_result);
// Step 2: Apply patch
std::vector<uint8_t> rom_copy = test_rom_;
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
auto patch_result =
wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
ASSERT_OK(patch_result.status());
EXPECT_TRUE(patch_result->success);
// Step 3: Verify results
EXPECT_GT(patch_result->symbols.size(), 0);
EXPECT_GT(patch_result->rom_size, 0);
// Step 4: Test symbol operations
auto entry_symbol = wrapper_->FindSymbol("yaze_test_entry");
EXPECT_TRUE(entry_symbol.has_value());
if (entry_symbol) {
auto symbols_at_address = wrapper_->GetSymbolsAtAddress(entry_symbol->address);
auto symbols_at_address =
wrapper_->GetSymbolsAtAddress(entry_symbol->address);
EXPECT_GT(symbols_at_address.size(), 0);
}
}

View File

@@ -1,233 +1,231 @@
#include "integration/dungeon_editor_test.h"
#include <cstring>
#include <vector>
#include "absl/strings/str_format.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
namespace yaze {
namespace test {
void DungeonEditorIntegrationTest::SetUp() {
ASSERT_TRUE(CreateMockRom().ok());
ASSERT_TRUE(LoadTestRoomData().ok());
dungeon_editor_ = std::make_unique<editor::DungeonEditor>(mock_rom_.get());
dungeon_editor_->Initialize();
}
using namespace yaze::zelda3;
void DungeonEditorIntegrationTest::TearDown() {
dungeon_editor_.reset();
mock_rom_.reset();
}
// ============================================================================
// Basic Room Loading Tests
// ============================================================================
absl::Status DungeonEditorIntegrationTest::CreateMockRom() {
mock_rom_ = std::make_unique<MockRom>();
// Generate mock ROM data
std::vector<uint8_t> mock_data(kMockRomSize, 0x00);
// Set up basic ROM structure
// Header at 0x7FC0
std::string title = "ZELDA3 TEST ROM";
std::memcpy(&mock_data[0x7FC0], title.c_str(), std::min(title.length(), size_t(21)));
// Set ROM size and type
mock_data[0x7FD7] = 0x21; // 2MB ROM
mock_data[0x7FD8] = 0x00; // SRAM size
mock_data[0x7FD9] = 0x00; // Country code (NTSC)
mock_data[0x7FDA] = 0x00; // License code
mock_data[0x7FDB] = 0x00; // Version
// Set up room header pointers
mock_data[0xB5DD] = 0x00; // Room header pointer low
mock_data[0xB5DE] = 0x00; // Room header pointer mid
mock_data[0xB5DF] = 0x00; // Room header pointer high
// Set up object pointers
mock_data[0x874C] = 0x00; // Object pointer low
mock_data[0x874D] = 0x00; // Object pointer mid
mock_data[0x874E] = 0x00; // Object pointer high
static_cast<MockRom*>(mock_rom_.get())->SetMockData(mock_data);
return absl::OkStatus();
}
absl::Status DungeonEditorIntegrationTest::LoadTestRoomData() {
// Generate test room data
auto room_header = GenerateMockRoomHeader(kTestRoomId);
auto object_data = GenerateMockObjectData();
auto graphics_data = GenerateMockGraphicsData();
static_cast<MockRom*>(mock_rom_.get())->SetMockRoomData(kTestRoomId, room_header);
static_cast<MockRom*>(mock_rom_.get())->SetMockObjectData(kTestObjectId, object_data);
return absl::OkStatus();
}
absl::Status DungeonEditorIntegrationTest::TestObjectParsing() {
// Test object parsing without SNES emulation
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
// Verify room was loaded correctly
TEST_F(DungeonEditorIntegrationTest, LoadRoomFromRealRom) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
EXPECT_NE(room.rom(), nullptr);
// Note: room_id_ is private, so we can't directly access it in tests
// Test object loading
room.LoadObjects();
EXPECT_FALSE(room.GetTileObjects().empty());
// Verify object properties
for (const auto& obj : room.GetTileObjects()) {
// Note: id_ is private, so we can't directly access it in tests
EXPECT_LE(obj.x_, 31); // Room width limit
EXPECT_LE(obj.y_, 31); // Room height limit
// Note: rom() method is not const, so we can't call it on const objects
}
return absl::OkStatus();
}
absl::Status DungeonEditorIntegrationTest::TestObjectRendering() {
// Test object rendering without SNES emulation
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
TEST_F(DungeonEditorIntegrationTest, LoadMultipleRooms) {
// Test loading several different rooms
for (int room_id : {0x00, 0x01, 0x02, 0x10, 0x20}) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), room_id);
EXPECT_NE(room.rom(), nullptr) << "Failed to load room " << std::hex << room_id;
room.LoadObjects();
// Some rooms may be empty, but loading should not fail
}
}
TEST_F(DungeonEditorIntegrationTest, DungeonEditorInitialization) {
// Initialize the editor before loading
dungeon_editor_->Initialize();
// Now load should succeed
auto status = dungeon_editor_->Load();
ASSERT_TRUE(status.ok()) << "Load failed: " << status.message();
}
// ============================================================================
// Object Encoding/Decoding Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, ObjectEncodingRoundTrip) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
// Test tile loading for objects
for (auto& obj : room.GetTileObjects()) {
obj.EnsureTilesLoaded();
EXPECT_FALSE(obj.tiles_.empty());
auto encoded = room.EncodeObjects();
EXPECT_FALSE(encoded.empty());
EXPECT_EQ(encoded[encoded.size()-1], 0xFF); // Terminator
}
TEST_F(DungeonEditorIntegrationTest, EncodeType1Object) {
// Type 1: xxxxxxss yyyyyyss iiiiiiii (ID < 0x100)
zelda3::RoomObject obj(0x10, 5, 7, 0x12, 0); // id, x, y, size, layer
auto bytes = obj.EncodeObjectToBytes();
// Verify encoding format
EXPECT_EQ((bytes.b1 >> 2), 5) << "X coordinate should be in upper 6 bits of b1";
EXPECT_EQ((bytes.b2 >> 2), 7) << "Y coordinate should be in upper 6 bits of b2";
EXPECT_EQ(bytes.b3, 0x10) << "Object ID should be in b3";
}
TEST_F(DungeonEditorIntegrationTest, EncodeType2Object) {
// Type 2: 111111xx xxxxyyyy yyiiiiii (ID >= 0x100 && < 0x200)
zelda3::RoomObject obj(0x150, 12, 8, 0, 0);
auto bytes = obj.EncodeObjectToBytes();
// Verify Type 2 marker
EXPECT_EQ((bytes.b1 & 0xFC), 0xFC) << "Type 2 objects should have 111111 prefix";
}
TEST_F(DungeonEditorIntegrationTest, EncodeType3Object) {
// Type 3: xxxxxxii yyyyyyii 11111iii (ID >= 0xF00)
zelda3::RoomObject obj(0xF23, 3, 4, 0, 0);
auto bytes = obj.EncodeObjectToBytes();
// Verify Type 3 encoding: bytes.b3 = (id_ >> 4) & 0xFF
// For ID 0xF23: (0xF23 >> 4) = 0xF2
EXPECT_EQ(bytes.b3, 0xF2) << "Type 3: (ID >> 4) should be in b3";
}
// ============================================================================
// Object Manipulation Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, AddObjectToRoom) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
size_t initial_count = room.GetTileObjects().size();
// Add a new object (Type 1, so size must be <= 15)
zelda3::RoomObject new_obj(0x20, 10, 10, 5, 0);
new_obj.set_rom(rom_.get());
auto status = room.AddObject(new_obj);
EXPECT_TRUE(status.ok()) << "Failed to add object: " << status.message();
EXPECT_EQ(room.GetTileObjects().size(), initial_count + 1);
}
TEST_F(DungeonEditorIntegrationTest, RemoveObjectFromRoom) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
size_t initial_count = room.GetTileObjects().size();
ASSERT_GT(initial_count, 0) << "Room should have at least one object";
// Remove first object
auto status = room.RemoveObject(0);
EXPECT_TRUE(status.ok()) << "Failed to remove object: " << status.message();
EXPECT_EQ(room.GetTileObjects().size(), initial_count - 1);
}
TEST_F(DungeonEditorIntegrationTest, UpdateObjectInRoom) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
ASSERT_FALSE(room.GetTileObjects().empty());
// Update first object's position
zelda3::RoomObject updated_obj = room.GetTileObjects()[0];
updated_obj.x_ = 15;
updated_obj.y_ = 15;
auto status = room.UpdateObject(0, updated_obj);
EXPECT_TRUE(status.ok()) << "Failed to update object: " << status.message();
EXPECT_EQ(room.GetTileObjects()[0].x_, 15);
EXPECT_EQ(room.GetTileObjects()[0].y_, 15);
}
// ============================================================================
// Object Validation Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, ValidateObjectBounds) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
// Test objects within valid bounds (0-63 for x and y)
zelda3::RoomObject valid_obj(0x10, 0, 0, 0, 0);
EXPECT_TRUE(room.ValidateObject(valid_obj));
zelda3::RoomObject valid_obj2(0x10, 31, 31, 0, 0);
EXPECT_TRUE(room.ValidateObject(valid_obj2));
zelda3::RoomObject valid_obj3(0x10, 63, 63, 0, 0);
EXPECT_TRUE(room.ValidateObject(valid_obj3));
// Test objects outside bounds (> 63)
zelda3::RoomObject invalid_obj(0x10, 64, 64, 0, 0);
EXPECT_FALSE(room.ValidateObject(invalid_obj));
zelda3::RoomObject invalid_obj2(0x10, 100, 100, 0, 0);
EXPECT_FALSE(room.ValidateObject(invalid_obj2));
}
// ============================================================================
// Save/Load Round-Trip Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, SaveAndReloadRoom) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
size_t original_count = room.GetTileObjects().size();
// Encode objects
auto encoded = room.EncodeObjects();
EXPECT_FALSE(encoded.empty());
// Create a new room and decode
auto room2 = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room2.LoadObjects();
// Verify object count matches
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
}
// ============================================================================
// Object Rendering Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, RenderObjectWithTiles) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
room.LoadObjects();
ASSERT_FALSE(room.GetTileObjects().empty());
// Ensure tiles are loaded for first object
auto& obj = room.GetTileObjects()[0];
const_cast<zelda3::RoomObject&>(obj).set_rom(rom_.get());
const_cast<zelda3::RoomObject&>(obj).EnsureTilesLoaded();
EXPECT_FALSE(obj.tiles_.empty()) << "Object should have tiles after loading";
}
// ============================================================================
// Multi-Layer Tests
// ============================================================================
TEST_F(DungeonEditorIntegrationTest, ObjectsOnDifferentLayers) {
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
// Add objects on different layers
zelda3::RoomObject obj_bg1(0x10, 5, 5, 0, 0); // Layer 0 (BG2)
zelda3::RoomObject obj_bg2(0x11, 6, 6, 0, 1); // Layer 1 (BG1)
zelda3::RoomObject obj_bg3(0x12, 7, 7, 0, 2); // Layer 2 (BG3)
room.AddObject(obj_bg1);
room.AddObject(obj_bg2);
room.AddObject(obj_bg3);
// Encode and verify layer separation
auto encoded = room.EncodeObjects();
// Should have layer terminators (0xFF 0xFF between layers)
int terminator_count = 0;
for (size_t i = 0; i < encoded.size() - 1; i++) {
if (encoded[i] == 0xFF && encoded[i+1] == 0xFF) {
terminator_count++;
}
}
// Test room graphics rendering
room.LoadRoomGraphics();
room.RenderRoomGraphics();
return absl::OkStatus();
}
absl::Status DungeonEditorIntegrationTest::TestRoomGraphics() {
// Test room graphics loading and rendering
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
// Test graphics loading
room.LoadRoomGraphics();
EXPECT_FALSE(room.blocks().empty());
// Test graphics rendering
room.RenderRoomGraphics();
return absl::OkStatus();
}
absl::Status DungeonEditorIntegrationTest::TestPaletteHandling() {
// Test palette loading and application
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
// Verify palette is set
EXPECT_GE(room.palette, 0);
EXPECT_LE(room.palette, 0x47); // Max palette index
return absl::OkStatus();
}
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockRoomHeader(int room_id) {
std::vector<uint8_t> header(32, 0x00);
// Basic room properties
header[0] = 0x00; // Background type, collision, light
header[1] = 0x00; // Palette
header[2] = 0x01; // Blockset
header[3] = 0x01; // Spriteset
header[4] = 0x00; // Effect
header[5] = 0x00; // Tag1
header[6] = 0x00; // Tag2
header[7] = 0x00; // Staircase planes
header[8] = 0x00; // Staircase planes continued
header[9] = 0x00; // Hole warp
header[10] = 0x00; // Staircase rooms
header[11] = 0x00;
header[12] = 0x00;
header[13] = 0x00;
return header;
}
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockObjectData() {
std::vector<uint8_t> data;
// Add a simple wall object
data.push_back(0x08); // X position (2 tiles)
data.push_back(0x08); // Y position (2 tiles)
data.push_back(0x01); // Object ID (wall)
// Add layer separator
data.push_back(0xFF);
data.push_back(0xFF);
// Add door section
data.push_back(0xF0);
data.push_back(0xFF);
return data;
}
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockGraphicsData() {
std::vector<uint8_t> data(0x4000, 0x00);
// Generate basic tile data
for (size_t i = 0; i < data.size(); i += 2) {
data[i] = 0x00; // Tile low byte
data[i + 1] = 0x00; // Tile high byte
}
return data;
}
void MockRom::SetMockData(const std::vector<uint8_t>& data) {
mock_data_ = data;
}
void MockRom::SetMockRoomData(int room_id, const std::vector<uint8_t>& data) {
mock_room_data_[room_id] = data;
}
void MockRom::SetMockObjectData(int object_id, const std::vector<uint8_t>& data) {
mock_object_data_[object_id] = data;
}
bool MockRom::ValidateRoomData(int room_id) const {
return mock_room_data_.find(room_id) != mock_room_data_.end();
}
bool MockRom::ValidateObjectData(int object_id) const {
return mock_object_data_.find(object_id) != mock_object_data_.end();
}
// Test cases
TEST_F(DungeonEditorIntegrationTest, ObjectParsingTest) {
EXPECT_TRUE(TestObjectParsing().ok());
}
TEST_F(DungeonEditorIntegrationTest, ObjectRenderingTest) {
EXPECT_TRUE(TestObjectRendering().ok());
}
TEST_F(DungeonEditorIntegrationTest, RoomGraphicsTest) {
EXPECT_TRUE(TestRoomGraphics().ok());
}
TEST_F(DungeonEditorIntegrationTest, PaletteHandlingTest) {
EXPECT_TRUE(TestPaletteHandling().ok());
}
TEST_F(DungeonEditorIntegrationTest, MockRomValidation) {
EXPECT_TRUE(static_cast<MockRom*>(mock_rom_.get())->ValidateRoomData(kTestRoomId));
EXPECT_TRUE(static_cast<MockRom*>(mock_rom_.get())->ValidateObjectData(kTestObjectId));
EXPECT_GE(terminator_count, 2) << "Should have at least 2 layer terminators";
}
} // namespace test
} // namespace yaze
} // namespace yaze

View File

@@ -4,72 +4,56 @@
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "app/editor/dungeon/dungeon_editor.h"
#include "app/editor/dungeon/dungeon_editor_v2.h"
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
#include "gtest/gtest.h"
namespace yaze {
namespace test {
/**
* @brief Integration test framework for dungeon editor components
* @brief Integration test framework using real ROM data
*
* This class provides a comprehensive testing framework for the dungeon editor,
* allowing modular testing of individual components and their interactions.
* Updated for DungeonEditorV2 with card-based architecture
*/
class DungeonEditorIntegrationTest : public ::testing::Test {
protected:
void SetUp() override;
void TearDown() override;
void SetUp() override {
// Use the real ROM (try multiple locations)
rom_ = std::make_unique<Rom>();
auto status = rom_->LoadFromFile("assets/zelda3.sfc");
if (!status.ok()) {
status = rom_->LoadFromFile("build/bin/zelda3.sfc");
}
if (!status.ok()) {
status = rom_->LoadFromFile("zelda3.sfc");
}
ASSERT_TRUE(status.ok()) << "Could not load zelda3.sfc from any location";
ASSERT_TRUE(rom_->InitializeForTesting().ok());
// Initialize DungeonEditorV2 with ROM
dungeon_editor_ = std::make_unique<editor::DungeonEditorV2>();
dungeon_editor_->set_rom(rom_.get());
// Load editor data
auto load_status = dungeon_editor_->Load();
ASSERT_TRUE(load_status.ok()) << "Failed to load dungeon editor: "
<< load_status.message();
}
// Test data setup
absl::Status CreateMockRom();
absl::Status LoadTestRoomData();
// Component testing helpers
absl::Status TestObjectParsing();
absl::Status TestObjectRendering();
absl::Status TestRoomGraphics();
absl::Status TestPaletteHandling();
// Mock data generators
std::vector<uint8_t> GenerateMockRoomHeader(int room_id);
std::vector<uint8_t> GenerateMockObjectData();
std::vector<uint8_t> GenerateMockGraphicsData();
void TearDown() override {
dungeon_editor_.reset();
rom_.reset();
}
std::unique_ptr<Rom> mock_rom_;
std::unique_ptr<editor::DungeonEditor> dungeon_editor_;
std::unique_ptr<Rom> rom_;
std::unique_ptr<editor::DungeonEditorV2> dungeon_editor_;
// Test constants
static constexpr int kTestRoomId = 0x01;
static constexpr int kTestObjectId = 0x10;
static constexpr size_t kMockRomSize = 0x200000; // 2MB mock ROM
};
/**
* @brief Mock ROM class for testing without real ROM files
*/
class MockRom : public Rom {
public:
MockRom() = default;
// Test data injection
void SetMockData(const std::vector<uint8_t>& data);
void SetMockRoomData(int room_id, const std::vector<uint8_t>& data);
void SetMockObjectData(int object_id, const std::vector<uint8_t>& data);
// Validation helpers
bool ValidateRoomData(int room_id) const;
bool ValidateObjectData(int object_id) const;
private:
std::vector<uint8_t> mock_data_;
std::map<int, std::vector<uint8_t>> mock_room_data_;
std::map<int, std::vector<uint8_t>> mock_object_data_;
};
} // namespace test
} // namespace yaze
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_TEST_H
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_TEST_H

View File

@@ -0,0 +1,250 @@
#include "integration/dungeon_editor_v2_test.h"
namespace yaze {
namespace test {
// ============================================================================
// Basic Initialization Tests
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, EditorInitialization) {
// Initialize should not fail
dungeon_editor_v2_->Initialize();
EXPECT_TRUE(dungeon_editor_v2_->rom() != nullptr);
}
TEST_F(DungeonEditorV2IntegrationTest, RomLoadStatus) {
EXPECT_TRUE(dungeon_editor_v2_->IsRomLoaded());
std::string status = dungeon_editor_v2_->GetRomStatus();
EXPECT_FALSE(status.empty());
EXPECT_NE(status, "No ROM loaded");
}
// ============================================================================
// Load Tests - Component Delegation
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, LoadAllRooms) {
// Test that Load() properly delegates to room_loader_
dungeon_editor_v2_->Initialize();
auto status = dungeon_editor_v2_->Load();
ASSERT_TRUE(status.ok()) << "Load failed: " << status.message();
}
TEST_F(DungeonEditorV2IntegrationTest, LoadWithoutRom) {
// Test error handling when ROM is not available
editor::DungeonEditorV2 editor(nullptr);
auto status = editor.Load();
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
TEST_F(DungeonEditorV2IntegrationTest, LoadSequence) {
// Test the full initialization sequence
dungeon_editor_v2_->Initialize();
auto load_status = dungeon_editor_v2_->Load();
ASSERT_TRUE(load_status.ok());
// After loading, Update() should work
(void)dungeon_editor_v2_->Update();
}
// ============================================================================
// Update Tests - UI Coordination
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, UpdateBeforeLoad) {
// Update before Load should show loading message but not crash
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonEditorV2IntegrationTest, UpdateAfterLoad) {
dungeon_editor_v2_->Initialize();
(void)dungeon_editor_v2_->Load();
// Update should delegate to components
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
// ============================================================================
// Save Tests - Component Delegation
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, SaveWithoutRom) {
// Test error handling when ROM is not available
editor::DungeonEditorV2 editor(nullptr);
auto status = editor.Save();
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
TEST_F(DungeonEditorV2IntegrationTest, SaveAfterLoad) {
dungeon_editor_v2_->Initialize();
auto load_status = dungeon_editor_v2_->Load();
ASSERT_TRUE(load_status.ok());
// Save should delegate to room objects
auto save_status = dungeon_editor_v2_->Save();
EXPECT_TRUE(save_status.ok());
}
// ============================================================================
// Room Management Tests
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, AddRoomTab) {
dungeon_editor_v2_->Initialize();
(void)dungeon_editor_v2_->Load();
// Add a room tab
dungeon_editor_v2_->add_room(kTestRoomId);
// This should not crash or fail
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonEditorV2IntegrationTest, AddMultipleRoomTabs) {
dungeon_editor_v2_->Initialize();
(void)dungeon_editor_v2_->Load();
// Add multiple rooms
dungeon_editor_v2_->add_room(0x00);
dungeon_editor_v2_->add_room(0x01);
dungeon_editor_v2_->add_room(0x02);
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
// ============================================================================
// Component Delegation Tests
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, RoomLoaderDelegation) {
// Verify that Load() delegates to room_loader_
dungeon_editor_v2_->Initialize();
auto status = dungeon_editor_v2_->Load();
// If Load succeeds, room_loader_ must have worked
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonEditorV2IntegrationTest, ComponentsInitializedAfterLoad) {
dungeon_editor_v2_->Initialize();
auto status = dungeon_editor_v2_->Load();
ASSERT_TRUE(status.ok());
// After Load(), all components should be properly initialized
// We can't directly test this, but Update() should work
(void)dungeon_editor_v2_->Update();
}
// ============================================================================
// ROM Management Tests
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, SetRomAfterConstruction) {
// Create editor without ROM
editor::DungeonEditorV2 editor;
EXPECT_EQ(editor.rom(), nullptr);
// Set ROM
editor.set_rom(rom_.get());
EXPECT_EQ(editor.rom(), rom_.get());
EXPECT_TRUE(editor.IsRomLoaded());
}
TEST_F(DungeonEditorV2IntegrationTest, SetRomAndLoad) {
// Create editor without ROM
editor::DungeonEditorV2 editor;
// Set ROM and load
editor.set_rom(rom_.get());
editor.Initialize();
auto status = editor.Load();
EXPECT_TRUE(status.ok());
}
// ============================================================================
// Unimplemented Methods Tests
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, UnimplementedMethods) {
// These should return UnimplementedError
EXPECT_EQ(dungeon_editor_v2_->Undo().code(),
absl::StatusCode::kUnimplemented);
EXPECT_EQ(dungeon_editor_v2_->Redo().code(),
absl::StatusCode::kUnimplemented);
EXPECT_EQ(dungeon_editor_v2_->Cut().code(),
absl::StatusCode::kUnimplemented);
EXPECT_EQ(dungeon_editor_v2_->Copy().code(),
absl::StatusCode::kUnimplemented);
EXPECT_EQ(dungeon_editor_v2_->Paste().code(),
absl::StatusCode::kUnimplemented);
EXPECT_EQ(dungeon_editor_v2_->Find().code(),
absl::StatusCode::kUnimplemented);
}
// ============================================================================
// Stress Testing
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, MultipleUpdateCycles) {
dungeon_editor_v2_->Initialize();
auto load_status = dungeon_editor_v2_->Load();
ASSERT_TRUE(load_status.ok());
// Run multiple update cycles
for (int i = 0; i < 10; i++) {
(void)dungeon_editor_v2_->Update();
}
}
// ============================================================================
// Edge Cases
// ============================================================================
TEST_F(DungeonEditorV2IntegrationTest, InvalidRoomId) {
dungeon_editor_v2_->Initialize();
(void)dungeon_editor_v2_->Load();
// Add invalid room ID (beyond 0x128)
dungeon_editor_v2_->add_room(0x200);
// Update should handle gracefully
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonEditorV2IntegrationTest, NegativeRoomId) {
dungeon_editor_v2_->Initialize();
(void)dungeon_editor_v2_->Load();
// Add negative room ID
dungeon_editor_v2_->add_room(-1);
// Update should handle gracefully
auto status = dungeon_editor_v2_->Update();
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonEditorV2IntegrationTest, LoadTwice) {
dungeon_editor_v2_->Initialize();
// Load twice
auto status1 = dungeon_editor_v2_->Load();
auto status2 = dungeon_editor_v2_->Load();
// Both should succeed
EXPECT_TRUE(status1.ok());
EXPECT_TRUE(status2.ok());
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,52 @@
#ifndef YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H
#define YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H
#include <memory>
#include <string>
#include "app/editor/dungeon/dungeon_editor_v2.h"
#include "app/rom.h"
#include "gtest/gtest.h"
namespace yaze {
namespace test {
/**
* @brief Integration test framework for DungeonEditorV2
*
* Tests the simplified component delegation architecture
*/
class DungeonEditorV2IntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Use the real ROM (try multiple locations)
rom_ = std::make_unique<Rom>();
auto status = rom_->LoadFromFile("assets/zelda3.sfc");
if (!status.ok()) {
status = rom_->LoadFromFile("build/bin/zelda3.sfc");
}
if (!status.ok()) {
status = rom_->LoadFromFile("zelda3.sfc");
}
ASSERT_TRUE(status.ok()) << "Could not load zelda3.sfc from any location";
// Create V2 editor with ROM
dungeon_editor_v2_ = std::make_unique<editor::DungeonEditorV2>(rom_.get());
}
void TearDown() override {
dungeon_editor_v2_.reset();
rom_.reset();
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<editor::DungeonEditorV2> dungeon_editor_v2_;
static constexpr int kTestRoomId = 0x01;
};
} // namespace test
} // namespace yaze
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H

View File

@@ -0,0 +1,207 @@
#define IMGUI_DEFINE_MATH_OPERATORS
#include "integration/editor/editor_integration_test.h"
#include <SDL.h>
#include "app/platform/window.h"
#include "app/gui/core/style.h"
#include "imgui/backends/imgui_impl_sdl2.h"
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
#include "imgui/imgui.h"
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
#include "imgui_test_engine/imgui_te_context.h"
#include "imgui_test_engine/imgui_te_engine.h"
#include "imgui_test_engine/imgui_te_imconfig.h"
#include "imgui_test_engine/imgui_te_ui.h"
#endif
namespace yaze {
namespace test {
EditorIntegrationTest::EditorIntegrationTest()
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
: engine_(nullptr), show_demo_window_(true)
#else
#endif
{}
EditorIntegrationTest::~EditorIntegrationTest() {
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
if (engine_) {
ImGuiTestEngine_Stop(engine_);
ImGuiTestEngine_DestroyContext(engine_);
}
#endif
}
absl::Status EditorIntegrationTest::Initialize() {
// Create renderer for test
test_renderer_ = std::make_unique<gfx::SDL2Renderer>();
RETURN_IF_ERROR(core::CreateWindow(window_, test_renderer_.get(), SDL_WINDOW_RESIZABLE));
IMGUI_CHECKVERSION();
ImGui::CreateContext();
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
// Initialize Test Engine
engine_ = ImGuiTestEngine_CreateContext();
ImGuiTestEngineIO& test_io = ImGuiTestEngine_GetIO(engine_);
test_io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info;
test_io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug;
#endif
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
// Initialize ImGui for SDL
SDL_Renderer* sdl_renderer = static_cast<SDL_Renderer*>(test_renderer_->GetBackendRenderer());
ImGui_ImplSDL2_InitForSDLRenderer(controller_.window(), sdl_renderer);
ImGui_ImplSDLRenderer2_Init(sdl_renderer);
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
// Register tests
RegisterTests(engine_);
ImGuiTestEngine_Start(engine_, ImGui::GetCurrentContext());
#endif
controller_.set_active(true);
// Set the default style
yaze::gui::ColorsYaze();
return absl::OkStatus();
}
int EditorIntegrationTest::RunTest() {
auto status = Initialize();
if (!status.ok()) {
return EXIT_FAILURE;
}
// Build a new ImGui frame
ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame();
while (controller_.IsActive()) {
controller_.OnInput();
auto status = Update();
if (!status.ok()) {
return EXIT_FAILURE;
}
controller_.DoRender();
}
return EXIT_SUCCESS;
}
absl::Status EditorIntegrationTest::Update() {
ImGui::NewFrame();
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
// Show test engine windows
ImGuiTestEngine_ShowTestEngineWindows(engine_, &show_demo_window_);
#endif
return absl::OkStatus();
}
// Helper methods for testing with a ROM
absl::Status EditorIntegrationTest::LoadTestRom(const std::string& filename) {
test_rom_ = std::make_unique<Rom>();
return test_rom_->LoadFromFile(filename);
}
absl::Status EditorIntegrationTest::SaveTestRom(const std::string& filename) {
if (!test_rom_) {
return absl::FailedPreconditionError("No test ROM loaded");
}
Rom::SaveSettings settings;
settings.backup = false;
settings.save_new = false;
settings.filename = filename;
return test_rom_->SaveToFile(settings);
}
absl::Status EditorIntegrationTest::TestEditorInitialize(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
editor->Initialize();
return absl::OkStatus();
}
absl::Status EditorIntegrationTest::TestEditorLoad(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Load();
}
absl::Status EditorIntegrationTest::TestEditorSave(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Save();
}
absl::Status EditorIntegrationTest::TestEditorUpdate(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Update();
}
absl::Status EditorIntegrationTest::TestEditorCut(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Cut();
}
absl::Status EditorIntegrationTest::TestEditorCopy(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Copy();
}
absl::Status EditorIntegrationTest::TestEditorPaste(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Paste();
}
absl::Status EditorIntegrationTest::TestEditorUndo(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Undo();
}
absl::Status EditorIntegrationTest::TestEditorRedo(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Redo();
}
absl::Status EditorIntegrationTest::TestEditorFind(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Find();
}
absl::Status EditorIntegrationTest::TestEditorClear(editor::Editor* editor) {
if (!editor) {
return absl::InternalError("Editor is null");
}
return editor->Clear();
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,91 @@
#ifndef YAZE_TEST_EDITOR_INTEGRATION_TEST_H
#define YAZE_TEST_EDITOR_INTEGRATION_TEST_H
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui/imgui.h"
#include "app/editor/editor.h"
#include "app/rom.h"
#include "app/controller.h"
#include "app/platform/window.h"
#include "app/gfx/backend/sdl2_renderer.h"
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
#include "imgui_test_engine/imgui_te_context.h"
#include "imgui_test_engine/imgui_te_engine.h"
#endif
namespace yaze {
namespace test {
/**
* @class EditorIntegrationTest
* @brief Base class for editor integration tests
*
* This class provides common functionality for testing editors in the application.
* It sets up the test environment and provides helper methods for ROM operations.
*
* For UI interaction testing, use the ImGui test engine API directly within your test functions:
*
* ImGuiTest* test = IM_REGISTER_TEST(engine, "test_suite", "test_name");
* test->TestFunc = [](ImGuiTestContext* ctx) {
* ctx->SetRef("Window Name");
* ctx->ItemClick("Button Name");
* };
*/
class EditorIntegrationTest {
public:
EditorIntegrationTest();
~EditorIntegrationTest();
// Initialize the test environment
absl::Status Initialize();
// Run the test
int RunTest();
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
// Register tests for a specific editor
virtual void RegisterTests(ImGuiTestEngine* engine) = 0;
#else
// Default implementation when ImGui Test Engine is disabled
virtual void RegisterTests(void* engine) {}
#endif
// Update the test environment
virtual absl::Status Update();
protected:
// Helper methods for testing with a ROM
absl::Status LoadTestRom(const std::string& filename);
absl::Status SaveTestRom(const std::string& filename);
// Helper methods for testing with a specific editor
absl::Status TestEditorInitialize(editor::Editor* editor);
absl::Status TestEditorLoad(editor::Editor* editor);
absl::Status TestEditorSave(editor::Editor* editor);
absl::Status TestEditorUpdate(editor::Editor* editor);
absl::Status TestEditorCut(editor::Editor* editor);
absl::Status TestEditorCopy(editor::Editor* editor);
absl::Status TestEditorPaste(editor::Editor* editor);
absl::Status TestEditorUndo(editor::Editor* editor);
absl::Status TestEditorRedo(editor::Editor* editor);
absl::Status TestEditorFind(editor::Editor* editor);
absl::Status TestEditorClear(editor::Editor* editor);
private:
Controller controller_;
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
ImGuiTestEngine* engine_;
bool show_demo_window_;
#endif
std::unique_ptr<Rom> test_rom_;
core::Window window_;
std::unique_ptr<gfx::SDL2Renderer> test_renderer_;
};
} // namespace test
} // namespace yaze
#endif // YAZE_TEST_EDITOR_INTEGRATION_TEST_H

View File

@@ -0,0 +1,309 @@
#include "app/editor/overworld/tile16_editor.h"
#include <iostream>
#include <memory>
#include <vector>
#include <gtest/gtest.h>
#include "app/rom.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/backend/sdl2_renderer.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/render/tilemap.h"
#include "zelda3/overworld/overworld.h"
#include "app/platform/window.h"
namespace yaze {
namespace editor {
namespace test {
class Tile16EditorIntegrationTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
// Initialize SDL and rendering system once for all tests
InitializeTestEnvironment();
}
static void TearDownTestSuite() {
// Clean up SDL
if (window_initialized_) {
auto shutdown_result = core::ShutdownWindow(test_window_);
(void)shutdown_result; // Suppress unused variable warning
window_initialized_ = false;
}
}
void SetUp() override {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!window_initialized_) {
GTEST_SKIP() << "Failed to initialize graphics system";
}
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(YAZE_TEST_ROM_PATH);
ASSERT_TRUE(load_result.ok()) << "Failed to load test ROM: " << load_result.message();
// Load overworld data
overworld_ = std::make_unique<zelda3::Overworld>(rom_.get());
auto overworld_load_result = overworld_->Load(rom_.get());
ASSERT_TRUE(overworld_load_result.ok()) << "Failed to load overworld: " << overworld_load_result.message();
// Create tile16 blockset
auto tile16_data = overworld_->tile16_blockset_data();
auto palette = overworld_->current_area_palette();
tile16_blockset_ = std::make_unique<gfx::Tilemap>(
gfx::CreateTilemap(nullptr, tile16_data, 0x80, 0x2000, 16,
zelda3::kNumTile16Individual, palette));
// Create graphics bitmap
current_gfx_bmp_ = std::make_unique<gfx::Bitmap>();
current_gfx_bmp_->Create(0x80, 512, 0x40, overworld_->current_graphics());
current_gfx_bmp_->SetPalette(palette);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, current_gfx_bmp_.get());
// Create tile16 blockset bitmap
tile16_blockset_bmp_ = std::make_unique<gfx::Bitmap>();
tile16_blockset_bmp_->Create(0x80, 0x2000, 0x08, tile16_data);
tile16_blockset_bmp_->SetPalette(palette);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, tile16_blockset_bmp_.get());
// Initialize the tile16 editor
editor_ = std::make_unique<Tile16Editor>(rom_.get(), tile16_blockset_.get());
auto init_result = editor_->Initialize(*tile16_blockset_bmp_, *current_gfx_bmp_,
*overworld_->mutable_all_tiles_types());
ASSERT_TRUE(init_result.ok()) << "Failed to initialize editor: " << init_result.message();
rom_loaded_ = true;
#else
// Fallback for non-ROM tests
rom_ = std::make_unique<Rom>();
tilemap_ = std::make_unique<gfx::Tilemap>();
editor_ = std::make_unique<Tile16Editor>(rom_.get(), tilemap_.get());
rom_loaded_ = false;
#endif
}
protected:
static void InitializeTestEnvironment() {
// Create renderer for test
test_renderer_ = std::make_unique<gfx::SDL2Renderer>();
auto window_result = core::CreateWindow(test_window_, test_renderer_.get(), SDL_WINDOW_HIDDEN);
if (window_result.ok()) {
window_initialized_ = true;
} else {
window_initialized_ = false;
// Log the error but don't fail test setup
std::cerr << "Failed to initialize test window: " << window_result.message() << std::endl;
}
}
static bool window_initialized_;
static core::Window test_window_;
static std::unique_ptr<gfx::SDL2Renderer> test_renderer_;
bool rom_loaded_ = false;
std::unique_ptr<Rom> rom_;
std::unique_ptr<gfx::Tilemap> tilemap_;
std::unique_ptr<gfx::Tilemap> tile16_blockset_;
std::unique_ptr<gfx::Bitmap> current_gfx_bmp_;
std::unique_ptr<gfx::Bitmap> tile16_blockset_bmp_;
std::unique_ptr<zelda3::Overworld> overworld_;
std::unique_ptr<Tile16Editor> editor_;
};
// Static member definitions
bool Tile16EditorIntegrationTest::window_initialized_ = false;
core::Window Tile16EditorIntegrationTest::test_window_;
std::unique_ptr<gfx::SDL2Renderer> Tile16EditorIntegrationTest::test_renderer_;
// Basic validation tests (no ROM required)
TEST_F(Tile16EditorIntegrationTest, BasicValidation) {
// Test with invalid tile ID
EXPECT_FALSE(editor_->IsTile16Valid(-1));
EXPECT_FALSE(editor_->IsTile16Valid(9999));
// Test scratch space operations with invalid slots
auto save_invalid = editor_->SaveTile16ToScratchSpace(-1);
EXPECT_FALSE(save_invalid.ok());
EXPECT_EQ(save_invalid.code(), absl::StatusCode::kInvalidArgument);
auto load_invalid = editor_->LoadTile16FromScratchSpace(5);
EXPECT_FALSE(load_invalid.ok());
EXPECT_EQ(load_invalid.code(), absl::StatusCode::kInvalidArgument);
// Test valid scratch space clearing
auto clear_valid = editor_->ClearScratchSpace(0);
EXPECT_TRUE(clear_valid.ok());
}
// ROM-dependent tests
TEST_F(Tile16EditorIntegrationTest, ValidateTile16DataWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Test validation with properly loaded ROM
auto status = editor_->ValidateTile16Data();
EXPECT_TRUE(status.ok()) << "Validation failed: " << status.message();
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, SetCurrentTileWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Test setting a valid tile
auto valid_tile_result = editor_->SetCurrentTile(0);
EXPECT_TRUE(valid_tile_result.ok()) << "Failed to set tile 0: " << valid_tile_result.message();
auto valid_tile_result2 = editor_->SetCurrentTile(100);
EXPECT_TRUE(valid_tile_result2.ok()) << "Failed to set tile 100: " << valid_tile_result2.message();
// Test invalid ranges still fail
auto invalid_low = editor_->SetCurrentTile(-1);
EXPECT_FALSE(invalid_low.ok());
EXPECT_EQ(invalid_low.code(), absl::StatusCode::kOutOfRange);
auto invalid_high = editor_->SetCurrentTile(10000);
EXPECT_FALSE(invalid_high.ok());
EXPECT_EQ(invalid_high.code(), absl::StatusCode::kOutOfRange);
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, FlipOperationsWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Set a valid tile first
auto set_result = editor_->SetCurrentTile(1);
ASSERT_TRUE(set_result.ok()) << "Failed to set initial tile: " << set_result.message();
// Test flip operations
auto flip_h_result = editor_->FlipTile16Horizontal();
EXPECT_TRUE(flip_h_result.ok()) << "Horizontal flip failed: " << flip_h_result.message();
auto flip_v_result = editor_->FlipTile16Vertical();
EXPECT_TRUE(flip_v_result.ok()) << "Vertical flip failed: " << flip_v_result.message();
auto rotate_result = editor_->RotateTile16();
EXPECT_TRUE(rotate_result.ok()) << "Rotation failed: " << rotate_result.message();
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, UndoRedoWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Set a tile and perform an operation to create undo state
auto set_result = editor_->SetCurrentTile(1);
ASSERT_TRUE(set_result.ok());
auto clear_result = editor_->ClearTile16();
ASSERT_TRUE(clear_result.ok()) << "Clear operation failed: " << clear_result.message();
// Test undo
auto undo_result = editor_->Undo();
EXPECT_TRUE(undo_result.ok()) << "Undo failed: " << undo_result.message();
// Test redo
auto redo_result = editor_->Redo();
EXPECT_TRUE(redo_result.ok()) << "Redo failed: " << redo_result.message();
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, PaletteOperationsWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Test palette cycling
auto cycle_forward = editor_->CyclePalette(true);
EXPECT_TRUE(cycle_forward.ok()) << "Palette cycle forward failed: " << cycle_forward.message();
auto cycle_backward = editor_->CyclePalette(false);
EXPECT_TRUE(cycle_backward.ok()) << "Palette cycle backward failed: " << cycle_backward.message();
// Test valid palette preview
auto valid_palette = editor_->PreviewPaletteChange(3);
EXPECT_TRUE(valid_palette.ok()) << "Palette preview failed: " << valid_palette.message();
// Test invalid palette
auto invalid_palette = editor_->PreviewPaletteChange(10);
EXPECT_FALSE(invalid_palette.ok());
EXPECT_EQ(invalid_palette.code(), absl::StatusCode::kInvalidArgument);
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, CopyPasteOperationsWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Set a tile first
auto set_result = editor_->SetCurrentTile(10);
ASSERT_TRUE(set_result.ok());
// Test copy operation
auto copy_result = editor_->CopyTile16ToClipboard(10);
EXPECT_TRUE(copy_result.ok()) << "Copy failed: " << copy_result.message();
// Test paste operation
auto paste_result = editor_->PasteTile16FromClipboard();
EXPECT_TRUE(paste_result.ok()) << "Paste failed: " << paste_result.message();
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
TEST_F(Tile16EditorIntegrationTest, ScratchSpaceWithROM) {
#ifdef YAZE_ENABLE_ROM_TESTS
if (!rom_loaded_) {
GTEST_SKIP() << "ROM not loaded, skipping integration test";
}
// Set a tile first
auto set_result = editor_->SetCurrentTile(15);
ASSERT_TRUE(set_result.ok());
// Test scratch space save
auto save_result = editor_->SaveTile16ToScratchSpace(0);
EXPECT_TRUE(save_result.ok()) << "Scratch save failed: " << save_result.message();
// Test scratch space load
auto load_result = editor_->LoadTile16FromScratchSpace(0);
EXPECT_TRUE(load_result.ok()) << "Scratch load failed: " << load_result.message();
// Test scratch space clear
auto clear_result = editor_->ClearScratchSpace(0);
EXPECT_TRUE(clear_result.ok()) << "Scratch clear failed: " << clear_result.message();
#else
GTEST_SKIP() << "ROM tests disabled";
#endif
}
} // namespace test
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,360 @@
#include "app/gfx/util/palette_manager.h"
#include <gtest/gtest.h>
#include "app/gfx/types/snes_color.h"
#include "app/gfx/types/snes_palette.h"
#include "app/rom.h"
namespace yaze {
namespace gfx {
namespace {
// Test fixture for PaletteManager integration tests
class PaletteManagerTest : public ::testing::Test {
protected:
void SetUp() override {
// PaletteManager is a singleton, so we need to reset it between tests
// Note: In a real scenario, we'd need a way to reset the singleton
// For now, we'll work with the existing instance
}
void TearDown() override {
// Clean up any test state
PaletteManager::Get().ClearHistory();
}
};
// ============================================================================
// Initialization Tests
// ============================================================================
TEST_F(PaletteManagerTest, InitializationState) {
auto& manager = PaletteManager::Get();
// Before initialization, should not be initialized
// Note: This might fail if other tests have already initialized it
// In production, we'd need a Reset() method for testing
// After initialization with null ROM, should handle gracefully
manager.Initialize(nullptr);
EXPECT_FALSE(manager.IsInitialized());
}
TEST_F(PaletteManagerTest, HasNoUnsavedChangesInitially) {
auto& manager = PaletteManager::Get();
// Should have no unsaved changes initially
EXPECT_FALSE(manager.HasUnsavedChanges());
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
}
// ============================================================================
// Dirty Tracking Tests
// ============================================================================
TEST_F(PaletteManagerTest, TracksModifiedGroups) {
auto& manager = PaletteManager::Get();
// Initially, no groups should be modified
auto modified_groups = manager.GetModifiedGroups();
EXPECT_TRUE(modified_groups.empty());
}
TEST_F(PaletteManagerTest, GetModifiedColorCount) {
auto& manager = PaletteManager::Get();
// Initially, no colors modified
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
// After initialization and making changes, count should increase
// (This would require a valid ROM to test properly)
}
// ============================================================================
// Undo/Redo Tests
// ============================================================================
TEST_F(PaletteManagerTest, UndoRedoInitialState) {
auto& manager = PaletteManager::Get();
// Initially, should not be able to undo or redo
EXPECT_FALSE(manager.CanUndo());
EXPECT_FALSE(manager.CanRedo());
EXPECT_EQ(manager.GetUndoStackSize(), 0);
EXPECT_EQ(manager.GetRedoStackSize(), 0);
}
TEST_F(PaletteManagerTest, ClearHistoryResetsStacks) {
auto& manager = PaletteManager::Get();
// Clear history should reset both stacks
manager.ClearHistory();
EXPECT_FALSE(manager.CanUndo());
EXPECT_FALSE(manager.CanRedo());
EXPECT_EQ(manager.GetUndoStackSize(), 0);
EXPECT_EQ(manager.GetRedoStackSize(), 0);
}
TEST_F(PaletteManagerTest, UndoWithoutChangesIsNoOp) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.CanUndo());
// Should not crash
manager.Undo();
EXPECT_FALSE(manager.CanUndo());
}
TEST_F(PaletteManagerTest, RedoWithoutUndoIsNoOp) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.CanRedo());
// Should not crash
manager.Redo();
EXPECT_FALSE(manager.CanRedo());
}
// ============================================================================
// Batch Operations Tests
// ============================================================================
TEST_F(PaletteManagerTest, BatchModeTracking) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.InBatch());
manager.BeginBatch();
EXPECT_TRUE(manager.InBatch());
manager.EndBatch();
EXPECT_FALSE(manager.InBatch());
}
TEST_F(PaletteManagerTest, NestedBatchOperations) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.InBatch());
manager.BeginBatch();
EXPECT_TRUE(manager.InBatch());
manager.BeginBatch(); // Nested
EXPECT_TRUE(manager.InBatch());
manager.EndBatch();
EXPECT_TRUE(manager.InBatch()); // Still in batch (outer)
manager.EndBatch();
EXPECT_FALSE(manager.InBatch()); // Now out of batch
}
TEST_F(PaletteManagerTest, EndBatchWithoutBeginIsNoOp) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.InBatch());
// Should not crash
manager.EndBatch();
EXPECT_FALSE(manager.InBatch());
}
// ============================================================================
// Change Notification Tests
// ============================================================================
TEST_F(PaletteManagerTest, RegisterAndUnregisterListener) {
auto& manager = PaletteManager::Get();
int callback_count = 0;
auto callback = [&callback_count](const PaletteChangeEvent& event) {
callback_count++;
};
// Register listener
int id = manager.RegisterChangeListener(callback);
EXPECT_GT(id, 0);
// Unregister listener
manager.UnregisterChangeListener(id);
// After unregistering, callback should not be called
// (Would need to trigger an event to test this properly)
}
TEST_F(PaletteManagerTest, MultipleListeners) {
auto& manager = PaletteManager::Get();
int callback1_count = 0;
int callback2_count = 0;
auto callback1 = [&callback1_count](const PaletteChangeEvent& event) {
callback1_count++;
};
auto callback2 = [&callback2_count](const PaletteChangeEvent& event) {
callback2_count++;
};
int id1 = manager.RegisterChangeListener(callback1);
int id2 = manager.RegisterChangeListener(callback2);
EXPECT_NE(id1, id2);
// Clean up
manager.UnregisterChangeListener(id1);
manager.UnregisterChangeListener(id2);
}
// ============================================================================
// Color Query Tests (without ROM)
// ============================================================================
TEST_F(PaletteManagerTest, GetColorWithoutInitialization) {
auto& manager = PaletteManager::Get();
// Getting color without initialization should return default color
SnesColor color = manager.GetColor("ow_main", 0, 0);
// Default SnesColor should have zero values
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(PaletteManagerTest, SetColorWithoutInitializationFails) {
auto& manager = PaletteManager::Get();
SnesColor new_color(0x7FFF);
auto status = manager.SetColor("ow_main", 0, 0, new_color);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
TEST_F(PaletteManagerTest, ResetColorWithoutInitializationReturnsError) {
auto& manager = PaletteManager::Get();
auto status = manager.ResetColor("ow_main", 0, 0);
// Should return an error or default color
// Exact behavior depends on implementation
}
TEST_F(PaletteManagerTest, ResetPaletteWithoutInitializationFails) {
auto& manager = PaletteManager::Get();
auto status = manager.ResetPalette("ow_main", 0);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
// ============================================================================
// Save/Discard Tests (without ROM)
// ============================================================================
TEST_F(PaletteManagerTest, SaveGroupWithoutInitializationFails) {
auto& manager = PaletteManager::Get();
auto status = manager.SaveGroup("ow_main");
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
TEST_F(PaletteManagerTest, SaveAllWithoutInitializationFails) {
auto& manager = PaletteManager::Get();
auto status = manager.SaveAllToRom();
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
TEST_F(PaletteManagerTest, DiscardGroupWithoutInitializationIsNoOp) {
auto& manager = PaletteManager::Get();
// Should not crash
manager.DiscardGroup("ow_main");
// No unsaved changes
EXPECT_FALSE(manager.HasUnsavedChanges());
}
TEST_F(PaletteManagerTest, DiscardAllWithoutInitializationIsNoOp) {
auto& manager = PaletteManager::Get();
// Should not crash
manager.DiscardAllChanges();
// No unsaved changes
EXPECT_FALSE(manager.HasUnsavedChanges());
}
// ============================================================================
// Group Modification Query Tests
// ============================================================================
TEST_F(PaletteManagerTest, IsGroupModifiedInitiallyFalse) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.IsGroupModified("ow_main"));
EXPECT_FALSE(manager.IsGroupModified("dungeon_main"));
EXPECT_FALSE(manager.IsGroupModified("global_sprites"));
}
TEST_F(PaletteManagerTest, IsPaletteModifiedInitiallyFalse) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 0));
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 5));
}
TEST_F(PaletteManagerTest, IsColorModifiedInitiallyFalse) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 0));
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 7));
}
// ============================================================================
// Invalid Input Tests
// ============================================================================
TEST_F(PaletteManagerTest, SetColorInvalidGroupName) {
auto& manager = PaletteManager::Get();
SnesColor color(0x7FFF);
auto status = manager.SetColor("invalid_group", 0, 0, color);
EXPECT_FALSE(status.ok());
}
TEST_F(PaletteManagerTest, GetColorInvalidGroupName) {
auto& manager = PaletteManager::Get();
SnesColor color = manager.GetColor("invalid_group", 0, 0);
// Should return default color
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(PaletteManagerTest, IsGroupModifiedInvalidGroupName) {
auto& manager = PaletteManager::Get();
EXPECT_FALSE(manager.IsGroupModified("invalid_group"));
}
} // namespace
} // namespace gfx
} // namespace yaze

View File

@@ -0,0 +1,578 @@
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <map>
#include <chrono>
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/dungeon_editor_system.h"
#include "zelda3/dungeon/dungeon_object_editor.h"
namespace yaze {
namespace zelda3 {
class DungeonEditorSystemIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests on Linux for automated github builds
#if defined(__linux__)
GTEST_SKIP();
#endif
// Use the real ROM from build directory
rom_path_ = "build/bin/zelda3.sfc";
// Load ROM
rom_ = std::make_unique<Rom>();
ASSERT_TRUE(rom_->LoadFromFile(rom_path_).ok());
// Initialize dungeon editor system
dungeon_editor_system_ = std::make_unique<DungeonEditorSystem>(rom_.get());
ASSERT_TRUE(dungeon_editor_system_->Initialize().ok());
// Load test room data
ASSERT_TRUE(LoadTestRoomData().ok());
}
void TearDown() override {
dungeon_editor_system_.reset();
rom_.reset();
}
absl::Status LoadTestRoomData() {
// Load representative rooms for testing
test_rooms_ = {0x0000, 0x0001, 0x0002, 0x0010, 0x0012, 0x0020};
for (int room_id : test_rooms_) {
auto room_result = dungeon_editor_system_->GetRoom(room_id);
if (room_result.ok()) {
rooms_[room_id] = room_result.value();
std::cout << "Loaded room 0x" << std::hex << room_id << std::dec << std::endl;
}
}
return absl::OkStatus();
}
std::string rom_path_;
std::unique_ptr<Rom> rom_;
std::unique_ptr<DungeonEditorSystem> dungeon_editor_system_;
std::vector<int> test_rooms_;
std::map<int, Room> rooms_;
};
// Test basic dungeon editor system initialization
TEST_F(DungeonEditorSystemIntegrationTest, BasicInitialization) {
EXPECT_NE(dungeon_editor_system_, nullptr);
EXPECT_EQ(dungeon_editor_system_->GetROM(), rom_.get());
EXPECT_FALSE(dungeon_editor_system_->IsDirty());
}
// Test room loading and management
TEST_F(DungeonEditorSystemIntegrationTest, RoomLoadingAndManagement) {
// Test loading a specific room
auto room_result = dungeon_editor_system_->GetRoom(0x0000);
ASSERT_TRUE(room_result.ok()) << "Failed to load room 0x0000: " << room_result.status().message();
const auto& room = room_result.value();
// Note: room_id_ is private, so we can't directly access it in tests
// Test setting current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
EXPECT_EQ(dungeon_editor_system_->GetCurrentRoom(), 0x0000);
// Test loading another room
auto room2_result = dungeon_editor_system_->GetRoom(0x0001);
ASSERT_TRUE(room2_result.ok()) << "Failed to load room 0x0001: " << room2_result.status().message();
const auto& room2 = room2_result.value();
// Note: room_id_ is private, so we can't directly access it in tests
}
// Test object editor integration
TEST_F(DungeonEditorSystemIntegrationTest, ObjectEditorIntegration) {
// Get object editor from system
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Test object insertion
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Verify objects were added
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test object selection
ASSERT_TRUE(object_editor->SelectObject(5 * 16, 5 * 16).ok());
auto selection = object_editor->GetSelection();
EXPECT_EQ(selection.selected_objects.size(), 1);
// Test object deletion
ASSERT_TRUE(object_editor->DeleteSelectedObjects().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 1);
}
// Test sprite management
TEST_F(DungeonEditorSystemIntegrationTest, SpriteManagement) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Create sprite data
DungeonEditorSystem::SpriteData sprite_data;
sprite_data.sprite_id = 1;
sprite_data.name = "Test Sprite";
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
sprite_data.x = 100;
sprite_data.y = 100;
sprite_data.layer = 0;
sprite_data.is_active = true;
// Add sprite
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
// Get sprites for room
auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(0x0000);
ASSERT_TRUE(sprites_result.ok()) << "Failed to get sprites: " << sprites_result.status().message();
const auto& sprites = sprites_result.value();
EXPECT_EQ(sprites.size(), 1);
EXPECT_EQ(sprites[0].sprite_id, 1);
EXPECT_EQ(sprites[0].name, "Test Sprite");
// Update sprite
sprite_data.x = 150;
ASSERT_TRUE(dungeon_editor_system_->UpdateSprite(1, sprite_data).ok());
// Get updated sprite
auto sprite_result = dungeon_editor_system_->GetSprite(1);
ASSERT_TRUE(sprite_result.ok());
EXPECT_EQ(sprite_result.value().x, 150);
// Remove sprite
ASSERT_TRUE(dungeon_editor_system_->RemoveSprite(1).ok());
// Verify sprite was removed
auto sprites_after = dungeon_editor_system_->GetSpritesByRoom(0x0000);
ASSERT_TRUE(sprites_after.ok());
EXPECT_EQ(sprites_after.value().size(), 0);
}
// Test item management
TEST_F(DungeonEditorSystemIntegrationTest, ItemManagement) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Create item data
DungeonEditorSystem::ItemData item_data;
item_data.item_id = 1;
item_data.type = DungeonEditorSystem::ItemType::kKey;
item_data.name = "Small Key";
item_data.x = 200;
item_data.y = 200;
item_data.room_id = 0x0000;
item_data.is_hidden = false;
// Add item
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
// Get items for room
auto items_result = dungeon_editor_system_->GetItemsByRoom(0x0000);
ASSERT_TRUE(items_result.ok()) << "Failed to get items: " << items_result.status().message();
const auto& items = items_result.value();
EXPECT_EQ(items.size(), 1);
EXPECT_EQ(items[0].item_id, 1);
EXPECT_EQ(items[0].name, "Small Key");
// Update item
item_data.is_hidden = true;
ASSERT_TRUE(dungeon_editor_system_->UpdateItem(1, item_data).ok());
// Get updated item
auto item_result = dungeon_editor_system_->GetItem(1);
ASSERT_TRUE(item_result.ok());
EXPECT_TRUE(item_result.value().is_hidden);
// Remove item
ASSERT_TRUE(dungeon_editor_system_->RemoveItem(1).ok());
// Verify item was removed
auto items_after = dungeon_editor_system_->GetItemsByRoom(0x0000);
ASSERT_TRUE(items_after.ok());
EXPECT_EQ(items_after.value().size(), 0);
}
// Test entrance management
TEST_F(DungeonEditorSystemIntegrationTest, EntranceManagement) {
// Create entrance data
DungeonEditorSystem::EntranceData entrance_data;
entrance_data.entrance_id = 1;
entrance_data.type = DungeonEditorSystem::EntranceType::kDoor;
entrance_data.name = "Test Entrance";
entrance_data.source_room_id = 0x0000;
entrance_data.target_room_id = 0x0001;
entrance_data.source_x = 100;
entrance_data.source_y = 100;
entrance_data.target_x = 200;
entrance_data.target_y = 200;
entrance_data.is_bidirectional = true;
// Add entrance
ASSERT_TRUE(dungeon_editor_system_->AddEntrance(entrance_data).ok());
// Get entrances for room
auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_result.ok()) << "Failed to get entrances: " << entrances_result.status().message();
const auto& entrances = entrances_result.value();
EXPECT_EQ(entrances.size(), 1);
EXPECT_EQ(entrances[0].name, "Test Entrance");
// Store the entrance ID for later removal
int entrance_id = entrances[0].entrance_id;
// Test room connection
ASSERT_TRUE(dungeon_editor_system_->ConnectRooms(0x0000, 0x0001, 150, 150, 250, 250).ok());
// Get updated entrances
auto entrances_after = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_after.ok());
EXPECT_GE(entrances_after.value().size(), 1);
// Remove entrance using the correct ID
ASSERT_TRUE(dungeon_editor_system_->RemoveEntrance(entrance_id).ok());
// Verify entrance was removed
auto entrances_final = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_final.ok());
EXPECT_EQ(entrances_final.value().size(), 0);
}
// Test door management
TEST_F(DungeonEditorSystemIntegrationTest, DoorManagement) {
// Create door data
DungeonEditorSystem::DoorData door_data;
door_data.door_id = 1;
door_data.name = "Test Door";
door_data.room_id = 0x0000;
door_data.x = 100;
door_data.y = 100;
door_data.direction = 0; // up
door_data.target_room_id = 0x0001;
door_data.target_x = 200;
door_data.target_y = 200;
door_data.requires_key = false;
door_data.key_type = 0;
door_data.is_locked = false;
// Add door
ASSERT_TRUE(dungeon_editor_system_->AddDoor(door_data).ok());
// Get doors for room
auto doors_result = dungeon_editor_system_->GetDoorsByRoom(0x0000);
ASSERT_TRUE(doors_result.ok()) << "Failed to get doors: " << doors_result.status().message();
const auto& doors = doors_result.value();
EXPECT_EQ(doors.size(), 1);
EXPECT_EQ(doors[0].door_id, 1);
EXPECT_EQ(doors[0].name, "Test Door");
// Update door
door_data.is_locked = true;
ASSERT_TRUE(dungeon_editor_system_->UpdateDoor(1, door_data).ok());
// Get updated door
auto door_result = dungeon_editor_system_->GetDoor(1);
ASSERT_TRUE(door_result.ok());
EXPECT_TRUE(door_result.value().is_locked);
// Set door key requirement
ASSERT_TRUE(dungeon_editor_system_->SetDoorKeyRequirement(1, true, 1).ok());
// Get door with key requirement
auto door_with_key = dungeon_editor_system_->GetDoor(1);
ASSERT_TRUE(door_with_key.ok());
EXPECT_TRUE(door_with_key.value().requires_key);
EXPECT_EQ(door_with_key.value().key_type, 1);
// Remove door
ASSERT_TRUE(dungeon_editor_system_->RemoveDoor(1).ok());
// Verify door was removed
auto doors_after = dungeon_editor_system_->GetDoorsByRoom(0x0000);
ASSERT_TRUE(doors_after.ok());
EXPECT_EQ(doors_after.value().size(), 0);
}
// Test chest management
TEST_F(DungeonEditorSystemIntegrationTest, ChestManagement) {
// Create chest data
DungeonEditorSystem::ChestData chest_data;
chest_data.chest_id = 1;
chest_data.room_id = 0x0000;
chest_data.x = 100;
chest_data.y = 100;
chest_data.is_big_chest = false;
chest_data.item_id = 10;
chest_data.item_quantity = 1;
chest_data.is_opened = false;
// Add chest
ASSERT_TRUE(dungeon_editor_system_->AddChest(chest_data).ok());
// Get chests for room
auto chests_result = dungeon_editor_system_->GetChestsByRoom(0x0000);
ASSERT_TRUE(chests_result.ok()) << "Failed to get chests: " << chests_result.status().message();
const auto& chests = chests_result.value();
EXPECT_EQ(chests.size(), 1);
EXPECT_EQ(chests[0].chest_id, 1);
EXPECT_EQ(chests[0].item_id, 10);
// Update chest item
ASSERT_TRUE(dungeon_editor_system_->SetChestItem(1, 20, 5).ok());
// Get updated chest
auto chest_result = dungeon_editor_system_->GetChest(1);
ASSERT_TRUE(chest_result.ok());
EXPECT_EQ(chest_result.value().item_id, 20);
EXPECT_EQ(chest_result.value().item_quantity, 5);
// Set chest as opened
ASSERT_TRUE(dungeon_editor_system_->SetChestOpened(1, true).ok());
// Get opened chest
auto opened_chest = dungeon_editor_system_->GetChest(1);
ASSERT_TRUE(opened_chest.ok());
EXPECT_TRUE(opened_chest.value().is_opened);
// Remove chest
ASSERT_TRUE(dungeon_editor_system_->RemoveChest(1).ok());
// Verify chest was removed
auto chests_after = dungeon_editor_system_->GetChestsByRoom(0x0000);
ASSERT_TRUE(chests_after.ok());
EXPECT_EQ(chests_after.value().size(), 0);
}
// Test room properties management
TEST_F(DungeonEditorSystemIntegrationTest, RoomPropertiesManagement) {
// Create room properties
DungeonEditorSystem::RoomProperties properties;
properties.room_id = 0x0000;
properties.name = "Test Room";
properties.description = "A test room for integration testing";
properties.dungeon_id = 1;
properties.floor_level = 0;
properties.is_boss_room = false;
properties.is_save_room = false;
properties.is_shop_room = false;
properties.music_id = 1;
properties.ambient_sound_id = 0;
// Set room properties
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
// Get room properties
auto properties_result = dungeon_editor_system_->GetRoomProperties(0x0000);
ASSERT_TRUE(properties_result.ok()) << "Failed to get room properties: " << properties_result.status().message();
const auto& retrieved_properties = properties_result.value();
EXPECT_EQ(retrieved_properties.room_id, 0x0000);
EXPECT_EQ(retrieved_properties.name, "Test Room");
EXPECT_EQ(retrieved_properties.description, "A test room for integration testing");
EXPECT_EQ(retrieved_properties.dungeon_id, 1);
// Update properties
properties.name = "Updated Test Room";
properties.is_boss_room = true;
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
// Verify update
auto updated_properties = dungeon_editor_system_->GetRoomProperties(0x0000);
ASSERT_TRUE(updated_properties.ok());
EXPECT_EQ(updated_properties.value().name, "Updated Test Room");
EXPECT_TRUE(updated_properties.value().is_boss_room);
}
// Test dungeon settings management
TEST_F(DungeonEditorSystemIntegrationTest, DungeonSettingsManagement) {
// Create dungeon settings
DungeonEditorSystem::DungeonSettings settings;
settings.dungeon_id = 1;
settings.name = "Test Dungeon";
settings.description = "A test dungeon for integration testing";
settings.total_rooms = 10;
settings.starting_room_id = 0x0000;
settings.boss_room_id = 0x0001;
settings.music_theme_id = 1;
settings.color_palette_id = 0;
settings.has_map = true;
settings.has_compass = true;
settings.has_big_key = true;
// Set dungeon settings
ASSERT_TRUE(dungeon_editor_system_->SetDungeonSettings(settings).ok());
// Get dungeon settings
auto settings_result = dungeon_editor_system_->GetDungeonSettings();
ASSERT_TRUE(settings_result.ok()) << "Failed to get dungeon settings: " << settings_result.status().message();
const auto& retrieved_settings = settings_result.value();
EXPECT_EQ(retrieved_settings.dungeon_id, 1);
EXPECT_EQ(retrieved_settings.name, "Test Dungeon");
EXPECT_EQ(retrieved_settings.total_rooms, 10);
EXPECT_EQ(retrieved_settings.starting_room_id, 0x0000);
EXPECT_EQ(retrieved_settings.boss_room_id, 0x0001);
EXPECT_TRUE(retrieved_settings.has_map);
EXPECT_TRUE(retrieved_settings.has_compass);
EXPECT_TRUE(retrieved_settings.has_big_key);
}
// Test undo/redo functionality
TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Get object editor
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
// Add some objects
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Verify objects were added
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test undo
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 1);
// Test redo
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test multiple undos
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 0);
// Test multiple redos
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 2);
}
// Test validation functionality
TEST_F(DungeonEditorSystemIntegrationTest, ValidationFunctionality) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Validate room
auto room_validation = dungeon_editor_system_->ValidateRoom(0x0000);
ASSERT_TRUE(room_validation.ok()) << "Room validation failed: " << room_validation.message();
// Validate dungeon
auto dungeon_validation = dungeon_editor_system_->ValidateDungeon();
ASSERT_TRUE(dungeon_validation.ok()) << "Dungeon validation failed: " << dungeon_validation.message();
}
// Test save/load functionality
TEST_F(DungeonEditorSystemIntegrationTest, SaveLoadFunctionality) {
// Set current room and add some objects
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Save room
ASSERT_TRUE(dungeon_editor_system_->SaveRoom(0x0000).ok());
// Reload room
ASSERT_TRUE(dungeon_editor_system_->ReloadRoom(0x0000).ok());
// Verify objects are still there
auto reloaded_objects = object_editor->GetObjects();
EXPECT_EQ(reloaded_objects.size(), 2);
// Save entire dungeon
ASSERT_TRUE(dungeon_editor_system_->SaveDungeon().ok());
}
// Test performance with multiple operations
TEST_F(DungeonEditorSystemIntegrationTest, PerformanceTest) {
auto start_time = std::chrono::high_resolution_clock::now();
// Perform many operations
for (int i = 0; i < 100; i++) {
// Add sprite
DungeonEditorSystem::SpriteData sprite_data;
sprite_data.sprite_id = i;
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
sprite_data.x = i * 10;
sprite_data.y = i * 10;
sprite_data.layer = 0;
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
// Add item
DungeonEditorSystem::ItemData item_data;
item_data.item_id = i;
item_data.type = DungeonEditorSystem::ItemType::kKey;
item_data.x = i * 15;
item_data.y = i * 15;
item_data.room_id = 0x0000;
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
}
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// Should complete in reasonable time (less than 5 seconds for 200 operations)
EXPECT_LT(duration.count(), 5000) << "Performance test too slow: " << duration.count() << "ms";
std::cout << "Performance test: 200 operations took " << duration.count() << "ms" << std::endl;
}
// Test error handling
TEST_F(DungeonEditorSystemIntegrationTest, ErrorHandling) {
// Test with invalid room ID
auto invalid_room = dungeon_editor_system_->GetRoom(-1);
EXPECT_FALSE(invalid_room.ok());
auto invalid_room_large = dungeon_editor_system_->GetRoom(10000);
EXPECT_FALSE(invalid_room_large.ok());
// Test with invalid sprite ID
auto invalid_sprite = dungeon_editor_system_->GetSprite(-1);
EXPECT_FALSE(invalid_sprite.ok());
// Test with invalid item ID
auto invalid_item = dungeon_editor_system_->GetItem(-1);
EXPECT_FALSE(invalid_item.ok());
// Test with invalid entrance ID
auto invalid_entrance = dungeon_editor_system_->GetEntrance(-1);
EXPECT_FALSE(invalid_entrance.ok());
// Test with invalid door ID
auto invalid_door = dungeon_editor_system_->GetDoor(-1);
EXPECT_FALSE(invalid_door.ok());
// Test with invalid chest ID
auto invalid_chest = dungeon_editor_system_->GetChest(-1);
EXPECT_FALSE(invalid_chest.ok());
}
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,216 @@
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <chrono>
#include "app/rom.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gfx/render/background_buffer.h"
#include "testing.h"
#include "test_utils.h"
namespace yaze {
namespace test {
/**
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
*
* These tests validate that ObjectDrawer correctly renders dungeon objects
* to BackgroundBuffers using pattern-based drawing routines.
*/
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
protected:
void SetUp() override {
BoundRomTest::SetUp();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
// Setup test palette
palette_group_ = CreateTestPaletteGroup();
}
void TearDown() override {
bg2_.reset();
bg1_.reset();
drawer_.reset();
BoundRomTest::TearDown();
}
gfx::PaletteGroup CreateTestPaletteGroup() {
gfx::PaletteGroup group;
gfx::SnesPalette palette;
// Create standard dungeon palette
for (int i = 0; i < 16; i++) {
int intensity = i * 16;
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
}
group.AddPalette(palette);
return group;
}
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
zelda3::RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom());
obj.EnsureTilesLoaded();
return obj;
}
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
gfx::PaletteGroup palette_group_;
};
// Test basic object drawing
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
// Verify buffers have content
auto& bg1_bitmap = bg1_->bitmap();
EXPECT_TRUE(bg1_bitmap.is_active());
EXPECT_GT(bg1_bitmap.width(), 0);
}
// Test objects on different layers
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok());
// Both buffers should be active
EXPECT_TRUE(bg1_->bitmap().is_active());
EXPECT_TRUE(bg2_->bitmap().is_active());
}
// Test empty object list
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
std::vector<zelda3::RoomObject> objects; // Empty
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Should succeed (drawing nothing is valid)
EXPECT_TRUE(status.ok());
}
// Test large object set
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
std::vector<zelda3::RoomObject> objects;
// Create 100 test objects
for (int i = 0; i < 100; i++) {
int x = (i % 10) * 5;
int y = (i / 10) * 5;
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
}
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto start = std::chrono::high_resolution_clock::now();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
auto end = std::chrono::high_resolution_clock::now();
ASSERT_TRUE(status.ok());
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Should complete in reasonable time
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
}
// Test boundary conditions
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
std::vector<zelda3::RoomObject> objects;
// Objects at boundaries
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_TRUE(status.ok());
}
// Test various object types
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
// Test common object types
std::vector<int> object_types = {
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
0x09, 0x0A, // Diagonal objects
0x10, 0x11, 0x12, // Standard objects
0x20, 0x21, // Decorative objects
0x34, // Solid block
};
for (int obj_type : object_types) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Some object types might not be valid, that's okay
if (!status.ok()) {
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
}
// Test error handling
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
// Test with null ROM
zelda3::ObjectDrawer null_drawer(nullptr);
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,212 @@
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <chrono>
#include "app/rom.h"
#include "app/gfx/snes_palette.h"
#include "app/gfx/background_buffer.h"
#include "testing.h"
#include "test_utils.h"
namespace yaze {
namespace test {
/**
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
*
* These tests validate that ObjectDrawer correctly renders dungeon objects
* to BackgroundBuffers using pattern-based drawing routines.
*/
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
protected:
void SetUp() override {
BoundRomTest::SetUp();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
// Setup test palette
palette_group_ = CreateTestPaletteGroup();
}
void TearDown() override {
bg2_.reset();
bg1_.reset();
drawer_.reset();
BoundRomTest::TearDown();
}
gfx::PaletteGroup CreateTestPaletteGroup() {
gfx::PaletteGroup group;
gfx::SnesPalette palette;
// Create standard dungeon palette
for (int i = 0; i < 16; i++) {
int intensity = i * 16;
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
}
group.AddPalette(palette);
return group;
}
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
zelda3::RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom());
obj.EnsureTilesLoaded();
return obj;
}
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
gfx::PaletteGroup palette_group_;
};
// Test basic object drawing
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
// Verify buffers have content
auto& bg1_bitmap = bg1_->bitmap();
EXPECT_TRUE(bg1_bitmap.is_active());
EXPECT_GT(bg1_bitmap.width(), 0);
}
// Test objects on different layers
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok());
// Both buffers should be active
EXPECT_TRUE(bg1_->bitmap().is_active());
EXPECT_TRUE(bg2_->bitmap().is_active());
}
// Test empty object list
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
std::vector<zelda3::RoomObject> objects; // Empty
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Should succeed (drawing nothing is valid)
EXPECT_TRUE(status.ok());
}
// Test large object set
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
std::vector<zelda3::RoomObject> objects;
// Create 100 test objects
for (int i = 0; i < 100; i++) {
int x = (i % 10) * 5;
int y = (i / 10) * 5;
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
}
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto start = std::chrono::high_resolution_clock::now();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
auto end = std::chrono::high_resolution_clock::now();
ASSERT_TRUE(status.ok());
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Should complete in reasonable time
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
}
// Test boundary conditions
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
std::vector<zelda3::RoomObject> objects;
// Objects at boundaries
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_TRUE(status.ok());
}
// Test various object types
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
// Test common object types
std::vector<int> object_types = {
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
0x09, 0x0A, // Diagonal objects
0x10, 0x11, 0x12, // Standard objects
0x20, 0x21, // Decorative objects
0x34, // Solid block
};
for (int obj_type : object_types) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Some object types might not be valid, that's okay
if (!status.ok()) {
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
}
// Test error handling
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
// Test with null ROM
zelda3::ObjectDrawer null_drawer(nullptr);
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,356 @@
#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.h"
#include "zelda3/dungeon/room_object.h"
namespace yaze {
namespace zelda3 {
class DungeonRenderingIntegrationTest : 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);
// Create test rooms
room_0x00_ = CreateTestRoom(0x00); // Link's House
room_0x01_ = CreateTestRoom(0x01); // Another test room
}
void TearDown() override {
rom_.reset();
}
std::unique_ptr<Rom> rom_;
// Create a test room with various objects
Room CreateTestRoom(int room_id) {
Room room(room_id, rom_.get());
// Add some test objects to the room
std::vector<RoomObject> objects;
// Add floor objects (object 0x00)
objects.emplace_back(0x00, 5, 5, 3, 0); // Horizontal floor
objects.emplace_back(0x00, 10, 10, 5, 0); // Another floor section
// Add wall objects (object 0x01)
objects.emplace_back(0x01, 15, 15, 2, 0); // Vertical wall
objects.emplace_back(0x01, 20, 20, 4, 1); // Horizontal wall on BG2
// Add diagonal stairs (object 0x09)
objects.emplace_back(0x09, 25, 25, 6, 0); // Diagonal stairs
// Add solid blocks (object 0x34)
objects.emplace_back(0x34, 30, 30, 1, 0); // Solid block
objects.emplace_back(0x34, 35, 35, 2, 1); // Another solid block on BG2
// Set ROM for all objects
for (auto& obj : objects) {
obj.set_rom(rom_.get());
}
// Add objects to room (this would normally be done by LoadObjects)
for (const auto& obj : objects) {
room.AddObject(obj);
}
return room;
}
// 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;
}
private:
Room room_0x00_;
Room room_0x01_;
};
// Test full room rendering with ObjectDrawer
TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
Room test_room = CreateTestRoom(0x00);
// Test that room has objects
EXPECT_GT(test_room.GetTileObjects().size(), 0);
// Test ObjectDrawer can render the room
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Test room rendering with different palette configurations
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithDifferentPalettes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
// Test with different palette configurations
std::vector<gfx::PaletteGroup> palette_groups;
// Create multiple palette groups
for (int i = 0; i < 3; ++i) {
palette_groups.push_back(CreateTestPaletteGroup());
}
for (const auto& palette_group : palette_groups) {
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
// Test room rendering with objects on different layers
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Separate objects by layer
std::vector<RoomObject> bg1_objects;
std::vector<RoomObject> bg2_objects;
for (const auto& obj : test_room.GetTileObjects()) {
if (obj.GetLayerValue() == 0) {
bg1_objects.push_back(obj);
} else if (obj.GetLayerValue() == 1) {
bg2_objects.push_back(obj);
}
}
// Render BG1 objects
if (!bg1_objects.empty()) {
auto status = drawer.DrawObjectList(bg1_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Render BG2 objects
if (!bg2_objects.empty()) {
auto status = drawer.DrawObjectList(bg2_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
// Test room rendering with various object sizes
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithVariousObjectSizes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Group objects by size
std::map<int, std::vector<RoomObject>> objects_by_size;
for (const auto& obj : test_room.GetTileObjects()) {
objects_by_size[obj.size_].push_back(obj);
}
// Render objects of each size
for (const auto& [size, objects] : objects_by_size) {
auto status = drawer.DrawObjectList(objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
// Test room rendering performance
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingPerformance) {
// Create a room with many objects
Room large_room(0x00, rom_.get());
// Add many test objects
for (int i = 0; i < 200; ++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
RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom_.get());
large_room.AddObject(obj);
}
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Time the rendering operation
auto start_time = std::chrono::high_resolution_clock::now();
auto status = drawer.DrawObjectList(large_room.GetTileObjects(),
large_room.bg1_buffer(),
large_room.bg2_buffer(),
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 2 seconds for 200 objects)
EXPECT_LT(duration.count(), 2000);
std::cout << "Rendered room with 200 objects in " << duration.count() << "ms" << std::endl;
}
// Test room rendering with edge case coordinates
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Add objects at edge coordinates
std::vector<RoomObject> edge_objects;
edge_objects.emplace_back(0x34, 0, 0, 1, 0); // Origin
edge_objects.emplace_back(0x34, 63, 63, 1, 0); // Near buffer edge
edge_objects.emplace_back(0x34, 32, 32, 1, 0); // Center
edge_objects.emplace_back(0x34, 1, 1, 1, 0); // Near origin
edge_objects.emplace_back(0x34, 62, 62, 1, 0); // Near edge
// Set ROM for all objects
for (auto& obj : edge_objects) {
obj.set_rom(rom_.get());
}
auto status = drawer.DrawObjectList(edge_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Test room rendering with mixed object types
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMixedObjectTypes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Add various object types
std::vector<RoomObject> mixed_objects;
// Floor objects
mixed_objects.emplace_back(0x00, 5, 5, 3, 0);
mixed_objects.emplace_back(0x01, 10, 10, 2, 0);
// Wall objects
mixed_objects.emplace_back(0x02, 15, 15, 4, 0);
mixed_objects.emplace_back(0x03, 20, 20, 1, 1);
// Diagonal objects
mixed_objects.emplace_back(0x09, 25, 25, 5, 0);
mixed_objects.emplace_back(0x0A, 30, 30, 3, 0);
// Solid objects
mixed_objects.emplace_back(0x34, 35, 35, 1, 0);
mixed_objects.emplace_back(0x33, 40, 40, 2, 1);
// Decorative objects
mixed_objects.emplace_back(0x36, 45, 45, 3, 0);
mixed_objects.emplace_back(0x38, 50, 50, 1, 0);
// Set ROM for all objects
for (auto& obj : mixed_objects) {
obj.set_rom(rom_.get());
}
auto status = drawer.DrawObjectList(mixed_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Test room rendering error handling
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingErrorHandling) {
Room test_room = CreateTestRoom(0x00);
// Test with null ROM
ObjectDrawer null_drawer(nullptr);
auto palette_group = CreateTestPaletteGroup();
auto status = null_drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
// Test room rendering with invalid object data
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithInvalidObjectData) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Create objects with invalid data
std::vector<RoomObject> invalid_objects;
invalid_objects.emplace_back(0x999, 5, 5, 1, 0); // Invalid object ID
invalid_objects.emplace_back(0x00, -1, -1, 1, 0); // Negative coordinates
invalid_objects.emplace_back(0x00, 100, 100, 1, 0); // Out of bounds coordinates
invalid_objects.emplace_back(0x00, 5, 5, 255, 0); // Maximum size
// Set ROM for all objects
for (auto& obj : invalid_objects) {
obj.set_rom(rom_.get());
}
// Should handle gracefully
auto status = drawer.DrawObjectList(invalid_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
// Should succeed or fail gracefully
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,34 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
namespace yaze {
namespace test {
class DungeonRoomTest : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests on Linux for automated github builds
#if defined(__linux__)
GTEST_SKIP();
#else
if (!rom_.LoadFromFile("./zelda3.sfc").ok()) {
GTEST_SKIP_("Failed to load test ROM");
}
#endif
}
void TearDown() override {}
Rom rom_;
};
TEST_F(DungeonRoomTest, SingleRoomLoadOk) {
zelda3::Room test_room(/*room_id=*/0, &rom_);
test_room = zelda3::LoadRoomFromRom(&rom_, /*room_id=*/0);
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,300 @@
#include <gtest/gtest.h>
#include <filesystem>
#include "app/editor/message/message_data.h"
#include "app/editor/message/message_editor.h"
#include "testing.h"
namespace yaze {
namespace test {
class MessageRomTest : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests if ROM is not available
if (getenv("YAZE_SKIP_ROM_TESTS")) {
GTEST_SKIP() << "ROM tests disabled";
}
// Check if ROM file exists
std::string rom_path = "zelda3.sfc";
if (!std::filesystem::exists(rom_path)) {
GTEST_SKIP() << "Test ROM not found: " << rom_path;
}
EXPECT_OK(rom_.LoadFromFile(rom_path));
dictionary_ = editor::BuildDictionaryEntries(&rom_);
}
void TearDown() override {}
Rom rom_;
editor::MessageEditor message_editor_;
std::vector<editor::DictionaryEntry> dictionary_;
};
TEST_F(MessageRomTest, ParseSingleMessage_CommandParsing) {
std::vector<uint8_t> mock_data = {0x6A, 0x7F, 0x00};
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
EXPECT_TRUE(result.ok());
const auto message_data = result.value();
// Verify that the command was recognized and parsed
EXPECT_EQ(message_data.ContentsParsed, "[L]");
EXPECT_EQ(pos, 2);
}
TEST_F(MessageRomTest, ParseSingleMessage_BasicAscii) {
// A, B, C, terminator
std::vector<uint8_t> mock_data = {0x00, 0x01, 0x02, 0x7F, 0x00};
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
ASSERT_TRUE(result.ok());
const auto message_data = result.value();
EXPECT_EQ(pos, 4); // consumed all 4 bytes
std::vector<editor::MessageData> message_data_vector = {message_data};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_THAT(parsed, ::testing::ElementsAre("ABC"));
}
TEST_F(MessageRomTest, FindMatchingCharacter_Success) {
EXPECT_EQ(editor::FindMatchingCharacter('A'), 0x00);
EXPECT_EQ(editor::FindMatchingCharacter('Z'), 0x19);
EXPECT_EQ(editor::FindMatchingCharacter('a'), 0x1A);
EXPECT_EQ(editor::FindMatchingCharacter('z'), 0x33);
}
TEST_F(MessageRomTest, FindMatchingCharacter_Failure) {
EXPECT_EQ(editor::FindMatchingCharacter('@'), 0xFF);
EXPECT_EQ(editor::FindMatchingCharacter('#'), 0xFF);
}
TEST_F(MessageRomTest, FindDictionaryEntry_Success) {
EXPECT_EQ(editor::FindDictionaryEntry(0x88), 0x00);
EXPECT_EQ(editor::FindDictionaryEntry(0x90), 0x08);
}
TEST_F(MessageRomTest, FindDictionaryEntry_Failure) {
EXPECT_EQ(editor::FindDictionaryEntry(0x00), -1);
EXPECT_EQ(editor::FindDictionaryEntry(0xFF), -1);
}
TEST_F(MessageRomTest, ParseMessageToData_Basic) {
std::string input = "[L][C:01]ABC";
auto result = editor::ParseMessageToData(input);
std::vector<uint8_t> expected = {0x6A, 0x77, 0x01, 0x00, 0x01, 0x02};
EXPECT_EQ(result, expected);
}
TEST_F(MessageRomTest, ReplaceAllDictionaryWords_Success) {
std::vector<editor::DictionaryEntry> mock_dict = {
editor::DictionaryEntry(0x00, "test"),
editor::DictionaryEntry(0x01, "message")};
std::string input = "This is a test message.";
auto result = editor::ReplaceAllDictionaryWords(input, mock_dict);
EXPECT_EQ(result, "This is a [D:00] [D:01].");
}
TEST_F(MessageRomTest, ReplaceAllDictionaryWords_NoMatch) {
std::vector<editor::DictionaryEntry> mock_dict = {
editor::DictionaryEntry(0x00, "hello")};
std::string input = "No matching words.";
auto result = editor::ReplaceAllDictionaryWords(input, mock_dict);
EXPECT_EQ(result, "No matching words.");
}
TEST_F(MessageRomTest, ParseTextDataByte_Success) {
EXPECT_EQ(editor::ParseTextDataByte(0x00), "A");
EXPECT_EQ(editor::ParseTextDataByte(0x74), "[1]");
EXPECT_EQ(editor::ParseTextDataByte(0x88), "[D:00]");
}
TEST_F(MessageRomTest, ParseTextDataByte_Failure) {
EXPECT_EQ(editor::ParseTextDataByte(0xFF), "");
}
TEST_F(MessageRomTest, ParseSingleMessage_SpecialCharacters) {
std::vector<uint8_t> mock_data = {0x4D, 0x4E, 0x4F, 0x50, 0x7F};
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
ASSERT_TRUE(result.ok());
const auto message_data = result.value();
EXPECT_EQ(message_data.ContentsParsed, "[UP][DOWN][LEFT][RIGHT]");
EXPECT_EQ(pos, 5);
}
TEST_F(MessageRomTest, ParseSingleMessage_DictionaryReference) {
std::vector<uint8_t> mock_data = {0x88, 0x89, 0x7F};
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
ASSERT_TRUE(result.ok());
const auto message_data = result.value();
EXPECT_EQ(message_data.ContentsParsed, "[D:00][D:01]");
EXPECT_EQ(pos, 3);
}
TEST_F(MessageRomTest, ParseSingleMessage_InvalidTerminator) {
std::vector<uint8_t> mock_data = {0x00, 0x01, 0x02}; // No terminator
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
EXPECT_FALSE(result.ok());
}
TEST_F(MessageRomTest, ParseSingleMessage_EmptyData) {
std::vector<uint8_t> mock_data = {0x7F};
int pos = 0;
auto result = editor::ParseSingleMessage(mock_data, &pos);
ASSERT_TRUE(result.ok());
const auto message_data = result.value();
EXPECT_EQ(message_data.ContentsParsed, "");
EXPECT_EQ(pos, 1);
}
TEST_F(MessageRomTest, OptimizeMessageForDictionary_Basic) {
std::vector<editor::DictionaryEntry> mock_dict = {
editor::DictionaryEntry(0x00, "Link"),
editor::DictionaryEntry(0x01, "Zelda")};
std::string input = "[L] rescued Zelda from danger.";
editor::MessageData message_data;
std::string optimized =
message_data.OptimizeMessageForDictionary(input, mock_dict);
EXPECT_EQ(optimized, "[L] rescued [D:01] from danger.");
}
TEST_F(MessageRomTest, SetMessage_Success) {
std::vector<editor::DictionaryEntry> mock_dict = {
editor::DictionaryEntry(0x00, "item")};
editor::MessageData message_data;
std::string input = "You got an item!";
message_data.SetMessage(input, mock_dict);
EXPECT_EQ(message_data.RawString, "You got an item!");
EXPECT_EQ(message_data.ContentsParsed, "You got an [D:00]!");
}
TEST_F(MessageRomTest, FindMatchingElement_CommandWithArgument) {
std::string input = "[W:02]";
editor::ParsedElement result = editor::FindMatchingElement(input);
EXPECT_TRUE(result.Active);
EXPECT_EQ(result.Parent.Token, "W");
EXPECT_EQ(result.Value, 0x02);
}
TEST_F(MessageRomTest, FindMatchingElement_InvalidCommand) {
std::string input = "[INVALID]";
editor::ParsedElement result = editor::FindMatchingElement(input);
EXPECT_FALSE(result.Active);
}
TEST_F(MessageRomTest, BuildDictionaryEntries_CorrectSize) {
auto result = editor::BuildDictionaryEntries(&rom_);
EXPECT_EQ(result.size(), editor::kNumDictionaryEntries);
EXPECT_FALSE(result.empty());
}
TEST_F(MessageRomTest, ParseMessageData_CommandWithArgument_NoExtraCharacters) {
// This test specifically checks for the bug where command arguments
// were being incorrectly parsed as characters (e.g., capital 'A' after [W])
// The bug was caused by using a range-based for loop while also tracking position
// Message: [W:01]ABC
// Bytes: 0x6B (W command), 0x01 (argument), 0x00 (A), 0x01 (B), 0x02 (C)
std::vector<uint8_t> data = {0x6B, 0x01, 0x00, 0x01, 0x02};
editor::MessageData message;
message.ID = 0;
message.Address = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
// Should be "[W:01]ABC" NOT "[W:01]BABC" or "[W:01]AABC"
EXPECT_EQ(parsed[0], "[W:01]ABC");
// The 'B' should not appear twice or be skipped
EXPECT_EQ(parsed[0].find("BABC"), std::string::npos);
EXPECT_EQ(parsed[0].find("AABC"), std::string::npos);
}
TEST_F(MessageRomTest, ParseMessageData_MultipleCommandsWithArguments) {
// Test multiple commands with arguments in sequence
// [W:01][C:02]AB
std::vector<uint8_t> data = {
0x6B, 0x01, // [W:01] - Window border command with arg
0x77, 0x02, // [C:02] - Color command with arg
0x00, 0x01 // AB - Regular characters
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[W:01][C:02]AB");
// Make sure argument bytes (0x01, 0x02) weren't parsed as characters
EXPECT_EQ(parsed[0].find("BAB"), std::string::npos);
EXPECT_EQ(parsed[0].find("CAB"), std::string::npos);
}
TEST_F(MessageRomTest, ParseMessageData_CommandWithoutArgument) {
// Test command without argument followed by text
// [K]ABC - Wait for key command (no arg) followed by ABC
std::vector<uint8_t> data = {
0x7E, // [K] - Wait for key (no argument)
0x00, 0x01, 0x02 // ABC
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[K]ABC");
}
TEST_F(MessageRomTest, ParseMessageData_MixedCommands) {
// Test mix of commands with and without arguments
// [W:01]A[K]B[C:02]C
std::vector<uint8_t> data = {
0x6B, 0x01, // [W:01] - with arg
0x00, // A
0x7E, // [K] - no arg
0x01, // B
0x77, 0x02, // [C:02] - with arg
0x02 // C
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[W:01]A[K]B[C:02]C");
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,406 @@
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <filesystem>
#include <string>
#include "app/rom.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_map.h"
#include "testing.h"
namespace yaze {
namespace zelda3 {
/**
* @brief Comprehensive overworld integration test that validates YAZE C++
* implementation against ZScream C# logic and existing test infrastructure
*
* This test suite:
* 1. Validates overworld loading logic matches ZScream behavior
* 2. Tests integration with ZSCustomOverworld versions (vanilla, v2, v3)
* 3. Uses existing RomDependentTestSuite infrastructure when available
* 4. Provides both mock data and real ROM testing capabilities
*/
class OverworldIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
#if defined(__linux__)
GTEST_SKIP();
#endif
// Check if we should use real ROM or mock data
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
const char* skip_rom_tests = getenv("YAZE_SKIP_ROM_TESTS");
if (skip_rom_tests) {
GTEST_SKIP() << "ROM tests disabled";
}
if (rom_path_env && std::filesystem::exists(rom_path_env)) {
// Use real ROM for testing
rom_ = std::make_unique<Rom>();
auto status = rom_->LoadFromFile(rom_path_env);
if (status.ok()) {
use_real_rom_ = true;
overworld_ = std::make_unique<Overworld>(rom_.get());
return;
}
}
// Fall back to mock data
use_real_rom_ = false;
rom_ = std::make_unique<Rom>();
SetupMockRomData();
rom_->LoadFromData(mock_rom_data_);
overworld_ = std::make_unique<Overworld>(rom_.get());
}
void TearDown() override {
overworld_.reset();
rom_.reset();
}
void SetupMockRomData() {
mock_rom_data_.resize(0x200000, 0x00);
// Basic ROM structure
mock_rom_data_[0x140145] = 0xFF; // Vanilla ASM
// Tile16 expansion flag
mock_rom_data_[0x017D28] = 0x0F; // Vanilla
// Tile32 expansion flag
mock_rom_data_[0x01772E] = 0x04; // Vanilla
// Basic map data
for (int i = 0; i < 160; i++) {
mock_rom_data_[0x012844 + i] = 0x00; // Small areas
}
// Setup entrance data (matches ZScream Constants.OWEntranceMap/Pos/EntranceId)
for (int i = 0; i < 129; i++) {
mock_rom_data_[0x0DB96F + (i * 2)] = i & 0xFF; // Map ID
mock_rom_data_[0x0DB96F + (i * 2) + 1] = (i >> 8) & 0xFF;
mock_rom_data_[0x0DBA71 + (i * 2)] = (i * 16) & 0xFF; // Map Position
mock_rom_data_[0x0DBA71 + (i * 2) + 1] = ((i * 16) >> 8) & 0xFF;
mock_rom_data_[0x0DBB73 + i] = i & 0xFF; // Entrance ID
}
// Setup exit data (matches ZScream Constants.OWExit*)
for (int i = 0; i < 0x4F; i++) {
mock_rom_data_[0x015D8A + (i * 2)] = i & 0xFF; // Room ID
mock_rom_data_[0x015D8A + (i * 2) + 1] = (i >> 8) & 0xFF;
mock_rom_data_[0x015E28 + i] = i & 0xFF; // Map ID
mock_rom_data_[0x015E77 + (i * 2)] = i & 0xFF; // VRAM
mock_rom_data_[0x015E77 + (i * 2) + 1] = (i >> 8) & 0xFF;
// Add other exit fields...
}
}
std::vector<uint8_t> mock_rom_data_;
std::unique_ptr<Rom> rom_;
std::unique_ptr<Overworld> overworld_;
bool use_real_rom_ = false;
};
// Test Tile32 expansion detection
TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
mock_rom_data_[0x01772E] = 0x04;
mock_rom_data_[0x140145] = 0xFF;
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x01772E] = 0x05;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
}
// Test Tile16 expansion detection
TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
mock_rom_data_[0x017D28] = 0x0F;
mock_rom_data_[0x140145] = 0xFF;
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x017D28] = 0x10;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
}
// Test entrance loading matches ZScream coordinate calculation
TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& entrances = overworld_->entrances();
EXPECT_EQ(entrances.size(), 129);
// Verify coordinate calculation matches ZScream logic:
// int p = mapPos >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
const auto& entrance = entrances[i];
uint16_t map_pos = i * 16; // Our test data
uint16_t map_id = i; // Our test data
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(entrance.x_, expected_x);
EXPECT_EQ(entrance.y_, expected_y);
EXPECT_EQ(entrance.entrance_id_, i);
EXPECT_FALSE(entrance.is_hole_);
}
}
// Test exit loading matches ZScream data structure
TEST_F(OverworldIntegrationTest, ExitDataLoading) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& exits = overworld_->exits();
EXPECT_EQ(exits->size(), 0x4F);
// Verify exit data matches our test data
for (int i = 0; i < std::min(5, static_cast<int>(exits->size())); i++) {
const auto& exit = exits->at(i);
// EXPECT_EQ(exit.room_id_, i);
// EXPECT_EQ(exit.map_id_, i);
// EXPECT_EQ(exit.map_pos_, i);
}
}
// Test ASM version detection affects item loading
TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
// Test vanilla ASM (should limit to 0x80 maps)
mock_rom_data_[0x140145] = 0xFF;
overworld_ = std::make_unique<Overworld>(rom_.get());
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& items = overworld_->all_items();
// Test v3+ ASM (should support all 0xA0 maps)
mock_rom_data_[0x140145] = 0x03;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& items_v3 = overworld_->all_items();
// v3 should have more comprehensive support
EXPECT_GE(items_v3.size(), items.size());
}
// Test map size assignment logic
TEST_F(OverworldIntegrationTest, MapSizeAssignment) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
// Verify all maps are initialized
for (const auto& map : maps) {
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
}
}
// Test integration with ZSCustomOverworld version detection
TEST_F(OverworldIntegrationTest, ZSCustomOverworldVersionIntegration) {
if (!use_real_rom_) {
GTEST_SKIP() << "Real ROM required for ZSCustomOverworld version testing";
}
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Check ASM version detection
auto version_byte = rom_->ReadByte(0x140145);
ASSERT_TRUE(version_byte.ok());
uint8_t asm_version = *version_byte;
if (asm_version == 0xFF) {
// Vanilla ROM
EXPECT_FALSE(overworld_->expanded_tile16());
EXPECT_FALSE(overworld_->expanded_tile32());
} else if (asm_version >= 0x02 && asm_version <= 0x03) {
// ZSCustomOverworld v2/v3
// Should have expanded features
EXPECT_TRUE(overworld_->expanded_tile16());
EXPECT_TRUE(overworld_->expanded_tile32());
}
// Verify version-specific features are properly detected
if (asm_version >= 0x03) {
// v3 features should be available
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160); // All 160 maps supported in v3
}
}
// Test compatibility with RomDependentTestSuite infrastructure
TEST_F(OverworldIntegrationTest, RomDependentTestSuiteCompatibility) {
if (!use_real_rom_) {
GTEST_SKIP() << "Real ROM required for RomDependentTestSuite compatibility testing";
}
// Test that our overworld loading works with the same patterns as RomDependentTestSuite
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Verify ROM-dependent features work correctly
EXPECT_TRUE(overworld_->is_loaded());
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
// Test that we can access the same data structures as RomDependentTestSuite
for (int i = 0; i < std::min(10, static_cast<int>(maps.size())); i++) {
const auto& map = maps[i];
// Verify map properties are accessible
EXPECT_GE(map.area_graphics(), 0);
EXPECT_GE(map.main_palette(), 0);
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
}
// Test that sprite data is accessible (matches RomDependentTestSuite expectations)
const auto& sprites = overworld_->sprites(0);
EXPECT_EQ(sprites.size(), 3); // Three game states
// Test that item data is accessible
const auto& items = overworld_->all_items();
EXPECT_GE(items.size(), 0);
// Test that entrance/exit data is accessible
const auto& entrances = overworld_->entrances();
const auto& exits = overworld_->exits();
EXPECT_EQ(entrances.size(), 129);
EXPECT_EQ(exits->size(), 0x4F);
}
// Test comprehensive overworld data integrity
TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Verify all major data structures are properly loaded
EXPECT_GT(overworld_->tiles16().size(), 0);
EXPECT_GT(overworld_->tiles32_unique().size(), 0);
// Verify map organization matches ZScream expectations
const auto& map_tiles = overworld_->map_tiles();
EXPECT_EQ(map_tiles.light_world.size(), 512);
EXPECT_EQ(map_tiles.dark_world.size(), 512);
EXPECT_EQ(map_tiles.special_world.size(), 512);
// Verify each world has proper 512x512 tile data
for (const auto& row : map_tiles.light_world) {
EXPECT_EQ(row.size(), 512);
}
for (const auto& row : map_tiles.dark_world) {
EXPECT_EQ(row.size(), 512);
}
for (const auto& row : map_tiles.special_world) {
EXPECT_EQ(row.size(), 512);
}
// Verify overworld maps are properly initialized
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
for (const auto& map : maps) {
// TODO: Find a way to compare
// EXPECT_TRUE(map.bitmap_data() != nullptr);
}
// Verify tile types are loaded
const auto& tile_types = overworld_->all_tiles_types();
EXPECT_EQ(tile_types.size(), 0x200);
}
// Test ZScream coordinate calculation compatibility
TEST_F(OverworldIntegrationTest, ZScreamCoordinateCompatibility) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& entrances = overworld_->entrances();
EXPECT_EQ(entrances.size(), 129);
// Test coordinate calculation matches ZScream logic exactly
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
const auto& entrance = entrances[i];
// ZScream coordinate calculation:
// int p = mapPos >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
uint16_t map_pos = entrance.map_pos_;
uint16_t map_id = entrance.map_id_;
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(entrance.x_, expected_x);
EXPECT_EQ(entrance.y_, expected_y);
}
// Test hole coordinate calculation with 0x400 offset
const auto& holes = overworld_->holes();
EXPECT_EQ(holes.size(), 0x13);
for (int i = 0; i < std::min(5, static_cast<int>(holes.size())); i++) {
const auto& hole = holes[i];
// ZScream hole coordinate calculation:
// int p = (mapPos + 0x400) >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
uint16_t map_pos = hole.map_pos_;
uint16_t map_id = hole.map_id_;
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(hole.x_, expected_x);
EXPECT_EQ(hole.y_, expected_y);
EXPECT_TRUE(hole.is_hole_);
}
}
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,327 @@
// Integration tests for Room object load/save cycle with real ROM data
// Phase 1, Task 2.1: Full round-trip verification
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
// Helper function for SNES to PC address conversion
inline int SnesToPc(int addr) {
int temp = (addr & 0x7FFF) + ((addr / 2) & 0xFF8000);
return (temp + 0x0);
}
namespace yaze {
namespace zelda3 {
namespace test {
class RoomIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Load the ROM file
rom_ = std::make_unique<Rom>();
// Check if ROM file exists
const char* rom_path = std::getenv("YAZE_TEST_ROM_PATH");
if (!rom_path) {
rom_path = "zelda3.sfc";
}
auto status = rom_->LoadFromFile(rom_path);
if (!status.ok()) {
GTEST_SKIP() << "ROM file not available: " << status.message();
}
// Create backup of ROM data for restoration after tests
original_rom_data_ = rom_->vector();
}
void TearDown() override {
// Restore original ROM data
if (rom_ && !original_rom_data_.empty()) {
for (size_t i = 0; i < original_rom_data_.size(); i++) {
rom_->WriteByte(i, original_rom_data_[i]);
}
}
}
std::unique_ptr<Rom> rom_;
std::vector<uint8_t> original_rom_data_;
};
// ============================================================================
// Test 1: Basic Load/Save Round-Trip
// ============================================================================
TEST_F(RoomIntegrationTest, BasicLoadSaveRoundTrip) {
// Load room 0 (Hyrule Castle Entrance)
Room room1(0x00, rom_.get());
// Get original object count
size_t original_count = room1.GetTileObjects().size();
ASSERT_GT(original_count, 0) << "Room should have objects";
// Store original objects
auto original_objects = room1.GetTileObjects();
// Save the room (should write same data back)
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Load the room again
Room room2(0x00, rom_.get());
// Verify object count matches
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
// Verify each object matches
auto reloaded_objects = room2.GetTileObjects();
ASSERT_EQ(reloaded_objects.size(), original_objects.size());
for (size_t i = 0; i < original_objects.size(); i++) {
SCOPED_TRACE("Object " + std::to_string(i));
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_) << "ID mismatch";
EXPECT_EQ(reload.x(), orig.x()) << "X position mismatch";
EXPECT_EQ(reload.y(), orig.y()) << "Y position mismatch";
EXPECT_EQ(reload.size(), orig.size()) << "Size mismatch";
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue()) << "Layer mismatch";
}
}
// ============================================================================
// Test 2: Multi-Room Verification
// ============================================================================
TEST_F(RoomIntegrationTest, MultiRoomLoadSaveRoundTrip) {
// Test several different rooms to ensure broad coverage
std::vector<int> test_rooms = {0x00, 0x01, 0x02, 0x10, 0x20};
for (int room_id : test_rooms) {
SCOPED_TRACE("Room " + std::to_string(room_id));
// Load room
Room room1(room_id, rom_.get());
auto original_objects = room1.GetTileObjects();
if (original_objects.empty()) {
continue; // Skip empty rooms
}
// Save objects
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Reload and verify
Room room2(room_id, rom_.get());
auto reloaded_objects = room2.GetTileObjects();
EXPECT_EQ(reloaded_objects.size(), original_objects.size());
// Verify objects match
for (size_t i = 0; i < std::min(original_objects.size(), reloaded_objects.size()); i++) {
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_);
EXPECT_EQ(reload.x(), orig.x());
EXPECT_EQ(reload.y(), orig.y());
EXPECT_EQ(reload.size(), orig.size());
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue());
}
}
}
// ============================================================================
// Test 3: Layer Verification
// ============================================================================
TEST_F(RoomIntegrationTest, LayerPreservation) {
// Load a room known to have multiple layers
Room room(0x01, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count objects per layer
int layer0_count = 0, layer1_count = 0, layer2_count = 0;
for (const auto& obj : objects) {
switch (obj.GetLayerValue()) {
case 0: layer0_count++; break;
case 1: layer1_count++; break;
case 2: layer2_count++; break;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x01, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify layer counts match
int reload_layer0 = 0, reload_layer1 = 0, reload_layer2 = 0;
for (const auto& obj : reloaded) {
switch (obj.GetLayerValue()) {
case 0: reload_layer0++; break;
case 1: reload_layer1++; break;
case 2: reload_layer2++; break;
}
}
EXPECT_EQ(reload_layer0, layer0_count);
EXPECT_EQ(reload_layer1, layer1_count);
EXPECT_EQ(reload_layer2, layer2_count);
}
// ============================================================================
// Test 4: Object Type Distribution
// ============================================================================
TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count object types
int type1_count = 0; // ID < 0x100
int type2_count = 0; // ID 0x100-0x13F
int type3_count = 0; // ID >= 0xF00
for (const auto& obj : objects) {
if (obj.id_ >= 0xF00) {
type3_count++;
} else if (obj.id_ >= 0x100) {
type2_count++;
} else {
type1_count++;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x00, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify type distribution matches
int reload_type1 = 0, reload_type2 = 0, reload_type3 = 0;
for (const auto& obj : reloaded) {
if (obj.id_ >= 0xF00) {
reload_type3++;
} else if (obj.id_ >= 0x100) {
reload_type2++;
} else {
reload_type1++;
}
}
EXPECT_EQ(reload_type1, type1_count);
EXPECT_EQ(reload_type2, type2_count);
EXPECT_EQ(reload_type3, type3_count);
}
// ============================================================================
// Test 5: Binary Data Verification
// ============================================================================
TEST_F(RoomIntegrationTest, BinaryDataExactMatch) {
// This test verifies that saving doesn't change ROM data
// when no modifications are made
Room room(0x02, rom_.get());
// Get the ROM location where objects are stored
auto rom_data = rom_->vector();
int object_pointer = (rom_data[0x874C + 2] << 16) +
(rom_data[0x874C + 1] << 8) +
(rom_data[0x874C]);
object_pointer = SnesToPc(object_pointer);
int room_address = object_pointer + (0x02 * 3);
int tile_address = (rom_data[room_address + 2] << 16) +
(rom_data[room_address + 1] << 8) +
rom_data[room_address];
int objects_location = SnesToPc(tile_address);
// Read original bytes (up to 500 bytes should cover most rooms)
std::vector<uint8_t> original_bytes;
for (int i = 0; i < 500 && objects_location + i < (int)rom_data.size(); i++) {
original_bytes.push_back(rom_data[objects_location + i]);
// Stop at final terminator
if (i > 0 && original_bytes[i] == 0xFF && original_bytes[i-1] == 0xFF) {
// Check if this is the final terminator (3rd layer end)
bool might_be_final = true;
for (int j = i - 10; j < i - 1; j += 2) {
if (j >= 0 && original_bytes[j] == 0xFF && original_bytes[j+1] == 0xFF) {
// Found another FF FF marker, keep going
break;
}
}
if (might_be_final) break;
}
}
// Save objects (should write identical data)
ASSERT_TRUE(room.SaveObjects().ok());
// Read bytes after save
rom_data = rom_->vector();
std::vector<uint8_t> saved_bytes;
for (size_t i = 0; i < original_bytes.size() && objects_location + i < rom_data.size(); i++) {
saved_bytes.push_back(rom_data[objects_location + i]);
}
// Verify binary match
ASSERT_EQ(saved_bytes.size(), original_bytes.size());
for (size_t i = 0; i < original_bytes.size(); i++) {
EXPECT_EQ(saved_bytes[i], original_bytes[i])
<< "Byte mismatch at offset " << i;
}
}
// ============================================================================
// Test 6: Known Room Data Verification
// ============================================================================
TEST_F(RoomIntegrationTest, KnownRoomData) {
// Room 0x00 (Hyrule Castle Entrance) - verify known objects exist
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0) << "Room 0x00 should have objects";
// Verify we can find common object types
bool found_type1 = false;
bool found_layer0 = false;
bool found_layer1 = false;
for (const auto& obj : objects) {
if (obj.id_ < 0x100) found_type1 = true;
if (obj.GetLayerValue() == 0) found_layer0 = true;
if (obj.GetLayerValue() == 1) found_layer1 = true;
}
EXPECT_TRUE(found_type1) << "Should have Type 1 objects";
EXPECT_TRUE(found_layer0) << "Should have Layer 0 objects";
// Verify coordinates are in valid range (0-63)
for (const auto& obj : objects) {
EXPECT_GE(obj.x(), 0);
EXPECT_LE(obj.x(), 63);
EXPECT_GE(obj.y(), 0);
EXPECT_LE(obj.y(), 63);
}
}
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,157 @@
#include <gtest/gtest.h>
#include <memory>
#include <iostream>
#include <iomanip>
#include <fstream>
#include "app/rom.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_map.h"
namespace yaze {
namespace zelda3 {
class SpritePositionTest : public ::testing::Test {
protected:
void SetUp() override {
// Try to load a vanilla ROM for testing
rom_ = std::make_unique<Rom>();
std::string rom_path = "bin/zelda3.sfc";
// Check if ROM exists in build directory
std::ifstream rom_file(rom_path);
if (rom_file.good()) {
ASSERT_TRUE(rom_->LoadFromFile(rom_path).ok()) << "Failed to load ROM from " << rom_path;
} else {
// Skip test if ROM not found
GTEST_SKIP() << "ROM file not found at " << rom_path;
}
overworld_ = std::make_unique<Overworld>(rom_.get());
ASSERT_TRUE(overworld_->Load(rom_.get()).ok()) << "Failed to load overworld";
}
void TearDown() override {
overworld_.reset();
rom_.reset();
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<Overworld> overworld_;
};
// Test sprite coordinate system understanding
TEST_F(SpritePositionTest, SpriteCoordinateSystem) {
// Test sprites from different worlds
for (int game_state = 0; game_state < 3; game_state++) {
const auto& sprites = overworld_->sprites(game_state);
std::cout << "\n=== Game State " << game_state << " ===" << std::endl;
std::cout << "Total sprites: " << sprites.size() << std::endl;
int sprite_count = 0;
for (const auto& sprite : sprites) {
if (!sprite.deleted() && sprite_count < 10) { // Show first 10 sprites
std::cout << "Sprite " << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(sprite.id()) << " (" << const_cast<Sprite&>(sprite).name() << ")" << std::endl;
std::cout << " Map ID: 0x" << std::hex << std::setw(2) << std::setfill('0')
<< sprite.map_id() << std::endl;
std::cout << " X: " << std::dec << sprite.x() << " (0x" << std::hex << sprite.x() << ")" << std::endl;
std::cout << " Y: " << std::dec << sprite.y() << " (0x" << std::hex << sprite.y() << ")" << std::endl;
std::cout << " map_x: " << std::dec << sprite.map_x() << std::endl;
std::cout << " map_y: " << std::dec << sprite.map_y() << std::endl;
// Calculate expected world ranges
int world_start = game_state * 0x40;
int world_end = world_start + 0x40;
std::cout << " World range: 0x" << std::hex << world_start << " - 0x" << world_end << std::endl;
sprite_count++;
}
}
}
}
// Test sprite filtering logic
TEST_F(SpritePositionTest, SpriteFilteringLogic) {
// Test the filtering logic used in DrawOverworldSprites
for (int current_world = 0; current_world < 3; current_world++) {
const auto& sprites = overworld_->sprites(current_world);
std::cout << "\n=== Testing World " << current_world << " Filtering ===" << std::endl;
int visible_sprites = 0;
int total_sprites = 0;
for (const auto& sprite : sprites) {
if (!sprite.deleted()) {
total_sprites++;
// This is the filtering logic from DrawOverworldSprites
bool should_show = (sprite.map_id() < 0x40 + (current_world * 0x40) &&
sprite.map_id() >= (current_world * 0x40));
if (should_show) {
visible_sprites++;
std::cout << " Visible: Sprite 0x" << std::hex << static_cast<int>(sprite.id())
<< " on map 0x" << sprite.map_id() << " at ("
<< std::dec << sprite.x() << ", " << sprite.y() << ")" << std::endl;
}
}
}
std::cout << "World " << current_world << ": " << visible_sprites << "/"
<< total_sprites << " sprites visible" << std::endl;
}
}
// Test map coordinate calculations
TEST_F(SpritePositionTest, MapCoordinateCalculations) {
// Test how map coordinates should be calculated
for (int current_world = 0; current_world < 3; current_world++) {
const auto& sprites = overworld_->sprites(current_world);
std::cout << "\n=== World " << current_world << " Coordinate Analysis ===" << std::endl;
for (const auto& sprite : sprites) {
if (!sprite.deleted() &&
sprite.map_id() < 0x40 + (current_world * 0x40) &&
sprite.map_id() >= (current_world * 0x40)) {
// Calculate map position
int sprite_map_id = sprite.map_id();
int local_map_index = sprite_map_id - (current_world * 0x40);
int map_col = local_map_index % 8;
int map_row = local_map_index / 8;
int map_canvas_x = map_col * 512; // kOverworldMapSize
int map_canvas_y = map_row * 512;
std::cout << "Sprite 0x" << std::hex << static_cast<int>(sprite.id())
<< " on map 0x" << sprite_map_id << std::endl;
std::cout << " Local map index: " << std::dec << local_map_index << std::endl;
std::cout << " Map position: (" << map_col << ", " << map_row << ")" << std::endl;
std::cout << " Map canvas pos: (" << map_canvas_x << ", " << map_canvas_y << ")" << std::endl;
std::cout << " Sprite global pos: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
std::cout << " Sprite local pos: (" << sprite.map_x() << ", " << sprite.map_y() << ")" << std::endl;
// Verify the calculation
int expected_global_x = map_canvas_x + sprite.map_x();
int expected_global_y = map_canvas_y + sprite.map_y();
std::cout << " Expected global: (" << expected_global_x << ", " << expected_global_y << ")" << std::endl;
std::cout << " Actual global: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
if (expected_global_x == sprite.x() && expected_global_y == sprite.y()) {
std::cout << " ✓ Coordinates match!" << std::endl;
} else {
std::cout << " ✗ Coordinate mismatch!" << std::endl;
}
break; // Only test first sprite for brevity
}
}
}
}
} // namespace zelda3
} // namespace yaze