Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest/qtest.h>
++
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderroot_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderdata_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderlayer_p.h>
++
++
++ class tst_NodeIndexing : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ tst_NodeIndexing() = default;
++ ~tst_NodeIndexing() = default;
++
++ private slots:
++ void initTestCase();
++ void testIndexingWithChildren();
++ void testIndexingWithMultipleViews();
++ void testIndexingWithImportScene();
++ void testVersionWrapAround();
++ void testActiveStateSuppressesReindex();
++ void testRemoveFromGraphClearsRootNodeRefOnChildren();
++ void testImportSceneStateAfterAddRemove();
++ void testImportSceneRemovePreservesSharedState();
++ void testRemoveChildIgnoresForeignNodeOnLayer();
++
++ private:
++ static void removeFromLayer(QSSGRenderLayer &layer, std::vector<QSSGRenderNode *> &nodes);
++
++ static void buildBasicNodeHierarchy(QSSGRenderNode *parent, std::vector<QSSGRenderNode *> &outList);
++
++ QSSGRenderRoot rootNode;
++
++ // 10 nodes with 2 children each means we have a total of 30 nodes
++ static constexpr int initialNodeCount = 10 + (10 * 2);
++ };
++
++ void tst_NodeIndexing::initTestCase()
++ {
++
++ }
++
++ void tst_NodeIndexing::testIndexingWithChildren()
++ {
++ QSSGRenderLayer layer;
++
++ layer.ref(&rootNode);
++ rootNode.addChild(layer);
++
++ std::vector<QSSGRenderNode *> nodes;
++
++ QVERIFY(layer.children.isEmpty());
++ QVERIFY(layer.rootNode == &rootNode);
++
++ buildBasicNodeHierarchy(&layer, nodes);
++
++ QVERIFY(!nodes.empty());
++
++ rootNode.reindex();
++
++ std::unordered_set<quint32> nodeIndices;
++ for (auto *node : nodes) {
++ QVERIFY(node->h.hasId());
++ nodeIndices.insert(node->h.index());
++ }
++
++ QCOMPARE(nodeIndices.size(), initialNodeCount);
++
++ removeFromLayer(layer, nodes);
++ rootNode.removeChild(layer);
++ qDeleteAll(nodes);
++ }
++
++ void tst_NodeIndexing::testIndexingWithMultipleViews()
++ {
++ // Multiple views means we have more then one layer
++ constexpr size_t LayerCount = 3;
++ std::array<QSSGRenderLayer, LayerCount> layers;
++
++ for (auto &layer : layers) {
++ QVERIFY(layer.children.isEmpty());
++ layer.ref(&rootNode);
++ }
++
++ std::array<std::vector<QSSGRenderNode *>, LayerCount> layerNodes;
++
++ // Add nodes to the extra layers
++ for (size_t i = 0; i < LayerCount; ++i) {
++ buildBasicNodeHierarchy(&layers[i], layerNodes[i]);
++ QVERIFY(!layerNodes[i].empty());
++ }
++
++ // Add the layers to the root node
++ for (auto &layer : layers) {
++ rootNode.addChild(layer);
++ QVERIFY(layer.rootNode == &rootNode);
++ }
++
++ // Reindex the root node, which should index all nodes in all layers
++ rootNode.reindex();
++
++ std::set<quint32> nodeIndices;
++ // Check that all nodes in the testNodes and layerNodes have unique indices
++ for (const auto &nodes : layerNodes) {
++ for (auto *node : nodes) {
++ QVERIFY(node->h.hasId());
++ nodeIndices.insert(node->h.index());
++ }
++ }
++
++ // Check that we have unique indices for all nodes
++ QCOMPARE(nodeIndices.size(), initialNodeCount * LayerCount);
++
++ quint32 expectedIndex = 1; // Start from 1 because the first index is reserved for the root/layer node(s)
++ for (auto begin = nodeIndices.begin(), end = nodeIndices.end(); begin != end; ++begin) {
++ // Check that the indices are without gaps
++ QCOMPARE(expectedIndex, *begin);
++ ++expectedIndex;
++ }
++
++ // Now remove all nodes from their layers
++ for (size_t i = 0; i < LayerCount; ++i) {
++ removeFromLayer(layers[i], layerNodes[i]);
++ QVERIFY(layers[i].children.isEmpty());
++ rootNode.removeChild(layers[i]);
++ }
++
++ for (auto &nodes : layerNodes)
++ qDeleteAll(nodes);
++ }
++
++ void tst_NodeIndexing::testIndexingWithImportScene()
++ {
++ // Multiple views means we have more then one layer
++ constexpr size_t LayerCount = 3;
++ std::array<QSSGRenderLayer, LayerCount> layers;
++
++ for (auto &layer : layers) {
++ QVERIFY(layer.children.isEmpty());
++ layer.ref(&rootNode);
++ }
++
++ // In this case we'll have two sets of nodes, one for the main layer
++ // and one for the import scene layers.
++ std::array<std::vector<QSSGRenderNode *>, LayerCount> layerNodes;
++
++ auto &mainLayerNodes = layerNodes[0];
++ auto &mainLayer = layers[0];
++
++ std::unique_ptr<QSSGRenderNode> sceneRoot(new QSSGRenderNode(QSSGRenderNode::Type::Node));
++ mainLayer.addChild(*sceneRoot);
++
++ // Build the main layer with the initial nodes
++ buildBasicNodeHierarchy(sceneRoot.get(), mainLayerNodes);
++
++ for (size_t i = 1; i < LayerCount; ++i) {
++ // For the other layers, we'll just copy the main layer nodes
++ layerNodes[i] = mainLayerNodes;
++ // Create a new layer for the import scene
++ auto ¤tLayer = layers[i];
++ QVERIFY(currentLayer.children.isEmpty());
++ // Replicate how we do import scenes
++ currentLayer.children.m_head = currentLayer.children.m_tail = sceneRoot.get();
++
++ }
++
++ // Add the layers to the root node
++ for (auto &layer : layers) {
++ rootNode.addChild(layer);
++ QVERIFY(layer.rootNode == &rootNode);
++ }
++
++ // Reindex the root node, which should index all nodes in all layers
++ rootNode.reindex();
++
++ std::set<quint32> nodeIndices;
++ size_t insertedNodeCount = 0;
++ // Check that all nodes in the testNodes and layerNodes have unique indices
++ for (const auto &nodes : layerNodes) {
++ for (auto *node : nodes) {
++ QVERIFY(node->h.hasId());
++ ++insertedNodeCount;
++ nodeIndices.insert(node->h.index());
++ }
++ }
++
++ // Need to add the scene root as well
++ QVERIFY(sceneRoot && sceneRoot->h.hasId());
++ ++insertedNodeCount;
++ nodeIndices.insert(sceneRoot->h.index());
++
++ // Check that we have unique indices for all nodes
++ // The total number of nodes should be initialNodeCount * LayerCount
++ // (It would be better to get the numbers from the reindex function,
++ // but this is good enough for now).
++ QCOMPARE(insertedNodeCount, (initialNodeCount * LayerCount) + 1 /* for the scene root node */);
++ // But the indices should only be as many as the main layer nodes
++ QCOMPARE(nodeIndices.size(), mainLayerNodes.size() + 1 /* for the scene root node */);
++ QCOMPARE(nodeIndices.size(), initialNodeCount + 1 /* for the scene root node */);
++
++ // Check that the indices are in the expected range
++ quint32 expectedIndex = 1; // Start from 1 because the first index is reserved for the root/layer node(s)
++ for (auto begin = nodeIndices.begin(), end = nodeIndices.end(); begin != end; ++begin) {
++ // Check that the indices are without gaps
++ QCOMPARE(expectedIndex, *begin);
++ ++expectedIndex;
++ }
++
++ // Now remove all nodes from their layers
++ removeFromLayer(mainLayer, mainLayerNodes);
++ mainLayer.removeChild(*sceneRoot);
++ QVERIFY(mainLayer.children.isEmpty());
++
++ // Ensure that the scene root isn't referenced by any layer
++ for (size_t i = 0; i < LayerCount; ++i) {
++ auto &layer = layers[i];
++ layer.children.clear(); // Clear the list (or the sceneRoot will be used after it's deleted)
++ rootNode.removeChild(layer);
++ }
++
++ qDeleteAll(mainLayerNodes);
++ }
++
++ void tst_NodeIndexing::testVersionWrapAround()
++ {
++ using VersionType = QSSGRenderNodeVersionType;
++
++ QSSGRenderLayer layer;
++ layer.ref(&rootNode);
++ rootNode.addChild(layer);
++
++ auto *node = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ layer.addChild(*node);
++
++ rootNode.reindex();
++ QVERIFY(node->h.hasId());
++
++ // Bypass the counter directly to just below the maximum, avoiding the need
++ // to iterate through all 65535 values in the test.
++ auto &gnd = *rootNode.globalNodeData();
++ gnd.m_version = std::numeric_limits<VersionType>::max() - 1;
++
++ // Reindex to reach the maximum version value.
++ rootNode.reindex();
++ QVERIFY(node->h.hasId());
++ QCOMPARE(node->h.version(), std::numeric_limits<VersionType>::max());
++ QCOMPARE(layer.lh.version(), std::numeric_limits<VersionType>::max());
++
++ // One more reindex wraps max + 1 to 0; the guard skips 0 and bumps to 1.
++ // Nodes and the layer handle must remain valid.
++ rootNode.reindex();
++ QVERIFY(node->h.hasId());
++ QCOMPARE(node->h.version(), VersionType(1));
++ QCOMPARE(layer.lh.version(), VersionType(1));
++
++ layer.removeChild(*node);
++ delete node;
++ rootNode.removeChild(layer);
++ }
++
++ void tst_NodeIndexing::testActiveStateSuppressesReindex()
++ {
++ QSSGRenderLayer layer;
++ layer.ref(&rootNode);
++ rootNode.addChild(layer);
++
++ std::vector<QSSGRenderNode *> nodes;
++ buildBasicNodeHierarchy(&layer, nodes);
++
++ rootNode.reindex();
++
++ auto &gnd = *rootNode.globalNodeData();
++ const auto versionBefore = gnd.m_version;
++
++ // Capture the current handle state of every node.
++ std::vector<std::pair<QSSGRenderNodeVersionType, quint32>> snapshot;
++ snapshot.reserve(nodes.size());
++ for (auto *node : nodes)
++ snapshot.push_back({ node->h.version(), node->h.index() });
++
++ // nodes[0] is added directly to the layer; nodes[1] and nodes[2] are its children.
++ // nodes[3..5] form a sibling group with no relation to nodes[0..2].
++ nodes.front()->setState(QSSGRenderNode::LocalState::Active, false);
++
++ // The GND version must not have changed — no reindex should have occurred.
++ QCOMPARE(gnd.m_version, versionBefore);
++
++ // All handles must remain valid and unchanged.
++ for (size_t i = 0; i < nodes.size(); ++i) {
++ QCOMPARE(nodes[i]->h.version(), snapshot[i].first);
++ QCOMPARE(nodes[i]->h.index(), snapshot[i].second);
++ }
++
++ // The changed node and its children must have ActiveDirty set.
++ // markDirty() propagates ActiveDirty to descendants via SubtreeUpdateMask.
++ using DirtyFlag = QSSGRenderNode::DirtyFlag;
++ QVERIFY( nodes[0]->isDirty(DirtyFlag::ActiveDirty));
++ QVERIFY( nodes[1]->isDirty(DirtyFlag::ActiveDirty));
++ QVERIFY( nodes[2]->isDirty(DirtyFlag::ActiveDirty));
++
++ // The sibling sub-tree must not be affected.
++ QVERIFY(!nodes[3]->isDirty(DirtyFlag::ActiveDirty));
++ QVERIFY(!nodes[4]->isDirty(DirtyFlag::ActiveDirty));
++ QVERIFY(!nodes[5]->isDirty(DirtyFlag::ActiveDirty));
++
++ removeFromLayer(layer, nodes);
++ rootNode.removeChild(layer);
++ qDeleteAll(nodes);
++ }
++
++ void tst_NodeIndexing::removeFromLayer(QSSGRenderLayer &layer, std::vector<QSSGRenderNode *> &nodes)
++ {
++ for (auto *node : nodes) {
++ QSSGRenderNode *parent = node->parent;
++ if (parent)
++ parent->removeChild(*node);
++ else
++ layer.removeChild(*node);
++ }
++
++ // Check that all nodes no longer have parents and
++ // their node index is reset.
++ for (auto *node : nodes) {
++ QCOMPARE(node->parent, nullptr);
++ QVERIFY(!node->h.hasId());
++ }
++ }
++
++ void tst_NodeIndexing::buildBasicNodeHierarchy(QSSGRenderNode *parent, std::vector<QSSGRenderNode *> &outList)
++ {
++ Q_ASSERT(parent);
++ Q_ASSERT(outList.size() == 0);
++
++ for (int i = 0; i < initialNodeCount; ++i) {
++ auto *node = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ outList.push_back(node);
++ }
++
++ // Add nodes to the second layer
++ for (int i = 0; i < initialNodeCount; i += 3) {
++ auto *node = outList[i];
++ QVERIFY(!node->h.hasId());
++ parent->addChild(*node);
++
++ // Add two children to each node
++ for (int j = 0; j < 2; ++j) {
++ auto *childNode = outList[i + j + 1];
++ QVERIFY(!childNode->h.hasId());
++ node->addChild(*childNode);
++ }
++ }
++ }
++
++ void tst_NodeIndexing::testRemoveFromGraphClearsRootNodeRefOnChildren()
++ {
++ // When removeFromGraph() orphans its direct children it must also clear
++ // their rootNodeRef, just as removeChild() does for the removed node
++ // itself. Without the fix, an orphaned node P retains a live
++ // rootNodeRef. If the root is then destroyed and a grandchild C calls
++ // removeFromGraph() it will execute P->removeChild(C), which accesses
++ // P->rootNodeRef via QSSGRenderRoot::get() — a use-after-free crash.
++ //
++ // Hierarchy under test:
++ // Root -> Layer -> sceneRoot -> GP -> P -> C
++ //
++ // sceneRoot is a plain node added directly to the layer. GP, P, and C
++ // form a chain beneath it. We remove GP from the graph and assert that
++ // P's rootNodeRef is null afterwards. We also exercise
++ // C->removeFromGraph() to verify it is safe when P->rootNodeRef is null
++ // (get() returns nullptr so markDirty is not called).
++ //
++ // Note: layers do not set the parent pointer on their direct children
++ // (see the Layer / ImportScene special case in addChild), so sceneRoot
++ // is needed to give GP a non-null parent that removeFromGraph() can
++ // walk up through.
++
++ QSSGRenderLayer layer;
++ layer.ref(&rootNode);
++ rootNode.addChild(layer);
++
++ auto *sceneRoot = new QSSGRenderNode;
++ auto *gp = new QSSGRenderNode;
++ auto *p = new QSSGRenderNode;
++ auto *c = new QSSGRenderNode;
++
++ layer.addChild(*sceneRoot);
++ sceneRoot->addChild(*gp);
++ gp->addChild(*p);
++ p->addChild(*c);
++
++ rootNode.reindex();
++
++ // Pre-condition: all nodes must have a rootNodeRef pointing to the root.
++ QVERIFY(sceneRoot->rootNodeRef != nullptr);
++ QVERIFY(gp->rootNodeRef != nullptr);
++ QVERIFY(p->rootNodeRef != nullptr);
++ QVERIFY(c->rootNodeRef != nullptr);
++
++ // Remove GP — this calls sceneRoot->removeChild(GP) which clears
++ // GP->rootNodeRef, then orphans P: P->parent = nullptr.
++ // The fix requires that P->rootNodeRef is also cleared here.
++ gp->removeFromGraph();
++
++ // GP itself must be fully detached by sceneRoot->removeChild(GP).
++ QCOMPARE(gp->parent, nullptr);
++ QCOMPARE(gp->rootNodeRef, nullptr);
++
++ // P was orphaned by the loop inside GP->removeFromGraph().
++ // Its rootNodeRef must be null — this is what the fix ensures.
++ QCOMPARE(p->parent, nullptr);
++ QCOMPARE(p->rootNodeRef, nullptr);
++
++ // C is still a child of P at this point (P->removeFromGraph not called).
++ // Calling C->removeFromGraph() will invoke P->removeChild(C).
++ // With P->rootNodeRef == nullptr, get() returns null and markDirty is
++ // skipped — this must not crash.
++ c->removeFromGraph();
++
++ QCOMPARE(c->parent, nullptr);
++ QCOMPARE(c->rootNodeRef, nullptr);
++
++ delete c;
++ delete p;
++ delete gp;
++ layer.removeChild(*sceneRoot);
++ delete sceneRoot;
++ rootNode.removeChild(layer);
++ }
++
++ void tst_NodeIndexing::testImportSceneStateAfterAddRemove()
++ {
++ // setImportScene()/removeImportScene() inject and detach a shared scene
++ // through addChild() and the dummy import-scene node. Verify that:
++ // - the dummy import-scene node is inserted at the *front* of the layer's
++ // child list, so the imported scene always gets the lowest indices,
++ // - the imported node keeps its existing parent (it belongs to its home
++ // tree) and gets the Imported state plus a rootNodeRef, and
++ // - removeImportScene() only resets the handle 'h', leaving the Imported
++ // state, rootNodeRef and parent untouched.
++
++ QSSGRenderLayer mainLayer;
++ QSSGRenderLayer importerLayer;
++ mainLayer.ref(&rootNode);
++ importerLayer.ref(&rootNode);
++
++ // Home tree: mainLayer -> container -> sceneRoot -> { leafA, leafB }.
++ // container is a SceneRoot so it gives sceneRoot a real, non-null parent
++ // without tripping the "already part of another scene graph" warning.
++ auto *container = new QSSGRenderNode(QSSGRenderNode::Type::SceneRoot);
++ auto *sceneRoot = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ auto *leafA = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ auto *leafB = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ mainLayer.addChild(*container);
++ container->addChild(*sceneRoot);
++ sceneRoot->addChild(*leafA);
++ sceneRoot->addChild(*leafB);
++ QCOMPARE(sceneRoot->parent, container);
++
++ // A plain node added to the importer layer before the import: the dummy
++ // import-scene node must end up *before* it in the child list.
++ auto *regular = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ importerLayer.addChild(*regular);
++
++ rootNode.addChild(mainLayer);
++ rootNode.addChild(importerLayer);
++
++ importerLayer.setImportScene(*sceneRoot);
++
++ // The dummy import-scene node exists and is at the front of the children.
++ auto *dummy = importerLayer.importSceneNode;
++ QVERIFY(dummy != nullptr);
++ QVERIFY(!importerLayer.children.isEmpty());
++ QCOMPARE(&importerLayer.children.front(), dummy);
++ // The imported node is the (only) child of the dummy node.
++ QVERIFY(!dummy->children.isEmpty());
++ QCOMPARE(&dummy->children.back(), sceneRoot);
++
++ // Import bookkeeping on the shared node.
++ QVERIFY(sceneRoot->getLocalState(QSSGRenderNode::LocalState::Imported));
++ QVERIFY(sceneRoot->rootNodeRef != nullptr);
++ QCOMPARE(sceneRoot->parent, container); // parent must be untouched
++
++ rootNode.reindex();
++
++ // Everything got an index, and the import-scene node indexes before the
++ // plain sibling node (front insertion).
++ QVERIFY(dummy->h.hasId());
++ QVERIFY(sceneRoot->h.hasId());
++ QVERIFY(regular->h.hasId());
++ QVERIFY(dummy->h.index() < regular->h.index());
++
++ // Detach the import scene.
++ importerLayer.removeImportScene(*sceneRoot);
++
++ // The dummy's child list is cleared and the handle is reset...
++ QVERIFY(dummy->children.isEmpty());
++ QVERIFY(!sceneRoot->h.hasId());
++ // ...but the Imported state, rootNodeRef and parent must be preserved.
++ QVERIFY(sceneRoot->getLocalState(QSSGRenderNode::LocalState::Imported));
++ QVERIFY(sceneRoot->rootNodeRef != nullptr);
++ QCOMPARE(sceneRoot->parent, container);
++
++ // Teardown.
++ importerLayer.removeChild(*regular);
++ sceneRoot->removeChild(*leafA);
++ sceneRoot->removeChild(*leafB);
++ container->removeChild(*sceneRoot);
++ mainLayer.removeChild(*container);
++ rootNode.removeChild(mainLayer);
++ rootNode.removeChild(importerLayer);
++ delete leafA;
++ delete leafB;
++ delete sceneRoot;
++ delete container;
++ delete regular;
++ }
++
++ void tst_NodeIndexing::testImportSceneRemovePreservesSharedState()
++ {
++ // The same imported scene may be imported by more than one view. Removing
++ // it from one importer must NOT clear the Imported state or rootNodeRef:
++ // neither is restored by the update/reindex, and the other importing view
++ // still relies on them.
++
++ QSSGRenderLayer mainLayer;
++ QSSGRenderLayer importerB;
++ QSSGRenderLayer importerC;
++ mainLayer.ref(&rootNode);
++ importerB.ref(&rootNode);
++ importerC.ref(&rootNode);
++
++ auto *container = new QSSGRenderNode(QSSGRenderNode::Type::SceneRoot);
++ auto *sceneRoot = new QSSGRenderNode(QSSGRenderNode::Type::Node);
++ mainLayer.addChild(*container);
++ container->addChild(*sceneRoot);
++
++ rootNode.addChild(mainLayer);
++ rootNode.addChild(importerB);
++ rootNode.addChild(importerC);
++
++ importerB.setImportScene(*sceneRoot);
++ importerC.setImportScene(*sceneRoot);
++ rootNode.reindex();
++
++ auto *dummyB = importerB.importSceneNode;
++ auto *dummyC = importerC.importSceneNode;
++ QVERIFY(dummyB != nullptr);
++ QVERIFY(dummyC != nullptr);
++ QVERIFY(sceneRoot->getLocalState(QSSGRenderNode::LocalState::Imported));
++ QVERIFY(sceneRoot->rootNodeRef != nullptr);
++ QVERIFY(sceneRoot->h.hasId());
++
++ // Remove from B only.
++ importerB.removeImportScene(*sceneRoot);
++
++ // B no longer references the scene...
++ QVERIFY(dummyB->children.isEmpty());
++ // ...but C still does, and the shared state survives.
++ QVERIFY(!dummyC->children.isEmpty());
++ QCOMPARE(&dummyC->children.back(), sceneRoot);
++ QVERIFY(sceneRoot->getLocalState(QSSGRenderNode::LocalState::Imported));
++ QVERIFY(sceneRoot->rootNodeRef != nullptr);
++
++ // Teardown.
++ importerC.removeImportScene(*sceneRoot);
++ container->removeChild(*sceneRoot);
++ mainLayer.removeChild(*container);
++ rootNode.removeChild(mainLayer);
++ rootNode.removeChild(importerB);
++ rootNode.removeChild(importerC);
++ delete sceneRoot;
++ delete container;
++ }
++
++ void tst_NodeIndexing::testRemoveChildIgnoresForeignNodeOnLayer()
++ {
++ // A layer does not set parent on its direct children (it does not own them), so removeChild()
++ // validates relationship by scanning its own child list rather than using inChild.parent. This
++ // verifies that scan correctly ignores a node that is not actually the layer's child.
++ // If not we could end-up corrupting the sibling list!
++
++ QSSGRenderLayer ownerLayer;
++ QSSGRenderLayer otherLayer;
++
++ QSSGRenderNode a;
++ QSSGRenderNode foreign;
++ QSSGRenderNode b;
++ ownerLayer.addChild(a);
++ ownerLayer.addChild(foreign);
++ ownerLayer.addChild(b);
++
++ // 'foreign' is a child of ownerLayer (and carries sibling links into it), but it is not a
++ // child of otherLayer. Removing it from otherLayer must be a no-op and must not disturb
++ // ownerLayer's list.
++ otherLayer.removeChild(foreign);
++
++ std::vector<QSSGRenderNode *> seen;
++ for (auto &child : ownerLayer.children)
++ seen.push_back(&child);
++ QCOMPARE(seen.size(), size_t(3));
++ QCOMPARE(seen[0], &a);
++ QCOMPARE(seen[1], &foreign);
++ QCOMPARE(seen[2], &b);
++
++ // Removing a genuine child still works.
++ ownerLayer.removeChild(foreign);
++ seen.clear();
++ for (auto &child : ownerLayer.children)
++ seen.push_back(&child);
++ QCOMPARE(seen.size(), size_t(2));
++ QCOMPARE(seen[0], &a);
++ QCOMPARE(seen[1], &b);
++
++ ownerLayer.removeChild(a);
++ ownerLayer.removeChild(b);
++ }
++
++ QTEST_APPLESS_MAIN(tst_NodeIndexing)
++
++ #include "tst_nodeindexing.moc"