LeechCraft 0.6.70-19214-g121e5318a7
Modular cross-platform feature rich live environment.
Loading...
Searching...
No Matches
flattofoldersproxymodeltest.cpp
Go to the documentation of this file.
1/**********************************************************************
2 * LeechCraft - modular cross-platform feature rich internet client.
3 * Copyright (C) 2006-2014 Georg Rudoy
4 *
5 * Distributed under the Boost Software License, Version 1.0.
6 * (See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
7 **********************************************************************/
8
10#include <algorithm>
11#include <memory>
12#include <optional>
13#include <QAbstractItemModelTester>
14#include <QMimeData>
15#include <QRandomGenerator>
16#include <QSignalSpy>
17#include <QSortFilterProxyModel>
18#include <QStandardItemModel>
19#include <QtTest>
23
25
26namespace LC::Util
27{
28 namespace
29 {
30 constexpr int IdRole = Qt::UserRole + 1;
31 constexpr int TagsRole = +CustomDataRoles::Tags;
32 constexpr auto TagMimeType = "x-leechcraft/tag";
33
34 struct TagsStub final : ITagsManager
35 {
36 tag_id GetID (const QString& tag) override { return tag; }
37 QString GetTag (tag_id id) const override { return id; }
38 QStringList GetAllTags () const override { return {}; }
39 QStringList Split (const QString& string) const override { return string.split (';', Qt::SkipEmptyParts); }
40 QList<tag_id> SplitToIDs (const QString& string) override { return Split (string); }
41 QString Join (const QStringList& tags) const override { return tags.join ("; "); }
42 QString JoinIDs (const QStringList& ids) const override { return Join (ids); }
43 QAbstractItemModel* GetModel () override { return nullptr; }
44 QObject* GetQObject () override { return nullptr; }
45 };
46
47 struct Source : QStandardItemModel
48 {
49 using QStandardItemModel::persistentIndexList;
50
51 mutable QList<int> LastDraggedIds_;
52 std::optional<QByteArray> LastDropTag_;
53
54 void Append (int id, const QString& text, const QStringList& tags)
55 {
56 const auto first = new QStandardItem { text };
57 first->setData (id, IdRole);
58 first->setData (tags, TagsRole);
59 appendRow ({ first, new QStandardItem { QString::number (id * 10) } });
60 }
61
62 QMimeData* mimeData (const QModelIndexList& indexes) const override
63 {
64 LastDraggedIds_.clear ();
65 for (const auto& index : indexes)
66 LastDraggedIds_ << index.data (IdRole).toInt ();
67 std::ranges::sort (LastDraggedIds_);
68 return QStandardItemModel::mimeData (indexes);
69 }
70
71 bool dropMimeData (const QMimeData *data, Qt::DropAction, int, int, const QModelIndex&) override
72 {
73 LastDropTag_ = data->hasFormat (TagMimeType) ?
74 std::optional { data->data (TagMimeType) } :
75 std::nullopt;
76 return true;
77 }
78 };
79
80 struct Filter : QSortFilterProxyModel
81 {
82 using QSortFilterProxyModel::persistentIndexList;
83 };
84
85 struct Fixture
86 {
87 TagsStub Tags_;
88 Source Source_;
89 Filter Filter_;
90 QAbstractItemModel& Upstream_;
91 FlatToFoldersProxyModel Proxy_ { &Tags_ };
92 QAbstractItemModelTester Tester_ { &Proxy_, QAbstractItemModelTester::FailureReportingMode::QtTest };
93
94 explicit Fixture (bool viaFilter)
95 : Upstream_ { viaFilter ? static_cast<QAbstractItemModel&> (Filter_) : Source_ }
96 {
97 Filter_.setSourceModel (&Source_);
98 }
99
100 void FillDefault ()
101 {
102 Source_.Append (1, "a", { "t1" });
103 Source_.Append (2, "b", { "t1", "t2" });
104 Source_.Append (3, "c", {});
105 }
106
107 void Attach ()
108 {
109 Proxy_.SetSourceModel (&Upstream_);
110 }
111
112 int UpstreamPersistentCount () const
113 {
114 return (&Upstream_ == &Filter_ ? Filter_.persistentIndexList () : Source_.persistentIndexList ()).size ();
115 }
116 };
117
118 struct Spies
119 {
120 QSignalSpy Inserted_;
121 QSignalSpy Removed_;
122 QSignalSpy Reset_;
123 QSignalSpy Layout_;
124 QSignalSpy Data_;
125
126 explicit Spies (QAbstractItemModel& model)
127 : Inserted_ { &model, &QAbstractItemModel::rowsInserted }
128 , Removed_ { &model, &QAbstractItemModel::rowsRemoved }
129 , Reset_ { &model, &QAbstractItemModel::modelReset }
130 , Layout_ { &model, &QAbstractItemModel::layoutChanged }
131 , Data_ { &model, &QAbstractItemModel::dataChanged }
132 {
133 }
134
135 int Structural () const
136 {
137 return Inserted_.size () + Removed_.size () + Reset_.size () + Layout_.size ();
138 }
139 };
140
141 QStringList TagsOf (const QModelIndex& index)
142 {
143 auto tags = index.data (TagsRole).toStringList ();
144 if (tags.isEmpty ())
145 tags << QString {};
146 return tags;
147 }
148
149 using Picture = QMap<QString, QSet<int>>;
150
151 Picture Expected (const QAbstractItemModel& upstream)
152 {
153 Picture result;
154 for (int row = 0; row < upstream.rowCount (); ++row)
155 {
156 const auto& index = upstream.index (row, 0);
157 for (const auto& tag : TagsOf (index))
158 result [tag] << index.data (IdRole).toInt ();
159 }
160 return result;
161 }
162
163 QString Describe (const Picture& picture)
164 {
165 QStringList folders;
166 for (const auto& [tag, ids] : picture.asKeyValueRange ())
167 {
168 auto sorted = ids.values ();
169 std::ranges::sort (sorted);
170 QStringList idStrings;
171 for (const auto id : sorted)
172 idStrings << QString::number (id);
173 folders << (tag.isEmpty () ? QStringLiteral ("<untagged>") : tag) + " {" + idStrings.join (' ') + '}';
174 }
175 return folders.join ("; ");
176 }
177
178 QStringList Dump (const FlatToFoldersProxyModel& proxy)
179 {
180 QStringList result;
181 for (int f = 0; f < proxy.rowCount (); ++f)
182 {
183 const auto& folder = proxy.index (f, 0);
184 QStringList ids;
185 for (int l = 0; l < proxy.rowCount (folder); ++l)
186 ids << QString::number (proxy.index (l, 0, folder).data (IdRole).toInt ());
187 result << folder.data (TagsRole).toString () + ": " + ids.join (' ');
188 }
189 return result;
190 }
191
192 void CheckTree (const FlatToFoldersProxyModel& proxy, const QAbstractItemModel& upstream)
193 {
194 QCOMPARE (proxy.columnCount (), upstream.columnCount ());
195
196 Picture actual;
197 int leaves = 0;
198 for (int f = 0; f < proxy.rowCount (); ++f)
199 {
200 const auto& folderIdx = proxy.index (f, 0);
201 QVERIFY (proxy.IsFolder (folderIdx));
202 QCOMPARE (proxy.parent (folderIdx), QModelIndex {});
203 if (upstream.columnCount () > 1)
204 QCOMPARE (proxy.rowCount (folderIdx.siblingAtColumn (1)), 0);
205
206 const auto& tag = folderIdx.data (TagsRole).toString ();
207 QVERIFY2 (!actual.contains (tag), qPrintable ("duplicate folder for tag " + tag));
208 QVERIFY2 (proxy.rowCount (folderIdx) > 0, qPrintable ("empty folder for tag " + tag));
209
210 auto& ids = actual [tag];
211 for (int l = 0; l < proxy.rowCount (folderIdx); ++l)
212 {
213 ++leaves;
214 const auto& leafIdx = proxy.index (l, 0, folderIdx);
215 QVERIFY (!proxy.IsFolder (leafIdx));
216 QCOMPARE (proxy.rowCount (leafIdx), 0);
217 QCOMPARE (proxy.parent (leafIdx), folderIdx);
218
219 const auto& source = proxy.MapToSource (leafIdx);
220 QVERIFY (source.isValid ());
221 QCOMPARE (source.model (), &upstream);
222 QVERIFY (TagsOf (source).contains (tag));
223 QVERIFY (proxy.MapFromSource (source).contains (leafIdx));
224 for (int col = 0; col < upstream.columnCount (); ++col)
225 QCOMPARE (proxy.index (l, col, folderIdx).data (), source.siblingAtColumn (col).data ());
226
227 ids << leafIdx.data (IdRole).toInt ();
228 }
229 }
230
231 const auto& expected = Expected (upstream);
232 QVERIFY2 (actual == expected,
233 qPrintable ("tree is " + Describe (actual) + ", source says " + Describe (expected)));
234
235 int expectedLeaves = 0;
236 for (int row = 0; row < upstream.rowCount (); ++row)
237 expectedLeaves += TagsOf (upstream.index (row, 0)).size ();
238 QCOMPARE (leaves, expectedLeaves);
239 }
240
241 QModelIndex Parent (const QList<QVariant>& signalArgs)
242 {
243 return signalArgs.at (0).value<QModelIndex> ();
244 }
245 }
246
247 void FlatToFoldersProxyModelTest::initTestCase ()
248 {
249 // The fixture attaches its own tester in QtTest reporting mode.
250 qunsetenv ("LC_MODEL_SANITIZER_MODE");
251 }
252
253 void FlatToFoldersProxyModelTest::initTestCase_data ()
254 {
255 QTest::addColumn<bool> ("viaFilter");
256 QTest::newRow ("direct source") << false;
257 QTest::newRow ("through a QSortFilterProxyModel") << true;
258 }
259
260 void FlatToFoldersProxyModelTest::testInitialTree ()
261 {
262 QFETCH_GLOBAL (bool, viaFilter);
263 Fixture f { viaFilter };
264 f.FillDefault ();
265 f.Attach ();
266 CheckTree (f.Proxy_, f.Upstream_);
267
268 QCOMPARE (Dump (f.Proxy_), (QStringList { "t1: 1 2", "t2: 2", ": 3" }));
269 QCOMPARE (f.Proxy_.headerData (0, Qt::Horizontal, Qt::DisplayRole), f.Upstream_.headerData (0, Qt::Horizontal, Qt::DisplayRole));
270 QCOMPARE (f.Proxy_.flags ({}), Qt::ItemIsDropEnabled);
271 }
272
273 void FlatToFoldersProxyModelTest::testInsertCreatesFoldersOnDemand ()
274 {
275 QFETCH_GLOBAL (bool, viaFilter);
276 Fixture f { viaFilter };
277 f.FillDefault ();
278 f.Attach ();
279 Spies spies { f.Proxy_ };
280
281 f.Source_.Append (4, "d", { "t3" });
282 QCOMPARE (spies.Inserted_.size (), 2);
283 QCOMPARE (Parent (spies.Inserted_.at (0)), QModelIndex {});
284 QVERIFY (Parent (spies.Inserted_.at (1)).isValid ());
285
286 f.Source_.Append (5, "e", { "t1" });
287 QCOMPARE (spies.Inserted_.size (), 3);
288 QCOMPARE (spies.Structural (), 3);
289 CheckTree (f.Proxy_, f.Upstream_);
290 }
291
292 void FlatToFoldersProxyModelTest::testRetagKeepsTheSurvivingLeaf ()
293 {
294 QFETCH_GLOBAL (bool, viaFilter);
295 Fixture f { viaFilter };
296 f.FillDefault ();
297 f.Attach ();
298 Spies spies { f.Proxy_ };
299
300 f.Source_.item (1)->setData (QStringList { "t2", "t3" }, TagsRole);
301
302 QCOMPARE (spies.Data_.size (), 1);
303 const auto& args = spies.Data_.first ();
304 const auto& topLeft = args.at (0).value<QModelIndex> ();
305 QCOMPARE (topLeft.data (IdRole).toInt (), 2);
306 QCOMPARE (topLeft.parent ().data (TagsRole).toString (), QString { "t2" });
307 QCOMPARE (args.at (1).value<QModelIndex> ().column (), f.Upstream_.columnCount () - 1);
308 QCOMPARE (args.at (2).value<QList<int>> (), (QList<int> { TagsRole }));
309
310 QCOMPARE (spies.Removed_.size (), 1);
311 QCOMPARE (spies.Inserted_.size (), 2);
312 CheckTree (f.Proxy_, f.Upstream_);
313 }
314
315 void FlatToFoldersProxyModelTest::testRemovingTheLastLeafRemovesTheFolder ()
316 {
317 QFETCH_GLOBAL (bool, viaFilter);
318 Fixture f { viaFilter };
319 f.FillDefault ();
320 f.Attach ();
321 Spies spies { f.Proxy_ };
322
323 f.Source_.removeRow (2);
324
325 QCOMPARE (spies.Removed_.size (), 2);
326 QVERIFY (Parent (spies.Removed_.at (0)).isValid ());
327 QCOMPARE (Parent (spies.Removed_.at (1)), QModelIndex {});
328 QCOMPARE (f.Proxy_.rowCount (), 2);
329 CheckTree (f.Proxy_, f.Upstream_);
330 }
331
332 void FlatToFoldersProxyModelTest::testRemovalFollowsOwnBookkeeping ()
333 {
334 QFETCH_GLOBAL (bool, viaFilter);
335 Fixture f { viaFilter };
336 f.FillDefault ();
337 f.Attach ();
338
339 f.Source_.blockSignals (true);
340 f.Source_.item (2)->setData (QStringList { "zz" }, TagsRole);
341 f.Source_.blockSignals (false);
342 f.Source_.removeRow (2);
343
344 QCOMPARE (f.Proxy_.rowCount (), 2);
345 CheckTree (f.Proxy_, f.Upstream_);
346 }
347
348 void FlatToFoldersProxyModelTest::testSourceSortLeavesTheTreeAlone ()
349 {
350 QFETCH_GLOBAL (bool, viaFilter);
351 Fixture f { viaFilter };
352 f.FillDefault ();
353 f.Attach ();
354 const auto& before = Dump (f.Proxy_);
355 Spies spies { f.Proxy_ };
356
357 f.Source_.sort (0, Qt::DescendingOrder);
358
359 QCOMPARE (spies.Layout_.size (), 0);
360 QCOMPARE (spies.Structural (), 0);
361 QCOMPARE (Dump (f.Proxy_), before);
362 CheckTree (f.Proxy_, f.Upstream_);
363 }
364
365 void FlatToFoldersProxyModelTest::testSourceResetIsOneReset ()
366 {
367 QFETCH_GLOBAL (bool, viaFilter);
368 Fixture f { viaFilter };
369 f.FillDefault ();
370 f.Attach ();
371 Spies spies { f.Proxy_ };
372
373 f.Source_.clear ();
374 QCOMPARE (spies.Reset_.size (), 1);
375 QCOMPARE (spies.Structural (), 1);
376 QCOMPARE (f.Proxy_.rowCount (), 0);
377
378 f.Source_.Append (4, "d", { "t1", "t2" });
379 QCOMPARE (spies.Inserted_.size (), 4);
380 CheckTree (f.Proxy_, f.Upstream_);
381 }
382
383 void FlatToFoldersProxyModelTest::testAttachAndDetach ()
384 {
385 QFETCH_GLOBAL (bool, viaFilter);
386 Fixture f { viaFilter };
387 f.FillDefault ();
388 Spies spies { f.Proxy_ };
389
390 f.Attach ();
391 QCOMPARE (spies.Reset_.size (), 1);
392 QCOMPARE (spies.Structural (), 1);
393 QCOMPARE (f.Proxy_.rowCount (), 3);
394 CheckTree (f.Proxy_, f.Upstream_);
395
396 f.Proxy_.SetSourceModel (nullptr);
397 QCOMPARE (spies.Reset_.size (), 2);
398 QCOMPARE (spies.Structural (), 2);
399 QCOMPARE (f.Proxy_.rowCount (), 0);
400 QCOMPARE (f.Proxy_.columnCount (), 0);
401 QCOMPARE (f.Proxy_.GetSourceModel (), nullptr);
402
403 f.Source_.Append (4, "d", { "t9" });
404 QCOMPARE (spies.Structural (), 2);
405 }
406
407 void FlatToFoldersProxyModelTest::testDragAndDropMapThroughFolders ()
408 {
409 QFETCH_GLOBAL (bool, viaFilter);
410 Fixture f { viaFilter };
411 f.FillDefault ();
412 f.Attach ();
413 const auto& t1 = f.Proxy_.index (0, 0);
414
415 std::unique_ptr<QMimeData> folderDrag { f.Proxy_.mimeData ({ t1 }) };
416 QVERIFY (folderDrag);
417 QCOMPARE (f.Source_.LastDraggedIds_, (QList<int> { 1, 2 }));
418
419 std::unique_ptr<QMimeData> leafDrag { f.Proxy_.mimeData ({ f.Proxy_.index (1, 0, t1) }) };
420 QVERIFY (leafDrag);
421 QCOMPARE (f.Source_.LastDraggedIds_, (QList<int> { 2 }));
422
423 QMimeData payload;
424 payload.setText ("payload");
425 QVERIFY (f.Proxy_.dropMimeData (&payload, Qt::CopyAction, -1, -1, t1));
426 QVERIFY (f.Source_.LastDropTag_.has_value ());
427 QCOMPARE (*f.Source_.LastDropTag_, QByteArray { "t1" });
428
429 QVERIFY (f.Proxy_.dropMimeData (&payload, Qt::CopyAction, -1, -1, {}));
430 QVERIFY (!f.Source_.LastDropTag_.has_value ());
431 }
432
433 void FlatToFoldersProxyModelTest::testReleasesUpstreamIndexes ()
434 {
435 QFETCH_GLOBAL (bool, viaFilter);
436 Fixture f { viaFilter };
437 f.FillDefault ();
438 const auto baseline = f.UpstreamPersistentCount ();
439
440 Source other;
441 other.Append (7, "x", { "t1" });
442 {
443 FlatToFoldersProxyModel proxy { &f.Tags_ };
444 QAbstractItemModelTester tester { &proxy, QAbstractItemModelTester::FailureReportingMode::QtTest };
445
446 proxy.SetSourceModel (&f.Upstream_);
447 QCOMPARE (f.UpstreamPersistentCount (), baseline + 3);
448
449 proxy.SetSourceModel (&other);
450 QCOMPARE (f.UpstreamPersistentCount (), baseline);
451
452 proxy.SetSourceModel (&f.Upstream_);
453 QCOMPARE (f.UpstreamPersistentCount (), baseline + 3);
454 }
455 QCOMPARE (f.UpstreamPersistentCount (), baseline);
456 }
457
458 void FlatToFoldersProxyModelTest::testRandomWalk ()
459 {
460 QFETCH_GLOBAL (bool, viaFilter);
461 Fixture f { viaFilter };
462 f.Attach ();
463
464 constexpr quint32 seed = 20261017;
465 QRandomGenerator gen { seed };
466 const QStringList pool { "t1", "t2", "t3", "t4" };
467 const auto randomTags = [&]
468 {
469 QStringList tags;
470 for (const auto& tag : pool)
471 if (!gen.bounded (3))
472 tags << tag;
473 return tags;
474 };
475 const auto randomText = [&] { return QString { QChar { 'a' + static_cast<int> (gen.bounded (6)) } }; };
476
477 int nextId = 1;
478 for (int step = 0; step < 400; ++step)
479 {
480 const auto rows = f.Source_.rowCount ();
481 switch (rows ? gen.bounded (6) : 0)
482 {
483 case 0:
484 f.Source_.Append (nextId++, randomText (), randomTags ());
485 break;
486 case 1:
487 f.Source_.removeRow (gen.bounded (rows));
488 break;
489 case 2:
490 f.Source_.item (gen.bounded (rows))->setData (randomTags (), TagsRole);
491 break;
492 case 3:
493 f.Source_.item (gen.bounded (rows))->setText (randomText ());
494 break;
495 case 4:
496 if (gen.bounded (2))
497 f.Source_.sort (0, gen.bounded (2) ? Qt::AscendingOrder : Qt::DescendingOrder);
498 else
499 f.Filter_.setFilterFixedString (gen.bounded (2) ? QString {} : randomText ());
500 break;
501 case 5:
502 if (!gen.bounded (20))
503 f.Source_.clear ();
504 break;
505 }
506
507 CheckTree (f.Proxy_, f.Upstream_);
508 if (QTest::currentTestFailed ())
509 {
510 qWarning () << "random walk failed at step" << step << "with seed" << seed << "upstream:" << Describe (Expected (f.Upstream_));
511 return;
512 }
513 }
514 }
515}
void Append(Res &result, T &&val) noexcept
Definition prelude.h:64
constexpr detail::ExprTree< detail::ExprType::LeafStaticPlaceholder, detail::MemberPtrs< Ptr > > f
Definition oral.h:1079
Container< T > Filter(const Container< T > &c, F f)
Definition prelude.h:118
constexpr auto Join(auto &&) noexcept