1 /*
2  * SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
3  *
4  * SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
5  */
6 
7 #include "reparentingmodel.h"
8 #include "korganizer_debug.h"
9 #include <QObject>
10 #include <QSortFilterProxyModel>
11 #include <QStandardItemModel>
12 #include <QTest>
13 
14 class DummyNode : public ReparentingModel::Node
15 {
16 public:
DummyNode(ReparentingModel & personModel,const QString & name,const QString & data=QString ())17     DummyNode(ReparentingModel &personModel, const QString &name, const QString &data = QString())
18         : ReparentingModel::Node(personModel)
19         , mUid(name)
20         , mParent(QStringLiteral("orphan"))
21         , mName(name)
22         , mData(data)
23     {
24     }
25 
~DummyNode()26     ~DummyNode() override
27     {
28     }
29 
operator ==(const Node & node) const30     bool operator==(const Node &node) const override
31     {
32         const auto dummyNode = dynamic_cast<const DummyNode *>(&node);
33         if (dummyNode) {
34             return dummyNode->mUid == mUid;
35         }
36         return false;
37     }
38 
39     QString mUid;
40     QString mParent;
41 
42 private:
data(int role) const43     QVariant data(int role) const override
44     {
45         if (role == Qt::DisplayRole) {
46             if (mName != mUid) {
47                 return QString(mUid + QLatin1Char('-') + mName);
48             } else {
49                 return mName;
50             }
51         } else if (role == Qt::UserRole) {
52             return mData;
53         }
54         return QVariant();
55     }
56 
setData(const QVariant & variant,int role)57     bool setData(const QVariant &variant, int role) override
58     {
59         Q_UNUSED(variant)
60         Q_UNUSED(role)
61         return false;
62     }
63 
isDuplicateOf(const QModelIndex & sourceIndex)64     bool isDuplicateOf(const QModelIndex &sourceIndex) override
65     {
66         return sourceIndex.data().toString() == mUid;
67     }
68 
adopts(const QModelIndex & sourceIndex)69     bool adopts(const QModelIndex &sourceIndex) override
70     {
71         return sourceIndex.data().toString().contains(mParent);
72     }
73 
update(const Node::Ptr & node)74     void update(const Node::Ptr &node) override
75     {
76         mName = node.staticCast<DummyNode>()->mName;
77         mData = node.staticCast<DummyNode>()->mData;
78     }
79 
80     QString mName;
81     QString mData;
82 };
83 
84 class ModelSignalSpy : public QObject
85 {
86     Q_OBJECT
87 public:
ModelSignalSpy(QAbstractItemModel & model)88     explicit ModelSignalSpy(QAbstractItemModel &model)
89         : start(0)
90         , end(0)
91     {
92         connect(&model, &QAbstractItemModel::rowsInserted, this, &ModelSignalSpy::onRowsInserted);
93         connect(&model, &QAbstractItemModel::rowsRemoved, this, &ModelSignalSpy::onRowsRemoved);
94         connect(&model, &QAbstractItemModel::rowsMoved, this, &ModelSignalSpy::onRowsMoved);
95         connect(&model, &QAbstractItemModel::dataChanged, this, &ModelSignalSpy::onDataChanged);
96         connect(&model, &QAbstractItemModel::layoutChanged, this, &ModelSignalSpy::onLayoutChanged);
97         connect(&model, &QAbstractItemModel::modelReset, this, &ModelSignalSpy::onModelReset);
98     }
99 
100     QStringList mSignals;
101     QModelIndex parent;
102     QModelIndex topLeft, bottomRight;
103     int start;
104     int end;
105 
106 public Q_SLOTS:
onRowsInserted(const QModelIndex & p,int s,int e)107     void onRowsInserted(const QModelIndex &p, int s, int e)
108     {
109         mSignals << QStringLiteral("rowsInserted");
110         parent = p;
111         start = s;
112         end = e;
113     }
114 
onRowsRemoved(const QModelIndex & p,int s,int e)115     void onRowsRemoved(const QModelIndex &p, int s, int e)
116     {
117         mSignals << QStringLiteral("rowsRemoved");
118         parent = p;
119         start = s;
120         end = e;
121     }
122 
onRowsMoved(const QModelIndex &,int,int,const QModelIndex &,int)123     void onRowsMoved(const QModelIndex &, int, int, const QModelIndex &, int)
124     {
125         mSignals << QStringLiteral("rowsMoved");
126     }
127 
onDataChanged(const QModelIndex & t,const QModelIndex & b)128     void onDataChanged(const QModelIndex &t, const QModelIndex &b)
129     {
130         mSignals << QStringLiteral("dataChanged");
131         topLeft = t;
132         bottomRight = b;
133     }
134 
onLayoutChanged()135     void onLayoutChanged()
136     {
137         mSignals << QStringLiteral("layoutChanged");
138     }
139 
onModelReset()140     void onModelReset()
141     {
142         mSignals << QStringLiteral("modelReset");
143     }
144 };
145 
getIndex(const char * string,const QAbstractItemModel & model)146 QModelIndex getIndex(const char *string, const QAbstractItemModel &model)
147 {
148     QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
149     if (list.isEmpty()) {
150         return {};
151     }
152     return list.first();
153 }
154 
getIndexList(const char * string,const QAbstractItemModel & model)155 QModelIndexList getIndexList(const char *string, const QAbstractItemModel &model)
156 {
157     return model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
158 }
159 
160 class ReparentingModelTest : public QObject
161 {
162     Q_OBJECT
163 private Q_SLOTS:
164     void testPopulation();
165     void testAddRemoveSourceItem();
166     void testInsertSourceRow();
167     void testInsertSourceRowSubnode();
168     void testAddRemoveProxyNode();
169     void testDeduplicate();
170     void testDeduplicateNested();
171     void testDeduplicateProxyNodeFirst();
172     void testNestedDeduplicateProxyNodeFirst();
173     void testUpdateNode();
174     void testReparent();
175     void testReparentSubcollections();
176     void testReparentResetWithoutCrash();
177     void testAddReparentedSourceItem();
178     void testRemoveReparentedSourceItem();
179     void testNestedReparentedSourceItem();
180     void testAddNestedReparentedSourceItem();
181     void testSourceDataChanged();
182     void testSourceLayoutChanged();
183     void testInvalidLayoutChanged();
184     void testAddRemoveNodeByNodeManager();
185     void testRemoveNodeByNodeManagerWithDataChanged();
186     void testDataChanged();
187 };
188 
testPopulation()189 void ReparentingModelTest::testPopulation()
190 {
191     QStandardItemModel sourceModel;
192     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
193     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
194 
195     ReparentingModel reparentingModel;
196     reparentingModel.setSourceModel(&sourceModel);
197 
198     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
199     QVERIFY(getIndex("row1", reparentingModel).isValid());
200     QVERIFY(getIndex("row2", reparentingModel).isValid());
201 }
202 
testAddRemoveSourceItem()203 void ReparentingModelTest::testAddRemoveSourceItem()
204 {
205     QStandardItemModel sourceModel;
206     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
207 
208     ReparentingModel reparentingModel;
209     reparentingModel.setSourceModel(&sourceModel);
210     ModelSignalSpy spy(reparentingModel);
211 
212     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
213     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
214     QVERIFY(getIndex("row1", reparentingModel).isValid());
215     QVERIFY(getIndex("row2", reparentingModel).isValid());
216     QCOMPARE(spy.parent, QModelIndex());
217     QCOMPARE(spy.start, 1);
218     QCOMPARE(spy.end, 1);
219 
220     sourceModel.removeRows(1, 1, QModelIndex());
221     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
222     QVERIFY(getIndex("row1", reparentingModel).isValid());
223     QVERIFY(!getIndex("row2", reparentingModel).isValid());
224     QCOMPARE(spy.parent, QModelIndex());
225     QCOMPARE(spy.start, 1);
226     QCOMPARE(spy.end, 1);
227 
228     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsRemoved"));
229 }
230 
231 // Ensure the model can deal with rows that are inserted out of order
testInsertSourceRow()232 void ReparentingModelTest::testInsertSourceRow()
233 {
234     QStandardItemModel sourceModel;
235     auto row2 = new QStandardItem(QStringLiteral("row2"));
236     sourceModel.appendRow(row2);
237 
238     ReparentingModel reparentingModel;
239     reparentingModel.setSourceModel(&sourceModel);
240     ModelSignalSpy spy(reparentingModel);
241 
242     auto row1 = new QStandardItem(QStringLiteral("row1"));
243     sourceModel.insertRow(0, row1);
244     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
245     QVERIFY(getIndex("row1", reparentingModel).isValid());
246     QVERIFY(getIndex("row2", reparentingModel).isValid());
247 
248     // The model does not try to reorder. First come, first serve.
249     QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
250     QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
251     reparentingModel.setData(reparentingModel.index(1, 0, QModelIndex()), QStringLiteral("row1foo"), Qt::DisplayRole);
252     reparentingModel.setData(reparentingModel.index(0, 0, QModelIndex()), QStringLiteral("row2foo"), Qt::DisplayRole);
253     QCOMPARE(row1->data(Qt::DisplayRole).toString(), QStringLiteral("row1foo"));
254     QCOMPARE(row2->data(Qt::DisplayRole).toString(), QStringLiteral("row2foo"));
255 }
256 
257 // Ensure the model can deal with rows that are inserted out of order in a subnode
testInsertSourceRowSubnode()258 void ReparentingModelTest::testInsertSourceRowSubnode()
259 {
260     auto parent = new QStandardItem(QStringLiteral("parent"));
261 
262     QStandardItemModel sourceModel;
263     sourceModel.appendRow(parent);
264     auto row2 = new QStandardItem(QStringLiteral("row2"));
265     parent->appendRow(row2);
266 
267     ReparentingModel reparentingModel;
268     reparentingModel.setSourceModel(&sourceModel);
269     ModelSignalSpy spy(reparentingModel);
270 
271     auto row1 = new QStandardItem(QStringLiteral("row1"));
272     parent->insertRow(0, row1);
273 
274     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
275     QVERIFY(getIndex("row1", reparentingModel).isValid());
276     QVERIFY(getIndex("row2", reparentingModel).isValid());
277     // The model does not try to reorder. First come, first serve.
278     QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
279     QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
280     reparentingModel.setData(reparentingModel.index(1, 0, getIndex("parent", reparentingModel)), QStringLiteral("row1foo"), Qt::DisplayRole);
281     reparentingModel.setData(reparentingModel.index(0, 0, getIndex("parent", reparentingModel)), QStringLiteral("row2foo"), Qt::DisplayRole);
282     QCOMPARE(row1->data(Qt::DisplayRole).toString(), QStringLiteral("row1foo"));
283     QCOMPARE(row2->data(Qt::DisplayRole).toString(), QStringLiteral("row2foo"));
284 }
285 
testAddRemoveProxyNode()286 void ReparentingModelTest::testAddRemoveProxyNode()
287 {
288     QStandardItemModel sourceModel;
289     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
290 
291     ReparentingModel reparentingModel;
292     reparentingModel.setSourceModel(&sourceModel);
293 
294     ModelSignalSpy spy(reparentingModel);
295 
296     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
297 
298     QTest::qWait(0);
299 
300     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
301     QVERIFY(getIndex("row1", reparentingModel).isValid());
302     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
303 
304     reparentingModel.removeNode(DummyNode(reparentingModel, QStringLiteral("proxy1")));
305 
306     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
307     QVERIFY(getIndex("row1", reparentingModel).isValid());
308     QVERIFY(!getIndex("proxy1", reparentingModel).isValid());
309 
310     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsRemoved"));
311 }
312 
testDeduplicate()313 void ReparentingModelTest::testDeduplicate()
314 {
315     QStandardItemModel sourceModel;
316     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
317 
318     ReparentingModel reparentingModel;
319     reparentingModel.setSourceModel(&sourceModel);
320 
321     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
322 
323     QTest::qWait(0);
324 
325     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
326     QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
327     // TODO ensure we actually have the source index and not the proxy index
328 }
329 
330 /**
331  * rebuildAll detects and handles nested duplicates
332  */
testDeduplicateNested()333 void ReparentingModelTest::testDeduplicateNested()
334 {
335     QStandardItemModel sourceModel;
336     auto item = new QStandardItem(QStringLiteral("row1"));
337     item->appendRow(new QStandardItem(QStringLiteral("child1")));
338     sourceModel.appendRow(item);
339 
340     ReparentingModel reparentingModel;
341     reparentingModel.setSourceModel(&sourceModel);
342 
343     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("child1"))));
344 
345     QTest::qWait(0);
346 
347     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
348     QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
349 }
350 
351 /**
352  * onSourceRowsInserted detects and removes duplicates
353  */
testDeduplicateProxyNodeFirst()354 void ReparentingModelTest::testDeduplicateProxyNodeFirst()
355 {
356     QStandardItemModel sourceModel;
357     ReparentingModel reparentingModel;
358     reparentingModel.setSourceModel(&sourceModel);
359     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
360 
361     QTest::qWait(0);
362 
363     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
364 
365     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
366     QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
367     // TODO ensure we actually have the source index and not the proxy index
368 }
369 
370 /**
371  * onSourceRowsInserted detects and removes nested duplicates
372  */
testNestedDeduplicateProxyNodeFirst()373 void ReparentingModelTest::testNestedDeduplicateProxyNodeFirst()
374 {
375     QStandardItemModel sourceModel;
376     ReparentingModel reparentingModel;
377     reparentingModel.setSourceModel(&sourceModel);
378     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("child1"))));
379 
380     QTest::qWait(0);
381 
382     auto item = new QStandardItem(QStringLiteral("row1"));
383     item->appendRow(new QStandardItem(QStringLiteral("child1")));
384     sourceModel.appendRow(item);
385 
386     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
387     QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
388     // TODO ensure we actually have the source index and not the proxy index
389 }
390 
391 /**
392  * updateNode should update the node data
393  */
testUpdateNode()394 void ReparentingModelTest::testUpdateNode()
395 {
396     QStandardItemModel sourceModel;
397     ReparentingModel reparentingModel;
398     reparentingModel.setSourceModel(&sourceModel);
399     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"), QStringLiteral("blub"))));
400 
401     QTest::qWait(0);
402 
403     QModelIndex index = getIndex("proxy1", reparentingModel);
404     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
405     QVERIFY(index.isValid());
406     QCOMPARE(reparentingModel.data(index, Qt::UserRole).toString(), QStringLiteral("blub"));
407 
408     ModelSignalSpy spy(reparentingModel);
409     reparentingModel.updateNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"), QStringLiteral("new data"))));
410     QTest::qWait(0);
411 
412     QModelIndex i2 = getIndex("proxy1", reparentingModel);
413     QCOMPARE(i2.column(), index.column());
414     QCOMPARE(i2.row(), index.row());
415 
416     QCOMPARE(spy.mSignals.count(), 1);
417     QCOMPARE(spy.mSignals.takeLast(), QStringLiteral("dataChanged"));
418     QCOMPARE(spy.topLeft, i2);
419     QCOMPARE(spy.bottomRight, i2);
420 
421     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
422     QCOMPARE(reparentingModel.data(i2, Qt::UserRole).toString(), QStringLiteral("new data"));
423 }
424 
testReparent()425 void ReparentingModelTest::testReparent()
426 {
427     QStandardItemModel sourceModel;
428     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
429 
430     ReparentingModel reparentingModel;
431     reparentingModel.setSourceModel(&sourceModel);
432 
433     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
434 
435     QTest::qWait(0);
436 
437     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
438     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
439     QCOMPARE(reparentingModel.rowCount(getIndex("proxy1", reparentingModel)), 1);
440 }
441 
testReparentSubcollections()442 void ReparentingModelTest::testReparentSubcollections()
443 {
444     QStandardItemModel sourceModel;
445     ReparentingModel reparentingModel;
446     reparentingModel.setSourceModel(&sourceModel);
447 
448     /* Source structure
449      -- +
450         -- + orphan
451            -- + col1
452               -- sub1
453               -- sub2
454            -- col2
455     */
456     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
457     sourceModel.item(0, 0)->appendRow(new QStandardItem(QStringLiteral("col1")));
458     sourceModel.item(0, 0)->child(0, 0)->appendRow(new QStandardItem(QStringLiteral("sub1")));
459     sourceModel.item(0, 0)->child(0, 0)->appendRow(new QStandardItem(QStringLiteral("sub2")));
460     sourceModel.item(0, 0)->appendRow(new QStandardItem(QStringLiteral("col2")));
461 
462     auto node = new DummyNode(reparentingModel, QStringLiteral("col1"));
463     node->mUid = QStringLiteral("uid");
464     node->mParent = QStringLiteral("col");
465 
466     /* new srutcure:
467      -- +
468         -- orphan
469         -- + uid-col1
470            -- + col1
471               -- sub1
472               -- sub2
473            -- col2
474     */
475     reparentingModel.addNode(ReparentingModel::Node::Ptr(node));
476 
477     QTest::qWait(0);
478 
479     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
480     QVERIFY(getIndex("col1", reparentingModel).isValid());
481     QCOMPARE(getIndex("col1", reparentingModel).parent(), getIndex("uid-col1", reparentingModel));
482     QCOMPARE(reparentingModel.rowCount(getIndex("col1", reparentingModel)), 2);
483     QCOMPARE(reparentingModel.rowCount(getIndex("uid-col1", reparentingModel)), 2);
484 
485     node = new DummyNode(reparentingModel, QStringLiteral("xxx"));
486     node->mUid = QStringLiteral("uid");
487     node->mParent = QStringLiteral("col");
488 
489     // same structure but new data
490     reparentingModel.updateNode(ReparentingModel::Node::Ptr(node));
491 
492     QTest::qWait(0);
493 
494     QCOMPARE(getIndex("col1", reparentingModel).parent(), getIndex("uid-xxx", reparentingModel));
495     QCOMPARE(reparentingModel.rowCount(getIndex("col1", reparentingModel)), 2);
496     QCOMPARE(reparentingModel.rowCount(getIndex("uid-xxx", reparentingModel)), 2);
497 }
498 
499 /*
500  * This test ensures we properly deal with reparented source nodes if the model is reset.
501  * 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.
502  *
503  * Note that this test is only useful with the model internal asserts.
504  */
testReparentResetWithoutCrash()505 void ReparentingModelTest::testReparentResetWithoutCrash()
506 {
507     QStandardItemModel sourceModel;
508     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
509 
510     ReparentingModel reparentingModel;
511     reparentingModel.setSourceModel(&sourceModel);
512 
513     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
514     QTest::qWait(0);
515 
516     reparentingModel.setSourceModel(&sourceModel);
517 
518     QTest::qWait(0);
519 
520     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
521 }
522 
testAddReparentedSourceItem()523 void ReparentingModelTest::testAddReparentedSourceItem()
524 {
525     QStandardItemModel sourceModel;
526 
527     ReparentingModel reparentingModel;
528     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
529     reparentingModel.setSourceModel(&sourceModel);
530 
531     QTest::qWait(0);
532 
533     ModelSignalSpy spy(reparentingModel);
534 
535     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
536 
537     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
538     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
539     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted"));
540     QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
541     QCOMPARE(spy.start, 0);
542     QCOMPARE(spy.end, 0);
543 }
544 
testRemoveReparentedSourceItem()545 void ReparentingModelTest::testRemoveReparentedSourceItem()
546 {
547     QStandardItemModel sourceModel;
548     sourceModel.appendRow(new QStandardItem(QStringLiteral("orphan")));
549     ReparentingModel reparentingModel;
550     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
551     reparentingModel.setSourceModel(&sourceModel);
552 
553     QTest::qWait(0);
554 
555     ModelSignalSpy spy(reparentingModel);
556 
557     sourceModel.removeRows(0, 1, QModelIndex());
558 
559     QTest::qWait(0);
560 
561     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
562     QVERIFY(getIndex("proxy1", reparentingModel).isValid());
563     QVERIFY(!getIndex("orphan", reparentingModel).isValid());
564     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsRemoved"));
565     QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
566     QCOMPARE(spy.start, 0);
567     QCOMPARE(spy.end, 0);
568 }
569 
testNestedReparentedSourceItem()570 void ReparentingModelTest::testNestedReparentedSourceItem()
571 {
572     QStandardItemModel sourceModel;
573     auto item = new QStandardItem(QStringLiteral("parent"));
574     item->appendRow(QList<QStandardItem *>() << new QStandardItem(QStringLiteral("orphan")));
575     sourceModel.appendRow(item);
576 
577     ReparentingModel reparentingModel;
578     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
579     reparentingModel.setSourceModel(&sourceModel);
580 
581     QTest::qWait(0);
582 
583     // toplevel should have both parent and proxy
584     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
585     QVERIFY(getIndex("orphan", reparentingModel).isValid());
586     QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
587 }
588 
testAddNestedReparentedSourceItem()589 void ReparentingModelTest::testAddNestedReparentedSourceItem()
590 {
591     QStandardItemModel sourceModel;
592 
593     ReparentingModel reparentingModel;
594     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("proxy1"))));
595     reparentingModel.setSourceModel(&sourceModel);
596 
597     QTest::qWait(0);
598 
599     ModelSignalSpy spy(reparentingModel);
600 
601     auto item = new QStandardItem(QStringLiteral("parent"));
602     item->appendRow(QList<QStandardItem *>() << new QStandardItem(QStringLiteral("orphan")));
603     sourceModel.appendRow(item);
604 
605     QTest::qWait(0);
606 
607     // toplevel should have both parent and proxy
608     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
609     QVERIFY(getIndex("orphan", reparentingModel).isValid());
610     QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
611     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsInserted") << QStringLiteral("rowsInserted"));
612 }
613 
testSourceDataChanged()614 void ReparentingModelTest::testSourceDataChanged()
615 {
616     QStandardItemModel sourceModel;
617     auto item = new QStandardItem(QStringLiteral("row1"));
618     sourceModel.appendRow(item);
619 
620     ReparentingModel reparentingModel;
621     reparentingModel.setSourceModel(&sourceModel);
622 
623     item->setText(QStringLiteral("rowX"));
624 
625     QVERIFY(!getIndex("row1", reparentingModel).isValid());
626     QVERIFY(getIndex("rowX", reparentingModel).isValid());
627 }
628 
testSourceLayoutChanged()629 void ReparentingModelTest::testSourceLayoutChanged()
630 {
631     QStandardItemModel sourceModel;
632     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
633     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
634 
635     QSortFilterProxyModel filter;
636     filter.setSourceModel(&sourceModel);
637 
638     ReparentingModel reparentingModel;
639     reparentingModel.setSourceModel(&filter);
640     ModelSignalSpy spy(reparentingModel);
641 
642     QPersistentModelIndex index1 = reparentingModel.index(0, 0, QModelIndex());
643     QPersistentModelIndex index2 = reparentingModel.index(1, 0, QModelIndex());
644 
645     // Emits layout changed and sorts the items the other way around
646     filter.sort(0, Qt::AscendingOrder);
647 
648     QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
649     QVERIFY(getIndex("row1", reparentingModel).isValid());
650     // Right now we don't even care about the order
651     // QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("layoutChanged"));
652     QCOMPARE(index1.data().toString(), QStringLiteral("row2"));
653     QCOMPARE(index2.data().toString(), QStringLiteral("row1"));
654 }
655 
656 /*
657  * This is a very implementation specific test that tries to crash the model
658  */
659 // Test for invalid implementation of layoutChanged
660 //*have proxy node in model
661 //*insert duplicate from source
662 //*issue layout changed so the model get's rebuilt
663 //*access node (which is not actually existing anymore)
664 // => crash
testInvalidLayoutChanged()665 void ReparentingModelTest::testInvalidLayoutChanged()
666 {
667     QStandardItemModel sourceModel;
668     QSortFilterProxyModel filter;
669     filter.setSourceModel(&sourceModel);
670     ReparentingModel reparentingModel;
671     reparentingModel.setSourceModel(&filter);
672     reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QStringLiteral("row1"))));
673 
674     QTest::qWait(0);
675 
676     // Take reference to proxy node
677     const QModelIndexList row1List = getIndexList("row1", reparentingModel);
678     QVERIFY(!row1List.isEmpty());
679     QPersistentModelIndex persistentIndex = row1List.first();
680     QVERIFY(persistentIndex.isValid());
681 
682     sourceModel.appendRow(new QStandardItem(QStringLiteral("row1")));
683     sourceModel.appendRow(new QStandardItem(QStringLiteral("row2")));
684 
685     // This rebuilds the model and invalidates the reference
686     // Emits layout changed and sorts the items the other way around
687     filter.sort(0, Qt::AscendingOrder);
688 
689     // This fails because the persistenIndex is no longer valid
690     persistentIndex.data().toString();
691     QVERIFY(!persistentIndex.isValid());
692 }
693 
694 class DummyNodeManager : public ReparentingModel::NodeManager
695 {
696 public:
DummyNodeManager(ReparentingModel & m)697     explicit DummyNodeManager(ReparentingModel &m)
698         : ReparentingModel::NodeManager(m)
699     {
700     }
701 
702 private:
checkSourceIndex(const QModelIndex & sourceIndex)703     void checkSourceIndex(const QModelIndex &sourceIndex) override
704     {
705         if (sourceIndex.data().toString() == QLatin1String("personfolder")) {
706             model.addNode(ReparentingModel::Node::Ptr(new DummyNode(model, QStringLiteral("personnode"))));
707         }
708     }
709 
checkSourceIndexRemoval(const QModelIndex & sourceIndex)710     void checkSourceIndexRemoval(const QModelIndex &sourceIndex) override
711     {
712         if (sourceIndex.data().toString() == QLatin1String("personfolder")) {
713             model.removeNode(DummyNode(model, QStringLiteral("personnode")));
714         }
715     }
716 };
717 
testAddRemoveNodeByNodeManager()718 void ReparentingModelTest::testAddRemoveNodeByNodeManager()
719 {
720     QStandardItemModel sourceModel;
721     sourceModel.appendRow(new QStandardItem(QStringLiteral("personfolder")));
722     ReparentingModel reparentingModel;
723     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
724     reparentingModel.setSourceModel(&sourceModel);
725 
726     QTest::qWait(0);
727 
728     QVERIFY(getIndex("personnode", reparentingModel).isValid());
729     QVERIFY(getIndex("personfolder", reparentingModel).isValid());
730 
731     sourceModel.removeRows(0, 1, QModelIndex());
732 
733     QTest::qWait(0);
734     QVERIFY(!getIndex("personnode", reparentingModel).isValid());
735     QVERIFY(!getIndex("personfolder", reparentingModel).isValid());
736 }
737 
738 /*
739  * This tests a special case that is caused by the delayed doAddNode call,
740  * causing a removed node to be readded immediately if it's removed while
741  * a doAddNode call is pending (that can be triggered by dataChanged).
742  */
testRemoveNodeByNodeManagerWithDataChanged()743 void ReparentingModelTest::testRemoveNodeByNodeManagerWithDataChanged()
744 {
745     QStandardItemModel sourceModel;
746     auto item = new QStandardItem(QStringLiteral("personfolder"));
747     sourceModel.appendRow(item);
748     ReparentingModel reparentingModel;
749     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
750     reparentingModel.setSourceModel(&sourceModel);
751 
752     QTest::qWait(0);
753 
754     QVERIFY(getIndex("personnode", reparentingModel).isValid());
755     QVERIFY(getIndex("personfolder", reparentingModel).isValid());
756 
757     // Trigger data changed
758     item->setStatusTip(QStringLiteral("sldkfjlfsj"));
759     sourceModel.removeRows(0, 1, QModelIndex());
760 
761     QTest::qWait(0);
762     QVERIFY(!getIndex("personnode", reparentingModel).isValid());
763     QVERIFY(!getIndex("personfolder", reparentingModel).isValid());
764 }
765 
testDataChanged()766 void ReparentingModelTest::testDataChanged()
767 {
768     QStandardItemModel sourceModel;
769     auto item = new QStandardItem(QStringLiteral("folder"));
770     sourceModel.appendRow(item);
771     ReparentingModel reparentingModel;
772     reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel)));
773     reparentingModel.setSourceModel(&sourceModel);
774     ModelSignalSpy spy(reparentingModel);
775 
776     QTest::qWait(0);
777 
778     // Trigger data changed
779     item->setStatusTip(QStringLiteral("sldkfjlfsj"));
780 
781     QTest::qWait(0);
782 
783     QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("dataChanged"));
784 }
785 
786 QTEST_MAIN(ReparentingModelTest)
787 
788 #include "reparentingmodeltest.moc"
789