13#include <QAbstractItemModelTester>
16#include <QRandomGenerator>
18#include <QSortFilterProxyModel>
19#include <QStandardItemModel>
29 constexpr int IdRole = Qt::UserRole + 1;
30 constexpr int KeysRole = Qt::UserRole + 2;
31 const auto DropFormat = QStringLiteral (
"x-test/folder");
32 const auto Unkeyed = QStringLiteral (
"unkeyed");
34 struct Source : QStandardItemModel
36 using QStandardItemModel::persistentIndexList;
38 mutable QList<int> LastDraggedIds_;
39 std::optional<QByteArray> LastDropKey_;
41 void Append (
int id,
const QString& text,
const QStringList& keys)
43 const auto first =
new QStandardItem { text };
44 first->setData (
id, IdRole);
45 first->setData (keys, KeysRole);
46 appendRow ({ first,
new QStandardItem { QString::number (
id * 10) } });
49 void SetKeys (
int row,
const QStringList& keys)
51 item (row)->setData (keys, KeysRole);
54 QMimeData* mimeData (
const QModelIndexList& indexes)
const override
56 LastDraggedIds_.clear ();
57 for (
const auto& index : indexes)
58 LastDraggedIds_ << index.data (IdRole).toInt ();
59 std::ranges::sort (LastDraggedIds_);
60 return QStandardItemModel::mimeData (indexes);
63 bool dropMimeData (
const QMimeData *data, Qt::DropAction,
int,
int,
const QModelIndex&)
override
65 LastDropKey_ = data->hasFormat (DropFormat) ?
66 std::optional { data->data (DropFormat) } :
72 struct Filter : QSortFilterProxyModel
74 using QSortFilterProxyModel::persistentIndexList;
81 QHash<QString, QStringList> Aliases_;
84 QAbstractItemModel& Upstream_;
85 FlatToFoldersProxyModel Proxy_ { MakeConfig () };
86 QAbstractItemModelTester Tester_ { &Proxy_, QAbstractItemModelTester::FailureReportingMode::QtTest };
88 explicit Fixture (
bool viaFilter)
89 : Upstream_ { viaFilter ? static_cast<QAbstractItemModel&> (Filter_) : Source_ }
91 Filter_.setSourceModel (&Source_);
94 FlatToFoldersProxyModel::Config MakeConfig ()
const
98 .KeysOf_ = [
this] (
const QModelIndex& index) {
return KeysOf (index); },
99 .UnkeyedLabel_ = Unkeyed,
100 .DropFormat_ = DropFormat,
104 QStringList KeysOf (
const QModelIndex& index)
const
107 for (
const auto& stored : index.data (KeysRole).toStringList ())
108 keys += Aliases_.value (stored, { stored });
114 Source_.Append (1,
"a", {
"t1" });
115 Source_.Append (2,
"b", {
"t1",
"t2" });
116 Source_.Append (3,
"c", {});
121 Proxy_.SetSourceModel (&Upstream_);
124 void ReplaceKey (
const QString& old,
const QStringList& newKeys)
126 for (
int row = 0; row < Source_.rowCount (); ++row)
127 for (
const auto& stored : Source_.index (row, 0).data (KeysRole).toStringList ())
129 auto keys = Aliases_.value (stored, { stored });
130 if (!keys.contains (old))
133 keys.removeAll (old);
134 for (
const auto& key : newKeys)
135 if (!keys.contains (key))
137 Aliases_ [stored] = keys;
139 Proxy_.ReplaceKey (old, newKeys);
142 int UpstreamPersistentCount ()
const
144 return (&Upstream_ == &Filter_ ? Filter_.persistentIndexList () : Source_.persistentIndexList ()).size ();
150 QSignalSpy Inserted_;
156 explicit Spies (QAbstractItemModel& model)
157 : Inserted_ { &model, &QAbstractItemModel::rowsInserted }
158 , Removed_ { &model, &QAbstractItemModel::rowsRemoved }
159 , Reset_ { &model, &QAbstractItemModel::modelReset }
160 , Layout_ { &model, &QAbstractItemModel::layoutChanged }
161 , Data_ { &model, &QAbstractItemModel::dataChanged }
165 int Structural ()
const
167 return Inserted_.size () + Removed_.size () + Reset_.size () + Layout_.size ();
171 QStringList ExpectedKeysOf (
const Fixture& fixture,
const QModelIndex& index)
173 auto keys = fixture.KeysOf (index);
174 keys.removeDuplicates ();
180 QString FolderName (
const QModelIndex& folder)
182 return folder.data ().toString ();
185 using Picture = QMap<QString, QSet<int>>;
187 Picture Expected (
const Fixture& fixture)
190 for (
int row = 0; row < fixture.Upstream_.rowCount (); ++row)
192 const auto& index = fixture.Upstream_.index (row, 0);
193 for (
const auto& key : ExpectedKeysOf (fixture, index))
194 result [key] << index.data (IdRole).toInt ();
199 QString Describe (
const Picture& picture)
202 for (
const auto& [key, ids] : picture.asKeyValueRange ())
204 auto sorted = ids.values ();
205 std::ranges::sort (sorted);
206 QStringList idStrings;
207 for (
const auto id : sorted)
208 idStrings << QString::number (
id);
209 folders << key +
" {" + idStrings.join (
' ') +
'}';
211 return folders.join (
"; ");
217 for (
int f = 0;
f < proxy.rowCount (); ++
f)
219 const auto& folder = proxy.index (f, 0);
221 for (
int l = 0; l < proxy.rowCount (folder); ++l)
222 ids << QString::number (proxy.index (l, 0, folder).data (IdRole).toInt ());
223 result << FolderName (folder) +
": " + ids.join (
' ');
228 void CheckTree (
const Fixture& fixture)
230 const auto& proxy = fixture.Proxy_;
231 const auto& upstream = fixture.Upstream_;
232 QCOMPARE (proxy.columnCount (), upstream.columnCount ());
236 for (
int f = 0;
f < proxy.rowCount (); ++
f)
238 const auto& folderIdx = proxy.index (f, 0);
239 QVERIFY (proxy.IsFolder (folderIdx));
240 QCOMPARE (proxy.parent (folderIdx), QModelIndex {});
241 if (upstream.columnCount () > 1)
242 QCOMPARE (proxy.rowCount (folderIdx.siblingAtColumn (1)), 0);
244 const auto& key = FolderName (folderIdx);
245 QVERIFY2 (!actual.contains (key), qPrintable (
"duplicate folder for key " + key));
246 QVERIFY2 (proxy.rowCount (folderIdx) > 0, qPrintable (
"empty folder for key " + key));
248 auto& ids = actual [key];
249 for (
int l = 0; l < proxy.rowCount (folderIdx); ++l)
252 const auto& leafIdx = proxy.index (l, 0, folderIdx);
253 QVERIFY (!proxy.IsFolder (leafIdx));
254 QCOMPARE (proxy.rowCount (leafIdx), 0);
255 QCOMPARE (proxy.parent (leafIdx), folderIdx);
257 const auto& source = proxy.MapToSource (leafIdx);
258 QVERIFY (source.isValid ());
259 QCOMPARE (source.model (), &upstream);
260 QVERIFY (ExpectedKeysOf (fixture, source).contains (key));
261 QVERIFY (proxy.MapFromSource (source).contains (leafIdx));
262 for (
int col = 0; col < upstream.columnCount (); ++col)
263 QCOMPARE (proxy.index (l, col, folderIdx).data (), source.siblingAtColumn (col).data ());
265 ids << leafIdx.data (IdRole).toInt ();
269 const auto& expected = Expected (fixture);
270 QVERIFY2 (actual == expected,
271 qPrintable (
"tree is " + Describe (actual) +
", source says " + Describe (expected)));
273 int expectedLeaves = 0;
274 for (
int row = 0; row < upstream.rowCount (); ++row)
275 expectedLeaves += ExpectedKeysOf (fixture, upstream.index (row, 0)).size ();
276 QCOMPARE (leaves, expectedLeaves);
279 QModelIndex Parent (
const QList<QVariant>& signalArgs)
281 return signalArgs.at (0).value<QModelIndex> ();
285 void FlatToFoldersProxyModelTest::initTestCase ()
288 qunsetenv (
"LC_MODEL_SANITIZER_MODE");
291 void FlatToFoldersProxyModelTest::initTestCase_data ()
293 QTest::addColumn<bool> (
"viaFilter");
294 QTest::newRow (
"direct source") <<
false;
295 QTest::newRow (
"through a QSortFilterProxyModel") <<
true;
298 void FlatToFoldersProxyModelTest::testInitialTree ()
300 QFETCH_GLOBAL (
bool, viaFilter);
301 Fixture
f { viaFilter };
306 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t1: 1 2",
"t2: 2",
"unkeyed: 3" }));
307 QCOMPARE (
f.Proxy_.headerData (0, Qt::Horizontal, Qt::DisplayRole),
f.Upstream_.headerData (0, Qt::Horizontal, Qt::DisplayRole));
308 QCOMPARE (
f.Proxy_.flags ({}), Qt::ItemIsDropEnabled);
311 void FlatToFoldersProxyModelTest::testFoldersFollowKeys ()
313 QFETCH_GLOBAL (
bool, viaFilter);
314 Fixture
f { viaFilter };
315 f.Source_.Append (1,
"a", {
"t1" });
316 f.Source_.Append (2,
"b", {
"t1" });
317 f.Source_.Append (3,
"c", {
"x",
"y" });
318 f.Source_.Append (4,
"d", {});
319 f.Source_.Append (5,
"e", {
"t1",
"t1" });
322 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t1: 1 2 5",
"x: 3",
"y: 3",
"unkeyed: 4" }));
323 Spies spies {
f.Proxy_ };
325 f.Source_.SetKeys (3, {
"t1" });
326 QCOMPARE (spies.Inserted_.size (), 1);
327 QCOMPARE (spies.Removed_.size (), 2);
329 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t1: 1 2 5 4",
"x: 3",
"y: 3" }));
332 void FlatToFoldersProxyModelTest::testInsertCreatesFoldersOnDemand ()
334 QFETCH_GLOBAL (
bool, viaFilter);
335 Fixture
f { viaFilter };
338 Spies spies {
f.Proxy_ };
340 f.Source_.Append (4,
"d", {
"t3" });
341 QCOMPARE (spies.Inserted_.size (), 2);
342 QCOMPARE (Parent (spies.Inserted_.at (0)), QModelIndex {});
343 QVERIFY (Parent (spies.Inserted_.at (1)).isValid ());
345 f.Source_.Append (5,
"e", {
"t1" });
346 QCOMPARE (spies.Inserted_.size (), 3);
347 QCOMPARE (spies.Structural (), 3);
351 void FlatToFoldersProxyModelTest::testRekeyKeepsTheSurvivingLeaf ()
353 QFETCH_GLOBAL (
bool, viaFilter);
354 Fixture
f { viaFilter };
357 Spies spies {
f.Proxy_ };
359 f.Source_.SetKeys (1, {
"t2",
"t3" });
361 QCOMPARE (spies.Data_.size (), 1);
362 const auto& args = spies.Data_.first ();
363 const auto& topLeft = args.at (0).value<QModelIndex> ();
364 QCOMPARE (topLeft.data (IdRole).toInt (), 2);
365 QCOMPARE (FolderName (topLeft.parent ()), QString {
"t2" });
366 QCOMPARE (args.at (1).value<QModelIndex> ().column (),
f.Upstream_.columnCount () - 1);
367 QCOMPARE (args.at (2).value<QList<int>> (), (QList<int> { KeysRole }));
369 QCOMPARE (spies.Removed_.size (), 1);
370 QCOMPARE (spies.Inserted_.size (), 2);
374 void FlatToFoldersProxyModelTest::testRemovingTheLastLeafRemovesTheFolder ()
376 QFETCH_GLOBAL (
bool, viaFilter);
377 Fixture
f { viaFilter };
380 Spies spies {
f.Proxy_ };
382 f.Source_.removeRow (2);
384 QCOMPARE (spies.Removed_.size (), 2);
385 QVERIFY (Parent (spies.Removed_.at (0)).isValid ());
386 QCOMPARE (Parent (spies.Removed_.at (1)), QModelIndex {});
387 QCOMPARE (
f.Proxy_.rowCount (), 2);
391 void FlatToFoldersProxyModelTest::testRemovalFollowsOwnBookkeeping ()
393 QFETCH_GLOBAL (
bool, viaFilter);
394 Fixture
f { viaFilter };
398 f.Source_.blockSignals (
true);
399 f.Source_.SetKeys (2, {
"zz" });
400 f.Source_.blockSignals (
false);
401 f.Source_.removeRow (2);
403 QCOMPARE (
f.Proxy_.rowCount (), 2);
407 void FlatToFoldersProxyModelTest::testSourceSortLeavesTheTreeAlone ()
409 QFETCH_GLOBAL (
bool, viaFilter);
410 Fixture
f { viaFilter };
413 const auto& before = Dump (
f.Proxy_);
414 Spies spies {
f.Proxy_ };
416 f.Source_.sort (0, Qt::DescendingOrder);
418 QCOMPARE (spies.Layout_.size (), 0);
419 QCOMPARE (spies.Structural (), 0);
420 QCOMPARE (Dump (
f.Proxy_), before);
424 void FlatToFoldersProxyModelTest::testSourceResetIsOneReset ()
426 QFETCH_GLOBAL (
bool, viaFilter);
427 Fixture
f { viaFilter };
430 Spies spies {
f.Proxy_ };
433 QCOMPARE (spies.Reset_.size (), 1);
434 QCOMPARE (spies.Structural (), 1);
435 QCOMPARE (
f.Proxy_.rowCount (), 0);
437 f.Source_.Append (4,
"d", {
"t1",
"t2" });
438 QCOMPARE (spies.Inserted_.size (), 4);
442 void FlatToFoldersProxyModelTest::testAttachAndDetach ()
444 QFETCH_GLOBAL (
bool, viaFilter);
445 Fixture
f { viaFilter };
447 Spies spies {
f.Proxy_ };
450 QCOMPARE (spies.Reset_.size (), 1);
451 QCOMPARE (spies.Structural (), 1);
452 QCOMPARE (
f.Proxy_.rowCount (), 3);
455 f.Proxy_.SetSourceModel (
nullptr);
456 QCOMPARE (spies.Reset_.size (), 2);
457 QCOMPARE (spies.Structural (), 2);
458 QCOMPARE (
f.Proxy_.rowCount (), 0);
459 QCOMPARE (
f.Proxy_.columnCount (), 0);
460 QCOMPARE (
f.Proxy_.GetSourceModel (),
nullptr);
462 f.Source_.Append (4,
"d", {
"t9" });
463 QCOMPARE (spies.Structural (), 2);
466 void FlatToFoldersProxyModelTest::testDragAndDropMapThroughFolders ()
468 QFETCH_GLOBAL (
bool, viaFilter);
469 Fixture
f { viaFilter };
472 const auto& t1 =
f.Proxy_.index (0, 0);
474 std::unique_ptr<QMimeData> folderDrag {
f.Proxy_.mimeData ({ t1 }) };
475 QVERIFY (folderDrag);
476 QCOMPARE (
f.Source_.LastDraggedIds_, (QList<int> { 1, 2 }));
478 std::unique_ptr<QMimeData> leafDrag {
f.Proxy_.mimeData ({
f.Proxy_.index (1, 0, t1) }) };
480 QCOMPARE (
f.Source_.LastDraggedIds_, (QList<int> { 2 }));
483 payload.setText (
"payload");
484 QVERIFY (
f.Proxy_.dropMimeData (&payload, Qt::CopyAction, -1, -1, t1));
485 QVERIFY (
f.Source_.LastDropKey_.has_value ());
486 QCOMPARE (*
f.Source_.LastDropKey_, QByteArray {
"t1" });
488 QVERIFY (
f.Proxy_.dropMimeData (&payload, Qt::CopyAction, -1, -1,
f.Proxy_.index (2, 0)));
489 QVERIFY (
f.Source_.LastDropKey_.has_value ());
490 QVERIFY (
f.Source_.LastDropKey_->isEmpty ());
492 QVERIFY (
f.Proxy_.dropMimeData (&payload, Qt::CopyAction, -1, -1, {}));
493 QVERIFY (!
f.Source_.LastDropKey_.has_value ());
496 void FlatToFoldersProxyModelTest::testReleasesUpstreamIndexes ()
498 QFETCH_GLOBAL (
bool, viaFilter);
499 Fixture
f { viaFilter };
501 const auto baseline =
f.UpstreamPersistentCount ();
504 other.Append (7,
"x", {
"t1" });
506 FlatToFoldersProxyModel proxy {
f.MakeConfig () };
507 QAbstractItemModelTester tester { &proxy, QAbstractItemModelTester::FailureReportingMode::QtTest };
509 proxy.SetSourceModel (&
f.Upstream_);
510 QCOMPARE (
f.UpstreamPersistentCount (), baseline + 3);
512 proxy.SetSourceModel (&other);
513 QCOMPARE (
f.UpstreamPersistentCount (), baseline);
515 proxy.SetSourceModel (&
f.Upstream_);
516 QCOMPARE (
f.UpstreamPersistentCount (), baseline + 3);
518 QCOMPARE (
f.UpstreamPersistentCount (), baseline);
521 void FlatToFoldersProxyModelTest::testRenameKeepsTheFolder ()
523 QFETCH_GLOBAL (
bool, viaFilter);
524 Fixture
f { viaFilter };
527 Spies spies {
f.Proxy_ };
529 f.ReplaceKey (
"t1", {
"renamed" });
531 QCOMPARE (spies.Structural (), 0);
532 QCOMPARE (spies.Data_.size (), 1);
533 const auto& args = spies.Data_.first ();
534 QCOMPARE (args.at (0).value<QModelIndex> (),
f.Proxy_.index (0, 0));
535 QCOMPARE (args.at (1).value<QModelIndex> (),
f.Proxy_.index (0, 0));
536 QCOMPARE (args.at (2).value<QList<int>> (), (QList<int> { Qt::DisplayRole }));
537 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"renamed: 1 2",
"t2: 2",
"unkeyed: 3" }));
541 void FlatToFoldersProxyModelTest::testRenameOntoAnExistingKeyMerges ()
543 QFETCH_GLOBAL (
bool, viaFilter);
544 Fixture
f { viaFilter };
547 Spies spies {
f.Proxy_ };
549 f.ReplaceKey (
"t1", {
"t2" });
551 QCOMPARE (spies.Inserted_.size (), 1);
552 QCOMPARE (spies.Removed_.size (), 3);
553 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t2: 2 1",
"unkeyed: 3" }));
557 void FlatToFoldersProxyModelTest::testSplitSpreadsTheRows ()
559 QFETCH_GLOBAL (
bool, viaFilter);
560 Fixture
f { viaFilter };
564 f.ReplaceKey (
"t1", {
"x",
"y" });
566 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t2: 2",
"unkeyed: 3",
"x: 1 2",
"y: 1 2" }));
570 void FlatToFoldersProxyModelTest::testRemovedKeyMovesRowsToUnkeyed ()
572 QFETCH_GLOBAL (
bool, viaFilter);
573 Fixture
f { viaFilter };
577 f.ReplaceKey (
"t1", {});
579 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t2: 2",
"unkeyed: 3 1" }));
583 void FlatToFoldersProxyModelTest::testKeepingTheOldKeyAddsFolders ()
585 QFETCH_GLOBAL (
bool, viaFilter);
586 Fixture
f { viaFilter };
590 f.ReplaceKey (
"t2", {
"t2",
"t3" });
592 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t1: 1 2",
"t2: 2",
"unkeyed: 3",
"t3: 2" }));
596 void FlatToFoldersProxyModelTest::testUnknownKeyChangesNothing ()
598 QFETCH_GLOBAL (
bool, viaFilter);
599 Fixture
f { viaFilter };
602 Spies spies {
f.Proxy_ };
604 f.ReplaceKey (
"nope", {
"t1" });
606 QCOMPARE (spies.Structural (), 0);
607 QCOMPARE (spies.Data_.size (), 0);
608 QCOMPARE (Dump (
f.Proxy_), (QStringList {
"t1: 1 2",
"t2: 2",
"unkeyed: 3" }));
611 void FlatToFoldersProxyModelTest::testRandomWalk ()
613 QFETCH_GLOBAL (
bool, viaFilter);
614 Fixture
f { viaFilter };
617 constexpr quint32 seed = 20261017;
618 QRandomGenerator gen { seed };
619 const QStringList pool {
"t1",
"t2",
"t3",
"t4" };
620 QStringList known {
"t1",
"t2",
"t3",
"t4" };
621 const auto randomKeys = [&]
624 for (
const auto& key : pool)
625 if (!gen.bounded (3))
627 if (!keys.isEmpty () && !gen.bounded (4))
628 keys << keys.first ();
631 const auto randomText = [&] {
return QString { QChar {
'a' +
static_cast<int> (gen.bounded (6)) } }; };
634 for (
int step = 0; step < 400; ++step)
636 const auto rows =
f.Source_.rowCount ();
637 switch (rows ? gen.bounded (7) : 0)
640 f.Source_.Append (nextId++, randomText (), randomKeys ());
643 f.Source_.removeRow (gen.bounded (rows));
646 f.Source_.SetKeys (gen.bounded (rows), randomKeys ());
649 f.Source_.item (gen.bounded (rows))->setText (randomText ());
653 f.Source_.sort (0, gen.bounded (2) ? Qt::AscendingOrder : Qt::DescendingOrder);
655 f.Filter_.setFilterFixedString (gen.bounded (2) ? QString {} : randomText ());
658 if (!gen.bounded (20))
663 const auto old = known [gen.bounded (
static_cast<int> (known.size ()))];
665 for (
const auto& key : known)
666 if (!gen.bounded (3))
669 newKeys << known.emplaceBack (
"r" + QString::number (step));
670 f.ReplaceKey (old, newKeys);
676 if (QTest::currentTestFailed ())
678 qWarning () <<
"random walk failed at step" << step <<
"with seed" << seed <<
"upstream:" << Describe (Expected (f));
void Append(Res &result, T &&val) noexcept
constexpr detail::ExprTree< detail::ExprType::LeafStaticPlaceholder, detail::MemberPtrs< Ptr > > f
Container< T > Filter(const Container< T > &c, F f)