From fc9e6ce14dab9461069995a290d299a35831c662 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 3 Jan 2025 17:51:56 +0100 Subject: [PATCH 01/11] test: add test for KeyValuePairsCompilerT6 --- .github/workflows/ci.yaml | 3 + premake5.lua | 2 + .../KeyValuePairs/KeyValuePairsCompilerT6.cpp | 112 ++++++++------- .../KeyValuePairs/KeyValuePairsCompilerT6.h | 23 +--- src/ObjCompiling/Game/T6/ObjCompilerT6.cpp | 2 +- src/Utils/Utils/MemoryManager.h | 53 +++---- .../Utils/TestMemoryManager.h | 14 ++ test/ObjCompilingTests.lua | 58 ++++++++ .../KeyValuePairsCompilerT6Test.cpp | 130 ++++++++++++++++++ 9 files changed, 307 insertions(+), 90 deletions(-) create mode 100644 test/ObjCommonTestUtils/Utils/TestMemoryManager.h create mode 100644 test/ObjCompilingTests.lua create mode 100644 test/ObjCompilingTests/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6Test.cpp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b3d90c21..02f4f100 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,6 +49,7 @@ jobs: working-directory: ${{ github.workspace }}/build/lib/Release_x86/tests run: | ./ObjCommonTests + ./ObjCompilingTests ./ObjLoadingTests ./ParserTests ./ZoneCodeGeneratorLibTests @@ -86,6 +87,8 @@ jobs: $combinedExitCode = 0 ./ObjCommonTests $combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE) + ./ObjCompilingTests + $combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE) ./ObjLoadingTests $combinedExitCode = [System.Math]::max($combinedExitCode, $LASTEXITCODE) ./ParserTests diff --git a/premake5.lua b/premake5.lua index 331ae3e4..7ff24f46 100644 --- a/premake5.lua +++ b/premake5.lua @@ -172,6 +172,7 @@ group "" -- ======================== include "test/ObjCommonTestUtils.lua" include "test/ObjCommonTests.lua" +include "test/ObjCompilingTests.lua" include "test/ObjLoadingTests.lua" include "test/ParserTestUtils.lua" include "test/ParserTests.lua" @@ -182,6 +183,7 @@ include "test/ZoneCommonTests.lua" group "Tests" ObjCommonTestUtils:project() ObjCommonTests:project() + ObjCompilingTests:project() ObjLoadingTests:project() ParserTestUtils:project() ParserTests:project() diff --git a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp index 11c98ea1..8cba6771 100644 --- a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp @@ -2,6 +2,7 @@ #include "Game/T6/CommonT6.h" #include "Game/T6/T6.h" +#include "KeyValuePairs/KeyValuePairsCreator.h" #include #include @@ -9,54 +10,73 @@ using namespace T6; -KeyValuePairsCompiler::KeyValuePairsCompiler(MemoryManager& memory, - const Zone& zone, - const ZoneDefinition& zoneDefinition, - ZoneAssetCreationStateContainer& zoneStates) - : m_memory(memory), - m_zone(zone), - m_zone_definition(zoneDefinition), - m_kvp_creator(zoneStates.GetZoneAssetCreationState()) +namespace { -} - -std::optional KeyValuePairsCompiler::GetHandlingAssetType() const -{ - return std::nullopt; -} - -AssetCreationResult KeyValuePairsCompiler::CreateAsset(const std::string& assetName, AssetCreationContext& context) -{ - return AssetCreationResult::NoAction(); -} - -void KeyValuePairsCompiler::FinalizeZone(AssetCreationContext& context) -{ - m_kvp_creator.Finalize(m_zone_definition); - const auto commonKvps = m_kvp_creator.GetFinalKeyValuePairs(); - if (commonKvps.empty()) - return; - - auto* gameKvps = m_memory.Alloc(); - gameKvps->name = m_memory.Dup(m_zone.m_name.c_str()); - gameKvps->numVariables = commonKvps.size(); - gameKvps->keyValuePairs = m_memory.Alloc(commonKvps.size()); - - const auto namespaceHash = Common::Com_HashKey(m_zone.m_name.c_str(), 64); - for (auto kvpIndex = 0u; kvpIndex < gameKvps->numVariables; kvpIndex++) + class KeyValuePairsCompiler final : public IAssetCreator { - const auto& commonKvp = commonKvps[kvpIndex]; - auto& gameKvp = gameKvps->keyValuePairs[kvpIndex]; + public: + KeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates) + : m_memory(memory), + m_zone(zone), + m_zone_definition(zoneDefinition), + m_kvp_creator(zoneStates.GetZoneAssetCreationState()) + { + } - assert(commonKvp.m_key_str || commonKvp.m_key_hash); - if (commonKvp.m_key_str) - gameKvp.keyHash = Common::Com_HashKey(commonKvp.m_key_str->c_str(), 64); - else - gameKvp.keyHash = *commonKvp.m_key_hash; + [[nodiscard]] std::optional GetHandlingAssetType() const override + { + return std::nullopt; + } - gameKvp.namespaceHash = namespaceHash; - gameKvp.value = m_memory.Dup(commonKvp.m_value.c_str()); + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + return AssetCreationResult::NoAction(); + } + + void FinalizeZone(AssetCreationContext& context) override + { + m_kvp_creator.Finalize(m_zone_definition); + const auto commonKvps = m_kvp_creator.GetFinalKeyValuePairs(); + if (commonKvps.empty()) + return; + + auto* gameKvps = m_memory.Alloc(); + gameKvps->name = m_memory.Dup(m_zone.m_name.c_str()); + gameKvps->numVariables = commonKvps.size(); + gameKvps->keyValuePairs = m_memory.Alloc(commonKvps.size()); + + const auto namespaceHash = Common::Com_HashKey(m_zone.m_name.c_str(), 64); + for (auto kvpIndex = 0u; kvpIndex < gameKvps->numVariables; kvpIndex++) + { + const auto& commonKvp = commonKvps[kvpIndex]; + auto& gameKvp = gameKvps->keyValuePairs[kvpIndex]; + + assert(commonKvp.m_key_str || commonKvp.m_key_hash); + if (commonKvp.m_key_str) + gameKvp.keyHash = Common::Com_HashKey(commonKvp.m_key_str->c_str(), 64); + else + gameKvp.keyHash = *commonKvp.m_key_hash; + + gameKvp.namespaceHash = namespaceHash; + gameKvp.value = m_memory.Dup(commonKvp.m_value.c_str()); + } + + context.AddAsset(AssetRegistration(m_zone.m_name, gameKvps)); + } + + private: + MemoryManager& m_memory; + const Zone& m_zone; + const ZoneDefinition& m_zone_definition; + KeyValuePairsCreator& m_kvp_creator; + }; +} // namespace + +namespace T6 +{ + std::unique_ptr + CreateKeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates) + { + return std::make_unique(memory, zone, zoneDefinition, zoneStates); } - - context.AddAsset(AssetRegistration(m_zone.m_name, gameKvps)); -} +} // namespace T6 diff --git a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h index fa5ea50e..c68c06b5 100644 --- a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h +++ b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h @@ -1,27 +1,16 @@ #pragma once #include "Asset/IAssetCreator.h" -#include "Game/T6/T6.h" -#include "KeyValuePairs/KeyValuePairsCreator.h" +#include "Asset/IZoneAssetCreationState.h" +#include "SearchPath/ISearchPath.h" #include "Utils/MemoryManager.h" #include "Zone/Definition/ZoneDefinition.h" #include "Zone/Zone.h" +#include + namespace T6 { - class KeyValuePairsCompiler final : public IAssetCreator - { - public: - KeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates); - - [[nodiscard]] std::optional GetHandlingAssetType() const override; - AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override; - void FinalizeZone(AssetCreationContext& context) override; - - private: - MemoryManager& m_memory; - const Zone& m_zone; - const ZoneDefinition& m_zone_definition; - KeyValuePairsCreator& m_kvp_creator; - }; + std::unique_ptr + CreateKeyValuePairsCompiler(MemoryManager& memory, const Zone& zone, const ZoneDefinition& zoneDefinition, ZoneAssetCreationStateContainer& zoneStates); } // namespace T6 diff --git a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp index 807f8aae..9be2a476 100644 --- a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp @@ -20,7 +20,7 @@ namespace { auto& memory = *zone.GetMemory(); - collection.AddAssetCreator(std::make_unique(memory, zone, zoneDefinition.m_zone_definition, zoneStates)); + collection.AddAssetCreator(CreateKeyValuePairsCompiler(memory, zone, zoneDefinition.m_zone_definition, zoneStates)); } void ConfigurePostProcessors(AssetCreatorCollection& collection, diff --git a/src/Utils/Utils/MemoryManager.h b/src/Utils/Utils/MemoryManager.h index bc26f1ee..c9eeefb5 100644 --- a/src/Utils/Utils/MemoryManager.h +++ b/src/Utils/Utils/MemoryManager.h @@ -6,6 +6,33 @@ class MemoryManager { +public: + MemoryManager(); + virtual ~MemoryManager(); + MemoryManager(const MemoryManager& other) = delete; + MemoryManager(MemoryManager&& other) noexcept = default; + MemoryManager& operator=(const MemoryManager& other) = delete; + MemoryManager& operator=(MemoryManager&& other) noexcept = default; + + void* AllocRaw(size_t size); + char* Dup(const char* str); + + template std::add_pointer_t Alloc(const size_t count = 1u) + { + return static_cast>(AllocRaw(sizeof(T) * count)); + } + + template std::add_pointer_t Create(ValType&&... val) + { + Allocation* allocation = new Allocation(std::forward(val)...); + m_destructible.emplace_back(allocation, &allocation->m_entry); + return &allocation->m_entry; + } + + void Free(const void* data); + void Delete(const void* data); + +protected: class IDestructible { public: @@ -47,30 +74,4 @@ class MemoryManager std::vector m_allocations; std::vector m_destructible; - -public: - MemoryManager(); - virtual ~MemoryManager(); - MemoryManager(const MemoryManager& other) = delete; - MemoryManager(MemoryManager&& other) noexcept = default; - MemoryManager& operator=(const MemoryManager& other) = delete; - MemoryManager& operator=(MemoryManager&& other) noexcept = default; - - void* AllocRaw(size_t size); - char* Dup(const char* str); - - template std::add_pointer_t Alloc(const size_t count = 1u) - { - return static_cast>(AllocRaw(sizeof(T) * count)); - } - - template std::add_pointer_t Create(ValType&&... val) - { - Allocation* allocation = new Allocation(std::forward(val)...); - m_destructible.emplace_back(allocation, &allocation->m_entry); - return &allocation->m_entry; - } - - void Free(const void* data); - void Delete(const void* data); }; diff --git a/test/ObjCommonTestUtils/Utils/TestMemoryManager.h b/test/ObjCommonTestUtils/Utils/TestMemoryManager.h new file mode 100644 index 00000000..3ac27e41 --- /dev/null +++ b/test/ObjCommonTestUtils/Utils/TestMemoryManager.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Utils/MemoryManager.h" + +#include + +class TestMemoryManager : public MemoryManager +{ +public: + [[nodiscard]] size_t GetAllocationCount() const + { + return m_allocations.size() + m_destructible.size(); + } +}; diff --git a/test/ObjCompilingTests.lua b/test/ObjCompilingTests.lua new file mode 100644 index 00000000..fc08d7a7 --- /dev/null +++ b/test/ObjCompilingTests.lua @@ -0,0 +1,58 @@ +ObjCompilingTests = {} + +function ObjCompilingTests:include(includes) + if includes:handle(self:name()) then + includedirs { + path.join(TestFolder(), "ObjCompilingTests") + } + end +end + +function ObjCompilingTests:link(links) + +end + +function ObjCompilingTests:use() + +end + +function ObjCompilingTests:name() + return "ObjCompilingTests" +end + +function ObjCompilingTests:project() + local folder = TestFolder() + local includes = Includes:create() + local links = Links:create() + + project(self:name()) + targetdir(TargetDirectoryTest) + location "%{wks.location}/test/%{prj.name}" + kind "ConsoleApp" + language "C++" + + files { + path.join(folder, "ObjCompilingTests/**.h"), + path.join(folder, "ObjCompilingTests/**.cpp") + } + + vpaths { + ["*"] = { + path.join(folder, "ObjCompilingTests") + } + } + + self:include(includes) + ObjCommonTestUtils:include(includes) + ParserTestUtils:include(includes) + ObjLoading:include(includes) + ObjCompiling:include(includes) + catch2:include(includes) + + links:linkto(ObjCommonTestUtils) + links:linkto(ParserTestUtils) + links:linkto(ObjLoading) + links:linkto(ObjCompiling) + links:linkto(catch2) + links:linkall() +end diff --git a/test/ObjCompilingTests/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6Test.cpp b/test/ObjCompilingTests/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6Test.cpp new file mode 100644 index 00000000..b25715b9 --- /dev/null +++ b/test/ObjCompilingTests/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6Test.cpp @@ -0,0 +1,130 @@ +#include "Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.h" + +#include "Game/T6/CommonT6.h" +#include "Game/T6/GameAssetPoolT6.h" +#include "KeyValuePairs/KeyValuePairsCreator.h" +#include "Utils/TestMemoryManager.h" + +#include +#include +#include +#include + +using namespace T6; +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_memory(), + m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_definition(), + m_zone_states(m_zone), + m_creators(m_zone), + m_ignored_assets(), + m_context(m_zone, &m_creators, &m_ignored_assets), + m_kvp_creator(m_zone_states.GetZoneAssetCreationState()) + { + } + + std::unique_ptr CreateSut() + { + return CreateKeyValuePairsCompiler(m_memory, m_zone, m_zone_definition, m_zone_states); + } + + TestMemoryManager m_memory; + Zone m_zone; + ZoneDefinition m_zone_definition; + ZoneAssetCreationStateContainer m_zone_states; + AssetCreatorCollection m_creators; + IgnoredAssetLookup m_ignored_assets; + AssetCreationContext m_context; + + KeyValuePairsCreator& m_kvp_creator; + }; +} // namespace + +namespace test::game::t6::keyvaluepairs +{ + TEST_CASE("KeyValuePairsCompilerT6: Does not handle any asset type", "[keyvaluepairs][t6]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + REQUIRE(!sut->GetHandlingAssetType().has_value()); + } + + TEST_CASE("KeyValuePairsCompilerT6: Does not take any action", "[keyvaluepairs][t6]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + REQUIRE(sut->CreateAsset("anyAsset", testContext.m_context).HasTakenAction() == false); + } + + TEST_CASE("KeyValuePairsCompilerT6: Does nothing without any KeyValuePairs", "[keyvaluepairs][t6]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + sut->FinalizeZone(testContext.m_context); + + REQUIRE(testContext.m_memory.GetAllocationCount() == 0u); + REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 0u); + } + + TEST_CASE("KeyValuePairsCompilerT6: Creates KeyValuePairs asset with identical name to the zone", "[keyvaluepairs][t6]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + testContext.m_kvp_creator.AddKeyValuePair(CommonKeyValuePair("ipak_read", "test_ipak")); + + sut->FinalizeZone(testContext.m_context); + + REQUIRE(testContext.m_memory.GetAllocationCount() > 0u); + REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 1u); + + XAssetInfo* assetInfo = *dynamic_cast(testContext.m_zone.m_pools.get())->m_key_value_pairs->begin(); + REQUIRE(assetInfo); + REQUIRE(assetInfo->m_name == "test"); + + auto* asset = assetInfo->Asset(); + REQUIRE(asset->name == "test"s); + REQUIRE(asset->numVariables == 1u); + REQUIRE(asset->keyValuePairs != nullptr); + + REQUIRE(asset->keyValuePairs[0].keyHash == 0x0001bdc1); + REQUIRE(asset->keyValuePairs[0].namespaceHash == 0x0000d2d3); + REQUIRE(asset->keyValuePairs[0].value == "test_ipak"s); + } + + TEST_CASE("KeyValuePairsCompilerT6: Creates KeyValuePairs asset with predefined hash", "[keyvaluepairs][t6]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + testContext.m_kvp_creator.AddKeyValuePair(CommonKeyValuePair(0xDDEEFFAA, "hello_there")); + + sut->FinalizeZone(testContext.m_context); + + REQUIRE(testContext.m_memory.GetAllocationCount() > 0u); + REQUIRE(testContext.m_zone.m_pools->GetTotalAssetCount() == 1u); + + XAssetInfo* assetInfo = *dynamic_cast(testContext.m_zone.m_pools.get())->m_key_value_pairs->begin(); + REQUIRE(assetInfo); + REQUIRE(assetInfo->m_name == "test"); + + auto* asset = assetInfo->Asset(); + REQUIRE(asset->name == "test"s); + REQUIRE(asset->numVariables == 1u); + REQUIRE(asset->keyValuePairs != nullptr); + + REQUIRE(asset->keyValuePairs[0].keyHash == 0xDDEEFFAA); + REQUIRE(asset->keyValuePairs[0].namespaceHash == 0x0000d2d3); + REQUIRE(asset->keyValuePairs[0].value == "hello_there"s); + } +} // namespace test::game::t6::keyvaluepairs From ce3786f086184bfd1894ded4f2d562010890fc66 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 5 Jan 2025 00:13:46 +0000 Subject: [PATCH 02/11] chore: check exact paths of test executable and provide temp dir --- premake5.lua | 2 ++ test/Catch2Common.lua | 51 ++++++++++++++++++++++++++++++ test/Catch2Common/OatTestPaths.cpp | 27 ++++++++++++++++ test/Catch2Common/OatTestPaths.h | 10 ++++++ test/Catch2Common/main.cpp | 48 ++++++++++++++++++++++++++++ test/ObjCommonTests.lua | 2 ++ test/ObjCompilingTests.lua | 2 ++ test/ObjLoadingTests.lua | 2 ++ test/ParserTests.lua | 2 ++ test/ZoneCodeGeneratorLibTests.lua | 2 ++ test/ZoneCommonTests.lua | 2 ++ thirdparty/catch2.lua | 1 + 12 files changed, 151 insertions(+) create mode 100644 test/Catch2Common.lua create mode 100644 test/Catch2Common/OatTestPaths.cpp create mode 100644 test/Catch2Common/OatTestPaths.h create mode 100644 test/Catch2Common/main.cpp diff --git a/premake5.lua b/premake5.lua index 7ff24f46..b3962cbf 100644 --- a/premake5.lua +++ b/premake5.lua @@ -170,6 +170,7 @@ group "" -- ======================== -- Tests -- ======================== +include "test/Catch2Common.lua" include "test/ObjCommonTestUtils.lua" include "test/ObjCommonTests.lua" include "test/ObjCompilingTests.lua" @@ -181,6 +182,7 @@ include "test/ZoneCommonTests.lua" -- Tests group: Unit test and other tests projects group "Tests" + Catch2Common:project() ObjCommonTestUtils:project() ObjCommonTests:project() ObjCompilingTests:project() diff --git a/test/Catch2Common.lua b/test/Catch2Common.lua new file mode 100644 index 00000000..514a4ca6 --- /dev/null +++ b/test/Catch2Common.lua @@ -0,0 +1,51 @@ +Catch2Common = {} + +function Catch2Common:include(includes) + if includes:handle(self:name()) then + includedirs { + path.join(TestFolder(), "Catch2Common") + } + end +end + +function Catch2Common:link(links) + links:add(self:name()) + links:linkto(catch2) +end + +function Catch2Common:use() + +end + +function Catch2Common:name() + return "Catch2Common" +end + +function Catch2Common:project() + local folder = TestFolder() + local includes = Includes:create() + local links = Links:create() + + project(self:name()) + targetdir(TargetDirectoryTest) + location "%{wks.location}/test/%{prj.name}" + kind "StaticLib" + language "C++" + + files { + path.join(folder, "Catch2Common/**.h"), + path.join(folder, "Catch2Common/**.cpp") + } + + vpaths { + ["*"] = { + path.join(folder, "Catch2Common") + } + } + + self:include(includes) + catch2:include(includes) + + links:linkto(catch2) + links:linkall() +end diff --git a/test/Catch2Common/OatTestPaths.cpp b/test/Catch2Common/OatTestPaths.cpp new file mode 100644 index 00000000..0370a1ba --- /dev/null +++ b/test/Catch2Common/OatTestPaths.cpp @@ -0,0 +1,27 @@ +#include "OatTestPaths.h" + +#include + +namespace fs = std::filesystem; + +namespace oat::paths +{ + std::filesystem::path GetSourceDirectory() + { + return fs::current_path() / "src"; + } + + std::filesystem::path GetTestDirectory() + { + return fs::current_path() / "test"; + } + + std::filesystem::path GetTempDirectory() + { + auto result = fs::current_path() / "build" / ".tmp"; + if (!fs::is_directory(result)) + fs::create_directories(result); + + return result; + } +} // namespace oat::paths diff --git a/test/Catch2Common/OatTestPaths.h b/test/Catch2Common/OatTestPaths.h new file mode 100644 index 00000000..c63783cf --- /dev/null +++ b/test/Catch2Common/OatTestPaths.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace oat::paths +{ + std::filesystem::path GetSourceDirectory(); + std::filesystem::path GetTestDirectory(); + std::filesystem::path GetTempDirectory(); +} // namespace oat::paths diff --git a/test/Catch2Common/main.cpp b/test/Catch2Common/main.cpp new file mode 100644 index 00000000..05cb7ccb --- /dev/null +++ b/test/Catch2Common/main.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +namespace fs = std::filesystem; + +int main(const int argc, char* argv[]) +{ + const fs::path absoluteBinDir(fs::absolute(argv[0]).parent_path()); + + const auto expectedLibDir = absoluteBinDir.parent_path().parent_path(); + const auto expectedBuildDir = expectedLibDir.parent_path(); + const auto expectedRootDir = expectedBuildDir.parent_path(); + + if (absoluteBinDir.filename() != "tests" || expectedLibDir.filename() != "lib" || expectedBuildDir.filename() != "build") + { + std::cerr << std::format("Expected test binary to be in the folder it was compiled into (build/lib/?/tests) but was {}\n", absoluteBinDir.string()); + std::cerr << "Please do not move test executable out of compilation folder\n"; + return 1; + } + + const auto expectedSrcDir = expectedRootDir / "src"; + if (!fs::is_directory(expectedSrcDir)) + { + std::cerr << std::format("Expected source directory to exist in {}, but it did not\n", expectedSrcDir.string()); + std::cerr << "Please do not move test executable out of compilation folder\n"; + return 1; + } + + const auto expectedTestDir = expectedRootDir / "test"; + if (!fs::is_directory(expectedTestDir)) + { + std::cerr << std::format("Expected test directory to exist in {}, but it did not\n", expectedTestDir.string()); + std::cerr << "Please do not move test executable out of compilation folder\n"; + return 1; + } + + fs::current_path(expectedRootDir); + + const auto result = Catch::Session().run(argc, argv); + + const auto tempDir = expectedBuildDir / ".tmp"; + if (fs::is_directory(tempDir)) + fs::remove_all(tempDir); + + return result; +} diff --git a/test/ObjCommonTests.lua b/test/ObjCommonTests.lua index a1360e05..68607a16 100644 --- a/test/ObjCommonTests.lua +++ b/test/ObjCommonTests.lua @@ -44,6 +44,7 @@ function ObjCommonTests:project() } self:include(includes) + Catch2Common:include(includes) ObjCommon:include(includes) ObjImage:include(includes) catch2:include(includes) @@ -51,5 +52,6 @@ function ObjCommonTests:project() links:linkto(ObjCommon) links:linkto(ObjImage) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() end diff --git a/test/ObjCompilingTests.lua b/test/ObjCompilingTests.lua index fc08d7a7..e521a743 100644 --- a/test/ObjCompilingTests.lua +++ b/test/ObjCompilingTests.lua @@ -43,6 +43,7 @@ function ObjCompilingTests:project() } self:include(includes) + Catch2Common:include(includes) ObjCommonTestUtils:include(includes) ParserTestUtils:include(includes) ObjLoading:include(includes) @@ -54,5 +55,6 @@ function ObjCompilingTests:project() links:linkto(ObjLoading) links:linkto(ObjCompiling) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() end diff --git a/test/ObjLoadingTests.lua b/test/ObjLoadingTests.lua index 2fc39089..339ca606 100644 --- a/test/ObjLoadingTests.lua +++ b/test/ObjLoadingTests.lua @@ -43,6 +43,7 @@ function ObjLoadingTests:project() } self:include(includes) + Catch2Common:include(includes) ObjCommonTestUtils:include(includes) ParserTestUtils:include(includes) ObjLoading:include(includes) @@ -52,5 +53,6 @@ function ObjLoadingTests:project() links:linkto(ParserTestUtils) links:linkto(ObjLoading) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() end diff --git a/test/ParserTests.lua b/test/ParserTests.lua index bc19f2de..1233c4bc 100644 --- a/test/ParserTests.lua +++ b/test/ParserTests.lua @@ -43,6 +43,7 @@ function ParserTests:project() } self:include(includes) + Catch2Common:include(includes) ParserTestUtils:include(includes) Parser:include(includes) catch2:include(includes) @@ -50,5 +51,6 @@ function ParserTests:project() links:linkto(ParserTestUtils) links:linkto(Parser) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() end diff --git a/test/ZoneCodeGeneratorLibTests.lua b/test/ZoneCodeGeneratorLibTests.lua index bb8679a1..3be5ae66 100644 --- a/test/ZoneCodeGeneratorLibTests.lua +++ b/test/ZoneCodeGeneratorLibTests.lua @@ -43,6 +43,7 @@ function ZoneCodeGeneratorLibTests:project() } self:include(includes) + Catch2Common:include(includes) ZoneCodeGeneratorLib:include(includes) ParserTestUtils:include(includes) catch2:include(includes) @@ -50,5 +51,6 @@ function ZoneCodeGeneratorLibTests:project() links:linkto(ZoneCodeGeneratorLib) links:linkto(ParserTestUtils) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() end diff --git a/test/ZoneCommonTests.lua b/test/ZoneCommonTests.lua index cec3edd9..4b237e01 100644 --- a/test/ZoneCommonTests.lua +++ b/test/ZoneCommonTests.lua @@ -45,6 +45,7 @@ function ZoneCommonTests:project() } self:include(includes) + Catch2Common:include(includes) ObjCommonTestUtils:include(includes) ZoneCommon:include(includes) catch2:include(includes) @@ -52,6 +53,7 @@ function ZoneCommonTests:project() links:linkto(ObjCommonTestUtils) links:linkto(ZoneCommon) links:linkto(catch2) + links:linkto(Catch2Common) links:linkall() ZoneCode:use() diff --git a/thirdparty/catch2.lua b/thirdparty/catch2.lua index f43d7205..0b7cdeaf 100644 --- a/thirdparty/catch2.lua +++ b/thirdparty/catch2.lua @@ -49,6 +49,7 @@ function catch2:project() defines { "DO_NOT_USE_WMAIN", + "CATCH_AMALGAMATED_CUSTOM_MAIN", "_CRT_SECURE_NO_WARNINGS" } From 83833cb84e11e22a175c7a1d6b484e6d590e99fa Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 5 Jan 2025 09:00:13 +0000 Subject: [PATCH 03/11] fix: use canonical instead of absolute in test initialization --- test/Catch2Common/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Catch2Common/main.cpp b/test/Catch2Common/main.cpp index 05cb7ccb..b00818dc 100644 --- a/test/Catch2Common/main.cpp +++ b/test/Catch2Common/main.cpp @@ -7,7 +7,7 @@ namespace fs = std::filesystem; int main(const int argc, char* argv[]) { - const fs::path absoluteBinDir(fs::absolute(argv[0]).parent_path()); + const fs::path absoluteBinDir(fs::canonical(argv[0]).parent_path()); const auto expectedLibDir = absoluteBinDir.parent_path().parent_path(); const auto expectedBuildDir = expectedLibDir.parent_path(); From 67fb11506c81051438898851019e4ac702c9e619 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 5 Jan 2025 09:30:59 +0000 Subject: [PATCH 04/11] chore: initialize ipak and iwd post processors immediately in constructor --- .../Image/ImageIPakPostProcessor.cpp | 28 +++++++------------ .../Image/ImageIPakPostProcessor.h | 3 +- .../Image/ImageIwdPostProcessor.cpp | 28 +++++++------------ .../Image/ImageIwdPostProcessor.h | 3 +- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp index 6c712d70..81932859 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp @@ -2,7 +2,8 @@ #include "IPak/IPakCreator.h" -#include +#include +#include AbstractImageIPakPostProcessor::AbstractImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, @@ -12,26 +13,24 @@ AbstractImageIPakPostProcessor::AbstractImageIPakPostProcessor(const ZoneDefinit m_search_path(searchPath), m_ipak_creator(zoneStates.GetZoneAssetCreationState()), m_out_dir(outDir), - m_initialized(false), m_obj_container_index(0u), m_current_ipak(nullptr), m_current_ipak_start_index(0u), m_current_ipak_end_index(0u) { + FindNextObjContainer(); } bool AbstractImageIPakPostProcessor::AppliesToZoneDefinition(const ZoneDefinitionContext& zoneDefinition) { - for (const auto& objContainer : zoneDefinition.m_zone_definition.m_obj_containers) - { - if (objContainer.m_type == ZoneDefinitionObjContainerType::IPAK) - return true; - } - - return false; + return std::ranges::any_of(zoneDefinition.m_zone_definition.m_obj_containers, + [](const ZoneDefinitionObjContainer& objContainer) + { + return objContainer.m_type == ZoneDefinitionObjContainerType::IPAK; + }); } -void AbstractImageIPakPostProcessor::FindNextObjContainer(AssetCreationContext& context) +void AbstractImageIPakPostProcessor::FindNextObjContainer() { const auto objContainerCount = m_zone_definition.m_zone_definition.m_obj_containers.size(); while (m_obj_container_index < objContainerCount) @@ -55,15 +54,8 @@ void AbstractImageIPakPostProcessor::PostProcessAsset(XAssetInfoGeneric& assetIn if (assetInfo.m_name.empty() || assetInfo.m_name[0] == ',') return; - // Initialize on first image occurance - if (!m_initialized) - { - FindNextObjContainer(context); - m_initialized = true; - } - while (m_current_ipak && m_zone_definition.m_asset_index_in_definition >= m_current_ipak_end_index) - FindNextObjContainer(context); + FindNextObjContainer(); if (m_current_ipak && m_zone_definition.m_asset_index_in_definition <= m_current_ipak_start_index) m_current_ipak->AddImage(assetInfo.m_name); diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.h b/src/ObjCompiling/Image/ImageIPakPostProcessor.h index 36638f34..2c99ee60 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.h +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.h @@ -20,14 +20,13 @@ public: void FinalizeZone(AssetCreationContext& context) override; private: - void FindNextObjContainer(AssetCreationContext& context); + void FindNextObjContainer(); const ZoneDefinitionContext& m_zone_definition; ISearchPath& m_search_path; IPakCreator& m_ipak_creator; const std::filesystem::path& m_out_dir; - bool m_initialized; unsigned m_obj_container_index; IPakToCreate* m_current_ipak; unsigned m_current_ipak_start_index; diff --git a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp index c9b05254..47eed997 100644 --- a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp @@ -2,8 +2,9 @@ #include "Iwd/IwdCreator.h" +#include #include -#include +#include AbstractImageIwdPostProcessor::AbstractImageIwdPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, @@ -13,26 +14,24 @@ AbstractImageIwdPostProcessor::AbstractImageIwdPostProcessor(const ZoneDefinitio m_search_path(searchPath), m_iwd_creator(zoneStates.GetZoneAssetCreationState()), m_out_dir(outDir), - m_initialized(false), m_obj_container_index(0u), m_current_iwd(nullptr), m_current_iwd_start_index(0u), m_current_iwd_end_index(0u) { + FindNextObjContainer(); } bool AbstractImageIwdPostProcessor::AppliesToZoneDefinition(const ZoneDefinitionContext& zoneDefinition) { - for (const auto& objContainer : zoneDefinition.m_zone_definition.m_obj_containers) - { - if (objContainer.m_type == ZoneDefinitionObjContainerType::IWD) - return true; - } - - return false; + return std::ranges::any_of(zoneDefinition.m_zone_definition.m_obj_containers, + [](const ZoneDefinitionObjContainer& objContainer) + { + return objContainer.m_type == ZoneDefinitionObjContainerType::IWD; + }); } -void AbstractImageIwdPostProcessor::FindNextObjContainer(AssetCreationContext& context) +void AbstractImageIwdPostProcessor::FindNextObjContainer() { const auto objContainerCount = m_zone_definition.m_zone_definition.m_obj_containers.size(); while (m_obj_container_index < objContainerCount) @@ -56,15 +55,8 @@ void AbstractImageIwdPostProcessor::PostProcessAsset(XAssetInfoGeneric& assetInf if (assetInfo.m_name.empty() || assetInfo.m_name[0] == ',') return; - // Initialize on first image occurance - if (!m_initialized) - { - FindNextObjContainer(context); - m_initialized = true; - } - while (m_current_iwd && m_zone_definition.m_asset_index_in_definition >= m_current_iwd_end_index) - FindNextObjContainer(context); + FindNextObjContainer(); if (m_current_iwd && m_zone_definition.m_asset_index_in_definition <= m_current_iwd_start_index) m_current_iwd->AddFile(std::format("images/{}.iwi", assetInfo.m_name)); diff --git a/src/ObjCompiling/Image/ImageIwdPostProcessor.h b/src/ObjCompiling/Image/ImageIwdPostProcessor.h index 62cfe50a..12c53828 100644 --- a/src/ObjCompiling/Image/ImageIwdPostProcessor.h +++ b/src/ObjCompiling/Image/ImageIwdPostProcessor.h @@ -20,14 +20,13 @@ public: void FinalizeZone(AssetCreationContext& context) override; private: - void FindNextObjContainer(AssetCreationContext& context); + void FindNextObjContainer(); const ZoneDefinitionContext& m_zone_definition; ISearchPath& m_search_path; IwdCreator& m_iwd_creator; const std::filesystem::path& m_out_dir; - bool m_initialized; unsigned m_obj_container_index; IwdToCreate* m_current_iwd; unsigned m_current_iwd_start_index; From cacccf64e175cb8a388e64a38d313dd84b23317c Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 5 Jan 2025 10:08:21 +0000 Subject: [PATCH 05/11] fix: make sure kvps are in a deterministic order --- .../KeyValuePairs/KeyValuePairsCreator.cpp | 19 ++++ .../KeyValuePairs/KeyValuePairsCreator.h | 2 +- .../KeyValuePairsCreatorTest.cpp | 101 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 test/ObjCompilingTests/KeyValuePairs/KeyValuePairsCreatorTest.cpp diff --git a/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.cpp b/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.cpp index eac08bae..348a6177 100644 --- a/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.cpp +++ b/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.cpp @@ -1,7 +1,9 @@ #include "KeyValuePairsCreator.h" +#include #include #include +#include CommonKeyValuePair::CommonKeyValuePair(std::string keyStr, std::string value) : m_key_str(std::move(keyStr)), @@ -49,6 +51,23 @@ void KeyValuePairsCreator::Finalize(const ZoneDefinition& zoneDefinition) } } } + + std::ranges::sort(m_key_value_pairs, + [](const CommonKeyValuePair& v0, const CommonKeyValuePair& v1) + { + if (v0.m_key_str.has_value()) + { + if (!v1.m_key_str.has_value()) + return true; + + return *v0.m_key_str < *v1.m_key_str; + } + + if (!v1.m_key_hash.has_value()) + return false; + + return *v0.m_key_hash < *v1.m_key_hash; + }); } std::vector KeyValuePairsCreator::GetFinalKeyValuePairs() diff --git a/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.h b/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.h index 372400d1..eb91d33f 100644 --- a/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.h +++ b/src/ObjCompiling/KeyValuePairs/KeyValuePairsCreator.h @@ -18,7 +18,7 @@ public: std::string m_value; }; -class KeyValuePairsCreator : public IZoneAssetCreationState +class KeyValuePairsCreator final : public IZoneAssetCreationState { public: void AddKeyValuePair(CommonKeyValuePair keyValuePair); diff --git a/test/ObjCompilingTests/KeyValuePairs/KeyValuePairsCreatorTest.cpp b/test/ObjCompilingTests/KeyValuePairs/KeyValuePairsCreatorTest.cpp new file mode 100644 index 00000000..48bffd61 --- /dev/null +++ b/test/ObjCompilingTests/KeyValuePairs/KeyValuePairsCreatorTest.cpp @@ -0,0 +1,101 @@ +#include "KeyValuePairs/KeyValuePairsCreator.h" + +#include "Utils/TestMemoryManager.h" + +#include +#include +#include +#include + +using namespace std::string_literals; + +namespace test::keyvaluepairs +{ + TEST_CASE("KeyValuePairsCreator: ZoneDefinition with no properties produces no KeyValuePairs", "[keyvaluepairs]") + { + KeyValuePairsCreator sut; + + ZoneDefinition zoneDefinition; + sut.Finalize(zoneDefinition); + + const auto kvps = sut.GetFinalKeyValuePairs(); + + REQUIRE(kvps.empty()); + } + + TEST_CASE("KeyValuePairsCreator: ZoneDefinition with unrelated properties produce no KeyValuePairs", "[keyvaluepairs]") + { + KeyValuePairsCreator sut; + + ZoneDefinition zoneDefinition; + zoneDefinition.m_properties.AddProperty("linker.test", "yes"); + sut.Finalize(zoneDefinition); + + const auto kvps = sut.GetFinalKeyValuePairs(); + + REQUIRE(kvps.empty()); + } + + TEST_CASE("KeyValuePairsCreator: ZoneDefinition with level properties produce KeyValuePairs", "[keyvaluepairs]") + { + KeyValuePairsCreator sut; + + ZoneDefinition zoneDefinition; + zoneDefinition.m_properties.AddProperty("linker.test", "yes"); + zoneDefinition.m_properties.AddProperty("level.random", "lemao"); + sut.Finalize(zoneDefinition); + + const auto kvps = sut.GetFinalKeyValuePairs(); + + REQUIRE(kvps.size() == 1); + REQUIRE(kvps[0].m_key_str); + REQUIRE(!kvps[0].m_key_hash); + REQUIRE(*kvps[0].m_key_str == "random"); + REQUIRE(kvps[0].m_value == "lemao"); + } + + TEST_CASE("KeyValuePairsCreator: ZoneDefinition can have level properties with hash", "[keyvaluepairs]") + { + KeyValuePairsCreator sut; + + ZoneDefinition zoneDefinition; + zoneDefinition.m_properties.AddProperty("level.@D34DB33F", "yes"); + zoneDefinition.m_properties.AddProperty("level.@112233", "no"); + zoneDefinition.m_properties.AddProperty("level.@0", "maybe"); + sut.Finalize(zoneDefinition); + + const auto kvps = sut.GetFinalKeyValuePairs(); + + REQUIRE(kvps.size() == 3); + REQUIRE(*kvps[0].m_key_hash == 0); + REQUIRE(kvps[0].m_value == "maybe"); + REQUIRE(*kvps[1].m_key_hash == 0x112233); + REQUIRE(kvps[1].m_value == "no"); + REQUIRE(*kvps[2].m_key_hash == 0xD34DB33F); + REQUIRE(kvps[2].m_value == "yes"); + } + + TEST_CASE("KeyValuePairsCreator: ZoneDefinition can have level properties with name and/or hash", "[keyvaluepairs]") + { + KeyValuePairsCreator sut; + + ZoneDefinition zoneDefinition; + zoneDefinition.m_properties.AddProperty("level.ipak_read", "asdf"); + zoneDefinition.m_properties.AddProperty("level.hello_world", "foo"); + zoneDefinition.m_properties.AddProperty("level.@D34DB33F", "yes"); + zoneDefinition.m_properties.AddProperty("level.@112233", "no"); + sut.Finalize(zoneDefinition); + + const auto kvps = sut.GetFinalKeyValuePairs(); + + REQUIRE(kvps.size() == 4); + REQUIRE(*kvps[0].m_key_str == "hello_world"); + REQUIRE(kvps[0].m_value == "foo"); + REQUIRE(*kvps[1].m_key_str == "ipak_read"); + REQUIRE(kvps[1].m_value == "asdf"); + REQUIRE(*kvps[2].m_key_hash == 0x112233); + REQUIRE(kvps[2].m_value == "no"); + REQUIRE(*kvps[3].m_key_hash == 0xD34DB33F); + REQUIRE(kvps[3].m_value == "yes"); + } +} // namespace test::keyvaluepairs From e0f8b3d3ca774ff9448b83c72ec00487beb864a6 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 7 Jan 2025 00:02:38 +0100 Subject: [PATCH 06/11] chore: add abstraction for opening output files to be able to mock it --- src/Linker/ZoneCreation/ZoneCreator.cpp | 5 +- src/ObjCommon/SearchPath/IOutputPath.h | 18 +++++++ .../SearchPath/OutputPathFilesystem.cpp | 27 ++++++++++ .../SearchPath/OutputPathFilesystem.h | 16 ++++++ src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp | 6 +-- src/ObjCompiling/Game/IW3/ObjCompilerIW3.h | 4 +- src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp | 6 +-- src/ObjCompiling/Game/IW4/ObjCompilerIW4.h | 4 +- src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp | 6 +-- src/ObjCompiling/Game/IW5/ObjCompilerIW5.h | 4 +- src/ObjCompiling/Game/T5/ObjCompilerT5.cpp | 6 +-- src/ObjCompiling/Game/T5/ObjCompilerT5.h | 4 +- src/ObjCompiling/Game/T6/ObjCompilerT6.cpp | 6 +-- src/ObjCompiling/Game/T6/ObjCompilerT6.h | 4 +- src/ObjCompiling/IObjCompiler.h | 10 ++-- src/ObjCompiling/Image/IPak/IPakCreator.cpp | 11 ++--- src/ObjCompiling/Image/IPak/IPakCreator.h | 8 +-- .../Image/ImageIPakPostProcessor.cpp | 2 +- .../Image/ImageIPakPostProcessor.h | 11 ++--- .../Image/ImageIwdPostProcessor.cpp | 2 +- .../Image/ImageIwdPostProcessor.h | 11 ++--- src/ObjCompiling/Iwd/IwdCreator.cpp | 14 +++--- src/ObjCompiling/Iwd/IwdCreator.h | 8 +-- .../SearchPath/MockOutputPath.cpp | 49 +++++++++++++++++++ .../SearchPath/MockOutputPath.h | 28 +++++++++++ 25 files changed, 202 insertions(+), 68 deletions(-) create mode 100644 src/ObjCommon/SearchPath/IOutputPath.h create mode 100644 src/ObjCommon/SearchPath/OutputPathFilesystem.cpp create mode 100644 src/ObjCommon/SearchPath/OutputPathFilesystem.h create mode 100644 test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp create mode 100644 test/ObjCommonTestUtils/SearchPath/MockOutputPath.h diff --git a/src/Linker/ZoneCreation/ZoneCreator.cpp b/src/Linker/ZoneCreation/ZoneCreator.cpp index 21bc80f0..29f35ed1 100644 --- a/src/Linker/ZoneCreation/ZoneCreator.cpp +++ b/src/Linker/ZoneCreation/ZoneCreator.cpp @@ -3,6 +3,7 @@ #include "Gdt/GdtLookup.h" #include "IObjCompiler.h" #include "IObjLoader.h" +#include "SearchPath/OutputPathFilesystem.h" #include @@ -67,8 +68,10 @@ namespace zone_creator ZoneDefinitionContext zoneDefinitionContext(*context.m_definition); AssetCreationContext creationContext(*zone, &creatorCollection, &ignoredAssetLookup); + OutputPathFilesystem outDir(context.m_out_dir); + OutputPathFilesystem cacheDir(context.m_cache_dir); objCompiler->ConfigureCreatorCollection( - creatorCollection, *zone, zoneDefinitionContext, *context.m_asset_search_path, lookup, creationContext, context.m_out_dir, context.m_cache_dir); + creatorCollection, *zone, zoneDefinitionContext, *context.m_asset_search_path, lookup, creationContext, outDir, cacheDir); objLoader->ConfigureCreatorCollection(creatorCollection, *zone, *context.m_asset_search_path, lookup); for (const auto& assetEntry : context.m_definition->m_assets) diff --git a/src/ObjCommon/SearchPath/IOutputPath.h b/src/ObjCommon/SearchPath/IOutputPath.h new file mode 100644 index 00000000..4073a12c --- /dev/null +++ b/src/ObjCommon/SearchPath/IOutputPath.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +class IOutputPath +{ +public: + IOutputPath() = default; + virtual ~IOutputPath() = default; + IOutputPath(const IOutputPath& other) = default; + IOutputPath(IOutputPath&& other) noexcept = default; + IOutputPath& operator=(const IOutputPath& other) = default; + IOutputPath& operator=(IOutputPath&& other) noexcept = default; + + virtual std::unique_ptr Open(const std::string& fileName) = 0; +}; diff --git a/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp new file mode 100644 index 00000000..c0954f94 --- /dev/null +++ b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp @@ -0,0 +1,27 @@ +#include "OutputPathFilesystem.h" + +#include + +namespace fs = std::filesystem; + +OutputPathFilesystem::OutputPathFilesystem(const fs::path& path) + : m_path(fs::canonical(path)) +{ +} + +std::unique_ptr OutputPathFilesystem::Open(const std::string& fileName) +{ + const auto fullNewPath = fs::canonical(m_path / fileName); + + if (!fullNewPath.string().starts_with(m_path.string())) + return nullptr; + + const auto containingDirectory = fullNewPath.parent_path(); + fs::create_directories(containingDirectory); + + std::ofstream stream(fullNewPath, std::ios::binary | std::ios::out); + if (!stream.is_open()) + return nullptr; + + return std::make_unique(std::move(stream)); +} diff --git a/src/ObjCommon/SearchPath/OutputPathFilesystem.h b/src/ObjCommon/SearchPath/OutputPathFilesystem.h new file mode 100644 index 00000000..827783f5 --- /dev/null +++ b/src/ObjCommon/SearchPath/OutputPathFilesystem.h @@ -0,0 +1,16 @@ +#pragma once + +#include "IOutputPath.h" + +#include + +class OutputPathFilesystem final : public IOutputPath +{ +public: + explicit OutputPathFilesystem(const std::filesystem::path& path); + + std::unique_ptr Open(const std::string& fileName) override; + +private: + std::filesystem::path m_path; +}; diff --git a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp index b3914ae8..d1be036d 100644 --- a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp +++ b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp @@ -22,7 +22,7 @@ namespace const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir) + IOutputPath& outDir) { auto& memory = *zone.GetMemory(); @@ -37,8 +37,8 @@ void ObjCompiler::ConfigureCreatorCollection(AssetCreatorCollection& collection, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir, - const fs::path& cacheDir) const + IOutputPath& outDir, + IOutputPath& cacheDir) const { ConfigurePostProcessors(collection, zone, zoneDefinition, searchPath, zoneStates, outDir); } diff --git a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.h b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.h index 771821a7..677d3256 100644 --- a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.h +++ b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.h @@ -13,7 +13,7 @@ namespace IW3 ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const override; + IOutputPath& outDir, + IOutputPath& cacheDir) const override; }; } // namespace IW3 diff --git a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp index 12f8e839..d1b3e766 100644 --- a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp +++ b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp @@ -22,7 +22,7 @@ namespace const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir) + IOutputPath& outDir) { auto& memory = *zone.GetMemory(); @@ -37,8 +37,8 @@ void ObjCompiler::ConfigureCreatorCollection(AssetCreatorCollection& collection, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir, - const fs::path& cacheDir) const + IOutputPath& outDir, + IOutputPath& cacheDir) const { ConfigurePostProcessors(collection, zone, zoneDefinition, searchPath, zoneStates, outDir); } diff --git a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.h b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.h index 522b3c51..ce83806e 100644 --- a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.h +++ b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.h @@ -13,7 +13,7 @@ namespace IW4 ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const override; + IOutputPath& outDir, + IOutputPath& cacheDir) const override; }; } // namespace IW4 diff --git a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp index dbfc20d0..b04f4aeb 100644 --- a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp +++ b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp @@ -22,7 +22,7 @@ namespace const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir) + IOutputPath& outDir) { auto& memory = *zone.GetMemory(); @@ -37,8 +37,8 @@ void ObjCompiler::ConfigureCreatorCollection(AssetCreatorCollection& collection, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir, - const fs::path& cacheDir) const + IOutputPath& outDir, + IOutputPath& cacheDir) const { ConfigurePostProcessors(collection, zone, zoneDefinition, searchPath, zoneStates, outDir); } diff --git a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.h b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.h index 80805f77..8fc6a101 100644 --- a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.h +++ b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.h @@ -13,7 +13,7 @@ namespace IW5 ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const override; + IOutputPath& outDir, + IOutputPath& cacheDir) const override; }; } // namespace IW5 diff --git a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp index 5fb02146..da94980a 100644 --- a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp +++ b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp @@ -22,7 +22,7 @@ namespace const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir) + IOutputPath& outDir) { auto& memory = *zone.GetMemory(); @@ -37,8 +37,8 @@ void ObjCompiler::ConfigureCreatorCollection(AssetCreatorCollection& collection, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir, - const fs::path& cacheDir) const + IOutputPath& outDir, + IOutputPath& cacheDir) const { ConfigurePostProcessors(collection, zone, zoneDefinition, searchPath, zoneStates, outDir); } diff --git a/src/ObjCompiling/Game/T5/ObjCompilerT5.h b/src/ObjCompiling/Game/T5/ObjCompilerT5.h index bd216b7c..fd31c8c3 100644 --- a/src/ObjCompiling/Game/T5/ObjCompilerT5.h +++ b/src/ObjCompiling/Game/T5/ObjCompilerT5.h @@ -13,7 +13,7 @@ namespace T5 ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const override; + IOutputPath& outDir, + IOutputPath& cacheDir) const override; }; } // namespace T5 diff --git a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp index 9be2a476..bc59cf06 100644 --- a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp @@ -28,7 +28,7 @@ namespace const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir) + IOutputPath& outDir) { auto& memory = *zone.GetMemory(); @@ -46,8 +46,8 @@ void ObjCompiler::ConfigureCreatorCollection(AssetCreatorCollection& collection, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const fs::path& outDir, - const fs::path& cacheDir) const + IOutputPath& outDir, + IOutputPath& cacheDir) const { ConfigureCompilers(collection, zone, zoneDefinition, searchPath, zoneStates); ConfigurePostProcessors(collection, zone, zoneDefinition, searchPath, zoneStates, outDir); diff --git a/src/ObjCompiling/Game/T6/ObjCompilerT6.h b/src/ObjCompiling/Game/T6/ObjCompilerT6.h index 73d30655..5e935b02 100644 --- a/src/ObjCompiling/Game/T6/ObjCompilerT6.h +++ b/src/ObjCompiling/Game/T6/ObjCompilerT6.h @@ -13,7 +13,7 @@ namespace T6 ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const override; + IOutputPath& outDir, + IOutputPath& cacheDir) const override; }; } // namespace T6 diff --git a/src/ObjCompiling/IObjCompiler.h b/src/ObjCompiling/IObjCompiler.h index 26bac9ba..8f7e32fd 100644 --- a/src/ObjCompiling/IObjCompiler.h +++ b/src/ObjCompiling/IObjCompiler.h @@ -4,14 +4,10 @@ #include "Asset/IZoneAssetCreationState.h" #include "Asset/ZoneDefinitionContext.h" #include "Gdt/IGdtQueryable.h" +#include "SearchPath/IOutputPath.h" #include "SearchPath/ISearchPath.h" -#include "Zone/Definition/ZoneDefinition.h" #include "Zone/Zone.h" -#include -#include -#include - class IObjCompiler { public: @@ -28,8 +24,8 @@ public: ISearchPath& searchPath, IGdtQueryable& gdt, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir, - const std::filesystem::path& cacheDir) const = 0; + IOutputPath& outDir, + IOutputPath& cacheDir) const = 0; static const IObjCompiler* GetObjCompilerForGame(GameId game); }; diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.cpp b/src/ObjCompiling/Image/IPak/IPakCreator.cpp index d18c127c..9ad4435c 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.cpp +++ b/src/ObjCompiling/Image/IPak/IPakCreator.cpp @@ -379,17 +379,16 @@ void IPakToCreate::AddImage(std::string imageName) m_image_names.emplace_back(std::move(imageName)); } -void IPakToCreate::Build(ISearchPath& searchPath, const std::filesystem::path& outPath) +void IPakToCreate::Build(ISearchPath& searchPath, IOutputPath& outPath) { - auto filePath = outPath / std::format("{}.ipak", m_name); - std::ofstream file(filePath, std::ios::out | std::ios::binary); - if (!file.is_open()) + const auto file = outPath.Open(std::format("{}.ipak", m_name)); + if (!file) { std::cerr << std::format("Failed to open file for ipak {}\n", m_name); return; } - IPakWriter writer(file, searchPath, m_image_names); + IPakWriter writer(*file, searchPath, m_image_names); writer.Write(); std::cout << std::format("Created ipak {} with {} entries\n", m_name, m_image_names.size()); @@ -421,7 +420,7 @@ IPakToCreate* IPakCreator::GetOrAddIPak(const std::string& ipakName) return result; } -void IPakCreator::Finalize(ISearchPath& searchPath, const std::filesystem::path& outPath) +void IPakCreator::Finalize(ISearchPath& searchPath, IOutputPath& outPath) { for (const auto& ipakToCreate : m_ipaks) ipakToCreate->Build(searchPath, outPath); diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.h b/src/ObjCompiling/Image/IPak/IPakCreator.h index 6b9a7565..1218d63c 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.h +++ b/src/ObjCompiling/Image/IPak/IPakCreator.h @@ -2,9 +2,9 @@ #include "Asset/IZoneAssetCreationState.h" #include "KeyValuePairs/KeyValuePairsCreator.h" +#include "SearchPath/IOutputPath.h" #include "SearchPath/ISearchPath.h" -#include #include #include #include @@ -15,14 +15,14 @@ public: explicit IPakToCreate(std::string name); void AddImage(std::string imageName); - void Build(ISearchPath& searchPath, const std::filesystem::path& outPath); + void Build(ISearchPath& searchPath, IOutputPath& outPath); private: std::string m_name; std::vector m_image_names; }; -class IPakCreator : public IZoneAssetCreationState +class IPakCreator final : public IZoneAssetCreationState { public: IPakCreator(); @@ -30,7 +30,7 @@ public: void Inject(ZoneAssetCreationInjection& inject) override; IPakToCreate* GetOrAddIPak(const std::string& ipakName); - void Finalize(ISearchPath& searchPath, const std::filesystem::path& outPath); + void Finalize(ISearchPath& searchPath, IOutputPath& outPath); private: KeyValuePairsCreator* m_kvp_creator; diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp index 81932859..09a66139 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp @@ -8,7 +8,7 @@ AbstractImageIPakPostProcessor::AbstractImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir) + IOutputPath& outDir) : m_zone_definition(zoneDefinition), m_search_path(searchPath), m_ipak_creator(zoneStates.GetZoneAssetCreationState()), diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.h b/src/ObjCompiling/Image/ImageIPakPostProcessor.h index 2c99ee60..675b79cf 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.h +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.h @@ -3,8 +3,7 @@ #include "Asset/IAssetPostProcessor.h" #include "Asset/ZoneDefinitionContext.h" #include "Image/IPak/IPakCreator.h" - -#include +#include "SearchPath/IOutputPath.h" class AbstractImageIPakPostProcessor : public IAssetPostProcessor { @@ -12,7 +11,7 @@ public: AbstractImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir); + IOutputPath& outDir); static bool AppliesToZoneDefinition(const ZoneDefinitionContext& zoneDefinition); @@ -25,7 +24,7 @@ private: const ZoneDefinitionContext& m_zone_definition; ISearchPath& m_search_path; IPakCreator& m_ipak_creator; - const std::filesystem::path& m_out_dir; + IOutputPath& m_out_dir; unsigned m_obj_container_index; IPakToCreate* m_current_ipak; @@ -41,7 +40,7 @@ public: ImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir) + IOutputPath& outDir) : AbstractImageIPakPostProcessor(zoneDefinition, searchPath, zoneStates, outDir) { } @@ -49,5 +48,5 @@ public: [[nodiscard]] asset_type_t GetHandlingAssetType() const override { return AssetType::EnumEntry; - }; + } }; diff --git a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp index 47eed997..586fa9c0 100644 --- a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp @@ -9,7 +9,7 @@ AbstractImageIwdPostProcessor::AbstractImageIwdPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir) + IOutputPath& outDir) : m_zone_definition(zoneDefinition), m_search_path(searchPath), m_iwd_creator(zoneStates.GetZoneAssetCreationState()), diff --git a/src/ObjCompiling/Image/ImageIwdPostProcessor.h b/src/ObjCompiling/Image/ImageIwdPostProcessor.h index 12c53828..8225c29f 100644 --- a/src/ObjCompiling/Image/ImageIwdPostProcessor.h +++ b/src/ObjCompiling/Image/ImageIwdPostProcessor.h @@ -3,8 +3,7 @@ #include "Asset/IAssetPostProcessor.h" #include "Asset/ZoneDefinitionContext.h" #include "Iwd/IwdCreator.h" - -#include +#include "SearchPath/IOutputPath.h" class AbstractImageIwdPostProcessor : public IAssetPostProcessor { @@ -12,7 +11,7 @@ public: AbstractImageIwdPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir); + IOutputPath& outDir); static bool AppliesToZoneDefinition(const ZoneDefinitionContext& zoneDefinition); @@ -25,7 +24,7 @@ private: const ZoneDefinitionContext& m_zone_definition; ISearchPath& m_search_path; IwdCreator& m_iwd_creator; - const std::filesystem::path& m_out_dir; + IOutputPath& m_out_dir; unsigned m_obj_container_index; IwdToCreate* m_current_iwd; @@ -41,7 +40,7 @@ public: ImageIwdPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, ZoneAssetCreationStateContainer& zoneStates, - const std::filesystem::path& outDir) + IOutputPath& outDir) : AbstractImageIwdPostProcessor(zoneDefinition, searchPath, zoneStates, outDir) { } @@ -49,5 +48,5 @@ public: [[nodiscard]] asset_type_t GetHandlingAssetType() const override { return AssetType::EnumEntry; - }; + } }; diff --git a/src/ObjCompiling/Iwd/IwdCreator.cpp b/src/ObjCompiling/Iwd/IwdCreator.cpp index f995c32b..9d4932fe 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.cpp +++ b/src/ObjCompiling/Iwd/IwdCreator.cpp @@ -18,19 +18,19 @@ void IwdToCreate::AddFile(std::string filePath) m_file_paths.emplace_back(std::move(filePath)); } -void IwdToCreate::Build(ISearchPath& searchPath, const std::filesystem::path& outPath) +void IwdToCreate::Build(ISearchPath& searchPath, IOutputPath& outPath) { - auto filePath = outPath / std::format("{}.iwd", m_name); - std::ofstream file(filePath, std::ios::out | std::ios::binary); - if (!file.is_open()) + const auto fileName = std::format("{}.iwd", m_name); + const auto file = outPath.Open(fileName); + if (!file) { std::cerr << std::format("Failed to open file for iwd {}\n", m_name); return; } - auto functions = FileToZlibWrapper::CreateFunctions32ForFile(&file); + auto functions = FileToZlibWrapper::CreateFunctions32ForFile(file.get()); - auto zipFile = zipOpen2(filePath.string().c_str(), APPEND_STATUS_CREATE, nullptr, &functions); + const auto zipFile = zipOpen2(fileName.c_str(), APPEND_STATUS_CREATE, nullptr, &functions); if (!zipFile) { std::cerr << std::format("Failed to open file as zip for iwd {}\n", m_name); @@ -92,7 +92,7 @@ IwdToCreate* IwdCreator::GetOrAddIwd(const std::string& iwdName) return result; } -void IwdCreator::Finalize(ISearchPath& searchPath, const std::filesystem::path& outPath) +void IwdCreator::Finalize(ISearchPath& searchPath, IOutputPath& outPath) { std::cout << std::format("Writing {} iwd files to disk\n", m_iwds.size()); for (const auto& iwdToCreate : m_iwds) diff --git a/src/ObjCompiling/Iwd/IwdCreator.h b/src/ObjCompiling/Iwd/IwdCreator.h index 43b31fe0..2b73be50 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.h +++ b/src/ObjCompiling/Iwd/IwdCreator.h @@ -1,9 +1,9 @@ #pragma once #include "Asset/IZoneAssetCreationState.h" +#include "SearchPath/IOutputPath.h" #include "SearchPath/ISearchPath.h" -#include #include #include #include @@ -14,18 +14,18 @@ public: explicit IwdToCreate(std::string name); void AddFile(std::string filePath); - void Build(ISearchPath& searchPath, const std::filesystem::path& outPath); + void Build(ISearchPath& searchPath, IOutputPath& outPath); private: std::string m_name; std::vector m_file_paths; }; -class IwdCreator : public IZoneAssetCreationState +class IwdCreator final : public IZoneAssetCreationState { public: IwdToCreate* GetOrAddIwd(const std::string& iwdName); - void Finalize(ISearchPath& searchPath, const std::filesystem::path& outPath); + void Finalize(ISearchPath& searchPath, IOutputPath& outPath); private: std::unordered_map m_iwd_lookup; diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp new file mode 100644 index 00000000..6f4c457a --- /dev/null +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -0,0 +1,49 @@ +#include "MockOutputPath.h" + +#include + +namespace +{ + class MockFileWrapper final : public std::ostringstream + { + public: + MockFileWrapper(std::string name, std::vector& files) + : m_name(std::move(name)), + m_files(files) + { + } + + ~MockFileWrapper() override + { + m_files.emplace_back(std::move(m_name), str()); + } + + private: + std::string m_name; + std::vector& m_files; + }; +} // namespace + +MockOutputFile::MockOutputFile() = default; + +MockOutputFile::MockOutputFile(std::string name, std::string data) + : m_name(std::move(name)), + m_data(std::move(data)) +{ +} + +std::unique_ptr MockOutputPath::Open(const std::string& fileName) +{ + return std::make_unique(fileName, m_files); +} + +const MockOutputFile* MockOutputPath::GetMockedFile(const std::string& name) const +{ + for (const auto& file : m_files) + { + if (file.m_name == name) + return &file; + } + + return nullptr; +} diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h new file mode 100644 index 00000000..092916b0 --- /dev/null +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h @@ -0,0 +1,28 @@ +#pragma once + +#include "SearchPath/IOutputPath.h" + +#include +#include +#include + +class MockOutputFile +{ +public: + std::string m_name; + std::string m_data; + + MockOutputFile(); + MockOutputFile(std::string name, std::string data); +}; + +class MockOutputPath final : public IOutputPath +{ +public: + std::unique_ptr Open(const std::string& fileName) override; + + [[nodiscard]] const MockOutputFile* GetMockedFile(const std::string& name) const; + +private: + std::vector m_files; +}; From 8c8ceae0bd3f96ca34953b6efd7e2c20e21cca6b Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 7 Jan 2025 23:48:17 +0100 Subject: [PATCH 07/11] test: add unit test for ImageIPakPostProcessor --- .../SearchPath/OutputPathFilesystem.cpp | 4 +- src/ObjCompiling/Image/IPak/IPakCreator.cpp | 6 + src/ObjCompiling/Image/IPak/IPakCreator.h | 1 + .../Image/ImageIPakPostProcessor.cpp | 3 +- src/ObjCompiling/Iwd/IwdCreator.cpp | 1 + .../Zone/Definition/ZoneDefinition.cpp | 7 +- .../Zone/Definition/ZoneDefinition.h | 1 + .../SearchPath/MockOutputPath.cpp | 84 +++++- .../SearchPath/MockOutputPath.h | 5 +- .../Image/ImageIPakPostProcessorTest.cpp | 278 ++++++++++++++++++ 10 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp diff --git a/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp index c0954f94..39210c76 100644 --- a/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp +++ b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp @@ -5,13 +5,13 @@ namespace fs = std::filesystem; OutputPathFilesystem::OutputPathFilesystem(const fs::path& path) - : m_path(fs::canonical(path)) + : m_path(fs::weakly_canonical(path)) { } std::unique_ptr OutputPathFilesystem::Open(const std::string& fileName) { - const auto fullNewPath = fs::canonical(m_path / fileName); + const auto fullNewPath = fs::weakly_canonical(m_path / fileName); if (!fullNewPath.string().starts_with(m_path.string())) return nullptr; diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.cpp b/src/ObjCompiling/Image/IPak/IPakCreator.cpp index 9ad4435c..b91abb58 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.cpp +++ b/src/ObjCompiling/Image/IPak/IPakCreator.cpp @@ -394,6 +394,11 @@ void IPakToCreate::Build(ISearchPath& searchPath, IOutputPath& outPath) std::cout << std::format("Created ipak {} with {} entries\n", m_name, m_image_names.size()); } +const std::vector& IPakToCreate::GetImageNames() const +{ + return m_image_names; +} + IPakCreator::IPakCreator() : m_kvp_creator(nullptr) { @@ -412,6 +417,7 @@ IPakToCreate* IPakCreator::GetOrAddIPak(const std::string& ipakName) auto newIPak = std::make_unique(ipakName); auto* result = newIPak.get(); + m_ipak_lookup.emplace(ipakName, result); m_ipaks.emplace_back(std::move(newIPak)); assert(m_kvp_creator); diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.h b/src/ObjCompiling/Image/IPak/IPakCreator.h index 1218d63c..31eed0b2 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.h +++ b/src/ObjCompiling/Image/IPak/IPakCreator.h @@ -16,6 +16,7 @@ public: void AddImage(std::string imageName); void Build(ISearchPath& searchPath, IOutputPath& outPath); + [[nodiscard]] const std::vector& GetImageNames() const; private: std::string m_name; diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp index 09a66139..24c0ace2 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp @@ -3,7 +3,6 @@ #include "IPak/IPakCreator.h" #include -#include AbstractImageIPakPostProcessor::AbstractImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, @@ -57,7 +56,7 @@ void AbstractImageIPakPostProcessor::PostProcessAsset(XAssetInfoGeneric& assetIn while (m_current_ipak && m_zone_definition.m_asset_index_in_definition >= m_current_ipak_end_index) FindNextObjContainer(); - if (m_current_ipak && m_zone_definition.m_asset_index_in_definition <= m_current_ipak_start_index) + if (m_current_ipak && m_zone_definition.m_asset_index_in_definition >= m_current_ipak_start_index) m_current_ipak->AddImage(assetInfo.m_name); } diff --git a/src/ObjCompiling/Iwd/IwdCreator.cpp b/src/ObjCompiling/Iwd/IwdCreator.cpp index 9d4932fe..b25493e3 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.cpp +++ b/src/ObjCompiling/Iwd/IwdCreator.cpp @@ -87,6 +87,7 @@ IwdToCreate* IwdCreator::GetOrAddIwd(const std::string& iwdName) auto newIwd = std::make_unique(iwdName); auto* result = newIwd.get(); + m_iwd_lookup.emplace(iwdName, result); m_iwds.emplace_back(std::move(newIwd)); return result; diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp index 58774642..41c401da 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp @@ -1,10 +1,15 @@ #include "ZoneDefinition.h" ZoneDefinitionObjContainer::ZoneDefinitionObjContainer(std::string name, const ZoneDefinitionObjContainerType type, const unsigned start) + : ZoneDefinitionObjContainer(std::move(name), type, start, 0u) +{ +} + +ZoneDefinitionObjContainer::ZoneDefinitionObjContainer(std::string name, const ZoneDefinitionObjContainerType type, const unsigned start, const unsigned end) : m_name(std::move(name)), m_type(type), m_asset_start(start), - m_asset_end(0u) + m_asset_end(end) { } diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h index a00a5b57..0d64f161 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h @@ -32,6 +32,7 @@ public: unsigned m_asset_end; ZoneDefinitionObjContainer(std::string name, ZoneDefinitionObjContainerType type, unsigned start); + ZoneDefinitionObjContainer(std::string name, ZoneDefinitionObjContainerType type, unsigned start, unsigned end); }; class ZoneDefinitionAsset diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp index 6f4c457a..4b4db64c 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -1,24 +1,98 @@ #include "MockOutputPath.h" -#include +#include "Utils/ObjStream.h" namespace { - class MockFileWrapper final : public std::ostringstream + class MockFileBuffer final : public std::streambuf + { + public: + MockFileBuffer() + : m_pos(0u) + { + } + + std::vector data() + { + return std::move(m_data); + } + + protected: + int_type overflow(const int_type b) override + { + if (!std::char_traits::eq_int_type(b, std::char_traits::eof())) + { + m_data.insert(m_data.begin() + static_cast(m_pos), static_cast(b)); + ++m_pos; + } + + return b; + } + + std::streamsize xsputn(const char* ptr, const std::streamsize count) override + { + const auto curSize = m_data.size(); + const auto overrideCount = m_pos < curSize ? std::min(curSize - m_pos, static_cast(count)) : 0u; + const auto insertCount = count - overrideCount; + + if (overrideCount > 0) + { + memcpy(&m_data[m_pos], ptr, overrideCount); + m_pos += overrideCount; + ptr += overrideCount; + } + + if (insertCount > 0) + { + m_data.insert(m_data.begin() + static_cast(m_pos), ptr, ptr + insertCount); + m_pos += static_cast(insertCount); + } + + return count; + } + + pos_type seekoff(const off_type off, const std::ios_base::seekdir dir, const std::ios_base::openmode mode) override + { + if (dir == std::ios::beg) + return seekpos(off, mode); + if (dir == std::ios::cur) + return seekpos(static_cast(m_pos) + off, mode); + if (off < static_cast(m_data.size())) + return seekpos(static_cast(m_data.size()) - off, mode); + return std::char_traits::eof(); + } + + pos_type seekpos(const pos_type pos, std::ios_base::openmode) override + { + if (pos > m_data.size()) + m_data.resize(static_cast(pos)); + m_pos = static_cast(pos); + + return pos; + } + + private: + std::vector m_data; + std::size_t m_pos; + }; + + class MockFileWrapper final : public std::ostream { public: MockFileWrapper(std::string name, std::vector& files) - : m_name(std::move(name)), + : std::ostream(&m_buf), + m_name(std::move(name)), m_files(files) { } ~MockFileWrapper() override { - m_files.emplace_back(std::move(m_name), str()); + m_files.emplace_back(std::move(m_name), m_buf.data()); } private: + MockFileBuffer m_buf; std::string m_name; std::vector& m_files; }; @@ -26,7 +100,7 @@ namespace MockOutputFile::MockOutputFile() = default; -MockOutputFile::MockOutputFile(std::string name, std::string data) +MockOutputFile::MockOutputFile(std::string name, std::vector data) : m_name(std::move(name)), m_data(std::move(data)) { diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h index 092916b0..e85a4251 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h @@ -2,6 +2,7 @@ #include "SearchPath/IOutputPath.h" +#include #include #include #include @@ -10,10 +11,10 @@ class MockOutputFile { public: std::string m_name; - std::string m_data; + std::vector m_data; MockOutputFile(); - MockOutputFile(std::string name, std::string data); + MockOutputFile(std::string name, std::vector data); }; class MockOutputPath final : public IOutputPath diff --git a/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp b/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp new file mode 100644 index 00000000..29dd627f --- /dev/null +++ b/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp @@ -0,0 +1,278 @@ +#include "Image/ImageIPakPostProcessor.h" + +#include "Game/T6/T6.h" +#include "KeyValuePairs/KeyValuePairsCreator.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include + +using namespace T6; +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_definition(), + m_zone_definition_context(m_zone_definition), + m_zone_states(m_zone), + m_creators(m_zone), + m_ignored_assets(), + m_out_dir(), + m_context(m_zone, &m_creators, &m_ignored_assets), + m_ipak_creator(m_zone_states.GetZoneAssetCreationState()) + { + } + + std::unique_ptr CreateSut() + { + return std::make_unique>(m_zone_definition_context, m_search_path, m_zone_states, m_out_dir); + } + + Zone m_zone; + ZoneDefinition m_zone_definition; + ZoneDefinitionContext m_zone_definition_context; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + AssetCreatorCollection m_creators; + IgnoredAssetLookup m_ignored_assets; + MockOutputPath m_out_dir; + AssetCreationContext m_context; + + IPakCreator& m_ipak_creator; + }; +} // namespace + +namespace test::image +{ + TEST_CASE("ImageIPakPostProcessor: Handles asset type of specified asset", "[image]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + REQUIRE(sut->GetHandlingAssetType() == ASSET_TYPE_IMAGE); + } + + TEST_CASE("ImageIPakPostProcessor: Adds images to ipak when obj container is specified", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage0"); + REQUIRE(imageNames[1] == "testImage1"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects lower obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage1"); + REQUIRE(imageNames[1] == "testImage2"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects upper obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage0"); + REQUIRE(imageNames[1] == "testImage1"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects upper and lower obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage1"); + REQUIRE(imageNames[1] == "testImage2"); + } + + TEST_CASE("ImageIPakPostProcessor: Can add images to multiple ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak0", ZoneDefinitionObjContainerType::IPAK, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak1", ZoneDefinitionObjContainerType::IPAK, 2, 4); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result0 = testContext.m_ipak_creator.GetOrAddIPak("testIpak0"); + REQUIRE(result0); + + const auto& imageNames0 = result0->GetImageNames(); + REQUIRE(imageNames0.size() == 2u); + REQUIRE(imageNames0[0] == "testImage0"); + REQUIRE(imageNames0[1] == "testImage1"); + + auto result1 = testContext.m_ipak_creator.GetOrAddIPak("testIpak1"); + REQUIRE(result1); + + const auto& imageNames1 = result1->GetImageNames(); + REQUIRE(imageNames1.size() == 2u); + REQUIRE(imageNames1[0] == "testImage2"); + REQUIRE(imageNames1[1] == "testImage3"); + } + + TEST_CASE("ImageIPakPostProcessor: Can add images to multiple ipak with gap between", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak0", ZoneDefinitionObjContainerType::IPAK, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak1", ZoneDefinitionObjContainerType::IPAK, 3, 5); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset4(ASSET_TYPE_IMAGE, "testImage4", nullptr); + sut->PostProcessAsset(imageAsset4, testContext.m_context); + + auto result0 = testContext.m_ipak_creator.GetOrAddIPak("testIpak0"); + REQUIRE(result0); + + const auto& imageNames0 = result0->GetImageNames(); + REQUIRE(imageNames0.size() == 2u); + REQUIRE(imageNames0[0] == "testImage0"); + REQUIRE(imageNames0[1] == "testImage1"); + + auto result1 = testContext.m_ipak_creator.GetOrAddIPak("testIpak1"); + REQUIRE(result1); + + const auto& imageNames1 = result1->GetImageNames(); + REQUIRE(imageNames1.size() == 2u); + REQUIRE(imageNames1[0] == "testImage3"); + REQUIRE(imageNames1[1] == "testImage4"); + } + + TEST_CASE("ImageIPakPostProcessor: Writes IPak when finalizing", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + testContext.m_search_path.AddFileData("images/testImage0.iwi", "asdf0"); + testContext.m_search_path.AddFileData("images/testImage1.iwi", "asdf1"); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + sut->FinalizeZone(testContext.m_context); + + const auto* mockFile = testContext.m_out_dir.GetMockedFile("testIpak.ipak"); + REQUIRE(mockFile); + } +} // namespace test::image From 3b5ca86b0dddc50c529fc20893f4ca0c433d2e14 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 8 Jan 2025 18:35:39 +0100 Subject: [PATCH 08/11] test: add unit test for ImageIwdPostProcessor --- .../Image/ImageIwdPostProcessor.cpp | 3 +- src/ObjCompiling/Iwd/IwdCreator.cpp | 5 + src/ObjCompiling/Iwd/IwdCreator.h | 1 + .../Image/ImageIwdPostProcessorTest.cpp | 277 ++++++++++++++++++ 4 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 test/ObjCompilingTests/Image/ImageIwdPostProcessorTest.cpp diff --git a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp index 586fa9c0..b329546c 100644 --- a/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIwdPostProcessor.cpp @@ -4,7 +4,6 @@ #include #include -#include AbstractImageIwdPostProcessor::AbstractImageIwdPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, @@ -58,7 +57,7 @@ void AbstractImageIwdPostProcessor::PostProcessAsset(XAssetInfoGeneric& assetInf while (m_current_iwd && m_zone_definition.m_asset_index_in_definition >= m_current_iwd_end_index) FindNextObjContainer(); - if (m_current_iwd && m_zone_definition.m_asset_index_in_definition <= m_current_iwd_start_index) + if (m_current_iwd && m_zone_definition.m_asset_index_in_definition >= m_current_iwd_start_index) m_current_iwd->AddFile(std::format("images/{}.iwi", assetInfo.m_name)); } diff --git a/src/ObjCompiling/Iwd/IwdCreator.cpp b/src/ObjCompiling/Iwd/IwdCreator.cpp index b25493e3..3715f480 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.cpp +++ b/src/ObjCompiling/Iwd/IwdCreator.cpp @@ -79,6 +79,11 @@ void IwdToCreate::Build(ISearchPath& searchPath, IOutputPath& outPath) std::cout << std::format("Created iwd {} with {} entries\n", m_name, m_file_paths.size()); } +const std::vector& IwdToCreate::GetFilePaths() const +{ + return m_file_paths; +} + IwdToCreate* IwdCreator::GetOrAddIwd(const std::string& iwdName) { const auto existingIwd = m_iwd_lookup.find(iwdName); diff --git a/src/ObjCompiling/Iwd/IwdCreator.h b/src/ObjCompiling/Iwd/IwdCreator.h index 2b73be50..ed7d74f9 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.h +++ b/src/ObjCompiling/Iwd/IwdCreator.h @@ -15,6 +15,7 @@ public: void AddFile(std::string filePath); void Build(ISearchPath& searchPath, IOutputPath& outPath); + [[nodiscard]] const std::vector& GetFilePaths() const; private: std::string m_name; diff --git a/test/ObjCompilingTests/Image/ImageIwdPostProcessorTest.cpp b/test/ObjCompilingTests/Image/ImageIwdPostProcessorTest.cpp new file mode 100644 index 00000000..8e971524 --- /dev/null +++ b/test/ObjCompilingTests/Image/ImageIwdPostProcessorTest.cpp @@ -0,0 +1,277 @@ +#include "Image/ImageIwdPostProcessor.h" + +#include "Game/T6/T6.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include + +using namespace T6; +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_definition(), + m_zone_definition_context(m_zone_definition), + m_zone_states(m_zone), + m_creators(m_zone), + m_ignored_assets(), + m_out_dir(), + m_context(m_zone, &m_creators, &m_ignored_assets), + m_iwd_creator(m_zone_states.GetZoneAssetCreationState()) + { + } + + std::unique_ptr CreateSut() + { + return std::make_unique>(m_zone_definition_context, m_search_path, m_zone_states, m_out_dir); + } + + Zone m_zone; + ZoneDefinition m_zone_definition; + ZoneDefinitionContext m_zone_definition_context; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + AssetCreatorCollection m_creators; + IgnoredAssetLookup m_ignored_assets; + MockOutputPath m_out_dir; + AssetCreationContext m_context; + + IwdCreator& m_iwd_creator; + }; +} // namespace + +namespace test::image +{ + TEST_CASE("ImageIwdPostProcessor: Handles asset type of specified asset", "[image]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + REQUIRE(sut->GetHandlingAssetType() == ASSET_TYPE_IMAGE); + } + + TEST_CASE("ImageIwdPostProcessor: Adds images to iwd when obj container is specified", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd", ZoneDefinitionObjContainerType::IWD, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + auto result = testContext.m_iwd_creator.GetOrAddIwd("testIwd"); + REQUIRE(result); + + const auto& filePaths = result->GetFilePaths(); + REQUIRE(filePaths.size() == 2u); + REQUIRE(filePaths[0] == "images/testImage0.iwi"); + REQUIRE(filePaths[1] == "images/testImage1.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Respects lower obj container boundary when adding images to iwd", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd", ZoneDefinitionObjContainerType::IWD, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_iwd_creator.GetOrAddIwd("testIwd"); + REQUIRE(result); + + const auto& filePaths = result->GetFilePaths(); + REQUIRE(filePaths.size() == 2u); + REQUIRE(filePaths[0] == "images/testImage1.iwi"); + REQUIRE(filePaths[1] == "images/testImage2.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Respects upper obj container boundary when adding images to iwd", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd", ZoneDefinitionObjContainerType::IWD, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_iwd_creator.GetOrAddIwd("testIwd"); + REQUIRE(result); + + const auto& filePaths = result->GetFilePaths(); + REQUIRE(filePaths.size() == 2u); + REQUIRE(filePaths[0] == "images/testImage0.iwi"); + REQUIRE(filePaths[1] == "images/testImage1.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Respects upper and lower obj container boundary when adding images to iwd", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd", ZoneDefinitionObjContainerType::IWD, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result = testContext.m_iwd_creator.GetOrAddIwd("testIwd"); + REQUIRE(result); + + const auto& filePaths = result->GetFilePaths(); + REQUIRE(filePaths.size() == 2u); + REQUIRE(filePaths[0] == "images/testImage1.iwi"); + REQUIRE(filePaths[1] == "images/testImage2.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Can add images to multiple iwd", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd0", ZoneDefinitionObjContainerType::IWD, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd1", ZoneDefinitionObjContainerType::IWD, 2, 4); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result0 = testContext.m_iwd_creator.GetOrAddIwd("testIwd0"); + REQUIRE(result0); + + const auto& filePaths0 = result0->GetFilePaths(); + REQUIRE(filePaths0.size() == 2u); + REQUIRE(filePaths0[0] == "images/testImage0.iwi"); + REQUIRE(filePaths0[1] == "images/testImage1.iwi"); + + auto result1 = testContext.m_iwd_creator.GetOrAddIwd("testIwd1"); + REQUIRE(result1); + + const auto& filePaths1 = result1->GetFilePaths(); + REQUIRE(filePaths1.size() == 2u); + REQUIRE(filePaths1[0] == "images/testImage2.iwi"); + REQUIRE(filePaths1[1] == "images/testImage3.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Can add images to multiple iwd with gap between", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd0", ZoneDefinitionObjContainerType::IWD, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd1", ZoneDefinitionObjContainerType::IWD, 3, 5); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset4(ASSET_TYPE_IMAGE, "testImage4", nullptr); + sut->PostProcessAsset(imageAsset4, testContext.m_context); + + auto result0 = testContext.m_iwd_creator.GetOrAddIwd("testIwd0"); + REQUIRE(result0); + + const auto& filePaths0 = result0->GetFilePaths(); + REQUIRE(filePaths0.size() == 2u); + REQUIRE(filePaths0[0] == "images/testImage0.iwi"); + REQUIRE(filePaths0[1] == "images/testImage1.iwi"); + + auto result1 = testContext.m_iwd_creator.GetOrAddIwd("testIwd1"); + REQUIRE(result1); + + const auto& filePaths1 = result1->GetFilePaths(); + REQUIRE(filePaths1.size() == 2u); + REQUIRE(filePaths1[0] == "images/testImage3.iwi"); + REQUIRE(filePaths1[1] == "images/testImage4.iwi"); + } + + TEST_CASE("ImageIwdPostProcessor: Writes Iwd when finalizing", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIwd", ZoneDefinitionObjContainerType::IWD, 0, 2); + + testContext.m_search_path.AddFileData("images/testImage0.iwi", "asdf0"); + testContext.m_search_path.AddFileData("images/testImage1.iwi", "asdf1"); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + sut->FinalizeZone(testContext.m_context); + + const auto* mockFile = testContext.m_out_dir.GetMockedFile("testIwd.iwd"); + REQUIRE(mockFile); + } +} // namespace test::image From 54e240e98cf0e680872888cb4bfb92876f29fcdb Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 8 Jan 2025 00:05:20 +0100 Subject: [PATCH 09/11] fix: compilation --- src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp | 1 - src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp | 1 - src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp | 1 - src/ObjCompiling/Game/T5/ObjCompilerT5.cpp | 1 - src/ObjCompiling/Game/T6/ObjCompilerT6.cpp | 1 - test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp | 4 +++- 6 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp index d1be036d..69c47261 100644 --- a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp +++ b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp @@ -6,7 +6,6 @@ #include using namespace IW3; -namespace fs = std::filesystem; namespace { diff --git a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp index d1b3e766..ed6b6963 100644 --- a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp +++ b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp @@ -6,7 +6,6 @@ #include using namespace IW4; -namespace fs = std::filesystem; namespace { diff --git a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp index b04f4aeb..e9faf12e 100644 --- a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp +++ b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp @@ -6,7 +6,6 @@ #include using namespace IW5; -namespace fs = std::filesystem; namespace { diff --git a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp index da94980a..e08a010f 100644 --- a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp +++ b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp @@ -6,7 +6,6 @@ #include using namespace T5; -namespace fs = std::filesystem; namespace { diff --git a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp index bc59cf06..5052d5ab 100644 --- a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp @@ -8,7 +8,6 @@ #include using namespace T6; -namespace fs = std::filesystem; namespace { diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp index 4b4db64c..5b0d761a 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -2,6 +2,8 @@ #include "Utils/ObjStream.h" +#include + namespace { class MockFileBuffer final : public std::streambuf @@ -37,7 +39,7 @@ namespace if (overrideCount > 0) { - memcpy(&m_data[m_pos], ptr, overrideCount); + std::memcpy(&m_data[m_pos], ptr, overrideCount); m_pos += overrideCount; ptr += overrideCount; } From fa249b0bd356c0ce52af514a51b274d602ddfa9f Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 8 Jan 2025 21:58:46 +0100 Subject: [PATCH 10/11] test: add unit test for IPakCreator --- src/ObjLoading/Game/T6/ObjLoaderT6.cpp | 10 +- src/ObjLoading/ObjContainer/IPak/IPak.cpp | 371 ++++++++---------- src/ObjLoading/ObjContainer/IPak/IPak.h | 29 +- .../T6/AssetDumpers/AssetDumperGfxImage.cpp | 2 +- .../SearchPath/MockOutputPath.cpp | 10 + .../SearchPath/MockOutputPath.h | 3 + .../Image/IPak/IPakCreatorTest.cpp | 86 ++++ 7 files changed, 292 insertions(+), 219 deletions(-) create mode 100644 test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp index cbce8c48..68230945 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp @@ -165,13 +165,13 @@ namespace T6 if (ObjLoading::Configuration.Verbose) std::cout << std::format("Trying to load ipak '{}' for zone '{}'\n", ipakName, zone.m_name); - auto* existingIPak = IPak::Repository.GetContainerByName(ipakName); + auto* existingIPak = IIPak::Repository.GetContainerByName(ipakName); if (existingIPak != nullptr) { if (ObjLoading::Configuration.Verbose) std::cout << std::format("Referencing loaded ipak '{}'.\n", ipakName); - IPak::Repository.AddContainerReference(existingIPak, &zone); + IIPak::Repository.AddContainerReference(existingIPak, &zone); return; } @@ -180,11 +180,11 @@ namespace T6 auto file = searchPath.Open(ipakFilename); if (file.IsOpen()) { - auto ipak = std::make_unique(ipakFilename, std::move(file.m_stream)); + auto ipak = IIPak::Create(ipakFilename, std::move(file.m_stream)); if (ipak->Initialize()) { - IPak::Repository.AddContainer(std::move(ipak), &zone); + IIPak::Repository.AddContainer(std::move(ipak), &zone); if (ObjLoading::Configuration.Verbose) std::cout << std::format("Found and loaded ipak '{}'.\n", ipakFilename); @@ -277,7 +277,7 @@ namespace T6 void ObjLoader::UnloadContainersOfZone(Zone& zone) const { - IPak::Repository.RemoveContainerReferences(&zone); + IIPak::Repository.RemoveContainerReferences(&zone); } namespace diff --git a/src/ObjLoading/ObjContainer/IPak/IPak.cpp b/src/ObjLoading/ObjContainer/IPak/IPak.cpp index 5f88c264..a963f28d 100644 --- a/src/ObjLoading/ObjContainer/IPak/IPak.cpp +++ b/src/ObjLoading/ObjContainer/IPak/IPak.cpp @@ -1,35 +1,22 @@ #include "IPak.h" -#include "Exception/IPakLoadException.h" #include "IPakStreamManager.h" #include "ObjContainer/IPak/IPakTypes.h" -#include "Utils/FileUtils.h" #include "zlib.h" #include +#include +#include #include -#include #include namespace fs = std::filesystem; -ObjContainerRepository IPak::Repository; +ObjContainerRepository IIPak::Repository; -class IPak::Impl : public ObjContainerReferenceable +namespace { - std::string m_path; - std::unique_ptr m_stream; - - bool m_initialized; - - std::unique_ptr m_index_section; - std::unique_ptr m_data_section; - - std::vector m_index_entries; - - IPakStreamManager m_stream_manager; - - static uint32_t R_HashString(const char* str, uint32_t hash) + std::uint32_t R_HashString(const char* str, std::uint32_t hash) { for (const auto* pos = str; *pos; pos++) { @@ -38,203 +25,189 @@ class IPak::Impl : public ObjContainerReferenceable return hash; } +} // namespace - bool ReadIndexSection() +namespace +{ + class IPak final : public IIPak { - m_stream->seekg(m_index_section->offset); - IPakIndexEntry indexEntry{}; - - for (unsigned itemIndex = 0; itemIndex < m_index_section->itemCount; itemIndex++) + public: + IPak(std::string path, std::unique_ptr stream) + : m_path(std::move(path)), + m_stream(std::move(stream)), + m_initialized(false), + m_index_section(nullptr), + m_data_section(nullptr), + m_stream_manager(*m_stream) { - m_stream->read(reinterpret_cast(&indexEntry), sizeof(indexEntry)); - if (m_stream->gcount() != sizeof(indexEntry)) - { - printf("Unexpected eof when trying to load index entry %u.\n", itemIndex); + } + + bool Initialize() override + { + if (m_initialized) + return true; + + if (!ReadHeader()) return false; - } - m_index_entries.push_back(indexEntry); - } - - std::ranges::sort(m_index_entries, - [](const IPakIndexEntry& entry1, const IPakIndexEntry& entry2) - { - return entry1.key.combinedKey < entry2.key.combinedKey; - }); - - return true; - } - - bool ReadSection() - { - IPakSection section{}; - - m_stream->read(reinterpret_cast(§ion), sizeof(section)); - if (m_stream->gcount() != sizeof(section)) - { - printf("Unexpected eof when trying to load section.\n"); - return false; - } - - switch (section.type) - { - case ipak_consts::IPAK_INDEX_SECTION: - m_index_section = std::make_unique(section); - break; - - case ipak_consts::IPAK_DATA_SECTION: - m_data_section = std::make_unique(section); - break; - - default: - break; - } - - return true; - } - - bool ReadHeader() - { - IPakHeader header{}; - - m_stream->read(reinterpret_cast(&header), sizeof(header)); - if (m_stream->gcount() != sizeof(header)) - { - printf("Unexpected eof when trying to load header.\n"); - return false; - } - - if (header.magic != ipak_consts::IPAK_MAGIC) - { - printf("Invalid ipak magic '0x%x'.\n", header.magic); - return false; - } - - if (header.version != ipak_consts::IPAK_VERSION) - { - printf("Unsupported ipak version '%u'.\n", header.version); - return false; - } - - for (unsigned section = 0; section < header.sectionCount; section++) - { - if (!ReadSection()) - return false; - } - - if (m_index_section == nullptr) - { - printf("IPak does not contain an index section.\n"); - return false; - } - - if (m_data_section == nullptr) - { - printf("IPak does not contain a data section.\n"); - return false; - } - - if (!ReadIndexSection()) - return false; - - return true; - } - -public: - Impl(std::string path, std::unique_ptr stream) - : m_path(std::move(path)), - m_stream(std::move(stream)), - m_initialized(false), - m_index_section(nullptr), - m_data_section(nullptr), - m_stream_manager(*m_stream) - { - } - - ~Impl() override = default; - - std::string GetName() override - { - return fs::path(m_path).filename().replace_extension("").string(); - } - - bool Initialize() - { - if (m_initialized) + m_initialized = true; return true; - - if (!ReadHeader()) - return false; - - m_initialized = true; - return true; - } - - std::unique_ptr GetEntryData(const Hash nameHash, const Hash dataHash) - { - IPakIndexEntryKey wantedKey{}; - wantedKey.nameHash = nameHash; - wantedKey.dataHash = dataHash; - - for (auto& entry : m_index_entries) - { - if (entry.key.combinedKey == wantedKey.combinedKey) - { - return m_stream_manager.OpenStream(static_cast(m_data_section->offset) + entry.offset, entry.size); - } - else if (entry.key.combinedKey > wantedKey.combinedKey) - { - // The index entries are sorted so if the current entry is higher than the wanted entry we can cancel here - return nullptr; - } } - return nullptr; - } + [[nodiscard]] std::unique_ptr GetEntryStream(const Hash nameHash, const Hash dataHash) const override + { + IPakIndexEntryKey wantedKey{}; + wantedKey.nameHash = nameHash; + wantedKey.dataHash = dataHash; - static Hash HashString(const std::string& str) - { - return R_HashString(str.c_str(), 0); - } + for (auto& entry : m_index_entries) + { + if (entry.key.combinedKey == wantedKey.combinedKey) + { + return m_stream_manager.OpenStream(static_cast(m_data_section->offset) + entry.offset, entry.size); + } + else if (entry.key.combinedKey > wantedKey.combinedKey) + { + // The index entries are sorted so if the current entry is higher than the wanted entry we can cancel here + return nullptr; + } + } - static Hash HashData(const void* data, const size_t dataSize) - { - return crc32(0, static_cast(data), dataSize); - } -}; + return nullptr; + } -IPak::IPak(std::string path, std::unique_ptr stream) + std::string GetName() override + { + return fs::path(m_path).filename().replace_extension("").string(); + } + + private: + bool ReadIndexSection() + { + m_stream->seekg(m_index_section->offset); + IPakIndexEntry indexEntry{}; + + for (unsigned itemIndex = 0; itemIndex < m_index_section->itemCount; itemIndex++) + { + m_stream->read(reinterpret_cast(&indexEntry), sizeof(indexEntry)); + if (m_stream->gcount() != sizeof(indexEntry)) + { + std::cerr << std::format("Unexpected eof when trying to load index entry {}.\n", itemIndex); + return false; + } + + m_index_entries.push_back(indexEntry); + } + + std::ranges::sort(m_index_entries, + [](const IPakIndexEntry& entry1, const IPakIndexEntry& entry2) + { + return entry1.key.combinedKey < entry2.key.combinedKey; + }); + + return true; + } + + bool ReadSection() + { + IPakSection section{}; + + m_stream->read(reinterpret_cast(§ion), sizeof(section)); + if (m_stream->gcount() != sizeof(section)) + { + std::cerr << "Unexpected eof when trying to load section.\n"; + return false; + } + + switch (section.type) + { + case ipak_consts::IPAK_INDEX_SECTION: + m_index_section = std::make_unique(section); + break; + + case ipak_consts::IPAK_DATA_SECTION: + m_data_section = std::make_unique(section); + break; + + default: + break; + } + + return true; + } + + bool ReadHeader() + { + IPakHeader header{}; + + m_stream->read(reinterpret_cast(&header), sizeof(header)); + if (m_stream->gcount() != sizeof(header)) + { + std::cerr << "Unexpected eof when trying to load header.\n"; + return false; + } + + if (header.magic != ipak_consts::IPAK_MAGIC) + { + std::cerr << std::format("Invalid ipak magic '{:#x}'.\n", header.magic); + return false; + } + + if (header.version != ipak_consts::IPAK_VERSION) + { + std::cerr << std::format("Unsupported ipak version '{}'.\n", header.version); + return false; + } + + for (unsigned section = 0; section < header.sectionCount; section++) + { + if (!ReadSection()) + return false; + } + + if (m_index_section == nullptr) + { + std::cerr << "IPak does not contain an index section.\n"; + return false; + } + + if (m_data_section == nullptr) + { + std::cerr << "IPak does not contain a data section.\n"; + return false; + } + + if (!ReadIndexSection()) + return false; + + return true; + } + + std::string m_path; + std::unique_ptr m_stream; + + bool m_initialized; + + std::unique_ptr m_index_section; + std::unique_ptr m_data_section; + + std::vector m_index_entries; + + IPakStreamManager m_stream_manager; + }; +} // namespace + +std::unique_ptr IIPak::Create(std::string path, std::unique_ptr stream) { - m_impl = new Impl(std::move(path), std::move(stream)); + return std::make_unique(std::move(path), std::move(stream)); } -IPak::~IPak() +IIPak::Hash IIPak::HashString(const std::string& str) { - delete m_impl; - m_impl = nullptr; + return R_HashString(str.c_str(), 0); } -std::string IPak::GetName() +IIPak::Hash IIPak::HashData(const void* data, const size_t dataSize) { - return m_impl->GetName(); -} - -bool IPak::Initialize() -{ - return m_impl->Initialize(); -} - -std::unique_ptr IPak::GetEntryStream(const Hash nameHash, const Hash dataHash) const -{ - return m_impl->GetEntryData(nameHash, dataHash); -} - -IPak::Hash IPak::HashString(const std::string& str) -{ - return Impl::HashString(str); -} - -IPak::Hash IPak::HashData(const void* data, const size_t dataSize) -{ - return Impl::HashData(data, dataSize); + return crc32(0, static_cast(data), dataSize); } diff --git a/src/ObjLoading/ObjContainer/IPak/IPak.h b/src/ObjLoading/ObjContainer/IPak/IPak.h index f60c1ba5..308513a5 100644 --- a/src/ObjLoading/ObjContainer/IPak/IPak.h +++ b/src/ObjLoading/ObjContainer/IPak/IPak.h @@ -2,30 +2,31 @@ #include "ObjContainer/ObjContainerReferenceable.h" #include "ObjContainer/ObjContainerRepository.h" -#include "Utils/ClassUtils.h" #include "Utils/ObjStream.h" #include "Zone/Zone.h" +#include #include +#include +#include -class IPak final : public ObjContainerReferenceable +class IIPak : public ObjContainerReferenceable { - class Impl; - Impl* m_impl; - public: - typedef uint32_t Hash; + static ObjContainerRepository Repository; + typedef std::uint32_t Hash; - static ObjContainerRepository Repository; + IIPak() = default; + virtual ~IIPak() = default; + IIPak(const IIPak& other) = default; + IIPak(IIPak&& other) noexcept = default; + IIPak& operator=(const IIPak& other) = default; + IIPak& operator=(IIPak&& other) noexcept = default; - IPak(std::string path, std::unique_ptr stream); - ~IPak() override; - - std::string GetName() override; - - bool Initialize(); - _NODISCARD std::unique_ptr GetEntryStream(Hash nameHash, Hash dataHash) const; + virtual bool Initialize() = 0; + [[nodiscard]] virtual std::unique_ptr GetEntryStream(Hash nameHash, Hash dataHash) const = 0; + static std::unique_ptr Create(std::string path, std::unique_ptr stream); static Hash HashString(const std::string& str); static Hash HashData(const void* data, size_t dataSize); }; diff --git a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp index bb89ddd3..60eb52c2 100644 --- a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp +++ b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp @@ -38,7 +38,7 @@ namespace { if (image->streamedPartCount > 0) { - for (auto* ipak : IPak::Repository) + for (auto* ipak : IIPak::Repository) { auto ipakStream = ipak->GetEntryStream(image->hash, image->streamedParts[0].hash); diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp index 5b0d761a..25993a48 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -108,6 +108,11 @@ MockOutputFile::MockOutputFile(std::string name, std::vector data) { } +std::string MockOutputFile::AsString() const +{ + return std::string(reinterpret_cast(m_data.data()), m_data.size()); +} + std::unique_ptr MockOutputPath::Open(const std::string& fileName) { return std::make_unique(fileName, m_files); @@ -123,3 +128,8 @@ const MockOutputFile* MockOutputPath::GetMockedFile(const std::string& name) con return nullptr; } + +const std::vector& MockOutputPath::GetMockedFileList() const +{ + return m_files; +} diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h index e85a4251..42abdcce 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h @@ -15,6 +15,8 @@ public: MockOutputFile(); MockOutputFile(std::string name, std::vector data); + + [[nodiscard]] std::string AsString() const; }; class MockOutputPath final : public IOutputPath @@ -23,6 +25,7 @@ public: std::unique_ptr Open(const std::string& fileName) override; [[nodiscard]] const MockOutputFile* GetMockedFile(const std::string& name) const; + [[nodiscard]] const std::vector& GetMockedFileList() const; private: std::vector m_files; diff --git a/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp b/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp new file mode 100644 index 00000000..bd49745f --- /dev/null +++ b/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp @@ -0,0 +1,86 @@ +#include "Image/IPak/IPakCreator.h" + +#include "Asset/AssetCreatorCollection.h" +#include "ObjContainer/IPak/IPak.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_states(m_zone), + m_out_dir() + { + } + + IPakCreator& CreateSut() + { + return m_zone_states.GetZoneAssetCreationState(); + } + + Zone m_zone; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + MockOutputPath m_out_dir; + }; +} // namespace + +namespace test::image::ipak +{ + TEST_CASE("IPakCreator: Does nothing if no ipak was defined", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + REQUIRE(testContext.m_out_dir.GetMockedFileList().empty()); + } + + TEST_CASE("IPakCreator: Writes IPak file", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + auto* ipak = sut.GetOrAddIPak("amazing"); + ipak->AddImage("random"); + + constexpr auto iwiData = "hello world"; + testContext.m_search_path.AddFileData("images/random.iwi", iwiData); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + + const auto* file = testContext.m_out_dir.GetMockedFile("amazing.ipak"); + REQUIRE(file); + + const auto* data = file->m_data.data(); + REQUIRE(data[0] == 'K'); + REQUIRE(data[1] == 'A'); + REQUIRE(data[2] == 'P'); + REQUIRE(data[3] == 'I'); + + auto readIpak = IIPak::Create("amazing.ipak", std::make_unique(file->AsString())); + REQUIRE(readIpak->Initialize()); + + auto entry = readIpak->GetEntryStream(IIPak::HashString("random"), IIPak::HashData(iwiData, std::char_traits::length(iwiData))); + REQUIRE(entry); + + char readBuffer[std::char_traits::length(iwiData) + 10]; + entry->read(readBuffer, sizeof(readBuffer)); + + REQUIRE(entry->gcount() == std::char_traits::length(iwiData)); + REQUIRE(std::strncmp(iwiData, readBuffer, std::char_traits::length(iwiData)) == 0); + } +} // namespace test::image::ipak From d1e6aa9da0efba4d1eaf2e551ad674edcf5315c3 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 7 Jan 2025 23:48:23 +0100 Subject: [PATCH 11/11] test: add unit test for IwdCreator --- test/ObjCompilingTests/Iwd/IwdCreatorTest.cpp | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/ObjCompilingTests/Iwd/IwdCreatorTest.cpp diff --git a/test/ObjCompilingTests/Iwd/IwdCreatorTest.cpp b/test/ObjCompilingTests/Iwd/IwdCreatorTest.cpp new file mode 100644 index 00000000..63ceeb17 --- /dev/null +++ b/test/ObjCompilingTests/Iwd/IwdCreatorTest.cpp @@ -0,0 +1,94 @@ +#include "Iwd/IwdCreator.h" + +#include "Asset/AssetCreatorCollection.h" +#include "Game/T6/T6.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "Utils/FileToZlibWrapper.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace T6; +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_states(m_zone), + m_out_dir() + { + } + + IwdCreator& CreateSut() + { + return m_zone_states.GetZoneAssetCreationState(); + } + + Zone m_zone; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + MockOutputPath m_out_dir; + }; +} // namespace + +namespace test::iwd +{ + TEST_CASE("IwdCreator: Does nothing if no iwd was defined", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + REQUIRE(testContext.m_out_dir.GetMockedFileList().empty()); + } + + TEST_CASE("IwdCreator: Writes Iwd file", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + auto* ipak = sut.GetOrAddIwd("amazing"); + ipak->AddFile("images/random.iwi"); + + constexpr auto iwiData = "hello world"; + testContext.m_search_path.AddFileData("images/random.iwi", iwiData); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + + const auto* file = testContext.m_out_dir.GetMockedFile("amazing.iwd"); + REQUIRE(file); + + const auto* data = file->m_data.data(); + REQUIRE(data[0] == 'P'); + REQUIRE(data[1] == 'K'); + + std::istringstream ss(file->AsString()); + auto zlibFunctions = FileToZlibWrapper::CreateFunctions32ForFile(&ss); + auto zip = unzOpen2("amazing.iwd", &zlibFunctions); + REQUIRE(zip); + + REQUIRE(unzGoToFirstFile(zip) == UNZ_OK); + + unz_file_info fileInfo; + char fileNameBuffer[64]; + char readBuffer[std::char_traits::length(iwiData) + 10]; + REQUIRE(unzGetCurrentFileInfo(zip, &fileInfo, fileNameBuffer, sizeof(fileNameBuffer), nullptr, 0, nullptr, 0) == UNZ_OK); + + REQUIRE("images/random.iwi"s == fileNameBuffer); + REQUIRE(fileInfo.uncompressed_size == std::char_traits::length(iwiData)); + + REQUIRE(unzOpenCurrentFile(zip) == UNZ_OK); + REQUIRE(unzReadCurrentFile(zip, readBuffer, sizeof(readBuffer)) == std::char_traits::length(iwiData)); + REQUIRE(std::strncmp(iwiData, readBuffer, std::char_traits::length(iwiData)) == 0); + } +} // namespace test::iwd