aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_notmuchworker.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-28 18:37:47 +0200
committerDanilo M. <danix@danix.xyz>2026-08-28 18:37:47 +0200
commit46acb489691e5298181ce52bfe847889ee06fe68 (patch)
treeb1dc608b415c2d6be5296e3a162fb65c37edf230 /tests/test_notmuchworker.cpp
parent2f902ee6a92456a9951b077796d4ad8828f7f840 (diff)
downloadqtmaildir-46acb489691e5298181ce52bfe847889ee06fe68.tar.gz
qtmaildir-46acb489691e5298181ce52bfe847889ee06fe68.zip
feat: read a thread's digest from the index
Senders, unread messages and an activity histogram for the dashboard, as a plain value struct over a queued signal. Everything comes from the index, so no message file is opened; the unread list is capped and unreadTotal carries the real number.
Diffstat (limited to 'tests/test_notmuchworker.cpp')
-rw-r--r--tests/test_notmuchworker.cpp136
1 files changed, 136 insertions, 0 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index f70a8f5..1766589 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -21,6 +21,7 @@
#include "notmuchfixture.h"
#include "notmuchworker.h"
+#include "threaddigest.h"
#include "types.h"
/// NotmuchWorker against a throwaway database. This is the only code in the
@@ -120,6 +121,10 @@ private slots:
void aQuerySeesMailIndexedAfterTheWorkerOpened();
+ void aDigestCountsSendersAndUnread();
+ void aDigestCapsItsUnreadListButNotItsCount();
+ void aOneMessageThreadGivesASaneSpan();
+
private:
/// Adds one read message in `folder` and reindexes, for the move tests.
/// Each of those takes its own message, because a move is destructive and
@@ -2178,5 +2183,136 @@ void TestNotmuchWorker::aSplitIndexListsTheMaildirsFolders()
"the index's own directory was listed as a mail folder");
}
+void TestNotmuchWorker::aDigestCountsSendersAndUnread()
+{
+ NotmuchFixture fixture;
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("d0@example.org"),
+ QStringLiteral("Digest root"),
+ QStringLiteral("alice@example.org"),
+ QStringLiteral("Mon, 24 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Root."), false));
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("d1@example.org"),
+ QStringLiteral("Re: Digest root"),
+ QStringLiteral("alice@example.org"),
+ QStringLiteral("Tue, 25 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Again."), false,
+ QStringLiteral("d0@example.org")));
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("d2@example.org"),
+ QStringLiteral("Re: Digest root"),
+ QStringLiteral("bob@example.org"),
+ QStringLiteral("Wed, 26 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Unread one."), true,
+ QStringLiteral("d0@example.org")));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy spy(&worker, &NotmuchWorker::threadDigestLoaded);
+
+ const QString threadId = worker.threadIdForTesting(
+ QStringLiteral("id:d0@example.org"));
+ QVERIFY(!threadId.isEmpty());
+ worker.loadThreadDigest(threadId, 1);
+
+ QCOMPARE(spy.count(), 1);
+ const ThreadDigest digest = spy.at(0).at(0).value<ThreadDigest>();
+
+ QCOMPARE(digest.totalCount, 3);
+ QCOMPARE(digest.unreadTotal, 1);
+ QCOMPARE(digest.unread.size(), 1);
+ QCOMPARE(digest.unread.at(0).messageId, QStringLiteral("d2@example.org"));
+
+ // Alice twice, Bob once, most prolific first.
+ QCOMPARE(digest.senders.size(), 2);
+ QCOMPARE(digest.senders.at(0).second, 2);
+ QCOMPARE(digest.senders.at(1).second, 1);
+
+ QCOMPARE(digest.buckets.size(), ThreadDigest::kBuckets);
+ int summed = 0;
+ for (int n : digest.buckets)
+ summed += n;
+ QCOMPARE(summed, 3);
+
+ // The dashboard reconstructs the busiest bucket's date from the span, so
+ // an index outside the histogram would name a date the thread never saw.
+ QVERIFY(digest.busiestBucket >= 0);
+ QVERIFY(digest.busiestBucket < ThreadDigest::kBuckets);
+ QVERIFY(digest.firstTimestamp <= digest.lastTimestamp);
+}
+
+void TestNotmuchWorker::aDigestCapsItsUnreadListButNotItsCount()
+{
+ NotmuchFixture fixture;
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("c0@example.org"),
+ QStringLiteral("Cap root"),
+ QStringLiteral("alice@example.org"),
+ QStringLiteral("Mon, 24 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Root."), false));
+ for (int i = 1; i <= 8; ++i) {
+ QVERIFY(fixture.addMessage(
+ QStringLiteral("inbox"),
+ QStringLiteral("c%1@example.org").arg(i),
+ QStringLiteral("Re: Cap root"),
+ QStringLiteral("bob@example.org"),
+ QStringLiteral("Tue, 25 Aug 2026 %1:00:00 +0200")
+ .arg(i, 2, 10, QLatin1Char('0')),
+ QStringLiteral("Reply."), true, QStringLiteral("c0@example.org")));
+ }
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy spy(&worker, &NotmuchWorker::threadDigestLoaded);
+ worker.loadThreadDigest(
+ worker.threadIdForTesting(QStringLiteral("id:c0@example.org")), 1);
+
+ QCOMPARE(spy.count(), 1);
+ const ThreadDigest digest = spy.at(0).at(0).value<ThreadDigest>();
+ QCOMPARE(digest.unreadTotal, 8);
+ QCOMPARE(digest.unread.size(), ThreadDigest::kUnreadShown);
+ // Newest first: c8 is the latest.
+ QCOMPARE(digest.unread.at(0).messageId, QStringLiteral("c8@example.org"));
+}
+
+void TestNotmuchWorker::aOneMessageThreadGivesASaneSpan()
+{
+ // The degenerate case the dashboard would otherwise divide by: one message
+ // is a zero-width span, and a bucket width of zero either divides by zero
+ // or throws every message into the last bucket.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("s0@example.org"),
+ QStringLiteral("Alone"),
+ QStringLiteral("alice@example.org"),
+ QStringLiteral("Mon, 24 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Only."), false));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy spy(&worker, &NotmuchWorker::threadDigestLoaded);
+ worker.loadThreadDigest(
+ worker.threadIdForTesting(QStringLiteral("id:s0@example.org")), 1);
+
+ QCOMPARE(spy.count(), 1);
+ const ThreadDigest digest = spy.at(0).at(0).value<ThreadDigest>();
+ QCOMPARE(digest.totalCount, 1);
+ QCOMPARE(digest.buckets.size(), ThreadDigest::kBuckets);
+ QCOMPARE(digest.buckets.at(0), 1);
+ QCOMPARE(digest.busiestBucket, 0);
+ QCOMPARE(digest.firstTimestamp, digest.lastTimestamp);
+
+ // An unknown thread yields an EMPTY digest rather than nothing at all, so a
+ // caller that arms state on the request always gets its reply.
+ QSignalSpy missing(&worker, &NotmuchWorker::threadDigestLoaded);
+ worker.loadThreadDigest(QStringLiteral("0000000000000000"), 2);
+ QCOMPARE(missing.count(), 1);
+ const ThreadDigest empty = missing.at(0).at(0).value<ThreadDigest>();
+ QCOMPARE(empty.totalCount, 0);
+ QCOMPARE(empty.buckets.size(), ThreadDigest::kBuckets);
+ QCOMPARE(empty.busiestBucket, -1);
+}
+
QTEST_MAIN(TestNotmuchWorker)
#include "test_notmuchworker.moc"