diff --git a/tests/src/Gui/CMakeLists.txt b/tests/src/Gui/CMakeLists.txt index aa7ed73357..5f1d4b10f4 100644 --- a/tests/src/Gui/CMakeLists.txt +++ b/tests/src/Gui/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(Gui_tests_run Assistant.cpp Camera.cpp + OriginManager.cpp StyleParameters/StyleParametersApplicationTest.cpp StyleParameters/ParserTest.cpp StyleParameters/ParameterManagerTest.cpp diff --git a/tests/src/Gui/OriginManager.cpp b/tests/src/Gui/OriginManager.cpp new file mode 100644 index 0000000000..e15f2e4336 --- /dev/null +++ b/tests/src/Gui/OriginManager.cpp @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + + +namespace +{ + +/** + * Minimal FileOrigin implementation for testing OriginManager. + * All document operations are stubs -- we only need identity, + * capability, and ownership methods to test the manager. + */ +class MockOrigin : public Gui::FileOrigin +{ +public: + explicit MockOrigin(std::string originId, + Gui::OriginType originType = Gui::OriginType::PLM, + bool ownsAll = false) + : _id(std::move(originId)) + , _type(originType) + , _ownsAll(ownsAll) + {} + + // Identity + std::string id() const override { return _id; } + std::string name() const override { return _id + " Name"; } + std::string nickname() const override { return _id; } + QIcon icon() const override { return {}; } + Gui::OriginType type() const override { return _type; } + + // Characteristics + bool tracksExternally() const override { return _type == Gui::OriginType::PLM; } + bool requiresAuthentication() const override { return _type == Gui::OriginType::PLM; } + + // Capabilities + bool supportsRevisions() const override { return _supportsRevisions; } + bool supportsBOM() const override { return _supportsBOM; } + bool supportsPartNumbers() const override { return _supportsPartNumbers; } + + // Document identity + std::string documentIdentity(App::Document* /*doc*/) const override { return {}; } + std::string documentDisplayId(App::Document* /*doc*/) const override { return {}; } + + bool ownsDocument(App::Document* doc) const override + { + if (!doc) { + return false; + } + return _ownsAll; + } + + // Document operations (stubs) + App::Document* newDocument(const std::string& /*name*/) override { return nullptr; } + App::Document* openDocument(const std::string& /*identity*/) override { return nullptr; } + App::Document* openDocumentInteractive() override { return nullptr; } + bool saveDocument(App::Document* /*doc*/) override { return false; } + bool saveDocumentAs(App::Document* /*doc*/, const std::string& /*id*/) override + { + return false; + } + bool saveDocumentAsInteractive(App::Document* /*doc*/) override { return false; } + + // Test controls + bool _supportsRevisions = true; + bool _supportsBOM = true; + bool _supportsPartNumbers = true; + +private: + std::string _id; + Gui::OriginType _type; + bool _ownsAll; +}; + +} // namespace + + +// ========================================================================= +// LocalFileOrigin identity tests +// ========================================================================= + +class LocalFileOriginTest : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + _origin = std::make_unique(); + } + + Gui::LocalFileOrigin* origin() { return _origin.get(); } + +private: + std::unique_ptr _origin; +}; + +TEST_F(LocalFileOriginTest, LocalOriginId) +{ + EXPECT_EQ(origin()->id(), "local"); +} + +TEST_F(LocalFileOriginTest, LocalOriginName) +{ + EXPECT_EQ(origin()->name(), "Local Files"); +} + +TEST_F(LocalFileOriginTest, LocalOriginNickname) +{ + EXPECT_EQ(origin()->nickname(), "Local"); +} + +TEST_F(LocalFileOriginTest, LocalOriginType) +{ + EXPECT_EQ(origin()->type(), Gui::OriginType::Local); +} + +TEST_F(LocalFileOriginTest, LocalOriginCapabilities) +{ + EXPECT_FALSE(origin()->tracksExternally()); + EXPECT_FALSE(origin()->requiresAuthentication()); + EXPECT_FALSE(origin()->supportsRevisions()); + EXPECT_FALSE(origin()->supportsBOM()); + EXPECT_FALSE(origin()->supportsPartNumbers()); + EXPECT_FALSE(origin()->supportsAssemblies()); +} + + +// ========================================================================= +// OriginManager tests +// ========================================================================= + +class OriginManagerTest : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + // Ensure clean singleton state for each test + Gui::OriginManager::destruct(); + _mgr = Gui::OriginManager::instance(); + } + + void TearDown() override + { + Gui::OriginManager::destruct(); + } + + Gui::OriginManager* mgr() { return _mgr; } + +private: + Gui::OriginManager* _mgr = nullptr; +}; + +// --- Registration --- + +TEST_F(OriginManagerTest, LocalOriginAlwaysPresent) +{ + auto* local = mgr()->getOrigin("local"); + ASSERT_NE(local, nullptr); + EXPECT_EQ(local->id(), "local"); +} + +TEST_F(OriginManagerTest, RegisterCustomOrigin) +{ + auto* raw = new MockOrigin("test-plm"); + EXPECT_TRUE(mgr()->registerOrigin(raw)); + EXPECT_EQ(mgr()->getOrigin("test-plm"), raw); +} + +TEST_F(OriginManagerTest, RejectDuplicateId) +{ + mgr()->registerOrigin(new MockOrigin("dup")); + // Second registration with same ID should fail + EXPECT_FALSE(mgr()->registerOrigin(new MockOrigin("dup"))); +} + +TEST_F(OriginManagerTest, RejectNullOrigin) +{ + EXPECT_FALSE(mgr()->registerOrigin(nullptr)); +} + +TEST_F(OriginManagerTest, OriginIdsIncludesAll) +{ + mgr()->registerOrigin(new MockOrigin("alpha")); + mgr()->registerOrigin(new MockOrigin("beta")); + + auto ids = mgr()->originIds(); + EXPECT_EQ(ids.size(), 3u); // local + alpha + beta + + auto has = [&](const std::string& id) { + return std::find(ids.begin(), ids.end(), id) != ids.end(); + }; + EXPECT_TRUE(has("local")); + EXPECT_TRUE(has("alpha")); + EXPECT_TRUE(has("beta")); +} + +// --- Unregistration --- + +TEST_F(OriginManagerTest, UnregisterCustomOrigin) +{ + mgr()->registerOrigin(new MockOrigin("removable")); + EXPECT_TRUE(mgr()->unregisterOrigin("removable")); + EXPECT_EQ(mgr()->getOrigin("removable"), nullptr); +} + +TEST_F(OriginManagerTest, CannotUnregisterLocal) +{ + EXPECT_FALSE(mgr()->unregisterOrigin("local")); + EXPECT_NE(mgr()->getOrigin("local"), nullptr); +} + +TEST_F(OriginManagerTest, UnregisterCurrentSwitchesToLocal) +{ + mgr()->registerOrigin(new MockOrigin("ephemeral")); + mgr()->setCurrentOrigin("ephemeral"); + EXPECT_EQ(mgr()->currentOriginId(), "ephemeral"); + + mgr()->unregisterOrigin("ephemeral"); + EXPECT_EQ(mgr()->currentOriginId(), "local"); +} + +// --- Current origin selection --- + +TEST_F(OriginManagerTest, DefaultCurrentIsLocal) +{ + EXPECT_EQ(mgr()->currentOriginId(), "local"); + EXPECT_NE(mgr()->currentOrigin(), nullptr); +} + +TEST_F(OriginManagerTest, SetCurrentOrigin) +{ + mgr()->registerOrigin(new MockOrigin("plm1")); + + std::string notified; + auto conn = mgr()->signalCurrentOriginChanged.connect([&](const std::string& id) { + notified = id; + }); + + EXPECT_TRUE(mgr()->setCurrentOrigin("plm1")); + EXPECT_EQ(mgr()->currentOriginId(), "plm1"); + EXPECT_EQ(notified, "plm1"); +} + +TEST_F(OriginManagerTest, SetCurrentRejectsUnknown) +{ + EXPECT_FALSE(mgr()->setCurrentOrigin("nonexistent")); + EXPECT_EQ(mgr()->currentOriginId(), "local"); +} + +TEST_F(OriginManagerTest, SetCurrentSameIdNoSignal) +{ + int signalCount = 0; + auto conn = mgr()->signalCurrentOriginChanged.connect([&](const std::string&) { + signalCount++; + }); + + mgr()->setCurrentOrigin("local"); // already current + EXPECT_EQ(signalCount, 0); +} + +// --- Document ownership --- + +class OriginManagerDocTest : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + Gui::OriginManager::destruct(); + _mgr = Gui::OriginManager::instance(); + _docName = App::GetApplication().getUniqueDocumentName("test"); + _doc = App::GetApplication().newDocument(_docName.c_str(), "testUser"); + } + + void TearDown() override + { + App::GetApplication().closeDocument(_docName.c_str()); + Gui::OriginManager::destruct(); + } + + Gui::OriginManager* mgr() { return _mgr; } + App::Document* doc() { return _doc; } + +private: + Gui::OriginManager* _mgr = nullptr; + std::string _docName; + App::Document* _doc = nullptr; +}; + +TEST_F(OriginManagerDocTest, LocalOwnsPlainDocument) +{ + // A document with no SiloItemId property should be owned by local + auto* local = mgr()->getOrigin("local"); + ASSERT_NE(local, nullptr); + EXPECT_TRUE(local->ownsDocument(doc())); +} + +TEST_F(OriginManagerDocTest, LocalDisownsTrackedDocument) +{ + // Add an object with a SiloItemId property -- local should reject ownership + auto* obj = doc()->addObject("App::FeaturePython", "TrackedPart"); + ASSERT_NE(obj, nullptr); + obj->addDynamicProperty("App::PropertyString", "SiloItemId"); + auto* prop = dynamic_cast(obj->getPropertyByName("SiloItemId")); + ASSERT_NE(prop, nullptr); + prop->setValue("some-uuid"); + + auto* local = mgr()->getOrigin("local"); + EXPECT_FALSE(local->ownsDocument(doc())); +} + +TEST_F(OriginManagerDocTest, FindOwningOriginPrefersNonLocal) +{ + // Register a PLM mock that claims ownership of everything + auto* plm = new MockOrigin("test-plm", Gui::OriginType::PLM, /*ownsAll=*/true); + mgr()->registerOrigin(plm); + + auto* owner = mgr()->findOwningOrigin(doc()); + ASSERT_NE(owner, nullptr); + EXPECT_EQ(owner->id(), "test-plm"); +} + +TEST_F(OriginManagerDocTest, FindOwningOriginFallsBackToLocal) +{ + // No PLM origins registered -- should fall back to local + auto* owner = mgr()->findOwningOrigin(doc()); + ASSERT_NE(owner, nullptr); + EXPECT_EQ(owner->id(), "local"); +} + +TEST_F(OriginManagerDocTest, OriginForNewDocumentReturnsCurrent) +{ + mgr()->registerOrigin(new MockOrigin("plm2")); + mgr()->setCurrentOrigin("plm2"); + + auto* origin = mgr()->originForNewDocument(); + ASSERT_NE(origin, nullptr); + EXPECT_EQ(origin->id(), "plm2"); +} + +TEST_F(OriginManagerDocTest, SetAndClearDocumentOrigin) +{ + auto* local = mgr()->getOrigin("local"); + mgr()->setDocumentOrigin(doc(), local); + EXPECT_EQ(mgr()->originForDocument(doc()), local); + + mgr()->clearDocumentOrigin(doc()); + // After clearing, originForDocument falls back to ownership detection + auto* resolved = mgr()->originForDocument(doc()); + ASSERT_NE(resolved, nullptr); + EXPECT_EQ(resolved->id(), "local"); +} + +TEST_F(OriginManagerDocTest, NullDocumentHandling) +{ + EXPECT_EQ(mgr()->findOwningOrigin(nullptr), nullptr); + EXPECT_EQ(mgr()->originForDocument(nullptr), nullptr); +}