aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-31 11:26:52 +0100
committerDanilo M. <danix@danix.xyz>2026-10-31 11:26:52 +0100
commit467fa84fd851459d5d9c9e5297e2500e938f0a18 (patch)
tree392d0237cac273a6a07db11b120a8a453e8e8ff6 /tests/test_threadlistmodel.cpp
parent9ae9473acdb7cb3310c3028765cc4b30fedfce27 (diff)
downloadqtmaildir-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.cpp49
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"