Files
XCEngine/tests/core/Asset/test_resource_manager.cpp

236 lines
7.4 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/AssetImportService.h>
#include <XCEngine/Core/Asset/ProjectAssetIndex.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <filesystem>
#include <fstream>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
using namespace XCEngine::Resources;
namespace {
class BlockingMeshLoader : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::Mesh; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> 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<std::mutex> lock(m_mutex);
m_started = true;
}
m_condition.notify_all();
{
std::unique_lock<std::mutex> 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<std::mutex> lock(m_mutex);
return m_condition.wait_for(lock, timeout, [this]() { return m_started; });
}
void AllowCompletion() {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_allowCompletion = true;
}
m_condition.notify_all();
}
int GetLoadCalls() const {
return m_loadCalls.load();
}
private:
std::atomic<int> 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<bool()>& 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<IResource*> callbackResources;
std::atomic<int> callbackCount{0};
const auto callback = [&](LoadResult result) {
EXPECT_TRUE(result);
EXPECT_NE(result.resource, nullptr);
{
std::lock_guard<std::mutex> 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