diff options
| -rw-r--r-- | src/notmuchworker.cpp | 31 | ||||
| -rw-r--r-- | src/notmuchworker.h | 13 | ||||
| -rw-r--r-- | tests/test_notmuchworker.cpp | 61 |
3 files changed, 105 insertions, 0 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index 24f3fd9..94fb79c 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -647,3 +647,34 @@ void NotmuchWorker::requestCounts(const QStringList &queries, quint64 generation emit countsReady(counts, generation); } + +void NotmuchWorker::requestMessageCounts(const QStringList &queries, + quint64 generation) +{ + if (!openReadOnly()) + return; + + QVector<int> counts; + counts.reserve(queries.size()); + + for (const QString &query : queries) { + NmQuery nmQuery(notmuch_query_create(m_db, query.toUtf8().constData())); + + unsigned int count = 0; + // -1 rather than a skipped entry, matching requestCounts: the caller + // pairs these with its own rules positionally, so a dropped answer + // would put a real number against the wrong rule. + if (!nmQuery || + notmuch_query_count_messages(nmQuery.get(), &count) + != NOTMUCH_STATUS_SUCCESS) { + counts.append(-1); + continue; + } + + // Messages, not threads: a rule tags messages, so counting threads + // would understate a rule matching part of a large thread. + counts.append(static_cast<int>(count)); + } + + emit messageCountsReady(counts, generation); +} diff --git a/src/notmuchworker.h b/src/notmuchworker.h index 2dd766f..b3fbed3 100644 --- a/src/notmuchworker.h +++ b/src/notmuchworker.h @@ -130,6 +130,14 @@ public slots: /// refreshing one nobody is looking at is work for nothing. void requestCounts(const QStringList &queries, quint64 generation); + /// Message counts for each query, positionally paired with the input. + /// + /// Beside requestCounts rather than replacing it: that one counts THREADS, + /// which is right for the placeholder pane because a click there produces + /// thread rows. A tagging rule tags messages, so a thread count would + /// understate any rule matching part of a large thread. + void requestMessageCounts(const QStringList &queries, quint64 generation); + /// Database-level facts for the Maildir overview (item 34): total messages, /// total threads, and the number of tags. /// @@ -158,6 +166,11 @@ signals: /// positional correspondence the caller relies on always holds. void countsReady(const QVector<int> &counts, quint64 generation); + /// The same positional contract as countsReady, over messages rather than + /// threads. A dry run over tagging rules pairs these with its own rules by + /// index, so an entry is never dropped. + void messageCountsReady(const QVector<int> &counts, quint64 generation); + /// Fields left at -1 are ones notmuch could not answer, which the dialog /// renders as unknown rather than as zero. void databaseStatsReady(const DatabaseStats &stats, quint64 generation); diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index fe0247c..1db26c7 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -77,6 +77,9 @@ private slots: void requestDatabaseStatsCountsMessagesNotThreads(); void requestDatabaseStatsOnUnreadableConfigEmitsError(); + void messageCountsCountMessagesNotThreads(); + void messageCountsReportAnInvalidQueryAsMinusOne(); + private: /// Tags of one message, read back through a fresh worker query. QStringList tagsOf(const QString &messageId); @@ -797,6 +800,64 @@ void TestNotmuchWorker::requestCountsKeepsPositionOnAnInvalidQuery() QCOMPARE(counts.at(2), 5); } +void TestNotmuchWorker::messageCountsCountMessagesNotThreads() +{ + // The fixture is 6 messages in 5 threads: thread A carries a reply, every + // other thread is a single message. That difference is the whole reason + // this slot exists beside requestCounts, and it is what the numbers below + // assert. A rule that matched one reply of a 30-message thread would be + // reported as 1 by a thread count, understating it by 29. + NotmuchWorker worker(m_fixture.configPath()); + + QSignalSpy messages(&worker, &NotmuchWorker::messageCountsReady); + QSignalSpy threads(&worker, &NotmuchWorker::countsReady); + + worker.requestMessageCounts({ QStringLiteral("*") }, 1); + worker.requestCounts({ QStringLiteral("*") }, 1); + + QCOMPARE(messages.count(), 1); + QCOMPARE(threads.count(), 1); + + const QVector<int> messageCounts = + messages.first().at(0).value<QVector<int>>(); + const QVector<int> threadCounts = + threads.first().at(0).value<QVector<int>>(); + + QCOMPARE(messageCounts, (QVector<int>{ 6 })); + // The guard that makes this test mean something: if requestMessageCounts + // were implemented with count_threads it would return 5 here and match + // the thread count, and the assertion above would be the only thing that + // caught it. + QCOMPARE(threadCounts, (QVector<int>{ 5 })); +} + +void TestNotmuchWorker::messageCountsReportAnInvalidQueryAsMinusOne() +{ + // Paired positionally with the caller's rules, so a dropped answer would + // put a real number against the wrong rule. + // + // **notmuch's query parser rejects almost nothing**, exactly as + // requestCountsKeepsPositionOnAnInvalidQuery records for the thread count: + // `from:((((` parses and matches nothing rather than failing, measured at + // 0 against a throwaway database rather than assumed. So this asserts the + // positional contract, which is the property a dry run depends on, and not + // a -1 that no query string can provoke. The -1 branch remains for a + // notmuch_query_create allocation failure, which a test cannot reach. + NotmuchWorker worker(m_fixture.configPath()); + + QSignalSpy spy(&worker, &NotmuchWorker::messageCountsReady); + worker.requestMessageCounts({ QStringLiteral("from:(((("), + QStringLiteral("*") }, 1); + + QCOMPARE(spy.count(), 1); + const QVector<int> counts = spy.first().at(0).value<QVector<int>>(); + QCOMPARE(counts.size(), 2); + QCOMPARE(counts.at(0), 0); + // The query beside it keeps its own answer at its own position, which is + // what pairs a count with the rule that produced it. + QCOMPARE(counts.at(1), 6); +} + void TestNotmuchWorker::requestDatabaseStatsCountsMessagesNotThreads() { NotmuchWorker worker(m_fixture.configPath()); |
