258 lines
10 KiB
C++
258 lines
10 KiB
C++
|
|
#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
|