#include #include "XCUIBackend/XCUIAssetDocumentSource.h" #include #include #include #include #include #include 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 "\n" " \n" " \n" " \n" "\n"; } std::string BuildMinimalThemeDocument() { return "\n" " \n" "\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 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