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