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"