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

@@ -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