Files
XCEngine/tests/NewEditor/test_xcui_asset_document_source.cpp

258 lines
10 KiB
C++
Raw Normal View History

2026-04-05 04:55:25 +08:00
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIAssetDocumentSource.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
namespace {
namespace fs = std::filesystem;
using XCEngine::Containers::String;
using XCEngine::Editor::XCUIBackend::XCUIAssetDocumentSource;
using XCEngine::Resources::ResourceManager;
String ToContainersString(const std::string& value) {
return String(value.c_str());
}
void WriteTextFile(const fs::path& path, const std::string& contents) {
fs::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output << contents;
}
std::string BuildMinimalViewDocument(const std::string& themeReference) {
return
"<View name=\"Test\" theme=\"" + themeReference + "\">\n"
" <Column id=\"root\">\n"
" <Text text=\"hello\" />\n"
" </Column>\n"
"</View>\n";
}
std::string BuildMinimalThemeDocument() {
return
"<Theme name=\"TestTheme\">\n"
" <Token name=\"color.text.primary\" type=\"color\" value=\"#FFFFFF\" />\n"
"</Theme>\n";
}
class ScopedCurrentPath {
public:
explicit ScopedCurrentPath(const fs::path& newPath) {
m_originalPath = fs::current_path();
fs::create_directories(newPath);
fs::current_path(newPath);
}
~ScopedCurrentPath() {
if (!m_originalPath.empty()) {
fs::current_path(m_originalPath);
}
}
private:
fs::path m_originalPath = {};
};
class XCUIAssetDocumentSourceTest : public ::testing::Test {
protected:
void SetUp() override {
m_originalResourceRoot = ResourceManager::Get().GetResourceRoot();
ResourceManager::Get().SetResourceRoot(String());
m_tempRoot = fs::temp_directory_path() /
fs::path("xcui_asset_document_source_tests");
m_tempRoot /= fs::path(::testing::UnitTest::GetInstance()->current_test_info()->name());
fs::remove_all(m_tempRoot);
fs::create_directories(m_tempRoot);
}
void TearDown() override {
ResourceManager::Get().UnloadAll();
ResourceManager::Get().SetResourceRoot(m_originalResourceRoot);
std::error_code ec;
fs::remove_all(m_tempRoot, ec);
}
fs::path CreateRepositorySubdir(const std::string& relativePath) const {
const fs::path path = m_tempRoot / fs::path(relativePath);
fs::create_directories(path);
return path;
}
void WriteProjectDocuments(const XCUIAssetDocumentSource::PathSet& paths) const {
WriteTextFile(
m_tempRoot / fs::path(paths.view.primaryRelativePath),
BuildMinimalViewDocument("Theme.xctheme"));
WriteTextFile(
m_tempRoot / fs::path(paths.theme.primaryRelativePath),
BuildMinimalThemeDocument());
}
void WriteLegacyDocuments(const XCUIAssetDocumentSource::PathSet& paths) const {
WriteTextFile(
m_tempRoot / fs::path(paths.view.legacyRelativePath),
BuildMinimalViewDocument(
fs::path(paths.theme.legacyRelativePath).filename().generic_string()));
WriteTextFile(
m_tempRoot / fs::path(paths.theme.legacyRelativePath),
BuildMinimalThemeDocument());
}
fs::path m_tempRoot = {};
private:
String m_originalResourceRoot = {};
};
TEST_F(XCUIAssetDocumentSourceTest, DemoAndLayoutLabPathSetsUseExpectedPaths) {
const XCUIAssetDocumentSource::PathSet demoPaths =
XCUIAssetDocumentSource::MakeDemoPathSet();
EXPECT_EQ(demoPaths.setName, "Demo");
EXPECT_EQ(demoPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/View.xcui");
EXPECT_EQ(demoPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/Theme.xctheme");
EXPECT_EQ(demoPaths.view.legacyRelativePath, "new_editor/resources/xcui_demo_view.xcui");
EXPECT_EQ(demoPaths.theme.legacyRelativePath, "new_editor/resources/xcui_demo_theme.xctheme");
const XCUIAssetDocumentSource::PathSet layoutLabPaths =
XCUIAssetDocumentSource::MakeLayoutLabPathSet();
EXPECT_EQ(layoutLabPaths.setName, "LayoutLab");
EXPECT_EQ(layoutLabPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/View.xcui");
EXPECT_EQ(layoutLabPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme");
EXPECT_EQ(layoutLabPaths.view.legacyRelativePath, "new_editor/resources/xcui_layout_lab_view.xcui");
EXPECT_EQ(layoutLabPaths.theme.legacyRelativePath, "new_editor/resources/xcui_layout_lab_theme.xctheme");
}
TEST_F(XCUIAssetDocumentSourceTest, MakePathSetSanitizesNamesAndBuildsLegacySnakeCase) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet(" Layout Lab! Debug-42 ");
EXPECT_EQ(paths.setName, "LayoutLabDebug-42");
EXPECT_EQ(
paths.view.primaryRelativePath,
"Assets/XCUI/NewEditor/LayoutLabDebug-42/View.xcui");
EXPECT_EQ(
paths.theme.legacyRelativePath,
"new_editor/resources/xcui_layout_lab_debug_42_theme.xctheme");
}
TEST_F(XCUIAssetDocumentSourceTest, CollectCandidatePathsPrefersProjectAssetsBeforeLegacyMirror) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1CandidateOrder");
WriteProjectDocuments(paths);
WriteLegacyDocuments(paths);
const std::vector<XCUIAssetDocumentSource::ResolutionCandidate> candidates =
XCUIAssetDocumentSource::CollectCandidatePaths(paths.view, m_tempRoot, fs::path());
ASSERT_EQ(candidates.size(), 2u);
EXPECT_EQ(candidates[0].origin, XCUIAssetDocumentSource::PathOrigin::ProjectAssets);
EXPECT_EQ(candidates[0].resolvedPath, fs::path(m_tempRoot / paths.view.primaryRelativePath).lexically_normal());
EXPECT_EQ(candidates[1].origin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_EQ(candidates[1].resolvedPath, fs::path(m_tempRoot / paths.view.legacyRelativePath).lexically_normal());
}
TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsProjectAssetAncestor) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakeDemoPathSet();
WriteProjectDocuments(paths);
const fs::path searchRoot = CreateRepositorySubdir("tools/worker1/deep");
const XCUIAssetDocumentSource::RepositoryDiscovery discovery =
XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false);
EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal());
ASSERT_EQ(discovery.probes.size(), 1u);
EXPECT_TRUE(discovery.probes[0].matched);
EXPECT_EQ(discovery.probes[0].searchRoot, searchRoot.lexically_normal());
EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.primaryRelativePath);
EXPECT_NE(discovery.statusMessage.find(paths.view.primaryRelativePath), std::string::npos);
}
TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsLegacyMirrorAncestor) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakeLayoutLabPathSet();
WriteLegacyDocuments(paths);
const fs::path searchRoot = CreateRepositorySubdir("sandbox/runtime/session");
const XCUIAssetDocumentSource::RepositoryDiscovery discovery =
XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false);
EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal());
ASSERT_EQ(discovery.probes.size(), 1u);
EXPECT_TRUE(discovery.probes[0].matched);
EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.legacyRelativePath);
EXPECT_NE(discovery.statusMessage.find(paths.view.legacyRelativePath), std::string::npos);
}
TEST_F(XCUIAssetDocumentSourceTest, ReloadUsesLegacyFallbackAndTracksSourceChanges) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1HotReloadRegression");
WriteLegacyDocuments(paths);
const fs::path sandboxPath = CreateRepositorySubdir("runtime/worker1");
ScopedCurrentPath scopedCurrentPath(sandboxPath);
XCUIAssetDocumentSource source(paths);
ASSERT_TRUE(source.Reload());
const XCUIAssetDocumentSource::LoadState& initialState = source.GetState();
EXPECT_TRUE(initialState.succeeded);
EXPECT_TRUE(initialState.usedLegacyFallback);
EXPECT_TRUE(initialState.changeTrackingReady);
EXPECT_TRUE(initialState.missingTrackedSourcePaths.empty());
EXPECT_EQ(initialState.repositoryRoot, m_tempRoot.lexically_normal());
EXPECT_EQ(initialState.view.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_EQ(initialState.theme.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_FALSE(initialState.trackedSourcePaths.empty());
EXPECT_NE(initialState.trackingStatusMessage.find("Tracking "), std::string::npos);
EXPECT_FALSE(source.HasTrackedChanges());
const fs::path themePath = m_tempRoot / fs::path(paths.theme.legacyRelativePath);
fs::last_write_time(
themePath,
fs::last_write_time(themePath) + std::chrono::seconds(2));
EXPECT_TRUE(source.HasTrackedChanges());
ASSERT_TRUE(source.ReloadIfChanged());
const XCUIAssetDocumentSource::LoadState& reloadedState = source.GetState();
EXPECT_EQ(
reloadedState.view.backend,
XCUIAssetDocumentSource::LoadBackend::CompilerFallback);
EXPECT_EQ(
reloadedState.theme.backend,
XCUIAssetDocumentSource::LoadBackend::CompilerFallback);
EXPECT_TRUE(reloadedState.changeTrackingReady);
}
TEST_F(XCUIAssetDocumentSourceTest, ReloadFailureIncludesRepositoryDiscoveryDiagnostic) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1MissingDocuments");
const fs::path sandboxPath = CreateRepositorySubdir("runtime/missing");
ScopedCurrentPath scopedCurrentPath(sandboxPath);
XCUIAssetDocumentSource source(paths);
EXPECT_FALSE(source.Reload());
const XCUIAssetDocumentSource::LoadState& state = source.GetState();
EXPECT_FALSE(state.succeeded);
EXPECT_TRUE(state.repositoryRoot.empty());
EXPECT_NE(state.repositoryDiscovery.statusMessage.find("Repository root not found"), std::string::npos);
EXPECT_NE(state.errorMessage.find(paths.view.primaryRelativePath), std::string::npos);
EXPECT_NE(state.errorMessage.find("Repository root not found"), std::string::npos);
EXPECT_TRUE(state.view.candidatePaths.empty());
EXPECT_TRUE(state.view.attemptMessages.empty());
}
} // namespace