/**************************************************************************** ** ** Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt3D module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "testpostmanarbiter.h" class tst_Nodes : public QObject { Q_OBJECT public: tst_Nodes() : QObject() { qRegisterMetaType(); } ~tst_Nodes() {} private slots: void initTestCase(); void defaultNodeConstruction(); void defaultComponentConstruction(); void defaultEntityConstrution(); void appendSingleChildNodeToNodeNoSceneExplicitParenting(); void appendSingleChildNodeToNodeNoSceneImplicitParenting(); void appendMultipleChildNodesToNodeNoScene(); void appendSingleChildNodeToNodeSceneExplicitParenting(); void appendSingleChildNodeToNodeSceneImplicitParenting(); void appendMultipleChildNodesToNodeScene(); void checkParentChangeToNull(); void checkParentChangeToOtherParent(); void checkParentChangeFromExistingBackendParentToNewlyCreatedParent(); void checkBackendNodesCreatedFromTopDown(); //QTBUG-74106 void checkBackendNodesCreatedFromTopDownWithReparenting(); void checkAllBackendCreationDoneInSingleFrame(); void removingSingleChildNodeFromNode(); void removingMultipleChildNodesFromNode(); void appendingChildEntitiesToNode(); void removingChildEntitiesFromNode(); void checkConstructionSetParentMix(); // QTBUG-60612 void checkParentingQEntityToQNode(); // QTBUG-73905 void checkConstructionWithParent(); void checkConstructionWithNonRootParent(); // QTBUG-73986 void checkConstructionAsListElement(); void checkSceneIsSetOnConstructionWithParent(); // QTBUG-69352 void checkSubNodePostConstructIsCalledWhenReferincingNodeProperty(); // QTBUG-79350 void appendingComponentToEntity(); void appendingParentlessComponentToEntityWithoutScene(); void appendingParentlessComponentToEntityWithScene(); void appendingParentlessComponentToNonRootEntity(); void removingComponentFromEntity(); void changeCustomProperty(); void checkDestruction(); void checkDefaultConstruction(); void checkPropertyChanges(); void checkCreationData(); void checkEnabledUpdate(); void checkPropertyTrackModeUpdate(); void checkTrackedPropertyNamesUpdate(); void checkNodeRemovedFromDirtyListOnDestruction(); }; class ObserverSpy; class SimplePostman : public Qt3DCore::QAbstractPostman { public: SimplePostman(ObserverSpy *spy) : m_spy(spy) {} void sceneChangeEvent(const Qt3DCore::QSceneChangePtr &) final {} void setScene(Qt3DCore::QScene *) final {} void notifyBackend(const Qt3DCore::QSceneChangePtr &change) final; bool shouldNotifyFrontend(const Qt3DCore::QSceneChangePtr &changee) final { Q_UNUSED(changee); return false; } private: ObserverSpy *m_spy; }; class ObserverSpy : public Qt3DCore::QAbstractArbiter { public: class ChangeRecord : public QPair { public: ChangeRecord(const Qt3DCore::QSceneChangePtr &event, bool locked) : QPair(event, locked) {} Qt3DCore::QSceneChangePtr change() const { return first; } bool wasLocked() const { return second; } }; ObserverSpy() : Qt3DCore::QAbstractArbiter() , m_postman(new SimplePostman(this)) { } ~ObserverSpy(); void sceneChangeEventWithLock(const Qt3DCore::QSceneChangePtr &e) override { events << ChangeRecord(e, true); } void sceneChangeEventWithLock(const Qt3DCore::QSceneChangeList &e) override { for (size_t i = 0, m = e.size(); i < m; ++i) { events << ChangeRecord(e.at(i), false); } } void sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e) override { events << ChangeRecord(e, false); } Qt3DCore::QAbstractPostman *postman() const final { return m_postman.data(); } void addDirtyFrontEndNode(Qt3DCore::QNode *node) final { if (!dirtyNodes.contains(node)) dirtyNodes << node; } void addDirtyFrontEndNode(Qt3DCore::QNode *node, Qt3DCore::QNode *subNode, const char *property, Qt3DCore::ChangeFlag change) final { if (!dirtyNodes.contains(node)) dirtyNodes << node; dirtySubNodes.push_back({node, subNode, change, property}); } void removeDirtyFrontEndNode(Qt3DCore::QNode *node) final { dirtyNodes.removeOne(node); } QVector dirtyNodes; QVector dirtySubNodes; QList events; QScopedPointer m_postman; }; ObserverSpy::~ObserverSpy() { } void SimplePostman::notifyBackend(const Qt3DCore::QSceneChangePtr &change) { m_spy->sceneChangeEventWithLock(change); } class MyQNode : public Qt3DCore::QNode { Q_OBJECT Q_PROPERTY(QString customProperty READ customProperty WRITE setCustomProperty NOTIFY customPropertyChanged) Q_PROPERTY(MyQNode *nodeProperty READ nodeProperty WRITE setNodeProperty NOTIFY nodePropertyChanged) public: explicit MyQNode(Qt3DCore::QNode *parent = nullptr) : QNode(parent) , m_nodeProperty(nullptr) {} ~MyQNode() { } void setCustomProperty(const QString &s) { if (m_customProperty == s) return; m_customProperty = s; emit customPropertyChanged(); } QString customProperty() const { return m_customProperty; } void setArbiterAndScene(Qt3DCore::QAbstractArbiter *arbiter, Qt3DCore::QScene *scene = nullptr) { Q_ASSERT(arbiter); if (scene) scene->setArbiter(arbiter); Qt3DCore::QNodePrivate::get(this)->setScene(scene); Qt3DCore::QNodePrivate::get(this)->setArbiter(arbiter); } void setSimulateBackendCreated(bool created) { Qt3DCore::QNodePrivate::get(this)->m_hasBackendNode = created; } MyQNode *nodeProperty() const { return m_nodeProperty; } public slots: void setNodeProperty(MyQNode *node) { Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(this); if (m_nodeProperty == node) return; if (m_nodeProperty) d->unregisterDestructionHelper(m_nodeProperty); if (node && !node->parent()) node->setParent(this); m_nodeProperty = node; // Ensures proper bookkeeping if (m_nodeProperty) d->registerDestructionHelper(m_nodeProperty, &MyQNode::setNodeProperty, m_nodeProperty); emit nodePropertyChanged(node); } void addAttribute(MyQNode *attribute) { Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(this); if (!m_attributes.contains(attribute)) { m_attributes.append(attribute); // Ensures proper bookkeeping d->registerDestructionHelper(attribute, &MyQNode::removeAttribute, m_attributes); // We need to add it as a child of the current node if it has been declared inline // Or not previously added as a child of the current node so that // 1) The backend gets notified about it's creation // 2) When the current node is destroyed, it gets destroyed as well if (!attribute->parent()) attribute->setParent(this); d->updateNode(attribute, "attribute", Qt3DCore::PropertyValueAdded); } } void removeAttribute(MyQNode *attribute) { Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(this); d->updateNode(attribute, "attribute", Qt3DCore::PropertyValueRemoved); m_attributes.removeOne(attribute); // Remove bookkeeping connection d->unregisterDestructionHelper(attribute); } signals: void customPropertyChanged(); void nodePropertyChanged(MyQNode *node); protected: QString m_customProperty; MyQNode *m_nodeProperty; QVector m_attributes; }; class MyQEntity : public Qt3DCore::QEntity { Q_OBJECT Q_PROPERTY(MyQNode *nodeProperty READ nodeProperty WRITE setNodeProperty NOTIFY nodePropertyChanged) public: explicit MyQEntity(Qt3DCore::QNode *parent = nullptr) : QEntity(parent) {} ~MyQEntity() { } void setArbiterAndScene(Qt3DCore::QAbstractArbiter *arbiter, Qt3DCore::QScene *scene = nullptr) { Q_ASSERT(arbiter); if (scene) scene->setArbiter(arbiter); Qt3DCore::QNodePrivate::get(this)->setScene(scene); Qt3DCore::QNodePrivate::get(this)->setArbiter(arbiter); } void setArbiterAndEngine(Qt3DCore::QAbstractArbiter *arbiter, Qt3DCore::QAspectEngine *engine) { Q_ASSERT(arbiter); Q_ASSERT(engine); auto scene = Qt3DCore::QAspectEnginePrivate::get(engine)->m_scene; Qt3DCore::QNodePrivate::get(this)->setScene(scene); Qt3DCore::QNodePrivate::get(this)->setArbiter(arbiter); if (scene) { scene->setArbiter(arbiter); engine->setRootEntity(Qt3DCore::QEntityPtr(this, [](QEntity *) {})); scene->setArbiter(arbiter); Qt3DCore::QNodePrivate::get(this)->setArbiter(arbiter); } } void setSimulateBackendCreated(bool created) { Qt3DCore::QNodePrivate::get(this)->m_hasBackendNode = created; } MyQNode *nodeProperty() const { return m_nodeProperty; } void addAttribute(MyQNode *attribute) { Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(this); if (!m_attributes.contains(attribute)) { m_attributes.append(attribute); // Ensures proper bookkeeping d->registerDestructionHelper(attribute, &MyQNode::removeAttribute, m_attributes); // We need to add it as a child of the current node if it has been declared inline // Or not previously added as a child of the current node so that // 1) The backend gets notified about it's creation // 2) When the current node is destroyed, it gets destroyed as well if (!attribute->parent()) attribute->setParent(this); d->updateNode(attribute, "attribute", Qt3DCore::PropertyValueRemoved); } } public slots: void setNodeProperty(MyQNode *node) { Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(this); if (m_nodeProperty == node) return; if (m_nodeProperty) d->unregisterDestructionHelper(m_nodeProperty); if (node && !node->parent()) node->setParent(this); m_nodeProperty = node; // Ensures proper bookkeeping if (m_nodeProperty) d->registerDestructionHelper(m_nodeProperty, &MyQEntity::setNodeProperty, m_nodeProperty); emit nodePropertyChanged(node); } signals: void nodePropertyChanged(MyQNode *node); private: MyQNode *m_nodeProperty; QVector m_attributes; }; class MyQComponent : public Qt3DCore::QComponent { Q_OBJECT public: explicit MyQComponent(Qt3DCore::QNode *parent = nullptr) : QComponent(parent) {} void setArbiter(Qt3DCore::QAbstractArbiter *arbiter) { Q_ASSERT(arbiter); Qt3DCore::QComponentPrivate::get(this)->setArbiter(arbiter); } }; class MyFakeMaterial : public Qt3DCore::QComponent { Q_OBJECT public: explicit MyFakeMaterial(Qt3DCore::QNode *parent = nullptr) : QComponent(parent) , m_effect(new MyQNode(this)) , m_technique(new MyQNode(m_effect)) , m_renderPass(new MyQNode(m_technique)) { } void setArbiter(Qt3DCore::QAbstractArbiter *arbiter) { Q_ASSERT(arbiter); Qt3DCore::QComponentPrivate::get(this)->setArbiter(arbiter); } MyQNode *m_effect; MyQNode *m_technique; MyQNode *m_renderPass; }; class TestAspectPrivate; class TestAspect : public Qt3DCore::QAbstractAspect { Q_OBJECT public: explicit TestAspect(QObject *parent = nullptr); enum ChangeType { Creation, Destruction }; void clearNodes() { events.clear(); allNodes.clear(); } void addEvent(const Qt3DCore::QNodeId &id, ChangeType change) { events.push_back(Event{ change, id}); } QVector filteredEvents(ChangeType change) const { QVector result; for (const auto &event : events) { if (event.type == change) result.push_back(event.nodeId); } return result; } struct Event{ ChangeType type; Qt3DCore::QNodeId nodeId; }; mutable QVector events; mutable QHash allNodes; private: Q_DECLARE_PRIVATE(TestAspect) explicit TestAspect(TestAspectPrivate &dd, QObject *parent); }; class TestFunctor : public Qt3DCore::QBackendNodeMapper { public: TestFunctor(TestAspect *aspect) : m_aspect(aspect) {} Qt3DCore::QBackendNode *create(const Qt3DCore::QNodeCreatedChangeBasePtr &change) const override { auto node = new Qt3DCore::QBackendNode; m_Nodes.insert(change->subjectId(), node); m_aspect->addEvent(change->subjectId(), TestAspect::Creation); return node; } Qt3DCore::QBackendNode *get(Qt3DCore::QNodeId id) const override { return m_Nodes.value(id, nullptr); } void destroy(Qt3DCore::QNodeId id) const override { if (m_Nodes.contains(id)) { m_aspect->addEvent(id, TestAspect::Destruction); delete m_Nodes.take(id); } } private: mutable QHash m_Nodes; TestAspect *m_aspect; }; class TestAspectPrivate : public Qt3DCore::QAbstractAspectPrivate { public: TestAspectPrivate() = default; void syncDirtyFrontEndNode(Qt3DCore::QNode *node, Qt3DCore::QBackendNode *backend, bool firstTime) const override { Q_UNUSED(backend) auto q = q_func(); if (firstTime) q->allNodes.insert(node->id(), node); } Q_DECLARE_PUBLIC(TestAspect) }; TestAspect::TestAspect(QObject *parent) : TestAspect(*new TestAspectPrivate, parent) { Q_D(TestAspect); d->registerBackendType(QSharedPointer::create(this)); d->registerBackendType(QSharedPointer::create(this)); d->registerBackendType(QSharedPointer::create(this)); d->registerBackendType(QSharedPointer::create(this)); } TestAspect::TestAspect(TestAspectPrivate &dd, QObject *parent) : Qt3DCore::QAbstractAspect(dd, parent) { setObjectName(QStringLiteral("Test Aspect")); } namespace { void verifyChildrenCreatedBeforeParents(Qt3DCore::QNode *root, TestAspect *aspect) { QSet processedNodes; processedNodes.insert(root->id()); for (const auto &nodeId : aspect->filteredEvents(TestAspect::Creation)) { const auto node = aspect->allNodes.value(nodeId); Q_ASSERT(node); const auto parentNode = node->parentNode(); QVERIFY(parentNode == nullptr || processedNodes.contains(parentNode->id())); processedNodes.insert(nodeId); } } } void tst_Nodes::initTestCase() { qRegisterMetaType("PropertyTrackingMode"); } void tst_Nodes::defaultNodeConstruction() { // GIVEN QScopedPointer node(new MyQNode()); // THEN QVERIFY(node != nullptr); QVERIFY(node->children().isEmpty()); // GIVEN MyQNode *node2 = new MyQNode(node.data()); QCoreApplication::processEvents(); // THEN QVERIFY(node2->parent() == node.data()); QVERIFY(!node->children().isEmpty()); QVERIFY(node2->children().isEmpty()); } void tst_Nodes::defaultComponentConstruction() { // GIVEN QScopedPointer comp(new MyQComponent()); MyQComponent *comp2 = new MyQComponent(comp.data()); QCoreApplication::processEvents(); // THEN QVERIFY(comp != nullptr); QCOMPARE(comp2->parent(), comp.data()); } void tst_Nodes::defaultEntityConstrution() { // GIVEN QScopedPointer entity(new Qt3DCore::QEntity()); Qt3DCore::QEntity *entity2 = new Qt3DCore::QEntity(entity.data()); QCoreApplication::processEvents(); // THEN QVERIFY(entity->components().isEmpty()); QVERIFY(entity2->components().isEmpty()); QCOMPARE(entity2->parent(), entity.data()); } void tst_Nodes::appendSingleChildNodeToNodeNoSceneExplicitParenting() { // Check nodes added when no scene is set // GIVEN ObserverSpy spy; QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&spy); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() == nullptr); // WHEN QScopedPointer child(new MyQNode()); // THEN QVERIFY(child->parent() == nullptr); // WHEN child->setParent(node.data()); // THEN QVERIFY(child->parent() == node.data()); QVERIFY(child->parentNode() == node.data()); QCOMPARE(node->children().count(), 1); // Events are only sent when a scene is set on the root node QCOMPARE(spy.events.size(), 0); } void tst_Nodes::appendSingleChildNodeToNodeNoSceneImplicitParenting() { // Check nodes added when no scene is set // GIVEN ObserverSpy spy; QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&spy); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() == nullptr); // WHEN QScopedPointer child(new MyQNode(node.data())); QCoreApplication::processEvents(); // THEN QVERIFY(child->parent() == node.data()); QVERIFY(child->parentNode() == node.data()); QCOMPARE(node->children().count(), 1); // Events are only sent when a scene is set on the root node QCOMPARE(spy.events.size(), 0); } void tst_Nodes::appendMultipleChildNodesToNodeNoScene() { // Check multiple nodes added with no scene set // GIVEN ObserverSpy spy; QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&spy); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() == nullptr); // WHEN for (int i = 0; i < 10; i++) { // WHEN Qt3DCore::QNode *child = nullptr; if (i % 2 == 0) { child = new MyQNode(node.data()); QCoreApplication::processEvents(); } else { child = new MyQNode(); child->setParent(node.data()); } // THEN QVERIFY(child->parent() == node.data()); } // THEN QCOMPARE(node->children().count(), 10); // Events are only sent when a scene is set on the root node QCOMPARE(spy.events.size(), 0); } void tst_Nodes::appendSingleChildNodeToNodeSceneExplicitParenting() { // Check nodes added when scene is set // GIVEN Qt3DCore::QScene scene; ObserverSpy spy; QScopedPointer node(new MyQNode()); // WHEN node->setArbiterAndScene(&spy, &scene); node->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() != nullptr); // WHEN QScopedPointer child(new MyQNode()); // THEN QVERIFY(child->parent() == nullptr); QVERIFY(Qt3DCore::QNodePrivate::get(child.data())->scene() == nullptr); // WHEN child->setParent(node.data()); // THEN QVERIFY(child->parent() == node.data()); QVERIFY(child->parentNode() == node.data()); QCOMPARE(spy.events.size(), 1); // Child Added QCOMPARE(node->children().count(), 1); QVERIFY(Qt3DCore::QNodePrivate::get(child.data())->scene() != nullptr); // Node Added event QVERIFY(spy.events.first().wasLocked()); Qt3DCore::QPropertyNodeAddedChangePtr additionEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(additionEvent); QCOMPARE(additionEvent->subjectId(), node->id()); QCOMPARE(additionEvent->addedNodeId(), child->id()); QCOMPARE(additionEvent->metaObject(), child->metaObject()); } void tst_Nodes::appendSingleChildNodeToNodeSceneImplicitParenting() { // Check nodes added when scene is set // GIVEN Qt3DCore::QScene scene; ObserverSpy spy; QScopedPointer node(new MyQNode()); // WHEN node->setArbiterAndScene(&spy, &scene); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() != nullptr); // WHEN QScopedPointer child(new MyQNode(node.data())); QCoreApplication::processEvents(); // THEN QVERIFY(child->parent() == node.data()); QVERIFY(child->parentNode() == node.data()); QVERIFY(Qt3DCore::QNodePrivate::get(child.data())->scene() != nullptr); QCOMPARE(spy.events.size(), 1); QVERIFY(spy.events.first().wasLocked()); QCOMPARE(node->children().count(), 1); // Node Added event QVERIFY(spy.events.first().wasLocked()); Qt3DCore::QPropertyNodeAddedChangePtr additionEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(additionEvent); QCOMPARE(additionEvent->subjectId(), node->id()); QCOMPARE(additionEvent->addedNodeId(), child->id()); QCOMPARE(additionEvent->metaObject(), child->metaObject()); } void tst_Nodes::appendMultipleChildNodesToNodeScene() { // Check nodes added when scene is set // GIVEN Qt3DCore::QScene scene; ObserverSpy spy; QScopedPointer node(new MyQNode()); // WHEN node->setArbiterAndScene(&spy, &scene); node->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(node.data())->scene() != nullptr); // WHEN const auto childCount = 10; for (int i = 0; i < childCount; i++) { // WHEN Qt3DCore::QNode *child = nullptr; if (i % 2 == 0) { child = new MyQNode(node.data()); } else { child = new MyQNode(); child->setParent(node.data()); } // THEN parent and scene should be set synchronously QVERIFY(child->parent() == node.data()); QVERIFY(Qt3DCore::QNodePrivate::get(child)->scene() == Qt3DCore::QNodePrivate::get(node.data())->m_scene); } // THEN QCOMPARE(node->children().count(), 10); // WHEN QCoreApplication::processEvents(); // THEN backend is notified after the event loop spins. The recorded events are a little // tricky to understand and differs for children with the parent being set at construction // time (even children and ids) and the children being created without a parent and then // explicitly calling setParent() after (odd children and ids). // // Even children: // child constructed // notifications to backend scheduled via the event loop as object is not yet fully constructed // // Odd children: // child constructed // parent set // notifications to backend sent immediately as object is fully constructed // // With this in mind, the recorded events should show: // // for each odd child: // child addition to parent of odd child // // followed by: // // for each even child: // child addition to parent of even child // const auto expectedEventCount = childCount; QCOMPARE(spy.events.size(), 10); for (auto i = 0; i < expectedEventCount; ++i) { const auto childIndex = i; Qt3DCore::QNode *child = node->childNodes().at(childIndex); const auto recordIndex = (i % 2 == 0) ? expectedEventCount / 2 + i / 2 : i / 2; const auto additionRecord = spy.events.at(recordIndex); Qt3DCore::QPropertyNodeAddedChangePtr additionEvent = additionRecord.change().dynamicCast(); QCOMPARE(additionEvent->subjectId(), node->id()); QCOMPARE(additionEvent->addedNodeId(), child->id()); QCOMPARE(additionEvent->metaObject(), child->metaObject()); } } void tst_Nodes::checkParentChangeToNull() { // GIVEN Qt3DCore::QScene scene; ObserverSpy spy; QScopedPointer root(new MyQNode()); // WHEN root->setArbiterAndScene(&spy, &scene); QScopedPointer child(new MyQNode(root.data())); QCoreApplication::processEvents(); // THEN QVERIFY(child->parent() == root.data()); QCOMPARE(spy.events.size(), 1); QCOMPARE(root->children().size(), 1); // WHEN spy.events.clear(); child->setParent(Q_NODE_NULLPTR); // THEN QVERIFY(child->parent() == nullptr); QCOMPARE(root->children().size(), 0); QCOMPARE(spy.events.size(), 0); } void tst_Nodes::checkParentChangeToOtherParent() { // GIVEN Qt3DCore::QScene scene; ObserverSpy spy; QScopedPointer root(new MyQNode()); root->setArbiterAndScene(&spy, &scene); QScopedPointer parent1(new MyQNode(root.data())); QScopedPointer parent2(new MyQNode(root.data())); QCoreApplication::processEvents(); // THEN QCOMPARE(spy.events.size(), 2); // 2 x (1 node added to children change) // WHEN spy.events.clear(); QScopedPointer child(new MyQNode(parent1.data())); QCoreApplication::processEvents(); // THEN QVERIFY(child->parent() == parent1.data()); QCOMPARE(parent1->children().size(), 1); QCOMPARE(parent2->children().size(), 0); QVERIFY(Qt3DCore::QNodePrivate::get(child.data())->scene() != nullptr); QCOMPARE(spy.events.size(), 1); // 1 node added to children change // WHEN spy.events.clear(); child->setParent(parent2.data()); // THEN QVERIFY(child->parent() == parent2.data()); QCOMPARE(parent1->children().size(), 0); QCOMPARE(parent2->children().size(), 1); QCOMPARE(spy.events.size(), 1); // CHECK event 1 is a Node Removed event // this no longer generated because nodes don't have backends yet QVERIFY(!Qt3DCore::QNodePrivate::get(child.data())->m_hasBackendNode); // QVERIFY(spy.events.first().wasLocked()); // const Qt3DCore::QPropertyNodeRemovedChangePtr event = spy.events.takeFirst().change().dynamicCast(); // QCOMPARE(event->type(), Qt3DCore::PropertyValueRemoved); // QCOMPARE(event->subjectId(), parent1->id()); // QCOMPARE(event->removedNodeId(), child->id()); // QCOMPARE(event->metaObject(), child->metaObject()); // CHECK event 2 is a Node Added event QVERIFY(spy.events.last().wasLocked()); const Qt3DCore::QPropertyNodeAddedChangePtr event2 = spy.events.last().change().dynamicCast(); QCOMPARE(event2->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event2->subjectId(), parent2->id()); QCOMPARE(event2->addedNodeId(), child->id()); QCOMPARE(event2->metaObject(), child->metaObject()); } void tst_Nodes::checkParentChangeFromExistingBackendParentToNewlyCreatedParent() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; engine.setRunMode(Qt3DCore::QAspectEngine::Manual); QScopedPointer root(new MyQEntity()); root->setArbiterAndEngine(&spy, &engine); auto aspect = new TestAspect; engine.registerAspect(aspect); MyQNode *child(new MyQNode(root.data())); MyQNode *child2(new MyQNode(root.data())); QCoreApplication::processEvents(); engine.processFrame(); // Due to the way we create root, it has a backend QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->m_hasBackendNode == true); QCOMPARE(aspect->events.count(), 2); QCOMPARE(aspect->events[0].type, TestAspect::Creation); QCOMPARE(aspect->events[0].nodeId, child->id()); QCOMPARE(aspect->events[1].type, TestAspect::Creation); QCOMPARE(aspect->events[1].nodeId, child2->id()); // THEN QCOMPARE(spy.events.size(), 2); // 2 x (1 child added to parent change) // WHEN -> Reparenting child with backend node to new parent with no backend yet aspect->clearNodes(); spy.events.clear(); QScopedPointer newParent(new MyQNode(root.data())); child->setParent(newParent.data()); // THEN QVERIFY(child->parent() == newParent.data()); QCOMPARE(newParent->childNodes().size(), 1); QCOMPARE(child2->childNodes().size(), 0); QCOMPARE(root->childNodes().size(), 2); QVERIFY(Qt3DCore::QNodePrivate::get(newParent.data())->m_hasBackendNode == false); QVERIFY(Qt3DCore::QNodePrivate::get(child)->m_hasBackendNode == false); QVERIFY(Qt3DCore::QNodePrivate::get(newParent.data())->scene() != nullptr); QVERIFY(Qt3DCore::QNodePrivate::get(child)->scene() != nullptr); // WHEN QCoreApplication::processEvents(); engine.processFrame(); // THEN QCOMPARE(spy.events.size(), 2); // 1 node removed change // 1 node added to children change QVERIFY(Qt3DCore::QNodePrivate::get(newParent.data())->m_hasBackendNode == true); QVERIFY(Qt3DCore::QNodePrivate::get(child)->m_hasBackendNode == true); { // CHECK event 1 is a Node Removed event QVERIFY(spy.events.first().wasLocked()); const Qt3DCore::QPropertyNodeRemovedChangePtr event = spy.events.takeFirst().change().staticCast(); QCOMPARE(event->type(), Qt3DCore::PropertyValueRemoved); QCOMPARE(event->subjectId(), root->id()); QCOMPARE(event->removedNodeId(), child->id()); QCOMPARE(event->metaObject(), child->metaObject()); const Qt3DCore::QPropertyNodeAddedChangePtr event2 = spy.events.takeFirst().change().staticCast(); QCOMPARE(event2->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event2->addedNodeId(), newParent->id()); QCOMPARE(event2->metaObject(), newParent->metaObject()); QCOMPARE(event2->subjectId(), root->id()); QCOMPARE(aspect->events.count(), 3); // child backend is destroyed because it was reparented to node without backend (newParent) QCOMPARE(aspect->events[0].type, TestAspect::Destruction); QCOMPARE(aspect->events[0].nodeId, child->id()); // newParent and child both get backends created QCOMPARE(aspect->events[1].type, TestAspect::Creation); QCOMPARE(aspect->events[1].nodeId, newParent->id()); QCOMPARE(aspect->events[2].type, TestAspect::Creation); QCOMPARE(aspect->events[2].nodeId, child->id()); } // WHEN -> Changing parent to node with existing backend aspect->clearNodes(); child->setParent(child2); // THEN QCOMPARE(spy.events.size(), 2); // 1 node removed change, 1 node added change { QVERIFY(spy.events.first().wasLocked()); const Qt3DCore::QPropertyNodeRemovedChangePtr event = spy.events.takeFirst().change().staticCast(); QCOMPARE(event->type(), Qt3DCore::PropertyValueRemoved); QCOMPARE(event->subjectId(), newParent->id()); QCOMPARE(event->removedNodeId(), child->id()); QCOMPARE(event->metaObject(), child->metaObject()); const Qt3DCore::QPropertyNodeAddedChangePtr event2 = spy.events.takeFirst().change().staticCast(); QCOMPARE(event2->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event2->addedNodeId(), child->id()); QCOMPARE(event2->metaObject(), child->metaObject()); QCOMPARE(event2->subjectId(), child2->id()); } QVERIFY(Qt3DCore::QNodePrivate::get(child)->m_hasBackendNode == true); QCOMPARE(root->childNodes().size(), 2); QCOMPARE(child2->childNodes().size(), 1); QCOMPARE(newParent->childNodes().size(), 0); // WHEN -> Changing to parent which has no backend QScopedPointer newParent2(new MyQNode(root.data())); child->setParent(newParent2.data()); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(newParent2.data())->m_hasBackendNode == false); QVERIFY(Qt3DCore::QNodePrivate::get(child)->m_hasBackendNode == false); QVERIFY(Qt3DCore::QNodePrivate::get(newParent2.data())->scene() != nullptr); QVERIFY(Qt3DCore::QNodePrivate::get(child)->scene() != nullptr); // WHEN QCoreApplication::processEvents(); engine.processFrame(); // THEN QCOMPARE(spy.events.size(), 2); // 1 node removed change // 1 node added to children change { // CHECK event 1 is a Node Removed event QVERIFY(spy.events.first().wasLocked()); const Qt3DCore::QPropertyNodeRemovedChangePtr event = spy.events.takeFirst().change().staticCast(); QCOMPARE(event->type(), Qt3DCore::PropertyValueRemoved); QCOMPARE(event->subjectId(), child2->id()); QCOMPARE(event->removedNodeId(), child->id()); QCOMPARE(event->metaObject(), child->metaObject()); const Qt3DCore::QPropertyNodeAddedChangePtr event2 = spy.events.takeFirst().change().staticCast(); QCOMPARE(event2->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event2->addedNodeId(), newParent2->id()); QCOMPARE(event2->metaObject(), newParent2->metaObject()); QCOMPARE(event2->subjectId(), root->id()); // child backend is destroyed because it was reparented to node without backend (newParent) QCOMPARE(aspect->events[0].type, TestAspect::Destruction); QCOMPARE(aspect->events[0].nodeId, child->id()); } } //Test creation changes happen for an entire subtree at once, starting at the top // so that parents are always created before their children. Even if the front-end // nodes are constructed in a different order. void tst_Nodes::checkBackendNodesCreatedFromTopDown() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; auto aspect = new TestAspect; engine.registerAspect(aspect); QScopedPointer root(new MyQEntity()); root->setArbiterAndEngine(&spy, &engine); QScopedPointer parentWithBackend(new MyQNode(root.data())); // create parent backend node QCoreApplication::processEvents(); QVERIFY(Qt3DCore::QNodePrivate::get(parentWithBackend.get())->m_hasBackendNode); // WHEN -> creating 3 nodes and setting one as a property on the other // child2 is set as property on child1, but is created AFTER child1 spy.events.clear(); MyQNode *dummyParent(new MyQNode(parentWithBackend.data())); MyQNode *child1(new MyQNode(parentWithBackend.data())); MyQNode *child2(new MyQNode(dummyParent)); child1->setNodeProperty(child2); // THEN - we should have no events because the new nodes have no backend yet QCOMPARE(spy.events.count(), 0); // WHEN - create the backend nodes QCoreApplication::processEvents(); // THEN QVERIFY(dummyParent->parent() == parentWithBackend.data()); QVERIFY(child1->parent() == parentWithBackend.data()); QVERIFY(child2->parent() == dummyParent); // THEN QCOMPARE(spy.events.size(), 2); // 1 node added to children change (dummyParent to parent) // 1 node added to children change (child1 to parent) { QVERIFY(Qt3DCore::QNodePrivate::get(dummyParent)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(child1)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(child2)->m_hasBackendNode); // 1st event: dummyParent added to parent const auto event = spy.events.takeFirst().change().dynamicCast(); QCOMPARE(event->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event->addedNodeId(), dummyParent->id()); QCOMPARE(event->subjectId(), parentWithBackend->id()); // 2nd event: child 1 added to parent const auto event2 = spy.events.takeFirst().change().dynamicCast(); QCOMPARE(event2->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event2->addedNodeId(), child1->id()); QCOMPARE(event2->subjectId(), parentWithBackend->id()); verifyChildrenCreatedBeforeParents(root.get(), aspect); } } void tst_Nodes::checkBackendNodesCreatedFromTopDownWithReparenting() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; auto aspect = new TestAspect; engine.registerAspect(aspect); QScopedPointer root(new MyQEntity()); root->setArbiterAndEngine(&spy, &engine); QScopedPointer parentWithBackend(new MyQNode(root.data())); // create parent backend node QCoreApplication::processEvents(); QCoreApplication::processEvents(); QVERIFY(Qt3DCore::QNodePrivate::get(parentWithBackend.get())->m_hasBackendNode); // WHEN -> creating a node with a parent with backend, then reparenting it to another // parent with backend created after it. spy.events.clear(); auto node1 = new MyQNode(parentWithBackend.data()); auto parent1 = new MyQNode(parentWithBackend.data()); node1->setParent(parent1); QCoreApplication::processEvents(); // THEN QVERIFY(node1->parent() == parent1); QVERIFY(parent1->parent() == parentWithBackend.data()); verifyChildrenCreatedBeforeParents(root.get(), aspect); // WHEN -> creating 2 nodes with no parent and reparenting the first to the second auto node2 = new MyQNode(nullptr); auto parent2 = new MyQNode(nullptr); node2->setParent(parent2); parent2->setParent(parentWithBackend.get()); QCoreApplication::processEvents(); // THEN QVERIFY(node2->parent() == parent2); QVERIFY(parent2->parent() == parentWithBackend.data()); verifyChildrenCreatedBeforeParents(root.get(), aspect); } void tst_Nodes::removingSingleChildNodeFromNode() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); QScopedPointer child(new MyQNode()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); child->setParent(root.data()); // Clear any creation event spy.events.clear(); // THEN QVERIFY(root->children().count() == 1); QVERIFY(child->parentNode() == root.data()); // WHEN child->setParent(Q_NODE_NULLPTR); // THEN QVERIFY(child->parent() == nullptr); QVERIFY(root->children().count() == 0); QCOMPARE(spy.events.size(), 1); QVERIFY(spy.events.first().wasLocked()); const Qt3DCore::QPropertyNodeRemovedChangePtr removalEvent = spy.events.takeFirst().change().dynamicCast(); QCOMPARE(removalEvent->subjectId(), root->id()); QCOMPARE(removalEvent->removedNodeId(), child->id()); QCOMPARE(removalEvent->metaObject(), child->metaObject()); } void tst_Nodes::checkAllBackendCreationDoneInSingleFrame() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; engine.setRunMode(Qt3DCore::QAspectEngine::Manual); auto aspect = new TestAspect; engine.registerAspect(aspect); QScopedPointer root(new MyQEntity()); root->setArbiterAndEngine(&spy, &engine); QCoreApplication::processEvents(); // THEN // Due to the way we create root, it has a backend QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->m_hasBackendNode == true); QCOMPARE(aspect->events.count(), 1); QCOMPARE(aspect->events[0].type, TestAspect::Creation); QCOMPARE(aspect->events[0].nodeId, root->id()); // WHEN -> create 2 children: // 1. a child with parent with backend node // 2. a child with no parent that is then reparented to a parent with backend node aspect->clearNodes(); auto child1 = new MyQNode(root.data()); auto child2 = new MyQNode; child2->setParent(root.data()); // THEN - reparented child should have a backend node, but other child should // still be waiting QCOMPARE(child1->parent(), root.data()); QCOMPARE(child2->parent(), root.data()); QVERIFY(Qt3DCore::QNodePrivate::get(child1)->m_hasBackendNode == false); QVERIFY(Qt3DCore::QNodePrivate::get(child2)->m_hasBackendNode == true); // WHEN QCoreApplication::processEvents(); engine.processFrame(); // THEN - both children have their backend nodes actually created. QCOMPARE(aspect->events.count(), 2); QCOMPARE(aspect->events[0].type, TestAspect::Creation); QCOMPARE(aspect->events[0].nodeId, child2->id()); QCOMPARE(aspect->events[1].type, TestAspect::Creation); QCOMPARE(aspect->events[1].nodeId, child1->id()); } void tst_Nodes::removingMultipleChildNodesFromNode() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN Qt3DCore::QNodeIdVector childIds(10); for (int i = 0; i < 10; i++) { auto child = new MyQNode(root.data()); childIds[i] = child->id(); } QCoreApplication::processEvents(); // THEN QCOMPARE(root->children().count(), 10); QCOMPARE(spy.events.size(), 10); // WHEN spy.events.clear(); qDeleteAll(root->children()); // THEN QVERIFY(root->children().count() == 0); QCOMPARE(spy.events.size(), 10); int i = 0; for (const ObserverSpy::ChangeRecord &r : qAsConst(spy.events)) { QVERIFY(r.wasLocked()); const Qt3DCore::QNodeId childId = childIds.at(i); Qt3DCore::QPropertyNodeRemovedChangePtr additionEvent = r.change().dynamicCast(); QCOMPARE(additionEvent->subjectId(), root->id()); QCOMPARE(additionEvent->removedNodeId(), childId); QCOMPARE(additionEvent->metaObject(), &MyQNode::staticMetaObject); ++i; } } void tst_Nodes::appendingChildEntitiesToNode() { // GIVEN QScopedPointer root(new MyQNode()); // WHEN Qt3DCore::QEntity *childEntity = new Qt3DCore::QEntity(root.data()); QCoreApplication::processEvents(); // THEN QVERIFY(root->children().first() == childEntity); QVERIFY(childEntity->parentEntity() == nullptr); QVERIFY(childEntity->parentNode() == root.data()); } void tst_Nodes::removingChildEntitiesFromNode() { // GIVEN QScopedPointer root(new MyQNode()); // WHEN Qt3DCore::QEntity *childEntity = new Qt3DCore::QEntity(root.data()); QCoreApplication::processEvents(); // THEN QVERIFY(root->children().first() == childEntity); QVERIFY(childEntity->parentEntity() == nullptr); QVERIFY(childEntity->parentNode() == root.data()); // WHEN childEntity->setParent(Q_NODE_NULLPTR); // THEN QVERIFY(root->children().isEmpty()); QVERIFY(childEntity->parentNode() == nullptr); QVERIFY(childEntity->parent() == nullptr); } void tst_Nodes::checkConstructionSetParentMix() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; auto aspect = new TestAspect; engine.registerAspect(aspect); QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN Qt3DCore::QEntity *subTreeRoot = new Qt3DCore::QEntity(root.data()); for (int i = 0; i < 100; ++i) { Qt3DCore::QEntity *child = new Qt3DCore::QEntity(); child->setParent(subTreeRoot); } // THEN QCoreApplication::processEvents(); QCOMPARE(root->children().count(), 1); QCOMPARE(subTreeRoot->children().count(), 100); QCOMPARE(spy.events.size(), 1); // 1 child added (subTree to root) // Ensure first event is subTreeRoot verifyChildrenCreatedBeforeParents(root.data(), aspect); const Qt3DCore::QPropertyNodeAddedChangePtr lastEvent = spy.events.takeLast().change().dynamicCast(); QVERIFY(!lastEvent.isNull()); QCOMPARE(lastEvent->subjectId(), root->id()); QCOMPARE(lastEvent->propertyName(), "children"); QCOMPARE(lastEvent->addedNodeId(), subTreeRoot->id()); } void tst_Nodes::checkParentingQEntityToQNode() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN auto subTreeRoot = new Qt3DCore::QEntity(root.data()); auto childEntity = new Qt3DCore::QEntity(subTreeRoot); auto childNode = new Qt3DCore::QNode(subTreeRoot); // THEN QCoreApplication::processEvents(); QCOMPARE(spy.events.size(), 1); QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(subTreeRoot)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(childEntity)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(childNode)->m_hasBackendNode); // WHEN we reparent the childEntity to the childNode (QNode) spy.events.clear(); childEntity->setParent(childNode); QCoreApplication::processEvents(); // THEN we should get // - one child removed change for childEntity->subTreeRoot, // - one child added change for childEntity->childNode, QCOMPARE(spy.events.size(), 2); const auto removedEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(!removedEvent.isNull()); QCOMPARE(removedEvent->subjectId(), subTreeRoot->id()); const auto addedEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(!addedEvent.isNull()); QCOMPARE(addedEvent->subjectId(), childNode->id()); // The arbiter's dirtyNodes should contain the childEntity and // - the dirty node's parent should be childNode // - the dirty node's parent entity should be the subTreeRoot QCOMPARE(spy.dirtyNodes.size(), 1); const auto dirtyEntity = qobject_cast(spy.dirtyNodes.takeFirst()); QVERIFY(dirtyEntity); QCOMPARE(dirtyEntity, childEntity); QCOMPARE(dirtyEntity->parent(), childNode); QCOMPARE(dirtyEntity->parentEntity(), subTreeRoot); } void tst_Nodes::checkConstructionWithParent() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN we create a child and then set it as a Node* property auto *node = new MyQNode(root.data()); root->setNodeProperty(node); // THEN we should get one child added change QCoreApplication::processEvents(); QCOMPARE(root->children().count(), 1); QCOMPARE(spy.events.size(), 1); // 1 child added change const auto newChildEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(!newChildEvent.isNull()); QCOMPARE(newChildEvent->subjectId(), root->id()); QCOMPARE(newChildEvent->propertyName(), "children"); QCOMPARE(newChildEvent->addedNodeId(), node->id()); // Ensure the parent node is dirty QCOMPARE(spy.events.size(), 0); QCOMPARE(spy.dirtyNodes.size(), 1); QCOMPARE(spy.dirtyNodes.front(), root.data()); } void tst_Nodes::checkConstructionWithNonRootParent() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); QScopedPointer parent(new MyQNode(root.data())); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); QVERIFY(Qt3DCore::QNodePrivate::get(parent.data())->scene() != nullptr); // WHEN we create a child and then set it as a Node* property auto *child = new MyQNode(parent.data()); root->setNodeProperty(child); // THEN we should get // - one creation change for parent, // - one creation change for child, // - one child added change for root->parent, // - and one property change event, // in that order. QCoreApplication::processEvents(); QCOMPARE(root->children().count(), 1); QCOMPARE(parent->children().count(), 1); QCOMPARE(spy.events.size(), 1); // 1 child added changes const auto parentNewChildEvent = spy.events.takeFirst().change().dynamicCast(); QVERIFY(!parentNewChildEvent.isNull()); QCOMPARE(parentNewChildEvent->subjectId(), root->id()); QCOMPARE(parentNewChildEvent->propertyName(), "children"); QCOMPARE(parentNewChildEvent->addedNodeId(), parent->id()); // Ensure second and last event is property set change QCOMPARE(spy.dirtyNodes.size(), 1); QCOMPARE(spy.dirtyNodes.front(), root.data()); } void tst_Nodes::checkConstructionAsListElement() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; QScopedPointer root(new MyQEntity()); // WHEN root->setArbiterAndEngine(&spy, &engine); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN we create a child and then set it as a Node* property auto *node = new MyQNode(root.data()); root->addAttribute(node); // THEN we should get one creation change, one child added change // and one property change event, in that order. QCoreApplication::processEvents(); QCOMPARE(root->children().count(), 1); QCOMPARE(spy.dirtyNodes.size(), 1); // 1 property change QCOMPARE(spy.dirtySubNodes.size(), 1); // 1 child added change } void tst_Nodes::checkSceneIsSetOnConstructionWithParent() { // GIVEN ObserverSpy spy; Qt3DCore::QScene scene; QScopedPointer root(new MyQNode()); // WHEN root->setArbiterAndScene(&spy, &scene); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN Qt3DCore::QEntity *subTreeRoot = new Qt3DCore::QEntity(root.data()); QVector children; QVector cmps; children.reserve(5); cmps.reserve(5); for (int i = 0; i < 5; ++i) { const auto cmp = new MyQComponent(subTreeRoot); cmps << cmp; children << new Qt3DCore::QEntity(cmp); // When cmp is full created, it will also send the creation change for its child entity } QCoreApplication::processEvents(); QCOMPARE(root->children().count(), 1); QCOMPARE(subTreeRoot->children().count(), 5); QCOMPARE(spy.events.size(), 1); // 1 child added (subTree to root) spy.events.clear(); // Add component in a separate loop to ensure components are added after // entities have been fully created for (int i = 0; i < 5; ++i) { children.at(i)->addComponent(cmps.at(i)); } // THEN QCOMPARE(spy.events.size(), 0); QCOMPARE(spy.dirtySubNodes.size(), 5); // 5 entities changed } void tst_Nodes::checkSubNodePostConstructIsCalledWhenReferincingNodeProperty() { // GIVEN ObserverSpy spy; Qt3DCore::QAspectEngine engine; Qt3DCore::QScene scene(&engine); QScopedPointer root(new MyQNode()); // WHEN root->setArbiterAndScene(&spy, &scene); root->setSimulateBackendCreated(true); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(root.data())->scene() != nullptr); // WHEN Qt3DCore::QEntity *subTreeRoot = new Qt3DCore::QEntity(root.data()); QCoreApplication::processEvents(); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(subTreeRoot)->m_hasBackendNode); // WHEN MyFakeMaterial *material = new MyFakeMaterial(subTreeRoot); subTreeRoot->addComponent(material); // THEN QVERIFY(Qt3DCore::QNodePrivate::get(material)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(material->m_effect)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(material->m_technique)->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(material->m_renderPass)->m_hasBackendNode); // WHEN MyQNode *fakeRenderState = new MyQNode(material); Qt3DCore::QNodePrivate *dPtr = Qt3DCore::QNodePrivate::get(fakeRenderState); // THEN QVERIFY(!dPtr->m_hasBackendNode); // WHEN material->m_renderPass->addAttribute(fakeRenderState); // THEN QVERIFY(dPtr->m_hasBackendNode); } void tst_Nodes::appendingParentlessComponentToEntityWithoutScene() { // GIVEN ObserverSpy entitySpy; ObserverSpy componentSpy; { QScopedPointer entity(new MyQEntity()); entity->setArbiterAndScene(&entitySpy); entity->setSimulateBackendCreated(true); MyQComponent *comp = new MyQComponent(); comp->setArbiter(&componentSpy); // THEN QVERIFY(entity->parentNode() == nullptr); QVERIFY(entity->children().count() == 0); QVERIFY(entity->components().empty()); QVERIFY(comp->parentNode() == nullptr); // WHEN entity->addComponent(comp); // THEN QVERIFY(entity->components().count() == 1); QVERIFY(entity->components().first() == comp); QVERIFY(comp->parentNode() == entity.data()); QCOMPARE(entitySpy.events.size(), 0); QCOMPARE(entitySpy.dirtyNodes.size(), 1); QCOMPARE(entitySpy.dirtySubNodes.size(), 1); const auto event = entitySpy.dirtySubNodes.first(); QCOMPARE(event.change, Qt3DCore::ComponentAdded); QCOMPARE(event.node, entity.data()); QCOMPARE(event.subNode, comp); QCOMPARE(event.property, nullptr); // Note: since QEntity has no scene in this test case, we only have the // ComponentAdded event In theory we should also get the NodeCreated event // when setting the parent but that doesn't happen since no scene is // actually set on the entity and that QNodePrivate::_q_addChild will // return early in such a case. } } void tst_Nodes::appendingParentlessComponentToNonRootEntity() { // GIVEN ObserverSpy eventSpy; Qt3DCore::QAspectEngine engine; { QScopedPointer root(new MyQEntity()); root->setArbiterAndEngine(&eventSpy, &engine); root->setSimulateBackendCreated(true); QCoreApplication::processEvents(); QScopedPointer entity(new MyQEntity(root.data())); MyQComponent *comp = new MyQComponent(); // THEN QVERIFY(root->parentNode() == nullptr); QVERIFY(root->children().count() == 1); QVERIFY(root->components().empty()); QVERIFY(entity->parentNode() == root.data()); QVERIFY(entity->children().count() == 0); QVERIFY(entity->components().empty()); QVERIFY(comp->parentNode() == nullptr); // WHEN entity->addComponent(comp); QCoreApplication::processEvents(); // THEN QVERIFY(entity->components().count() == 1); QVERIFY(entity->components().first() == comp); QVERIFY(comp->parentNode() == entity.data()); QCOMPARE(eventSpy.events.size(), 1); // - entity added as child to root QVERIFY(eventSpy.events.first().wasLocked()); QVERIFY(Qt3DCore::QNodePrivate::get(entity.data())->m_hasBackendNode); QVERIFY(Qt3DCore::QNodePrivate::get(comp)->m_hasBackendNode); { const auto event = eventSpy.events.takeFirst().change().dynamicCast(); QVERIFY(!event.isNull()); QCOMPARE(event->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event->subjectId(), root->id()); QCOMPARE(event->propertyName(), QByteArrayLiteral("children")); QCOMPARE(event->addedNodeId(), entity->id()); } } } void tst_Nodes::appendingParentlessComponentToEntityWithScene() { // GIVEN ObserverSpy eventSpy; Qt3DCore::QAspectEngine engine; { QScopedPointer entity(new MyQEntity()); entity->setArbiterAndEngine(&eventSpy, &engine); entity->setSimulateBackendCreated(true); QCoreApplication::processEvents(); MyQComponent *comp = new MyQComponent(); // THEN QVERIFY(entity->parentNode() == nullptr); QVERIFY(entity->children().count() == 0); QVERIFY(entity->components().empty()); QVERIFY(comp->parentNode() == nullptr); // WHEN entity->addComponent(comp); QCoreApplication::processEvents(); // THEN QVERIFY(entity->components().count() == 1); QVERIFY(entity->components().first() == comp); QVERIFY(comp->parentNode() == entity.data()); QCOMPARE(eventSpy.events.size(), 1); // - child added QVERIFY(eventSpy.events.first().wasLocked()); { const auto event = eventSpy.events.takeFirst().change().dynamicCast(); QVERIFY(!event.isNull()); QCOMPARE(event->type(), Qt3DCore::PropertyValueAdded); QCOMPARE(event->subjectId(), entity->id()); QCOMPARE(event->propertyName(), QByteArrayLiteral("children")); QCOMPARE(event->addedNodeId(), comp->id()); } } } void tst_Nodes::appendingComponentToEntity() { // GIVEN ObserverSpy entitySpy; ObserverSpy componentSpy; { QScopedPointer entity(new MyQEntity()); entity->setArbiterAndScene(&entitySpy); MyQComponent *comp = new MyQComponent(entity.data()); comp->setArbiter(&componentSpy); QCoreApplication::processEvents(); // THEN QVERIFY(entity->parentNode() == nullptr); QVERIFY(entity->children().count() == 1); QVERIFY(entity->components().empty()); QVERIFY(comp->parentNode() == entity.data()); // WHEN entity->addComponent(comp); // THEN QVERIFY(entity->components().count() == 1); QVERIFY(entity->components().first() == comp); QVERIFY(comp->parentNode() == entity.data()); QCOMPARE(entitySpy.events.size(), 0); QCOMPARE(entitySpy.dirtyNodes.size(), 1); QCOMPARE(entitySpy.dirtySubNodes.size(), 1); QCOMPARE(entitySpy.dirtyNodes.first(), entity.data()); const auto event = entitySpy.dirtySubNodes.takeFirst(); QCOMPARE(event.node, entity.data()); QCOMPARE(event.subNode, comp); QCOMPARE(event.change, Qt3DCore::ComponentAdded); QCOMPARE(event.property, nullptr); } } void tst_Nodes::removingComponentFromEntity() { // GIVEN ObserverSpy entitySpy; ObserverSpy componentSpy; { QScopedPointer entity(new MyQEntity()); entity->setArbiterAndScene(&entitySpy); MyQComponent *comp = new MyQComponent(); comp->setArbiter(&componentSpy); // WHEN entity->addComponent(comp); // THEN QVERIFY(entity->components().count() == 1); QCOMPARE(entity->children().count(), 1); QVERIFY(comp->parent() == entity.data()); // WHEN entitySpy.events.clear(); entitySpy.dirtyNodes.clear(); entitySpy.dirtySubNodes.clear(); componentSpy.events.clear(); entity->removeComponent(comp); // THEN QVERIFY(entity->components().count() == 0); QVERIFY(comp->parent() == entity.data()); QVERIFY(entity->children().count() == 1); QCOMPARE(entitySpy.events.size(), 0); QCOMPARE(entitySpy.dirtyNodes.size(), 1); QCOMPARE(entitySpy.dirtySubNodes.size(), 1); QCOMPARE(componentSpy.events.size(), 0); { const auto event = entitySpy.dirtySubNodes.takeFirst(); qDebug() << event.change; QCOMPARE(event.change, Qt3DCore::ComponentRemoved); QCOMPARE(event.node, entity.data()); QCOMPARE(event.subNode, comp); QCOMPARE(event.property, nullptr); } } } void tst_Nodes::changeCustomProperty() { // GIVEN ObserverSpy spy; QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&spy); // WHEN node->setCustomProperty(QStringLiteral("foo")); // THEN QCOMPARE(spy.dirtyNodes.size(), 1); QCOMPARE(spy.dirtyNodes.front(), node.data()); } void tst_Nodes::checkDestruction() { // GIVEN QScopedPointer root(new MyQNode()); Qt3DCore::QEntity *entity = new Qt3DCore::QEntity(root.data()); QCoreApplication::processEvents(); MyQComponent *comp1 = new MyQComponent(); MyQComponent *comp2 = new MyQComponent(); MyQComponent *comp3 = new MyQComponent(); entity->addComponent(comp1); entity->addComponent(comp2); entity->addComponent(comp3); // THEN QVERIFY(!root->children().isEmpty()); // WHEN delete entity; // THEN QVERIFY(root->children().isEmpty()); } void tst_Nodes::checkDefaultConstruction() { // GIVEN Qt3DCore::QNode node; // THEN QCOMPARE(node.parentNode(), nullptr); QCOMPARE(node.isEnabled(), true); QCOMPARE(node.defaultPropertyTrackingMode(), Qt3DCore::QNode::TrackFinalValues); } void tst_Nodes::checkPropertyChanges() { // GIVEN Qt3DCore::QNode parentNode; Qt3DCore::QNode node; { // WHEN QSignalSpy spy(&node, SIGNAL(parentChanged(QObject *))); Qt3DCore::QNode *newValue = &parentNode; node.setParent(newValue); // THEN QVERIFY(spy.isValid()); QCOMPARE(node.parentNode(), newValue); QCOMPARE(spy.count(), 1); // WHEN spy.clear(); node.setParent(newValue); // THEN QCOMPARE(node.parentNode(), newValue); QCOMPARE(spy.count(), 0); } { // WHEN QSignalSpy spy(&node, SIGNAL(enabledChanged(bool))); const bool newValue = false; node.setEnabled(newValue); // THEN QVERIFY(spy.isValid()); QCOMPARE(node.isEnabled(), newValue); QCOMPARE(spy.count(), 1); // WHEN spy.clear(); node.setEnabled(newValue); // THEN QCOMPARE(node.isEnabled(), newValue); QCOMPARE(spy.count(), 0); } { // WHEN QSignalSpy spy(&node, SIGNAL(defaultPropertyTrackingModeChanged(PropertyTrackingMode))); const Qt3DCore::QNode::PropertyTrackingMode newValue = Qt3DCore::QNode::TrackAllValues; node.setDefaultPropertyTrackingMode(newValue); // THEN QVERIFY(spy.isValid()); QCOMPARE(node.defaultPropertyTrackingMode(), newValue); QCOMPARE(spy.count(), 1); // WHEN spy.clear(); node.setDefaultPropertyTrackingMode(newValue); // THEN QCOMPARE(node.defaultPropertyTrackingMode(), newValue); QCOMPARE(spy.count(), 0); } { // WHEN const QString enabledPropertyName = QStringLiteral("enabled"); node.setDefaultPropertyTrackingMode(Qt3DCore::QNode::DontTrackValues); node.setPropertyTracking(enabledPropertyName, Qt3DCore::QNode::TrackAllValues); // THEN QCOMPARE(node.propertyTracking(enabledPropertyName), Qt3DCore::QNode::TrackAllValues); // WHEN node.clearPropertyTracking(enabledPropertyName); // THEN QCOMPARE(node.propertyTracking(enabledPropertyName), Qt3DCore::QNode::DontTrackValues); } } void tst_Nodes::checkCreationData() { // GIVEN Qt3DCore::QNode root; Qt3DCore::QNode node; node.setParent(&root); node.setEnabled(true); const QString enabledPropertyName = QStringLiteral("enabled"); node.setDefaultPropertyTrackingMode(Qt3DCore::QNode::DontTrackValues); node.setPropertyTracking(enabledPropertyName, Qt3DCore::QNode::TrackAllValues); // WHEN QVector creationChanges; { Qt3DCore::QNodeCreatedChangeGenerator creationChangeGenerator(&node); creationChanges = creationChangeGenerator.creationChanges(); } // THEN { QCOMPARE(creationChanges.size(), 1); const auto creationChangeData = qSharedPointerCast(creationChanges.first()); QCOMPARE(node.id(), creationChangeData->subjectId()); QCOMPARE(node.isEnabled(), true); QCOMPARE(node.isEnabled(), creationChangeData->isNodeEnabled()); QCOMPARE(node.metaObject(), creationChangeData->metaObject()); } // WHEN node.setEnabled(false); { Qt3DCore::QNodeCreatedChangeGenerator creationChangeGenerator(&node); creationChanges = creationChangeGenerator.creationChanges(); } // THEN { QCOMPARE(creationChanges.size(), 1); const auto creationChangeData = qSharedPointerCast(creationChanges.first()); QCOMPARE(node.id(), creationChangeData->subjectId()); QCOMPARE(node.isEnabled(), false); QCOMPARE(node.isEnabled(), creationChangeData->isNodeEnabled()); QCOMPARE(node.metaObject(), creationChangeData->metaObject()); } } void tst_Nodes::checkEnabledUpdate() { // GIVEN TestArbiter arbiter; Qt3DCore::QNode node; arbiter.setArbiterOnNode(&node); { // WHEN node.setEnabled(false); // THEN QCOMPARE(arbiter.events.size(), 0); QCOMPARE(arbiter.dirtyNodes.size(), 1); QCOMPARE(arbiter.dirtyNodes.front(), &node); arbiter.dirtyNodes.clear(); } { // WHEN node.setEnabled(false); // THEN QCOMPARE(arbiter.events.size(), 0); QCOMPARE(arbiter.dirtyNodes.size(), 0); } } void tst_Nodes::checkPropertyTrackModeUpdate() { // GIVEN TestArbiter arbiter; Qt3DCore::QNode node; arbiter.setArbiterOnNode(&node); { // WHEN node.setDefaultPropertyTrackingMode(Qt3DCore::QNode::TrackAllValues); QCoreApplication::processEvents(); // THEN -> this properties is non notifying QCOMPARE(arbiter.events.size(), 0); } { // WHEN node.setDefaultPropertyTrackingMode(Qt3DCore::QNode::TrackAllValues); QCoreApplication::processEvents(); // THEN QCOMPARE(arbiter.events.size(), 0); } } void tst_Nodes::checkTrackedPropertyNamesUpdate() { // GIVEN TestArbiter arbiter; Qt3DCore::QNode node; arbiter.setArbiterOnNode(&node); { // WHEN node.setPropertyTracking(QStringLiteral("883"), Qt3DCore::QNode::TrackAllValues); QCoreApplication::processEvents(); // THEN -> this properties is non notifying QCOMPARE(arbiter.events.size(), 0); } { // WHEN node.setPropertyTracking(QStringLiteral("883"), Qt3DCore::QNode::DontTrackValues); QCoreApplication::processEvents(); // THEN QCOMPARE(arbiter.events.size(), 0); } } void tst_Nodes::checkNodeRemovedFromDirtyListOnDestruction() { // GIVEN TestArbiter arbiter; Qt3DCore::QScene scene; { // GIVEN QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&arbiter, &scene); // WHEN node->setEnabled(false); // THEN QCOMPARE(arbiter.events.size(), 0); QCOMPARE(arbiter.dirtyNodes.size(), 1); QCOMPARE(arbiter.dirtyNodes.front(), node.data()); // WHEN // scene should be unset and node removed from arbiter dirtyList node.reset(); // THEN QCOMPARE(arbiter.events.size(), 0); QCOMPARE(arbiter.dirtyNodes.size(), 0); } { // GIVEN QScopedPointer node(new MyQNode()); node->setArbiterAndScene(&arbiter, &scene); Qt3DCore::QNode *child = new Qt3DCore::QNode(node.data()); // Wait for deferred initialization of child node QCoreApplication::processEvents(); // WHEN child->setEnabled(false); // THEN QCOMPARE(arbiter.events.size(), 1); // childAdded QCOMPARE(arbiter.dirtyNodes.size(), 1); QCOMPARE(arbiter.dirtyNodes.front(), child); // WHEN // scene should be unset and child node removed from arbiter dirtyList node.reset(); // THEN QCOMPARE(arbiter.events.size(), 1); // childRemoved (no destroyed change since we had no backend) QCOMPARE(arbiter.dirtyNodes.size(), 0); } } QTEST_MAIN(tst_Nodes) #include "tst_nodes.moc"