diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-31 11:26:52 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-31 11:26:52 +0100 |
| commit | 467fa84fd851459d5d9c9e5297e2500e938f0a18 (patch) | |
| tree | 392d0237cac273a6a07db11b120a8a453e8e8ff6 /tests/test_threadlistmodel.cpp | |
| parent | 9ae9473acdb7cb3310c3028765cc4b30fedfce27 (diff) | |
| download | qtmaildir-467fa84fd851459d5d9c9e5297e2500e938f0a18.tar.gz qtmaildir-467fa84fd851459d5d9c9e5297e2500e938f0a18.zip | |
fix: keep bulk tag actions linear, and say when they land
Select all on a 10k-row search, then Archive, froze the window with one
CPU pinned. gdb on the live process showed the UI thread cubic before the
write was ever sent: each optimistic row write emitted dataChanged, whose
handler refreshed the action labels by walking the whole selection, and
each step of that walk looked the message up with a linear scan of every
thread.
ThreadListModel now answers id lookups from a lazily rebuilt hash,
invalidated by its own structural signals and by hand at the two in-place
id changes. MainWindow::batchModelWrites() holds the label refresh while a
loop of optimistic writes runs, then refreshes once. Two list membership
tests became sets.
The announcement also used to fade on a timer whether or not the write
had landed. It now reads "applying" until a round trip through the
worker's queue says the write has run, then "done". An error takes the
label back from a late "done".
Item 211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests/test_threadlistmodel.cpp')
| -rw-r--r-- | tests/test_threadlistmodel.cpp | 49 |
1 files changed, 49 insertions, 0 deletions
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp index 28d3a8b..76ac0e8 100644 --- a/tests/test_threadlistmodel.cpp +++ b/tests/test_threadlistmodel.cpp @@ -109,6 +109,7 @@ private slots: void aConversationListsItsFirstMessageAsAChild(); void aMessageRowDoesNotListItselfBeneathItself(); void aMixedSelectionCarriesBothScopes(); + void idLookupsFollowEveryChangeToTheRows(); }; static ThreadSummary makeThread(const QString &id, const QString &subject) @@ -2355,5 +2356,53 @@ void TestThreadListModel::aMixedSelectionCarriesBothScopes() QCOMPARE(scope.threadIds, QStringList{ QStringLiteral("t2") }); } +void TestThreadListModel::idLookupsFollowEveryChangeToTheRows() +{ + // Item 211. Lookups by id go through a lazily rebuilt hash rather than a + // walk, so every way the rows can change has to invalidate it. Each step + // asks a question whose answer moved, so a stale index answers wrongly. + const QString a = QStringLiteral("a@example.org"); + const QString c = QStringLiteral("c@example.org"); + const QString d = QStringLiteral("d@example.org"); + const QString e = QStringLiteral("e@example.org"); + const QString t1 = QStringLiteral("t1"); + const QString t2 = QStringLiteral("t2"); + + ThreadListModel model; + ThreadSummary one = makeThread(t1, QStringLiteral("one")); + one.firstMessageId = a; + ThreadSummary two = makeThread(t2, QStringLiteral("two")); + two.firstMessageId = c; + model.appendBatch({ one, two }); + QCOMPARE(model.threadIdForMessage(a), t1); // builds the index + + // Expansion adds ids the index has never seen. + MessageNode first = makeNode(c, 0); + first.threadId = t2; + MessageNode reply = makeNode(d, 1); + reply.threadId = t2; + model.setThreadMessages(t2, { first, reply }); + QCOMPARE(model.threadIdForMessage(d), t2); + + // A removal renumbers: t2 moves from row 1 to row 0. + model.applyTagChange(t1, {}, { QStringLiteral("inbox") }); + model.removeThreadsWithoutTag({ t1 }, QStringLiteral("inbox")); + QVERIFY(!model.hasThread(t1)); + QVERIFY(model.threadIdForMessage(a).isEmpty()); + QCOMPARE(model.threadCountFor(t2), 2); + model.applyMessageTagChange(d, { QStringLiteral("x") }, {}); + QVERIFY(model.messageById(d).tags.contains(QStringLiteral("x"))); + + // reconcile() replaces a summary IN PLACE, with no structural signal. + two.firstMessageId = e; + two.tags = QStringList{ QStringLiteral("inbox") }; + model.reconcile({ two }); + QCOMPARE(model.threadIdForMessage(e), t2); + + model.clear(); + QVERIFY(!model.hasThread(t2)); + QVERIFY(model.messageById(d).messageId.isEmpty()); +} + QTEST_MAIN(TestThreadListModel) #include "test_threadlistmodel.moc" |
