File indexing completed on 2025-05-04 04:55:57

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0005  */
0006 
0007 #include "reparentingmodel.h"
0008 #include "korganizer_debug.h"
0009 #include <QObject>
0010 #include <QSortFilterProxyModel>
0011 #include <QStandardItemModel>
0012 #include <QTest>
0013 
0014 class DummyNode : public ReparentingModel::Node
0015 {
0016 public:
0017     DummyNode(ReparentingModel &personModel, const QString &name, const QString &data = QString())
0018         : ReparentingModel::Node(personModel)
0019         , mUid(name)
0020         , mParent(QStringLiteral("orphan"))
0021         , mName(name)
0022         , mData(data)
0023     {
0024     }
0025 
0026     ~DummyNode() override = default;
0027 
0028     bool operator==(const Node &node) const override
0029     {
0030         const auto dummyNode = dynamic_cast<const DummyNode *>(&node);
0031         if (dummyNode) {
0032             return dummyNode->mUid == mUid;
0033         }
0034         return false;
0035     }
0036 
0037     QString mUid;
0038     QString mParent;
0039 
0040 private:
0041     [[nodiscard]] QVariant data(int role) const override
0042     {
0043         if (role == Qt::DisplayRole) {
0044             if (mName != mUid) {
0045                 return QString(mUid + QLatin1Char('-') + mName);
0046             } else {
0047                 return mName;
0048             }
0049         } else if (role == Qt::UserRole) {
0050             return mData;
0051         }
0052         return {};
0053     }
0054 
0055     bool setData(const QVariant &variant, int role) override
0056     {
0057         Q_UNUSED(variant)
0058         Q_UNUSED(role)
0059         return false;
0060     }
0061 
0062     bool isDuplicateOf(const QModelIndex &sourceIndex) override
0063     {
0064         return sourceIndex.data().toString() == mUid;
0065     }
0066 
0067     bool adopts(const QModelIndex &sourceIndex) override
0068     {
0069         return sourceIndex.data().toString().contains(mParent);
0070     }
0071 
0072     void update(const Node::Ptr &node) override
0073     {
0074         mName = node.staticCast<DummyNode>()->mName;
0075         mData = node.staticCast<DummyNode>()->mData;
0076     }
0077 
0078     QString mName;
0079     QString mData;
0080 };
0081 
0082 class ModelSignalSpy : public QObject
0083 {
0084     Q_OBJECT
0085 public:
0086     explicit ModelSignalSpy(QAbstractItemModel &model)
0087         : start(0)
0088         , end(0)
0089     {
0090         connect(&model, &QAbstractItemModel::rowsInserted, this, &ModelSignalSpy::onRowsInserted);
0091         connect(&model, &QAbstractItemModel::rowsRemoved, this, &ModelSignalSpy::onRowsRemoved);
0092         connect(&model, &QAbstractItemModel::rowsMoved, this, &ModelSignalSpy::onRowsMoved);
0093         connect(&model, &QAbstractItemModel::dataChanged, this, &ModelSignalSpy::onDataChanged);
0094         connect(&model, &QAbstractItemModel::layoutChanged, this, &ModelSignalSpy::onLayoutChanged);
0095         connect(&model, &QAbstractItemModel::modelReset, this, &ModelSignalSpy::onModelReset);
0096     }
0097 
0098     QStringList mSignals;
0099     QModelIndex parent;
0100     QModelIndex topLeft, bottomRight;
0101     int start;
0102     int end;
0103 
0104 public Q_SLOTS:
0105     void onRowsInserted(const QModelIndex &p, int s, int e)
0106     {
0107         mSignals << QStringLiteral("rowsInserted");
0108         parent = p;
0109         start = s;
0110         end = e;
0111     }
0112 
0113     void onRowsRemoved(const QModelIndex &p, int s, int e)
0114     {
0115         mSignals << QStringLiteral("rowsRemoved");
0116         parent = p;
0117         start = s;
0118         end = e;
0119     }
0120 
0121     void onRowsMoved(const QModelIndex &, int, int, const QModelIndex &, int)
0122     {
0123         mSignals << QStringLiteral("rowsMoved");
0124     }
0125 
0126     void onDataChanged(const QModelIndex &t, const QModelIndex &b)
0127     {
0128         mSignals << QStringLiteral("dataChanged");
0129         topLeft = t;
0130         bottomRight = b;
0131     }
0132 
0133     void onLayoutChanged()
0134     {
0135         mSignals << QStringLiteral("layoutChanged");
0136     }
0137 
0138     void onModelReset()
0139     {
0140         mSignals << QStringLiteral("modelReset");
0141     }
0142 };
0143 
0144 QModelIndex getIndex(const char *string, const QAbstractItemModel &model)
0145 {
0146     QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
0147     if (list.isEmpty()) {
0148         return {};
0149     }
0150     return list.first();
0151 }
0152 
0153 QModelIndexList getIndexList(const char *string, const QAbstractItemModel &model)
0154 {
0155     return model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
0156 }
0157 
0158 class ReparentingModelTest : public QObject
0159 {
0160     Q_OBJECT
0161 private Q_SLOTS:
0162     void testPopulation();
0163     void testAddRemoveSourceItem();
0164     void testInsertSourceRow();
0165     void testInsertSourceRowSubnode();
0166     void testAddRemoveProxyNode();
0167     void testDeduplicate();
0168     void testDeduplicateNested();
0169     void testDeduplicateProxyNodeFirst();
0170     void testNestedDeduplicateProxyNodeFirst();
0171     void testUpdateNode();
0172     void testReparent();
0173     void testReparentSubcollections();
0174     void testReparentResetWithoutCrash();
0175     void testAddReparentedSourceItem();
0176     void testRemoveReparentedSourceItem();
0177     void testNestedReparentedSourceItem();
0178     void testAddNestedReparentedSourceItem();
0179     void testSourceDataChanged();
0180     void testSourceLayoutChanged();
0181     void testInvalidLayoutChanged();
0182     void testAddRemoveNodeByNodeManager();
0183     void testRemoveNodeByNodeManagerWithDataChanged();
0184     void testDataChanged();
0185 };
0186 
0187 void ReparentingModelTest::testPopulation()
0188 {
0189     QStandardItemModel sourceModel;
0190     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0191     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
0192 
0193     ReparentingModel reparentingModel;
0194     reparentingModel.setSourceModel(&sourceModel);
0195 
0196     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0197     QVERIFY(getIndex("row1", reparentingModel).isValid());
0198     QVERIFY(getIndex("row2", reparentingModel).isValid());
0199 }
0200 
0201 void ReparentingModelTest::testAddRemoveSourceItem()
0202 {
0203     QStandardItemModel sourceModel;
0204     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0205 
0206     ReparentingModel reparentingModel;
0207     reparentingModel.setSourceModel(&sourceModel);
0208     ModelSignalSpy spy(reparentingModel);
0209 
0210     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
0211     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0212     QVERIFY(getIndex("row1", reparentingModel).isValid());
0213     QVERIFY(getIndex("row2", reparentingModel).isValid());
0214     QCOMPARE(spy.parent, QModelIndex());
0215     QCOMPARE(spy.start, 1);
0216     QCOMPARE(spy.end, 1);
0217 
0218     sourceModel.removeRows(1, 1, QModelIndex());
0219     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0220     QVERIFY(getIndex("row1", reparentingModel).isValid());
0221     QVERIFY(!getIndex("row2", reparentingModel).isValid());
0222     QCOMPARE(spy.parent, QModelIndex());
0223     QCOMPARE(spy.start, 1);
0224     QCOMPARE(spy.end, 1);
0225 
0226     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsRemoved"));
0227 }
0228 
0229 // Ensure the model can deal with rows that are inserted out of order
0230 void ReparentingModelTest::testInsertSourceRow()
0231 {
0232     QStandardItemModel sourceModel;
0233     auto row2 = new QStandardItem(QStringLiteral("row2"));
0234     sourceModel.appendRow(row2);
0235 
0236     ReparentingModel reparentingModel;
0237     reparentingModel.setSourceModel(&sourceModel);
0238     ModelSignalSpy spy(reparentingModel);
0239 
0240     auto row1 = new QStandardItem(QStringLiteral("row1"));
0241     sourceModel.insertRow(0, row1);
0242     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0243     QVERIFY(getIndex("row1", reparentingModel).isValid());
0244     QVERIFY(getIndex("row2", reparentingModel).isValid());
0245 
0246     // The model does not try to reorder. First come, first serve.
0247     QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
0248     QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
0249     reparentingModel.setData(reparentingModel.index(1, 0, QModelIndex()), QStringLiteral("row1foo"), Qt::DisplayRole);
0250     reparentingModel.setData(reparentingModel.index(0, 0, QModelIndex()), QStringLiteral("row2foo"), Qt::DisplayRole);
0251     QCOMPARE(row1->data(Qt::DisplayRole).toString(), QStringLiteral("row1foo"));
0252     QCOMPARE(row2->data(Qt::DisplayRole).toString(), QStringLiteral("row2foo"));
0253 }
0254 
0255 // Ensure the model can deal with rows that are inserted out of order in a subnode
0256 void ReparentingModelTest::testInsertSourceRowSubnode()
0257 {
0258     auto parent = new QStandardItem(QStringLiteral("parent"));
0259 
0260     QStandardItemModel sourceModel;
0261     sourceModel.appendRow(parent);
0262     auto row2 = new QStandardItem(QStringLiteral("row2"));
0263     parent->appendRow(row2);
0264 
0265     ReparentingModel reparentingModel;
0266     reparentingModel.setSourceModel(&sourceModel);
0267     ModelSignalSpy spy(reparentingModel);
0268 
0269     auto row1 = new QStandardItem(QStringLiteral("row1"));
0270     parent->insertRow(0, row1);
0271 
0272     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0273     QVERIFY(getIndex("row1", reparentingModel).isValid());
0274     QVERIFY(getIndex("row2", reparentingModel).isValid());
0275     // The model does not try to reorder. First come, first serve.
0276     QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
0277     QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
0278     reparentingModel.setData(reparentingModel.index(1, 0, getIndex("parent", reparentingModel)), QStringLiteral("row1foo"), Qt::DisplayRole);
0279     reparentingModel.setData(reparentingModel.index(0, 0, getIndex("parent", reparentingModel)), QStringLiteral("row2foo"), Qt::DisplayRole);
0280     QCOMPARE(row1->data(Qt::DisplayRole).toString(), QStringLiteral("row1foo"));
0281     QCOMPARE(row2->data(Qt::DisplayRole).toString(), QStringLiteral("row2foo"));
0282 }
0283 
0284 void ReparentingModelTest::testAddRemoveProxyNode()
0285 {
0286     QStandardItemModel sourceModel;
0287     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0288 
0289     ReparentingModel reparentingModel;
0290     reparentingModel.setSourceModel(&sourceModel);
0291 
0292     ModelSignalSpy spy(reparentingModel);
0293 
0294     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0295 
0296     QTest::qWait(0);
0297 
0298     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0299     QVERIFY(getIndex("row1", reparentingModel).isValid());
0300     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
0301 
0302     reparentingModel.removeNode(DummyNode(reparentingModel, QStringLiteral("proxy1")));
0303 
0304     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0305     QVERIFY(getIndex("row1", reparentingModel).isValid());
0306     QVERIFY(!getIndex("proxy1", reparentingModel).isValid());
0307 
0308     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsRemoved"));
0309 }
0310 
0311 void ReparentingModelTest::testDeduplicate()
0312 {
0313     QStandardItemModel sourceModel;
0314     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0315 
0316     ReparentingModel reparentingModel;
0317     reparentingModel.setSourceModel(&sourceModel);
0318 
0319     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
0320 
0321     QTest::qWait(0);
0322 
0323     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0324     QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
0325     // TODO ensure we actually have the source index and not the proxy index
0326 }
0327 
0328 /**
0329  * rebuildAll detects and handles nested duplicates
0330  */
0331 void ReparentingModelTest::testDeduplicateNested()
0332 {
0333     QStandardItemModel sourceModel;
0334     auto item = new QStandardItem(QStringLiteral("row1"));
0335     item->appendRow(new QStandardItem(QStringLiteral("child1")));
0336     sourceModel.appendRow(item);
0337 
0338     ReparentingModel reparentingModel;
0339     reparentingModel.setSourceModel(&sourceModel);
0340 
0341     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("child1"))));
0342 
0343     QTest::qWait(0);
0344 
0345     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0346     QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
0347 }
0348 
0349 /**
0350  * onSourceRowsInserted detects and removes duplicates
0351  */
0352 void ReparentingModelTest::testDeduplicateProxyNodeFirst()
0353 {
0354     QStandardItemModel sourceModel;
0355     ReparentingModel reparentingModel;
0356     reparentingModel.setSourceModel(&sourceModel);
0357     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
0358 
0359     QTest::qWait(0);
0360 
0361     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0362 
0363     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0364     QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
0365     // TODO ensure we actually have the source index and not the proxy index
0366 }
0367 
0368 /**
0369  * onSourceRowsInserted detects and removes nested duplicates
0370  */
0371 void ReparentingModelTest::testNestedDeduplicateProxyNodeFirst()
0372 {
0373     QStandardItemModel sourceModel;
0374     ReparentingModel reparentingModel;
0375     reparentingModel.setSourceModel(&sourceModel);
0376     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("child1"))));
0377 
0378     QTest::qWait(0);
0379 
0380     auto item = new QStandardItem(QStringLiteral("row1"));
0381     item->appendRow(new QStandardItem(QStringLiteral("child1")));
0382     sourceModel.appendRow(item);
0383 
0384     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0385     QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
0386     // TODO ensure we actually have the source index and not the proxy index
0387 }
0388 
0389 /**
0390  * updateNode should update the node data
0391  */
0392 void ReparentingModelTest::testUpdateNode()
0393 {
0394     QStandardItemModel sourceModel;
0395     ReparentingModel reparentingModel;
0396     reparentingModel.setSourceModel(&sourceModel);
0397     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"), QStringLiteral("blub"))));
0398 
0399     QTest::qWait(0);
0400 
0401     QModelIndex index = getIndex("proxy1", reparentingModel);
0402     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0403     QVERIFY(index.isValid());
0404     QCOMPARE(reparentingModel.data(index, Qt::UserRole).toString(), QStringLiteral("blub"));
0405 
0406     ModelSignalSpy spy(reparentingModel);
0407     reparentingModel.updateNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"), QStringLiteral("new data"))));
0408     QTest::qWait(0);
0409 
0410     QModelIndex i2 = getIndex("proxy1", reparentingModel);
0411     QCOMPARE(i2.column(), index.column());
0412     QCOMPARE(i2.row(), index.row());
0413 
0414     QCOMPARE(spy.mSignals.count(), 1);
0415     QCOMPARE(spy.mSignals.takeLast(), QStringLiteral("dataChanged"));
0416     QCOMPARE(spy.topLeft, i2);
0417     QCOMPARE(spy.bottomRight, i2);
0418 
0419     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0420     QCOMPARE(reparentingModel.data(i2, Qt::UserRole).toString(), QStringLiteral("new data"));
0421 }
0422 
0423 void ReparentingModelTest::testReparent()
0424 {
0425     QStandardItemModel sourceModel;
0426     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
0427 
0428     ReparentingModel reparentingModel;
0429     reparentingModel.setSourceModel(&sourceModel);
0430 
0431     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0432 
0433     QTest::qWait(0);
0434 
0435     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0436     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
0437     QCOMPARE(reparentingModel.rowCount(getIndex("proxy1", reparentingModel)), 1);
0438 }
0439 
0440 void ReparentingModelTest::testReparentSubcollections()
0441 {
0442     QStandardItemModel sourceModel;
0443     ReparentingModel reparentingModel;
0444     reparentingModel.setSourceModel(&sourceModel);
0445 
0446     /* Source structure
0447      -- +
0448         -- + orphan
0449            -- + col1
0450               -- sub1
0451               -- sub2
0452            -- col2
0453     */
0454     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
0455     sourceModel.item(0, 0)->appendRow(new QStandardItem(QStringLiteral("col1")));
0456     sourceModel.item(0, 0)->child(0, 0)->appendRow(new QStandardItem(QStringLiteral("sub1")));
0457     sourceModel.item(0, 0)->child(0, 0)->appendRow(new QStandardItem(QStringLiteral("sub2")));
0458     sourceModel.item(0, 0)->appendRow(new QStandardItem(QStringLiteral("col2")));
0459 
0460     auto node = new DummyNode(reparentingModel, QStringLiteral("col1"));
0461     node->mUid = QStringLiteral("uid");
0462     node->mParent = QStringLiteral("col");
0463 
0464     /* new srutcure:
0465      -- +
0466         -- orphan
0467         -- + uid-col1
0468            -- + col1
0469               -- sub1
0470               -- sub2
0471            -- col2
0472     */
0473     reparentingModel.addNode(ReparentingModel::Node::Ptr(node));
0474 
0475     QTest::qWait(0);
0476 
0477     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0478     QVERIFY(getIndex("col1", reparentingModel).isValid());
0479     QCOMPARE(getIndex("col1", reparentingModel).parent(), getIndex("uid-col1", reparentingModel));
0480     QCOMPARE(reparentingModel.rowCount(getIndex("col1", reparentingModel)), 2);
0481     QCOMPARE(reparentingModel.rowCount(getIndex("uid-col1", reparentingModel)), 2);
0482 
0483     node = new DummyNode(reparentingModel, QStringLiteral("xxx"));
0484     node->mUid = QStringLiteral("uid");
0485     node->mParent = QStringLiteral("col");
0486 
0487     // same structure but new data
0488     reparentingModel.updateNode(ReparentingModel::Node::Ptr(node));
0489 
0490     QTest::qWait(0);
0491 
0492     QCOMPARE(getIndex("col1", reparentingModel).parent(), getIndex("uid-xxx", reparentingModel));
0493     QCOMPARE(reparentingModel.rowCount(getIndex("col1", reparentingModel)), 2);
0494     QCOMPARE(reparentingModel.rowCount(getIndex("uid-xxx", reparentingModel)), 2);
0495 }
0496 
0497 /*
0498  * This test ensures we properly deal with reparented source nodes if the model is reset.
0499  * This is important since source nodes are removed during the model reset while the proxy nodes (to which the source nodes have been reparented) remain.
0500  *
0501  * Note that this test is only useful with the model internal asserts.
0502  */
0503 void ReparentingModelTest::testReparentResetWithoutCrash()
0504 {
0505     QStandardItemModel sourceModel;
0506     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
0507 
0508     ReparentingModel reparentingModel;
0509     reparentingModel.setSourceModel(&sourceModel);
0510 
0511     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0512     QTest::qWait(0);
0513 
0514     reparentingModel.setSourceModel(&sourceModel);
0515 
0516     QTest::qWait(0);
0517 
0518     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0519 }
0520 
0521 void ReparentingModelTest::testAddReparentedSourceItem()
0522 {
0523     QStandardItemModel sourceModel;
0524 
0525     ReparentingModel reparentingModel;
0526     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0527     reparentingModel.setSourceModel(&sourceModel);
0528 
0529     QTest::qWait(0);
0530 
0531     ModelSignalSpy spy(reparentingModel);
0532 
0533     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
0534 
0535     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0536     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
0537     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted"));
0538     QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
0539     QCOMPARE(spy.start, 0);
0540     QCOMPARE(spy.end, 0);
0541 }
0542 
0543 void ReparentingModelTest::testRemoveReparentedSourceItem()
0544 {
0545     QStandardItemModel sourceModel;
0546     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
0547     ReparentingModel reparentingModel;
0548     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0549     reparentingModel.setSourceModel(&sourceModel);
0550 
0551     QTest::qWait(0);
0552 
0553     ModelSignalSpy spy(reparentingModel);
0554 
0555     sourceModel.removeRows(0, 1, QModelIndex());
0556 
0557     QTest::qWait(0);
0558 
0559     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
0560     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
0561     QVERIFY(!getIndex("orphan", reparentingModel).isValid());
0562     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsRemoved"));
0563     QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
0564     QCOMPARE(spy.start, 0);
0565     QCOMPARE(spy.end, 0);
0566 }
0567 
0568 void ReparentingModelTest::testNestedReparentedSourceItem()
0569 {
0570     QStandardItemModel sourceModel;
0571     auto item = new QStandardItem(QStringLiteral("parent"));
0572     item->appendRow(QList<QStandardItem *>() << new QStandardItem(QStringLiteral("orphan")));
0573     sourceModel.appendRow(item);
0574 
0575     ReparentingModel reparentingModel;
0576     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0577     reparentingModel.setSourceModel(&sourceModel);
0578 
0579     QTest::qWait(0);
0580 
0581     // toplevel should have both parent and proxy
0582     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0583     QVERIFY(getIndex("orphan", reparentingModel).isValid());
0584     QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
0585 }
0586 
0587 void ReparentingModelTest::testAddNestedReparentedSourceItem()
0588 {
0589     QStandardItemModel sourceModel;
0590 
0591     ReparentingModel reparentingModel;
0592     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
0593     reparentingModel.setSourceModel(&sourceModel);
0594 
0595     QTest::qWait(0);
0596 
0597     ModelSignalSpy spy(reparentingModel);
0598 
0599     auto item = new QStandardItem(QStringLiteral("parent"));
0600     item->appendRow(QList<QStandardItem *>() << new QStandardItem(QStringLiteral("orphan")));
0601     sourceModel.appendRow(item);
0602 
0603     QTest::qWait(0);
0604 
0605     // toplevel should have both parent and proxy
0606     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0607     QVERIFY(getIndex("orphan", reparentingModel).isValid());
0608     QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
0609     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsInserted"));
0610 }
0611 
0612 void ReparentingModelTest::testSourceDataChanged()
0613 {
0614     QStandardItemModel sourceModel;
0615     auto item = new QStandardItem(QStringLiteral("row1"));
0616     sourceModel.appendRow(item);
0617 
0618     ReparentingModel reparentingModel;
0619     reparentingModel.setSourceModel(&sourceModel);
0620 
0621     item->setText(QStringLiteral("rowX"));
0622 
0623     QVERIFY(!getIndex("row1", reparentingModel).isValid());
0624     QVERIFY(getIndex("rowX", reparentingModel).isValid());
0625 }
0626 
0627 void ReparentingModelTest::testSourceLayoutChanged()
0628 {
0629     QStandardItemModel sourceModel;
0630     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
0631     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0632 
0633     QSortFilterProxyModel filter;
0634     filter.setSourceModel(&sourceModel);
0635 
0636     ReparentingModel reparentingModel;
0637     reparentingModel.setSourceModel(&filter);
0638     ModelSignalSpy spy(reparentingModel);
0639 
0640     QPersistentModelIndex index1 = reparentingModel.index(0, 0, QModelIndex());
0641     QPersistentModelIndex index2 = reparentingModel.index(1, 0, QModelIndex());
0642 
0643     // Emits layout changed and sorts the items the other way around
0644     filter.sort(0, Qt::AscendingOrder);
0645 
0646     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
0647     QVERIFY(getIndex("row1", reparentingModel).isValid());
0648     // Right now we don't even care about the order
0649     // QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("layoutChanged"));
0650     QCOMPARE(index1.data().toString(), QStringLiteral("row2"));
0651     QCOMPARE(index2.data().toString(), QStringLiteral("row1"));
0652 }
0653 
0654 /*
0655  * This is a very implementation specific test that tries to crash the model
0656  */
0657 // Test for invalid implementation of layoutChanged
0658 //*have proxy node in model
0659 //*insert duplicate from source
0660 //*issue layout changed so the model gets rebuilt
0661 //*access node (which is not actually existing anymore)
0662 // => crash
0663 void ReparentingModelTest::testInvalidLayoutChanged()
0664 {
0665     QStandardItemModel sourceModel;
0666     QSortFilterProxyModel filter;
0667     filter.setSourceModel(&sourceModel);
0668     ReparentingModel reparentingModel;
0669     reparentingModel.setSourceModel(&filter);
0670     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
0671 
0672     QTest::qWait(0);
0673 
0674     // Take reference to proxy node
0675     const QModelIndexList row1List = getIndexList("row1", reparentingModel);
0676     QVERIFY(!row1List.isEmpty());
0677     QPersistentModelIndex persistentIndex = row1List.first();
0678     QVERIFY(persistentIndex.isValid());
0679 
0680     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
0681     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
0682 
0683     // This rebuilds the model and invalidates the reference
0684     // Emits layout changed and sorts the items the other way around
0685     filter.sort(0, Qt::AscendingOrder);
0686 
0687     // This fails because the persistenIndex is no longer valid
0688     persistentIndex.data().toString();
0689     QVERIFY(!persistentIndex.isValid());
0690 }
0691 
0692 class DummyNodeManager : public ReparentingModel::NodeManager
0693 {
0694 public:
0695     explicit DummyNodeManager(ReparentingModel &m)
0696         : ReparentingModel::NodeManager(m)
0697     {
0698     }
0699 
0700 private:
0701     void checkSourceIndex(const QModelIndex &sourceIndex) override
0702     {
0703         if (sourceIndex.data().toString() == QLatin1StringView("personfolder")) {
0704             model.addNode(ReparentingModel::Node::Ptr(new DummyNode(model, QStringLiteral("personnode"))));
0705         }
0706     }
0707 
0708     void checkSourceIndexRemoval(const QModelIndex &sourceIndex) override
0709     {
0710         if (sourceIndex.data().toString() == QLatin1StringView("personfolder")) {
0711             model.removeNode(DummyNode(model, QStringLiteral("personnode")));
0712         }
0713     }
0714 };
0715 
0716 void ReparentingModelTest::testAddRemoveNodeByNodeManager()
0717 {
0718     QStandardItemModel sourceModel;
0719     sourceModel.appendRow(new QStandardItem(QStringLiteral("personfolder")));
0720     ReparentingModel reparentingModel;
0721     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
0722     reparentingModel.setSourceModel(&sourceModel);
0723 
0724     QTest::qWait(0);
0725 
0726     QVERIFY(getIndex("personnode", reparentingModel).isValid());
0727     QVERIFY(getIndex("personfolder", reparentingModel).isValid());
0728 
0729     sourceModel.removeRows(0, 1, QModelIndex());
0730 
0731     QTest::qWait(0);
0732     QVERIFY(!getIndex("personnode", reparentingModel).isValid());
0733     QVERIFY(!getIndex("personfolder", reparentingModel).isValid());
0734 }
0735 
0736 /*
0737  * This tests a special case that is caused by the delayed doAddNode call,
0738  * causing a removed node to be read immediately if it's removed while
0739  * a doAddNode call is pending (that can be triggered by dataChanged).
0740  */
0741 void ReparentingModelTest::testRemoveNodeByNodeManagerWithDataChanged()
0742 {
0743     QStandardItemModel sourceModel;
0744     auto item = new QStandardItem(QStringLiteral("personfolder"));
0745     sourceModel.appendRow(item);
0746     ReparentingModel reparentingModel;
0747     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
0748     reparentingModel.setSourceModel(&sourceModel);
0749 
0750     QTest::qWait(0);
0751 
0752     QVERIFY(getIndex("personnode", reparentingModel).isValid());
0753     QVERIFY(getIndex("personfolder", reparentingModel).isValid());
0754 
0755     // Trigger data changed
0756     item->setStatusTip(QStringLiteral("sldkfjlfsj"));
0757     sourceModel.removeRows(0, 1, QModelIndex());
0758 
0759     QTest::qWait(0);
0760     QVERIFY(!getIndex("personnode", reparentingModel).isValid());
0761     QVERIFY(!getIndex("personfolder", reparentingModel).isValid());
0762 }
0763 
0764 void ReparentingModelTest::testDataChanged()
0765 {
0766     QStandardItemModel sourceModel;
0767     auto item = new QStandardItem(QStringLiteral("folder"));
0768     sourceModel.appendRow(item);
0769     ReparentingModel reparentingModel;
0770     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
0771     reparentingModel.setSourceModel(&sourceModel);
0772     ModelSignalSpy spy(reparentingModel);
0773 
0774     QTest::qWait(0);
0775 
0776     // Trigger data changed
0777     item->setStatusTip(QStringLiteral("sldkfjlfsj"));
0778 
0779     QTest::qWait(0);
0780 
0781     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("dataChanged"));
0782 }
0783 
0784 QTEST_MAIN(ReparentingModelTest)
0785 
0786 #include "reparentingmodeltest.moc"