#include "core/patch/asm_patch.h" #include #include #include #include "core/patch/patch_manager.h" namespace yaze { namespace core { namespace { // Helper to create a temporary patch file class TempPatchFile { public: explicit TempPatchFile(const std::string& content) { path_ = std::filesystem::temp_directory_path() / ("test_patch_" + std::to_string(rand()) + ".asm"); std::ofstream file(path_); file << content; file.close(); } ~TempPatchFile() { if (std::filesystem::exists(path_)) { std::filesystem::remove(path_); } } std::string path() const { return path_.string(); } private: std::filesystem::path path_; }; // ============================================================================ // Basic Parsing Tests // ============================================================================ TEST(AsmPatchTest, ParseBasicMetadata) { const std::string content = R"(;#PATCH_NAME=Test Patch ;#PATCH_AUTHOR=Test Author ;#PATCH_VERSION=1.0 ;#ENABLED=true lorom org $1BBDF4 NOP )"; TempPatchFile file(content); AsmPatch patch(file.path(), "TestFolder"); EXPECT_TRUE(patch.is_valid()); EXPECT_EQ(patch.name(), "Test Patch"); EXPECT_EQ(patch.author(), "Test Author"); EXPECT_EQ(patch.version(), "1.0"); EXPECT_TRUE(patch.enabled()); EXPECT_EQ(patch.folder(), "TestFolder"); } TEST(AsmPatchTest, ParseDescription) { const std::string content = R"(;#PATCH_NAME=Test Patch ;#PATCH_DESCRIPTION ; This is a multi-line ; description of the patch. ;#ENDPATCH_DESCRIPTION ;#ENABLED=true lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); EXPECT_EQ(patch.description(), "This is a multi-line\ndescription of the patch."); } TEST(AsmPatchTest, ParseDisabledPatch) { const std::string content = R"(;#PATCH_NAME=Disabled Patch ;#ENABLED=false lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); EXPECT_FALSE(patch.enabled()); } // ============================================================================ // Parameter Parsing Tests // ============================================================================ TEST(AsmPatchTest, ParseByteParameter) { const std::string content = R"(;#PATCH_NAME=Byte Test ;#ENABLED=true ;#DEFINE_START ;#name=Test Byte Value ;#type=byte ;#range=$00,$FF !TEST_BYTE = $42 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.define_name, "!TEST_BYTE"); EXPECT_EQ(param.display_name, "Test Byte Value"); EXPECT_EQ(param.type, PatchParameterType::kByte); EXPECT_EQ(param.value, 0x42); EXPECT_EQ(param.min_value, 0x00); EXPECT_EQ(param.max_value, 0xFF); } TEST(AsmPatchTest, ParseWordParameter) { const std::string content = R"(;#PATCH_NAME=Word Test ;#ENABLED=true ;#DEFINE_START ;#name=Test Word Value ;#type=word !TEST_WORD = $1234 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.type, PatchParameterType::kWord); EXPECT_EQ(param.value, 0x1234); EXPECT_EQ(param.max_value, 0xFFFF); } TEST(AsmPatchTest, ParseBoolParameter) { const std::string content = R"(;#PATCH_NAME=Bool Test ;#ENABLED=true ;#DEFINE_START ;#name=Enable Feature ;#type=bool ;#checkedvalue=$01 ;#uncheckedvalue=$00 !FEATURE_ON = $01 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.type, PatchParameterType::kBool); EXPECT_EQ(param.value, 0x01); EXPECT_EQ(param.checked_value, 0x01); EXPECT_EQ(param.unchecked_value, 0x00); } TEST(AsmPatchTest, ParseChoiceParameter) { const std::string content = R"(;#PATCH_NAME=Choice Test ;#ENABLED=true ;#DEFINE_START ;#name=Select Mode ;#type=choice ;#choice0=Mode A ;#choice1=Mode B ;#choice2=Mode C !MODE_SELECT = $01 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.type, PatchParameterType::kChoice); EXPECT_EQ(param.value, 0x01); ASSERT_EQ(param.choices.size(), 3u); EXPECT_EQ(param.choices[0], "Mode A"); EXPECT_EQ(param.choices[1], "Mode B"); EXPECT_EQ(param.choices[2], "Mode C"); } TEST(AsmPatchTest, ParseBitfieldParameter) { const std::string content = R"(;#PATCH_NAME=Bitfield Test ;#ENABLED=true ;#DEFINE_START ;#name=Crystal Requirements ;#type=bitfield ;#bit0=Crystal 1 ;#bit1=Crystal 2 ;#bit6=Crystal 7 !CRYSTAL_BITS = $43 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.type, PatchParameterType::kBitfield); EXPECT_EQ(param.value, 0x43); // bits 0, 1, 6 set ASSERT_GE(param.choices.size(), 7u); EXPECT_EQ(param.choices[0], "Crystal 1"); EXPECT_EQ(param.choices[1], "Crystal 2"); EXPECT_EQ(param.choices[6], "Crystal 7"); } TEST(AsmPatchTest, ParseMultipleParameters) { const std::string content = R"(;#PATCH_NAME=Multi Param Test ;#ENABLED=true ;#DEFINE_START ;#name=First Value ;#type=byte !FIRST = $10 ;#name=Second Value ;#type=word !SECOND = $2000 ;#name=Third Flag ;#type=bool !THIRD = $01 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); EXPECT_EQ(patch.parameters().size(), 3u); } // ============================================================================ // Value Modification Tests // ============================================================================ TEST(AsmPatchTest, SetParameterValue) { const std::string content = R"(;#PATCH_NAME=Value Test ;#ENABLED=true ;#DEFINE_START ;#name=Test Value ;#type=byte !TEST_VAL = $10 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.SetParameterValue("!TEST_VAL", 0x42)); EXPECT_EQ(patch.GetParameter("!TEST_VAL")->value, 0x42); } TEST(AsmPatchTest, SetParameterValueClamped) { const std::string content = R"(;#PATCH_NAME=Clamp Test ;#ENABLED=true ;#DEFINE_START ;#name=Test Value ;#type=byte ;#range=$10,$20 !TEST_VAL = $15 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); // Try to set out of range patch.SetParameterValue("!TEST_VAL", 0x50); EXPECT_EQ(patch.GetParameter("!TEST_VAL")->value, 0x20); // Clamped to max patch.SetParameterValue("!TEST_VAL", 0x05); EXPECT_EQ(patch.GetParameter("!TEST_VAL")->value, 0x10); // Clamped to min } TEST(AsmPatchTest, SetNonExistentParameter) { const std::string content = R"(;#PATCH_NAME=Test ;#ENABLED=true lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_FALSE(patch.SetParameterValue("!NONEXISTENT", 0x42)); } // ============================================================================ // Content Generation Tests // ============================================================================ TEST(AsmPatchTest, GenerateContentPreservesStructure) { const std::string content = R"(;#PATCH_NAME=Gen Test ;#ENABLED=true ;#DEFINE_START ;#name=Test ;#type=byte !TEST = $10 ;#DEFINE_END lorom org $1BBDF4 NOP )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); std::string generated = patch.GenerateContent(); // Should contain the ASM code EXPECT_NE(generated.find("lorom"), std::string::npos); EXPECT_NE(generated.find("org $1BBDF4"), std::string::npos); EXPECT_NE(generated.find("NOP"), std::string::npos); } TEST(AsmPatchTest, GenerateContentUpdatesEnabled) { const std::string content = R"(;#PATCH_NAME=Enabled Test ;#ENABLED=true lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); patch.set_enabled(false); std::string generated = patch.GenerateContent(); EXPECT_NE(generated.find(";#ENABLED=false"), std::string::npos); EXPECT_EQ(generated.find(";#ENABLED=true"), std::string::npos); } // ============================================================================ // Edge Cases // ============================================================================ TEST(AsmPatchTest, ParseDecimalValue) { const std::string content = R"(;#PATCH_NAME=Decimal Test ;#ENABLED=true ;#DEFINE_START ;#name=Decimal Value ;#type=byte ;#decimal !DEC_VAL = 42 ;#DEFINE_END lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); EXPECT_TRUE(patch.is_valid()); ASSERT_EQ(patch.parameters().size(), 1u); const auto& param = patch.parameters()[0]; EXPECT_EQ(param.value, 42); EXPECT_TRUE(param.use_decimal); } TEST(AsmPatchTest, DefaultNameFromFilename) { const std::string content = R"(;#ENABLED=true lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); // Should use filename (minus .asm extension) as default name EXPECT_FALSE(patch.name().empty()); } TEST(AsmPatchTest, HandleMissingEnabledLine) { const std::string content = R"(;#PATCH_NAME=No Enabled Test lorom )"; TempPatchFile file(content); AsmPatch patch(file.path(), "Test"); // Should default to enabled and prepend the line EXPECT_TRUE(patch.is_valid()); EXPECT_TRUE(patch.enabled()); } // ============================================================================ // PatchManager Tests // ============================================================================ class PatchManagerTest : public ::testing::Test { protected: void SetUp() override { // Create temp directory structure temp_dir_ = std::filesystem::temp_directory_path() / ("test_patches_" + std::to_string(rand())); std::filesystem::create_directories(temp_dir_ / "Misc"); std::filesystem::create_directories(temp_dir_ / "Sprites"); // Create test patches CreatePatchFile("Misc", "TestPatch1.asm", R"(;#PATCH_NAME=Test Patch 1 ;#PATCH_AUTHOR=Test ;#ENABLED=true lorom )"); CreatePatchFile("Misc", "TestPatch2.asm", R"(;#PATCH_NAME=Test Patch 2 ;#ENABLED=false lorom )"); CreatePatchFile("Sprites", "SpritePatch.asm", R"(;#PATCH_NAME=Sprite Patch ;#ENABLED=true lorom )"); } void TearDown() override { if (std::filesystem::exists(temp_dir_)) { std::filesystem::remove_all(temp_dir_); } } void CreatePatchFile(const std::string& folder, const std::string& name, const std::string& content) { std::ofstream file(temp_dir_ / folder / name); file << content; file.close(); } std::filesystem::path temp_dir_; }; TEST_F(PatchManagerTest, LoadPatches) { PatchManager manager; auto status = manager.LoadPatches(temp_dir_.string()); EXPECT_TRUE(status.ok()) << status.message(); EXPECT_TRUE(manager.is_loaded()); EXPECT_EQ(manager.patches().size(), 3u); } TEST_F(PatchManagerTest, GetPatchesInFolder) { PatchManager manager; manager.LoadPatches(temp_dir_.string()); auto misc_patches = manager.GetPatchesInFolder("Misc"); EXPECT_EQ(misc_patches.size(), 2u); auto sprite_patches = manager.GetPatchesInFolder("Sprites"); EXPECT_EQ(sprite_patches.size(), 1u); } TEST_F(PatchManagerTest, GetPatchByName) { PatchManager manager; manager.LoadPatches(temp_dir_.string()); auto* patch = manager.GetPatch("Misc", "TestPatch1.asm"); ASSERT_NE(patch, nullptr); EXPECT_EQ(patch->name(), "Test Patch 1"); } TEST_F(PatchManagerTest, GetEnabledPatchCount) { PatchManager manager; manager.LoadPatches(temp_dir_.string()); // TestPatch1 and SpritePatch are enabled, TestPatch2 is disabled EXPECT_EQ(manager.GetEnabledPatchCount(), 2); } TEST_F(PatchManagerTest, GetFolders) { PatchManager manager; manager.LoadPatches(temp_dir_.string()); const auto& folders = manager.folders(); EXPECT_EQ(folders.size(), 2u); EXPECT_NE(std::find(folders.begin(), folders.end(), "Misc"), folders.end()); EXPECT_NE(std::find(folders.begin(), folders.end(), "Sprites"), folders.end()); } } // namespace } // namespace core } // namespace yaze