Finalize library bootstrap status and stabilize async asset regressions

This commit is contained in:
2026-04-04 19:44:59 +08:00
parent 013e5a73b9
commit bcef1f145b
25 changed files with 3415 additions and 81 deletions

View File

@@ -9,6 +9,26 @@ using namespace XCEngine::Math;
namespace {
struct TrackingMaterial final : Material {
static int s_destructorCount;
~TrackingMaterial() override {
++s_destructorCount;
}
};
int TrackingMaterial::s_destructorCount = 0;
struct TrackingTexture final : Texture {
static int s_destructorCount;
~TrackingTexture() override {
++s_destructorCount;
}
};
int TrackingTexture::s_destructorCount = 0;
TEST(Mesh, DefaultConstructor) {
Mesh mesh;
EXPECT_EQ(mesh.GetVertexCount(), 0u);
@@ -88,6 +108,27 @@ TEST(Mesh, AddMaterial) {
EXPECT_GT(mesh.GetMemorySize(), 0u);
}
TEST(Mesh, ReleaseDeletesOwnedSubresourcesOnce) {
TrackingMaterial::s_destructorCount = 0;
TrackingTexture::s_destructorCount = 0;
auto* mesh = new Mesh();
mesh->AddMaterial(new TrackingMaterial());
mesh->AddTexture(new TrackingTexture());
mesh->Release();
EXPECT_EQ(TrackingMaterial::s_destructorCount, 1);
EXPECT_EQ(TrackingTexture::s_destructorCount, 1);
EXPECT_EQ(mesh->GetMaterials().Size(), 0u);
EXPECT_EQ(mesh->GetTextures().Size(), 0u);
delete mesh;
EXPECT_EQ(TrackingMaterial::s_destructorCount, 1);
EXPECT_EQ(TrackingTexture::s_destructorCount, 1);
}
TEST(Mesh, SetBounds) {
Mesh mesh;

View File

@@ -169,4 +169,58 @@ TEST(TextureLoader, ResourceManagerLoadsTextureByAssetRefFromProjectAssets) {
fs::remove_all(projectRoot);
}
TEST(TextureLoader, ResourceManagerLoadsLibraryArtifactTextureWithoutReimportingIt) {
namespace fs = std::filesystem;
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_texture_library_artifact_direct_load_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path texturePath = assetsDir / "checker.bmp";
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
fs::copy_file(GetTextureFixturePath("checker.bmp"), texturePath, fs::copy_options::overwrite_existing);
manager.SetResourceRoot(projectRoot.string().c_str());
const AssetImportService::ImportStatusSnapshot bootstrapStatus = manager.GetProjectAssetImportStatus();
EXPECT_TRUE(bootstrapStatus.HasValue());
EXPECT_TRUE(bootstrapStatus.success);
EXPECT_EQ(std::string(bootstrapStatus.operation.CStr()), "Bootstrap Project");
EXPECT_GT(bootstrapStatus.startedAtMs, 0u);
EXPECT_GE(bootstrapStatus.completedAtMs, bootstrapStatus.startedAtMs);
EXPECT_EQ(bootstrapStatus.durationMs, bootstrapStatus.completedAtMs - bootstrapStatus.startedAtMs);
AssetDatabase database;
database.Initialize(projectRoot.string().c_str());
AssetDatabase::ResolvedAsset resolvedAsset;
ASSERT_TRUE(database.EnsureArtifact("Assets/checker.bmp", ResourceType::Texture, resolvedAsset));
ASSERT_TRUE(resolvedAsset.artifactReady);
std::error_code ec;
const fs::path relativeArtifactPath =
fs::relative(fs::path(resolvedAsset.artifactMainPath.CStr()), projectRoot, ec);
ASSERT_FALSE(ec);
ASSERT_FALSE(relativeArtifactPath.empty());
const auto textureHandle = manager.Load<Texture>(relativeArtifactPath.generic_string().c_str());
ASSERT_TRUE(textureHandle.IsValid());
EXPECT_EQ(textureHandle->GetWidth(), 2u);
EXPECT_EQ(textureHandle->GetHeight(), 2u);
EXPECT_EQ(textureHandle->GetPath(), relativeArtifactPath.generic_string().c_str());
const AssetImportService::ImportStatusSnapshot postLoadStatus = manager.GetProjectAssetImportStatus();
EXPECT_TRUE(postLoadStatus.HasValue());
EXPECT_EQ(std::string(postLoadStatus.operation.CStr()), "Bootstrap Project");
EXPECT_GT(postLoadStatus.startedAtMs, 0u);
EXPECT_GE(postLoadStatus.completedAtMs, postLoadStatus.startedAtMs);
EXPECT_EQ(postLoadStatus.durationMs, postLoadStatus.completedAtMs - postLoadStatus.startedAtMs);
database.Shutdown();
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
} // namespace

View File

@@ -0,0 +1,30 @@
# ============================================================
# UI Resource Tests
# ============================================================
set(UI_RESOURCE_TEST_SOURCES
test_ui_document_loader.cpp
)
add_executable(ui_resource_tests ${UI_RESOURCE_TEST_SOURCES})
if(MSVC)
set_target_properties(ui_resource_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(ui_resource_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(ui_resource_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/Fixtures
)
include(GoogleTest)
gtest_discover_tests(ui_resource_tests)

View File

@@ -0,0 +1,215 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/AssetImportService.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
using namespace XCEngine::Resources;
namespace {
void WriteTextFile(const std::filesystem::path& path, const std::string& contents) {
std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary | std::ios::trunc);
ASSERT_TRUE(output.is_open());
output << contents;
ASSERT_TRUE(static_cast<bool>(output));
}
bool ContainsExtension(const XCEngine::Containers::Array<XCEngine::Containers::String>& values,
const char* expectedValue) {
for (const auto& value : values) {
if (value == expectedValue) {
return true;
}
}
return false;
}
bool ContainsDependencyFile(const XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
const char* expectedFileName) {
namespace fs = std::filesystem;
for (const auto& dependency : dependencies) {
if (fs::path(dependency.CStr()).filename().string() == expectedFileName) {
return true;
}
}
return false;
}
TEST(UIDocumentLoader, LoadersExposeExpectedTypesAndExtensions) {
UIViewLoader viewLoader;
UIThemeLoader themeLoader;
UISchemaLoader schemaLoader;
EXPECT_EQ(viewLoader.GetResourceType(), ResourceType::UIView);
EXPECT_EQ(themeLoader.GetResourceType(), ResourceType::UITheme);
EXPECT_EQ(schemaLoader.GetResourceType(), ResourceType::UISchema);
const auto viewExtensions = viewLoader.GetSupportedExtensions();
EXPECT_TRUE(ContainsExtension(viewExtensions, "xcui"));
EXPECT_TRUE(ContainsExtension(viewExtensions, "xcuiasset"));
EXPECT_TRUE(viewLoader.CanLoad("panel.xcui"));
EXPECT_TRUE(viewLoader.CanLoad("panel.xcuiasset"));
EXPECT_FALSE(viewLoader.CanLoad("panel.txt"));
const auto themeExtensions = themeLoader.GetSupportedExtensions();
EXPECT_TRUE(ContainsExtension(themeExtensions, "xctheme"));
EXPECT_TRUE(ContainsExtension(themeExtensions, "xcthemeasset"));
EXPECT_TRUE(themeLoader.CanLoad("editor.xctheme"));
EXPECT_TRUE(themeLoader.CanLoad("editor.xcthemeasset"));
const auto schemaExtensions = schemaLoader.GetSupportedExtensions();
EXPECT_TRUE(ContainsExtension(schemaExtensions, "xcschema"));
EXPECT_TRUE(ContainsExtension(schemaExtensions, "xcschemaasset"));
EXPECT_TRUE(schemaLoader.CanLoad("entity.xcschema"));
EXPECT_TRUE(schemaLoader.CanLoad("entity.xcschemaasset"));
}
TEST(UIDocumentLoader, CompileAndLoadViewTracksDependencies) {
namespace fs = std::filesystem;
const fs::path root = fs::temp_directory_path() / "xc_ui_document_compile_test";
fs::remove_all(root);
WriteTextFile(root / "shared" / "toolbar.xcui", "<View name=\"Toolbar\" />\n");
WriteTextFile(root / "themes" / "editor.xctheme", "<Theme name=\"EditorTheme\" />\n");
WriteTextFile(root / "schemas" / "entity.xcschema", "<Schema name=\"EntitySchema\" />\n");
WriteTextFile(
root / "main.xcui",
"<!-- root comment -->\n"
"<View name=\"MainPanel\" theme=\"themes/editor.xctheme\">\n"
" <Column>\n"
" <Use view=\"shared/toolbar.xcui\" />\n"
" <AutoForm schema=\"schemas/entity.xcschema\" />\n"
" </Column>\n"
"</View>\n");
UIViewLoader loader;
UIDocumentCompileResult compileResult = {};
ASSERT_TRUE(loader.CompileDocument((root / "main.xcui").string().c_str(), compileResult));
ASSERT_TRUE(compileResult.succeeded);
ASSERT_TRUE(compileResult.document.valid);
EXPECT_EQ(compileResult.document.rootNode.tagName, "View");
EXPECT_EQ(compileResult.document.rootNode.children.Size(), 1u);
EXPECT_EQ(compileResult.document.rootNode.children[0].tagName, "Column");
EXPECT_EQ(compileResult.document.dependencies.Size(), 3u);
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "toolbar.xcui"));
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "editor.xctheme"));
EXPECT_TRUE(ContainsDependencyFile(compileResult.document.dependencies, "entity.xcschema"));
LoadResult loadResult = loader.Load((root / "main.xcui").string().c_str());
ASSERT_TRUE(loadResult);
ASSERT_NE(loadResult.resource, nullptr);
auto* view = static_cast<UIView*>(loadResult.resource);
ASSERT_NE(view, nullptr);
EXPECT_TRUE(view->IsValid());
EXPECT_EQ(view->GetName(), "MainPanel");
EXPECT_EQ(view->GetRootNode().tagName, "View");
EXPECT_EQ(view->GetDependencies().Size(), 3u);
delete view;
fs::remove_all(root);
}
TEST(UIDocumentLoader, AssetDatabaseImportsViewArtifactAndReimportsWhenDependencyChanges) {
namespace fs = std::filesystem;
using namespace std::chrono_literals;
const fs::path projectRoot = fs::temp_directory_path() / "xc_ui_artifact_reimport_test";
const fs::path assetsRoot = projectRoot / "Assets";
fs::remove_all(projectRoot);
WriteTextFile(assetsRoot / "UI" / "Shared" / "toolbar.xcui", "<View name=\"Toolbar\" />\n");
WriteTextFile(
assetsRoot / "UI" / "Main.xcui",
"<View name=\"Inspector\">\n"
" <Use view=\"Shared/toolbar.xcui\" />\n"
"</View>\n");
AssetDatabase database;
database.Initialize(projectRoot.string().c_str());
AssetDatabase::ResolvedAsset firstResolve;
ASSERT_TRUE(database.EnsureArtifact("Assets/UI/Main.xcui", ResourceType::UIView, firstResolve));
ASSERT_TRUE(firstResolve.artifactReady);
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().string(), ".xcuiasset");
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
UIViewLoader loader;
LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr());
ASSERT_TRUE(firstLoad);
auto* firstView = static_cast<UIView*>(firstLoad.resource);
ASSERT_NE(firstView, nullptr);
EXPECT_EQ(firstView->GetRootNode().tagName, "View");
EXPECT_EQ(firstView->GetDependencies().Size(), 1u);
EXPECT_TRUE(ContainsDependencyFile(firstView->GetDependencies(), "toolbar.xcui"));
delete firstView;
const XCEngine::Containers::String firstArtifactPath = firstResolve.artifactMainPath;
database.Shutdown();
std::this_thread::sleep_for(50ms);
WriteTextFile(
assetsRoot / "UI" / "Shared" / "toolbar.xcui",
"<View name=\"Toolbar\">\n"
" <Button id=\"refresh\" />\n"
"</View>\n");
database.Initialize(projectRoot.string().c_str());
AssetDatabase::ResolvedAsset secondResolve;
ASSERT_TRUE(database.EnsureArtifact("Assets/UI/Main.xcui", ResourceType::UIView, secondResolve));
ASSERT_TRUE(secondResolve.artifactReady);
EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath);
EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr()));
database.Shutdown();
fs::remove_all(projectRoot);
}
TEST(UIDocumentLoader, AssetImportServiceReportsDetailedDiagnosticsForMissingDependency) {
namespace fs = std::filesystem;
const fs::path projectRoot = fs::temp_directory_path() / "xc_ui_import_error_test";
const fs::path assetsRoot = projectRoot / "Assets";
fs::remove_all(projectRoot);
WriteTextFile(
assetsRoot / "UI" / "Broken.xcui",
"<View>\n"
" <Use view=\"Shared/missing_toolbar.xcui\" />\n"
"</View>\n");
AssetImportService importService;
importService.Initialize();
importService.SetProjectRoot(projectRoot.string().c_str());
AssetImportService::ImportedAsset importedAsset;
EXPECT_FALSE(importService.EnsureArtifact("Assets/UI/Broken.xcui", ResourceType::UIView, importedAsset));
const AssetImportService::ImportStatusSnapshot status = importService.GetLastImportStatus();
EXPECT_TRUE(status.HasValue());
EXPECT_FALSE(status.success);
const std::string message = status.message.CStr();
EXPECT_NE(message.find("Failed to build asset artifact: Assets/UI/Broken.xcui"), std::string::npos);
EXPECT_NE(message.find("Referenced UI document was not found"), std::string::npos);
EXPECT_NE(message.find("missing_toolbar.xcui"), std::string::npos);
importService.Shutdown();
fs::remove_all(projectRoot);
}
} // namespace

View File

@@ -565,12 +565,12 @@ TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) {
std::filesystem::remove(scenePath);
}
TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) {
TEST_F(SceneTest, SaveAndLoad_PreservesBuiltinMeshComponentPaths) {
GameObject* meshObject = testScene->CreateGameObject("Backpack");
auto* meshFilter = meshObject->AddComponent<MeshFilterComponent>();
auto* meshRenderer = meshObject->AddComponent<MeshRendererComponent>();
meshFilter->SetMeshPath("Assets/Models/backpack/backpack.obj");
meshRenderer->SetMaterialPath(0, "Assets/Materials/backpack.mat");
meshFilter->SetMeshPath("builtin://meshes/cube");
meshRenderer->SetMaterialPath(0, "builtin://materials/default-primitive");
meshRenderer->SetCastShadows(false);
meshRenderer->SetReceiveShadows(true);
meshRenderer->SetRenderLayer(4);
@@ -589,9 +589,9 @@ TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) {
ASSERT_NE(loadedMeshFilter, nullptr);
ASSERT_NE(loadedMeshRenderer, nullptr);
EXPECT_EQ(loadedMeshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
EXPECT_EQ(loadedMeshFilter->GetMeshPath(), "builtin://meshes/cube");
ASSERT_EQ(loadedMeshRenderer->GetMaterialCount(), 1u);
EXPECT_EQ(loadedMeshRenderer->GetMaterialPath(0), "Assets/Materials/backpack.mat");
EXPECT_EQ(loadedMeshRenderer->GetMaterialPath(0), "builtin://materials/default-primitive");
EXPECT_FALSE(loadedMeshRenderer->GetCastShadows());
EXPECT_TRUE(loadedMeshRenderer->GetReceiveShadows());
EXPECT_EQ(loadedMeshRenderer->GetRenderLayer(), 4u);
@@ -699,7 +699,7 @@ TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
EXPECT_NE(loadedScene.Find("Camera"), nullptr);
EXPECT_NE(loadedScene.Find("Light"), nullptr);
EXPECT_NE(loadedScene.Find("Cube"), nullptr);
EXPECT_NE(loadedScene.Find("Sphere"), nullptr);
EXPECT_EQ(loadedScene.Find("BackpackMesh"), nullptr);
EXPECT_EQ(FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj").size(), 0u);
}

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
@@ -107,6 +108,45 @@ bool PumpAsyncLoadsUntil(ResourceManager& manager,
return condition();
}
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::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();
@@ -232,4 +272,313 @@ TEST(ProjectAssetIndex_Test, RefreshesSnapshotThroughImportServiceOnCacheMiss) {
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 / "SourceAssetDB" / "assets.db"));
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "artifacts.db"));
EXPECT_TRUE(importService.RebuildLibraryCache());
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "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(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 / "SourceAssetDB" / "assets.db"));
EXPECT_TRUE(manager.RebuildProjectAssetCache());
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "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, 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(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, 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