backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
412
test/e2e/rom_dependent/graphics_editor_save_test.cc
Normal file
412
test/e2e/rom_dependent/graphics_editor_save_test.cc
Normal file
@@ -0,0 +1,412 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/util/compression.h"
|
||||
#include "rom/rom.h"
|
||||
#include "zelda3/game_data.h"
|
||||
#include "zelda3/game_data.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief E2E Test Suite for Graphics Editor Save Operations
|
||||
*
|
||||
* Validates the complete graphics editing workflow:
|
||||
* 1. Load ROM and graphics sheets
|
||||
* 2. Modify pixel data in sheets
|
||||
* 3. Save changes (8BPP→SNES indexed + LC-LZ2 compression)
|
||||
* 4. Reload ROM and verify edits persisted
|
||||
* 5. Verify no data corruption occurred
|
||||
*/
|
||||
class GraphicsEditorSaveTest : 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";
|
||||
}
|
||||
|
||||
// Get ROM path from environment or use default (vanilla.sfc to avoid edited ROMs)
|
||||
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
|
||||
vanilla_rom_path_ = rom_path_env ? rom_path_env : "vanilla.sfc";
|
||||
|
||||
if (!std::filesystem::exists(vanilla_rom_path_)) {
|
||||
GTEST_SKIP() << "Test ROM not found: " << vanilla_rom_path_;
|
||||
}
|
||||
|
||||
// Create test ROM copies
|
||||
test_rom_path_ = "test_graphics_edit.sfc";
|
||||
backup_rom_path_ = "test_graphics_backup.sfc";
|
||||
|
||||
// Copy vanilla ROM for testing
|
||||
std::filesystem::copy_file(
|
||||
vanilla_rom_path_, test_rom_path_,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
std::filesystem::copy_file(
|
||||
vanilla_rom_path_, backup_rom_path_,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up test files
|
||||
if (std::filesystem::exists(test_rom_path_)) {
|
||||
std::filesystem::remove(test_rom_path_);
|
||||
}
|
||||
if (std::filesystem::exists(backup_rom_path_)) {
|
||||
std::filesystem::remove(backup_rom_path_);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to load ROM and verify basic integrity
|
||||
static absl::Status LoadAndVerifyROM(const std::string& path,
|
||||
std::unique_ptr<Rom>& rom) {
|
||||
rom = std::make_unique<Rom>();
|
||||
RETURN_IF_ERROR(rom->LoadFromFile(path));
|
||||
|
||||
// Basic ROM integrity checks
|
||||
EXPECT_EQ(rom->size(), 0x200000) << "ROM size should be 2MB";
|
||||
EXPECT_NE(rom->data(), nullptr) << "ROM data should not be null";
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Helper to load graphics sheets from ROM into Arena
|
||||
static absl::Status LoadGraphicsFromRom(Rom& rom) {
|
||||
zelda3::GameData game_data;
|
||||
RETURN_IF_ERROR(zelda3::LoadGameData(rom, game_data));
|
||||
|
||||
// Copy loaded sheets to Arena
|
||||
auto& arena_sheets = gfx::Arena::Get().gfx_sheets();
|
||||
for (size_t i = 0; i < zelda3::kNumGfxSheets; i++) {
|
||||
arena_sheets[i] = std::move(game_data.gfx_bitmaps[i]);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Helper to save modified sheets to ROM (mirrors GraphicsEditor::Save())
|
||||
static absl::Status SaveSheetToRom(Rom& rom, uint16_t sheet_id) {
|
||||
if (sheet_id >= zelda3::kNumGfxSheets) {
|
||||
return absl::InvalidArgumentError("Sheet ID out of range");
|
||||
}
|
||||
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
auto& sheet = sheets[sheet_id];
|
||||
|
||||
if (!sheet.is_active()) {
|
||||
return absl::FailedPreconditionError("Sheet not active");
|
||||
}
|
||||
|
||||
// Determine BPP and compression based on sheet range
|
||||
int bpp = 3; // Default 3BPP
|
||||
bool compressed = true;
|
||||
|
||||
// Sheets 113-114, 218+ are 2BPP
|
||||
if (sheet_id == 113 || sheet_id == 114 || sheet_id >= 218) {
|
||||
bpp = 2;
|
||||
}
|
||||
|
||||
// Sheets 115-126 are uncompressed
|
||||
if (sheet_id >= 115 && sheet_id <= 126) {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
// Convert 8BPP bitmap data to SNES indexed format
|
||||
auto indexed_data = gfx::Bpp8SnesToIndexed(sheet.vector(), bpp);
|
||||
|
||||
std::vector<uint8_t> final_data;
|
||||
if (compressed) {
|
||||
// Compress using Hyrule Magic LC-LZ2
|
||||
int compressed_size = 0;
|
||||
auto compressed_data = gfx::HyruleMagicCompress(
|
||||
indexed_data.data(), static_cast<int>(indexed_data.size()),
|
||||
&compressed_size, 1);
|
||||
final_data.assign(compressed_data.begin(),
|
||||
compressed_data.begin() + compressed_size);
|
||||
} else {
|
||||
final_data = std::move(indexed_data);
|
||||
}
|
||||
|
||||
// Calculate ROM offset for this sheet
|
||||
// Use JP version constants as default for vanilla ROM
|
||||
const auto& vc = zelda3::kVersionConstantsMap.at(zelda3_version::JP);
|
||||
uint32_t offset = zelda3::GetGraphicsAddress(
|
||||
rom.data(), static_cast<uint8_t>(sheet_id),
|
||||
vc.kOverworldGfxPtr1, vc.kOverworldGfxPtr2,
|
||||
vc.kOverworldGfxPtr3, rom.size());
|
||||
|
||||
// Write data to ROM buffer
|
||||
for (size_t i = 0; i < final_data.size(); i++) {
|
||||
RETURN_IF_ERROR(rom.WriteByte(offset + i, final_data[i]));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string vanilla_rom_path_;
|
||||
std::string test_rom_path_;
|
||||
std::string backup_rom_path_;
|
||||
};
|
||||
|
||||
// Test 1: Single sheet edit, save, and reload verification
|
||||
TEST_F(GraphicsEditorSaveTest, SingleSheetEdit_SaveAndReload) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
|
||||
// Load graphics into Arena
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
// Get initial pixel value from sheet 0
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
auto& sheet = sheets[0];
|
||||
ASSERT_TRUE(sheet.is_active()) << "Sheet 0 should be active after loading";
|
||||
|
||||
// Record original pixel value at (0,0)
|
||||
uint8_t original_pixel = sheet.GetPixel(0, 0);
|
||||
|
||||
// Modify pixel (cycle to next value, wrapping at 16 for 4-bit indexed)
|
||||
uint8_t new_pixel = (original_pixel + 1) % 16;
|
||||
sheet.WriteToPixel(0, 0, new_pixel);
|
||||
|
||||
// Verify modification took effect in memory
|
||||
EXPECT_EQ(sheet.GetPixel(0, 0), new_pixel)
|
||||
<< "Pixel modification should be reflected immediately";
|
||||
|
||||
// Save modified sheet to ROM
|
||||
ASSERT_OK(SaveSheetToRom(*rom, 0));
|
||||
|
||||
// Save ROM to file
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
// --- Reload and verify ---
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
|
||||
// Load graphics from reloaded ROM
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
// Verify pixel change persisted
|
||||
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[0];
|
||||
EXPECT_EQ(reloaded_sheet.GetPixel(0, 0), new_pixel)
|
||||
<< "Pixel modification should persist after save/reload";
|
||||
}
|
||||
|
||||
// Test 2: Multiple sheet edits save atomically
|
||||
TEST_F(GraphicsEditorSaveTest, MultipleSheetEdit_Atomicity) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
|
||||
// Modify sheets 0, 50, and 100 with distinct values
|
||||
const std::vector<uint16_t> test_sheets = {0, 50, 100};
|
||||
const std::vector<uint8_t> test_values = {5, 10, 15};
|
||||
|
||||
for (size_t i = 0; i < test_sheets.size(); i++) {
|
||||
auto& sheet = sheets[test_sheets[i]];
|
||||
if (sheet.is_active()) {
|
||||
sheet.WriteToPixel(0, 0, test_values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Save all modified sheets
|
||||
for (uint16_t sheet_id : test_sheets) {
|
||||
if (sheets[sheet_id].is_active()) {
|
||||
ASSERT_OK(SaveSheetToRom(*rom, sheet_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Save ROM
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
// Reload and verify ALL changes persisted
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
|
||||
for (size_t i = 0; i < test_sheets.size(); i++) {
|
||||
if (reloaded_sheets[test_sheets[i]].is_active()) {
|
||||
EXPECT_EQ(reloaded_sheets[test_sheets[i]].GetPixel(0, 0), test_values[i])
|
||||
<< "Sheet " << test_sheets[i] << " modification should persist";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Compression integrity for LC-LZ2 compressed sheets
|
||||
TEST_F(GraphicsEditorSaveTest, CompressionIntegrity_LZ2Sheets) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
// Sheet 50 should be compressed (in range 0-112)
|
||||
const uint16_t test_sheet = 50;
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
auto& sheet = sheets[test_sheet];
|
||||
|
||||
if (!sheet.is_active()) {
|
||||
GTEST_SKIP() << "Sheet 50 not active in this ROM";
|
||||
}
|
||||
|
||||
// Record original data for a small region
|
||||
std::vector<uint8_t> original_region;
|
||||
for (int y = 0; y < 8; y++) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
original_region.push_back(sheet.GetPixel(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Modify a single pixel
|
||||
uint8_t original_pixel = sheet.GetPixel(4, 4);
|
||||
uint8_t new_pixel = (original_pixel + 7) % 16;
|
||||
sheet.WriteToPixel(4, 4, new_pixel);
|
||||
|
||||
// Save and reload
|
||||
ASSERT_OK(SaveSheetToRom(*rom, test_sheet));
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
// Verify the modified pixel
|
||||
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[test_sheet];
|
||||
EXPECT_EQ(reloaded_sheet.GetPixel(4, 4), new_pixel)
|
||||
<< "Modified pixel should persist through compression round-trip";
|
||||
|
||||
// Verify surrounding pixels weren't corrupted
|
||||
for (int y = 0; y < 8; y++) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
if (x == 4 && y == 4) continue; // Skip modified pixel
|
||||
int idx = y * 8 + x;
|
||||
EXPECT_EQ(reloaded_sheet.GetPixel(x, y), original_region[idx])
|
||||
<< "Pixel at (" << x << "," << y << ") should not be corrupted";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Uncompressed sheets (115-126) save correctly
|
||||
TEST_F(GraphicsEditorSaveTest, UncompressedSheets_SaveCorrectly) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
// Sheet 115 is uncompressed
|
||||
const uint16_t test_sheet = 115;
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
auto& sheet = sheets[test_sheet];
|
||||
|
||||
if (!sheet.is_active()) {
|
||||
GTEST_SKIP() << "Sheet 115 not active in this ROM";
|
||||
}
|
||||
|
||||
// Modify pixel
|
||||
uint8_t original_pixel = sheet.GetPixel(0, 0);
|
||||
uint8_t new_pixel = (original_pixel + 3) % 16;
|
||||
sheet.WriteToPixel(0, 0, new_pixel);
|
||||
|
||||
// Save and reload
|
||||
ASSERT_OK(SaveSheetToRom(*rom, test_sheet));
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[test_sheet];
|
||||
EXPECT_EQ(reloaded_sheet.GetPixel(0, 0), new_pixel)
|
||||
<< "Uncompressed sheet modification should persist";
|
||||
}
|
||||
|
||||
// Test 5: Save without corrupting adjacent sheet data
|
||||
TEST_F(GraphicsEditorSaveTest, SaveWithoutCorruption_AdjacentData) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
|
||||
// Record pixel values from adjacent sheets (49 and 51)
|
||||
uint8_t sheet49_pixel = 0;
|
||||
uint8_t sheet51_pixel = 0;
|
||||
|
||||
if (sheets[49].is_active()) {
|
||||
sheet49_pixel = sheets[49].GetPixel(0, 0);
|
||||
}
|
||||
if (sheets[51].is_active()) {
|
||||
sheet51_pixel = sheets[51].GetPixel(0, 0);
|
||||
}
|
||||
|
||||
// Modify only sheet 50
|
||||
if (sheets[50].is_active()) {
|
||||
sheets[50].WriteToPixel(0, 0, 8);
|
||||
ASSERT_OK(SaveSheetToRom(*rom, 50));
|
||||
}
|
||||
|
||||
// Save and reload
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
|
||||
|
||||
// Verify adjacent sheets weren't corrupted
|
||||
if (reloaded_sheets[49].is_active()) {
|
||||
EXPECT_EQ(reloaded_sheets[49].GetPixel(0, 0), sheet49_pixel)
|
||||
<< "Sheet 49 should not be corrupted by saving sheet 50";
|
||||
}
|
||||
if (reloaded_sheets[51].is_active()) {
|
||||
EXPECT_EQ(reloaded_sheets[51].GetPixel(0, 0), sheet51_pixel)
|
||||
<< "Sheet 51 should not be corrupted by saving sheet 50";
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: Round-trip with no modifications preserves data
|
||||
TEST_F(GraphicsEditorSaveTest, RoundTrip_NoModifications) {
|
||||
std::unique_ptr<Rom> rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*rom));
|
||||
|
||||
// Record sample pixel values from multiple sheets
|
||||
auto& sheets = gfx::Arena::Get().gfx_sheets();
|
||||
std::map<uint16_t, uint8_t> original_pixels;
|
||||
|
||||
for (uint16_t id : {0, 25, 50, 75, 100, 150, 200}) {
|
||||
if (sheets[id].is_active()) {
|
||||
original_pixels[id] = sheets[id].GetPixel(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Save ROM without modifications
|
||||
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
|
||||
|
||||
// Reload
|
||||
std::unique_ptr<Rom> reloaded_rom;
|
||||
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
|
||||
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
|
||||
|
||||
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
|
||||
|
||||
// Verify all recorded pixels match
|
||||
for (const auto& [id, pixel] : original_pixels) {
|
||||
if (reloaded_sheets[id].is_active()) {
|
||||
EXPECT_EQ(reloaded_sheets[id].GetPixel(0, 0), pixel)
|
||||
<< "Sheet " << id << " should preserve data through round-trip";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user