857 lines
31 KiB
C++
857 lines
31 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/Material/MaterialLoader.h>
|
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#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();
|
|
}
|
|
|
|
std::filesystem::path GetRepositoryRoot() {
|
|
std::filesystem::path current = std::filesystem::path(__FILE__).parent_path();
|
|
while (!current.empty()) {
|
|
if (std::filesystem::exists(current / "project") &&
|
|
std::filesystem::exists(current / "engine")) {
|
|
return current;
|
|
}
|
|
current = current.parent_path();
|
|
}
|
|
|
|
return std::filesystem::path(__FILE__).parent_path();
|
|
}
|
|
|
|
bool DirectoryHasEntries(const std::filesystem::path& directoryPath) {
|
|
std::error_code ec;
|
|
if (!std::filesystem::exists(directoryPath, ec) || !std::filesystem::is_directory(directoryPath, ec)) {
|
|
return false;
|
|
}
|
|
|
|
return std::filesystem::directory_iterator(directoryPath) != std::filesystem::directory_iterator();
|
|
}
|
|
|
|
std::vector<std::string> SplitTabSeparatedLine(const std::string& line) {
|
|
std::vector<std::string> fields;
|
|
std::stringstream stream(line);
|
|
std::string field;
|
|
while (std::getline(stream, field, '\t')) {
|
|
fields.push_back(field);
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
std::string ReadSourceHashFromAssetsDb(const std::filesystem::path& assetsDbPath,
|
|
const std::string& relativePath) {
|
|
std::ifstream input(assetsDbPath, std::ios::binary);
|
|
if (!input.is_open()) {
|
|
return std::string();
|
|
}
|
|
|
|
std::string line;
|
|
while (std::getline(input, line)) {
|
|
if (line.empty() || line[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
const std::vector<std::string> fields = SplitTabSeparatedLine(line);
|
|
if (fields.size() > 7 && fields[1] == relativePath) {
|
|
return fields[7];
|
|
}
|
|
}
|
|
|
|
return std::string();
|
|
}
|
|
|
|
std::vector<std::filesystem::path> ListArtifactEntries(const std::filesystem::path& artifactsRoot) {
|
|
namespace fs = std::filesystem;
|
|
|
|
std::vector<fs::path> entries;
|
|
std::error_code ec;
|
|
if (!fs::exists(artifactsRoot, ec) || !fs::is_directory(artifactsRoot, ec)) {
|
|
return entries;
|
|
}
|
|
|
|
for (const auto& shardEntry : fs::directory_iterator(artifactsRoot, ec)) {
|
|
if (ec) {
|
|
break;
|
|
}
|
|
if (!shardEntry.is_directory()) {
|
|
entries.push_back(shardEntry.path());
|
|
continue;
|
|
}
|
|
|
|
for (const auto& artifactEntry : fs::directory_iterator(shardEntry.path(), ec)) {
|
|
if (ec) {
|
|
break;
|
|
}
|
|
entries.push_back(artifactEntry.path());
|
|
}
|
|
}
|
|
|
|
std::sort(entries.begin(), entries.end());
|
|
return entries;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, BootstrapProjectBuildsLookupSnapshotAndReportsStatus) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_bootstrap_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
EXPECT_TRUE(importService.BootstrapProject());
|
|
|
|
AssetImportService::LookupSnapshot snapshot;
|
|
importService.BuildLookupSnapshot(snapshot);
|
|
EXPECT_GE(snapshot.assetGuidByPathKey.size(), 2u);
|
|
EXPECT_GE(snapshot.assetPathByGuid.size(), 2u);
|
|
EXPECT_NE(snapshot.assetGuidByPathKey.find("assets/runtime.material"), snapshot.assetGuidByPathKey.end());
|
|
|
|
AssetRef assetRef;
|
|
EXPECT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef));
|
|
EXPECT_TRUE(assetRef.IsValid());
|
|
const auto snapshotPathIt = snapshot.assetPathByGuid.find(assetRef.assetGuid);
|
|
ASSERT_NE(snapshotPathIt, snapshot.assetPathByGuid.end());
|
|
EXPECT_EQ(std::string(snapshotPathIt->second.CStr()), "Assets/runtime.material");
|
|
|
|
const AssetImportService::ImportStatusSnapshot status = importService.GetLastImportStatus();
|
|
EXPECT_TRUE(status.HasValue());
|
|
EXPECT_FALSE(status.inProgress);
|
|
EXPECT_TRUE(status.success);
|
|
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
|
EXPECT_EQ(std::string(status.targetPath.CStr()), projectRoot.string());
|
|
EXPECT_GT(status.startedAtMs, 0u);
|
|
EXPECT_GE(status.completedAtMs, status.startedAtMs);
|
|
EXPECT_EQ(status.durationMs, status.completedAtMs - status.startedAtMs);
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, RebuildLibraryCacheKeepsStableAssetRefs) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_rebuild_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
ASSERT_TRUE(importService.BootstrapProject());
|
|
|
|
AssetRef firstAssetRef;
|
|
ASSERT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, firstAssetRef));
|
|
ASSERT_TRUE(firstAssetRef.IsValid());
|
|
|
|
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
|
|
|
EXPECT_TRUE(importService.RebuildLibraryCache());
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
|
|
|
AssetRef secondAssetRef;
|
|
ASSERT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
|
ASSERT_TRUE(secondAssetRef.IsValid());
|
|
EXPECT_EQ(firstAssetRef.assetGuid, secondAssetRef.assetGuid);
|
|
EXPECT_EQ(firstAssetRef.localID, secondAssetRef.localID);
|
|
EXPECT_EQ(firstAssetRef.resourceType, secondAssetRef.resourceType);
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, EnsureArtifactExposesContainerEntryRuntimeLoadPath) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_entry_runtime_path_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
|
|
AssetImportService::ImportedAsset importedAsset;
|
|
ASSERT_TRUE(importService.EnsureArtifact("Assets/runtime.material", ResourceType::Material, importedAsset));
|
|
ASSERT_TRUE(importedAsset.exists);
|
|
ASSERT_TRUE(importedAsset.artifactReady);
|
|
EXPECT_EQ(importedAsset.artifactStorageKind, ArtifactStorageKind::SingleFileContainer);
|
|
EXPECT_EQ(importedAsset.mainEntryName, "main");
|
|
EXPECT_FALSE(importedAsset.artifactMainPath.Empty());
|
|
EXPECT_FALSE(importedAsset.artifactMainEntryPath.Empty());
|
|
EXPECT_EQ(importedAsset.runtimeLoadPath, importedAsset.artifactMainEntryPath);
|
|
EXPECT_NE(importedAsset.artifactMainEntryPath, importedAsset.artifactMainPath);
|
|
EXPECT_NE(std::string(importedAsset.runtimeLoadPath.CStr()).find("@entry=main"), std::string::npos);
|
|
|
|
{
|
|
const fs::path artifactDbPath = fs::path(importService.GetLibraryRoot().CStr()) / "artifacts.db";
|
|
std::ifstream artifactDbInput(artifactDbPath, std::ios::binary);
|
|
ASSERT_TRUE(artifactDbInput.is_open());
|
|
std::stringstream artifactDbBuffer;
|
|
artifactDbBuffer << artifactDbInput.rdbuf();
|
|
const std::string artifactDbText = artifactDbBuffer.str();
|
|
EXPECT_NE(artifactDbText.find("# schema=2"), std::string::npos);
|
|
EXPECT_NE(artifactDbText.find("storageKind\tmainEntryName"), std::string::npos);
|
|
}
|
|
|
|
MaterialLoader loader;
|
|
LoadResult loadResult = loader.Load(importedAsset.runtimeLoadPath);
|
|
ASSERT_TRUE(loadResult);
|
|
ASSERT_NE(loadResult.resource, nullptr);
|
|
delete loadResult.resource;
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, BootstrapProjectDefersSourceHashUntilArtifactIsNeeded) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_deferred_source_hash_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
ASSERT_TRUE(importService.BootstrapProject());
|
|
|
|
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
|
const fs::path assetsDbPath = libraryRoot / "assets.db";
|
|
ASSERT_TRUE(fs::exists(assetsDbPath));
|
|
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty());
|
|
|
|
AssetImportService::ImportedAsset importedAsset;
|
|
ASSERT_TRUE(importService.EnsureArtifact("Assets/runtime.material", ResourceType::Material, importedAsset));
|
|
EXPECT_TRUE(importedAsset.exists);
|
|
EXPECT_TRUE(importedAsset.artifactReady);
|
|
EXPECT_TRUE(importedAsset.imported);
|
|
EXPECT_FALSE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty());
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
|
namespace fs = std::filesystem;
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_rebuild_cache_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
|
|
|
AssetRef firstAssetRef;
|
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, firstAssetRef));
|
|
ASSERT_TRUE(firstAssetRef.IsValid());
|
|
|
|
const fs::path libraryRoot(manager.GetProjectLibraryRoot().CStr());
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
|
|
|
EXPECT_TRUE(manager.RebuildProjectAssetCache());
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
|
|
|
AssetRef secondAssetRef;
|
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
|
ASSERT_TRUE(secondAssetRef.IsValid());
|
|
EXPECT_EQ(firstAssetRef.assetGuid, secondAssetRef.assetGuid);
|
|
|
|
manager.SetResourceRoot("");
|
|
manager.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(ResourceManager_Test, AssetImportServiceMigratesLegacyLibraryDatabasePathsToLibraryRoot) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_library_db_path_migration_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path legacySourceDbPath = projectRoot / "Library" / "SourceAssetDB" / "assets.db";
|
|
const fs::path legacyArtifactDbPath = projectRoot / "Library" / "ArtifactDB" / "artifacts.db";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
fs::create_directories(legacySourceDbPath.parent_path());
|
|
fs::create_directories(legacyArtifactDbPath.parent_path());
|
|
|
|
{
|
|
std::ofstream sourceDbFile(legacySourceDbPath);
|
|
ASSERT_TRUE(sourceDbFile.is_open());
|
|
sourceDbFile << "# legacy source database\n";
|
|
}
|
|
{
|
|
std::ofstream artifactDbFile(legacyArtifactDbPath);
|
|
ASSERT_TRUE(artifactDbFile.is_open());
|
|
artifactDbFile << "# legacy artifact database\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
|
|
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
|
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
|
EXPECT_FALSE(fs::exists(legacySourceDbPath));
|
|
EXPECT_FALSE(fs::exists(legacyArtifactDbPath));
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(ResourceManager_Test, SetResourceRootBootstrapsProjectAssetCache) {
|
|
namespace fs = std::filesystem;
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_bootstrap_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
|
|
|
AssetRef assetRef;
|
|
EXPECT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef));
|
|
EXPECT_TRUE(assetRef.IsValid());
|
|
|
|
const AssetImportService::ImportStatusSnapshot status = manager.GetProjectAssetImportStatus();
|
|
EXPECT_TRUE(status.HasValue());
|
|
EXPECT_FALSE(status.inProgress);
|
|
EXPECT_TRUE(status.success);
|
|
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
|
EXPECT_GT(status.startedAtMs, 0u);
|
|
EXPECT_GE(status.completedAtMs, status.startedAtMs);
|
|
EXPECT_EQ(status.durationMs, status.completedAtMs - status.startedAtMs);
|
|
|
|
manager.SetResourceRoot("");
|
|
manager.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(ResourceManager_ProjectSample, BootstrapProjectKeepsCloudSourceHashDeferred) {
|
|
namespace fs = std::filesystem;
|
|
|
|
const fs::path repositoryRoot = GetRepositoryRoot();
|
|
const fs::path projectRoot = repositoryRoot / "project";
|
|
const fs::path volumePath = projectRoot / "Assets" / "cloud.nvdb";
|
|
|
|
if (!fs::exists(volumePath)) {
|
|
GTEST_SKIP() << "Project cloud volume fixture is not available.";
|
|
}
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.Initialize();
|
|
|
|
struct ResourceManagerGuard {
|
|
ResourceManager* manager = nullptr;
|
|
~ResourceManagerGuard() {
|
|
if (manager != nullptr) {
|
|
manager->Shutdown();
|
|
}
|
|
}
|
|
} resourceManagerGuard{ &manager };
|
|
|
|
struct CurrentPathGuard {
|
|
fs::path previousPath;
|
|
~CurrentPathGuard() {
|
|
if (!previousPath.empty()) {
|
|
fs::current_path(previousPath);
|
|
}
|
|
}
|
|
} currentPathGuard{ fs::current_path() };
|
|
|
|
fs::current_path(projectRoot);
|
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
|
|
|
AssetRef volumeRef;
|
|
EXPECT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, volumeRef));
|
|
EXPECT_TRUE(volumeRef.IsValid());
|
|
|
|
const AssetImportService::ImportStatusSnapshot status = manager.GetProjectAssetImportStatus();
|
|
EXPECT_TRUE(status.HasValue());
|
|
EXPECT_FALSE(status.inProgress);
|
|
EXPECT_TRUE(status.success);
|
|
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
|
|
|
const fs::path assetsDbPath = projectRoot / "Library" / "assets.db";
|
|
ASSERT_TRUE(fs::exists(assetsDbPath));
|
|
EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/cloud.nvdb").empty());
|
|
|
|
manager.SetResourceRoot("");
|
|
}
|
|
|
|
TEST(AssetImportService_Test, ClearLibraryAndReimportAllAssetsManageArtifactsExplicitly) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_tooling_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
|
|
ResourceType importType = ResourceType::Unknown;
|
|
EXPECT_TRUE(importService.TryGetImportableResourceType("Assets/runtime.material", importType));
|
|
EXPECT_EQ(importType, ResourceType::Material);
|
|
|
|
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
|
EXPECT_TRUE(importService.ReimportAllAssets());
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
EXPECT_TRUE(importService.ClearLibraryCache());
|
|
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
EXPECT_TRUE(importService.ReimportAllAssets());
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, OnlyShaderAuthoringFilesAreImportableShaderSources) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_shader_source_scope";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path shaderPath = assetsDir / "runtime.shader";
|
|
const fs::path includePath = assetsDir / "shared.hlsl";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream shaderFile(shaderPath);
|
|
ASSERT_TRUE(shaderFile.is_open());
|
|
shaderFile << "Shader \"Test/Runtime\" { SubShader { Pass { HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; } float4 MainPS() : SV_TARGET { return 1; } ENDHLSL } } }";
|
|
}
|
|
{
|
|
std::ofstream includeFile(includePath);
|
|
ASSERT_TRUE(includeFile.is_open());
|
|
includeFile << "float4 SharedHelper() { return 1; }";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
|
|
ResourceType shaderType = ResourceType::Unknown;
|
|
EXPECT_TRUE(importService.TryGetImportableResourceType("Assets/runtime.shader", shaderType));
|
|
EXPECT_EQ(shaderType, ResourceType::Shader);
|
|
|
|
ResourceType includeType = ResourceType::Unknown;
|
|
EXPECT_FALSE(importService.TryGetImportableResourceType("Assets/shared.hlsl", includeType));
|
|
EXPECT_EQ(includeType, ResourceType::Unknown);
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(AssetImportService_Test, ImportStatusTracksExplicitOperationsAndRefreshCleanup) {
|
|
namespace fs = std::filesystem;
|
|
|
|
AssetImportService importService;
|
|
importService.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_status_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
importService.SetProjectRoot(projectRoot.string().c_str());
|
|
EXPECT_FALSE(importService.GetLastImportStatus().HasValue());
|
|
|
|
EXPECT_TRUE(importService.ReimportAllAssets());
|
|
const AssetImportService::ImportStatusSnapshot reimportStatus = importService.GetLastImportStatus();
|
|
EXPECT_TRUE(reimportStatus.HasValue());
|
|
EXPECT_FALSE(reimportStatus.inProgress);
|
|
EXPECT_TRUE(reimportStatus.success);
|
|
EXPECT_EQ(std::string(reimportStatus.operation.CStr()), "Reimport All Assets");
|
|
EXPECT_EQ(reimportStatus.importedAssetCount, 1u);
|
|
EXPECT_GT(reimportStatus.startedAtMs, 0u);
|
|
EXPECT_GE(reimportStatus.completedAtMs, reimportStatus.startedAtMs);
|
|
EXPECT_EQ(reimportStatus.durationMs, reimportStatus.completedAtMs - reimportStatus.startedAtMs);
|
|
|
|
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
|
const std::vector<fs::path> artifactEntries = ListArtifactEntries(libraryRoot / "Artifacts");
|
|
ASSERT_EQ(artifactEntries.size(), 1u);
|
|
EXPECT_TRUE(fs::exists(artifactEntries.front()));
|
|
|
|
std::error_code ec;
|
|
fs::remove(materialPath, ec);
|
|
ec.clear();
|
|
fs::remove(fs::path(materialPath.string() + ".meta"), ec);
|
|
|
|
importService.Refresh();
|
|
const AssetImportService::ImportStatusSnapshot refreshStatus = importService.GetLastImportStatus();
|
|
EXPECT_TRUE(refreshStatus.HasValue());
|
|
EXPECT_TRUE(refreshStatus.success);
|
|
EXPECT_EQ(std::string(refreshStatus.operation.CStr()), "Refresh");
|
|
EXPECT_GE(refreshStatus.removedArtifactCount, 1u);
|
|
EXPECT_GT(refreshStatus.startedAtMs, 0u);
|
|
EXPECT_GE(refreshStatus.completedAtMs, refreshStatus.startedAtMs);
|
|
EXPECT_EQ(refreshStatus.durationMs, refreshStatus.completedAtMs - refreshStatus.startedAtMs);
|
|
EXPECT_FALSE(fs::exists(artifactEntries.front()));
|
|
|
|
importService.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
TEST(ResourceManager_Test, ReimportProjectAssetBuildsArtifactForSelectedPath) {
|
|
namespace fs = std::filesystem;
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.Initialize();
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_reimport_asset_test";
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
const fs::path materialPath = assetsDir / "runtime.material";
|
|
|
|
fs::remove_all(projectRoot);
|
|
fs::create_directories(assetsDir);
|
|
{
|
|
std::ofstream materialFile(materialPath);
|
|
ASSERT_TRUE(materialFile.is_open());
|
|
materialFile << "{\n";
|
|
materialFile << " \"renderQueue\": \"geometry\"\n";
|
|
materialFile << "}\n";
|
|
}
|
|
|
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
|
EXPECT_TRUE(manager.CanReimportProjectAsset("Assets/runtime.material"));
|
|
|
|
const fs::path libraryRoot(manager.GetProjectLibraryRoot().CStr());
|
|
EXPECT_TRUE(manager.ReimportProjectAsset("Assets/runtime.material"));
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
manager.SetResourceRoot("");
|
|
manager.Shutdown();
|
|
fs::remove_all(projectRoot);
|
|
}
|
|
|
|
} // namespace
|