#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Resources; namespace { class BlockingMeshLoader : public IResourceLoader { public: ResourceType GetResourceType() const override { return ResourceType::Mesh; } XCEngine::Containers::Array GetSupportedExtensions() const override { XCEngine::Containers::Array extensions; extensions.PushBack("mesh"); return extensions; } bool CanLoad(const XCEngine::Containers::String& path) const override { (void)path; return true; } LoadResult Load(const XCEngine::Containers::String& path, const ImportSettings* settings = nullptr) override { (void)settings; ++m_loadCalls; { std::lock_guard lock(m_mutex); m_started = true; } m_condition.notify_all(); { std::unique_lock lock(m_mutex); m_condition.wait(lock, [this]() { return m_allowCompletion; }); } auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = "BlockingMesh"; params.path = path; params.guid = ResourceGUID::Generate(path); mesh->Initialize(params); const StaticMeshVertex vertices[3] = {}; const XCEngine::Core::uint32 indices[3] = {0, 1, 2}; mesh->SetVertexData(vertices, sizeof(vertices), 3, sizeof(StaticMeshVertex), VertexAttribute::Position); mesh->SetIndexData(indices, sizeof(indices), 3, true); return LoadResult(mesh); } ImportSettings* GetDefaultSettings() const override { return nullptr; } bool WaitForStart(std::chrono::milliseconds timeout) { std::unique_lock lock(m_mutex); return m_condition.wait_for(lock, timeout, [this]() { return m_started; }); } void AllowCompletion() { { std::lock_guard lock(m_mutex); m_allowCompletion = true; } m_condition.notify_all(); } int GetLoadCalls() const { return m_loadCalls.load(); } private: std::atomic m_loadCalls{0}; mutable std::mutex m_mutex; std::condition_variable m_condition; bool m_started = false; bool m_allowCompletion = false; }; bool PumpAsyncLoadsUntil(ResourceManager& manager, const std::function& condition, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (!condition() && std::chrono::steady_clock::now() < deadline) { manager.UpdateAsyncLoads(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } manager.UpdateAsyncLoads(); return condition(); } TEST(ResourceManager_Test, ConcurrentAsyncLoadsCoalesceSameMeshPath) { ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); struct LoaderGuard { ResourceManager* manager = nullptr; IResourceLoader* loader = nullptr; ~LoaderGuard() { if (manager != nullptr && loader != nullptr) { manager->RegisterLoader(loader); } } } loaderGuard{ &manager, manager.GetLoader(ResourceType::Mesh) }; BlockingMeshLoader blockingLoader; manager.RegisterLoader(&blockingLoader); std::mutex resultsMutex; std::vector callbackResources; std::atomic callbackCount{0}; const auto callback = [&](LoadResult result) { EXPECT_TRUE(result); EXPECT_NE(result.resource, nullptr); { std::lock_guard lock(resultsMutex); callbackResources.push_back(result.resource); } ++callbackCount; }; manager.LoadAsync("Meshes/concurrent.mesh", ResourceType::Mesh, callback); manager.LoadAsync("Meshes/concurrent.mesh", ResourceType::Mesh, callback); ASSERT_TRUE(blockingLoader.WaitForStart(std::chrono::milliseconds(1000))); EXPECT_EQ(blockingLoader.GetLoadCalls(), 1); blockingLoader.AllowCompletion(); ASSERT_TRUE(PumpAsyncLoadsUntil( manager, [&]() { return callbackCount.load() == 2 && manager.GetAsyncPendingCount() == 0; }, std::chrono::milliseconds(2000))); EXPECT_EQ(blockingLoader.GetLoadCalls(), 1); ASSERT_EQ(callbackResources.size(), 2u); EXPECT_EQ(callbackResources[0], callbackResources[1]); manager.Shutdown(); } TEST(ResourceManager_Test, AssetLookupFallbackRefreshesSnapshotForNewProjectAsset) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_asset_lookup_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path materialPath = assetsDir / "runtime.material"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); manager.SetResourceRoot(projectRoot.string().c_str()); { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"renderQueue\": \"geometry\"\n"; materialFile << "}\n"; } AssetRef assetRef; EXPECT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef)); EXPECT_TRUE(assetRef.IsValid()); XCEngine::Containers::String resolvedPath; EXPECT_TRUE(manager.TryResolveAssetPath(assetRef, resolvedPath)); EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material"); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(ProjectAssetIndex_Test, RefreshesSnapshotThroughImportServiceOnCacheMiss) { namespace fs = std::filesystem; AssetImportService importService; importService.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_project_asset_index_refresh_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path materialPath = assetsDir / "runtime.material"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); importService.SetProjectRoot(projectRoot.string().c_str()); ProjectAssetIndex assetIndex; assetIndex.RefreshFrom(importService); { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"renderQueue\": \"geometry\"\n"; materialFile << "}\n"; } AssetRef assetRef; EXPECT_TRUE(assetIndex.TryGetAssetRef(importService, "Assets/runtime.material", ResourceType::Material, assetRef)); EXPECT_TRUE(assetRef.IsValid()); XCEngine::Containers::String resolvedPath; EXPECT_TRUE(assetIndex.TryResolveAssetPath(importService, assetRef, resolvedPath)); EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material"); importService.Shutdown(); fs::remove_all(projectRoot); } } // namespace