aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_notmuchworker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_notmuchworker.cpp')
-rw-r--r--tests/test_notmuchworker.cpp156
1 files changed, 137 insertions, 19 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index 9b64f8e..c4da146 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -59,7 +59,10 @@ private slots:
void aThreadCarriesItsCardMessagesOwnTags();
void applyTagsToThreadsTagsEveryMessage();
- void applyTagsToQueryWritesOnlyWhatWouldChange();
+ void applyRuleWritesEachTagOnlyWhereItMoves();
+ void applyRuleResolvesItsQueryOnceForEveryTag();
+ void applyRuleThatChangesNothingStillAnswers();
+ void applyRuleAnswersAcrossAQueuedConnection();
void applyTagsToThreadsSpansMultipleThreads();
void applyTagsToThreadsWithNoThreadsDoesNothing();
@@ -1291,36 +1294,151 @@ void TestNotmuchWorker::applyTagsToThreadsTagsEveryMessage()
restore.applyTags(emitted.inverted());
}
-void TestNotmuchWorker::applyTagsToQueryWritesOnlyWhatWouldChange()
+void TestNotmuchWorker::applyRuleWritesEachTagOnlyWhereItMoves()
{
- // Item 212. Two messages in OPPOSITE states (item 87's lesson): a1
- // already carries the tag, a2 does not. Only a2 may be reported, or the
- // undo would strip the tag from a message that had it before.
+ // Item 212. Two messages in OPPOSITE states (item 87's lesson): a1 already
+ // carries the added tag, a2 does not, and both carry the removed one. The
+ // effective set is per message, so a single `+add -drop` write would
+ // report a1 for both tags and its undo would strip `add` from a1. One
+ // operation per tag reports a1 only where it moved.
const QString threadId = threadIdOf(QStringLiteral("Release notes"));
QVERIFY(!threadId.isEmpty());
+ const QString a1 = QStringLiteral("a1@example.org");
+ const QString a2 = QStringLiteral("a2@example.org");
NotmuchWorker setup(m_fixture.configPath());
- setup.applyTags(TagChange{ { QStringLiteral("a1@example.org") },
- { QStringLiteral("backfill") }, {},
+ setup.applyTags(TagChange{ { a1 }, { QStringLiteral("ruleadd") }, {},
+ QStringLiteral("seed") });
+ setup.applyTags(TagChange{ { a1, a2 }, { QStringLiteral("ruledrop") }, {},
QStringLiteral("seed") });
NotmuchWorker worker(m_fixture.configPath());
QSignalSpy applied(&worker, &NotmuchWorker::tagsApplied);
- worker.applyTagsToQuery(QStringLiteral("thread:%1").arg(threadId),
- { QStringLiteral("backfill") }, {},
- QStringLiteral("Apply rule"));
+ QSignalSpy ruled(&worker, &NotmuchWorker::ruleApplied);
+ worker.applyRule(QStringLiteral("thread:%1").arg(threadId),
+ { QStringLiteral("ruleadd") },
+ { QStringLiteral("ruledrop") },
+ QStringLiteral("Apply rule"), 7);
+
+ QCOMPARE(applied.size(), 0);
+ QCOMPARE(ruled.size(), 1);
+ QCOMPARE(ruled.first().at(0).value<quint64>(), quint64(7));
+ const auto changes = ruled.first().at(1).value<QVector<TagChange>>();
+ QCOMPARE(changes.size(), 2);
+ QCOMPARE(changes.at(0).added, QStringList{ QStringLiteral("ruleadd") });
+ QVERIFY(changes.at(0).removed.isEmpty());
+ QCOMPARE(changes.at(0).messageIds, QStringList{ a2 });
+ QVERIFY(changes.at(1).added.isEmpty());
+ QCOMPARE(changes.at(1).removed, QStringList{ QStringLiteral("ruledrop") });
+ QCOMPARE(QSet<QString>(changes.at(1).messageIds.cbegin(),
+ changes.at(1).messageIds.cend()),
+ (QSet<QString>{ a1, a2 }));
+ QCOMPARE(changes.at(0).description, QStringLiteral("Apply rule"));
+ QVERIFY(tagsOf(a2).contains(QStringLiteral("ruleadd")));
+ QVERIFY(!tagsOf(a1).contains(QStringLiteral("ruledrop")));
- QCOMPARE(applied.size(), 1);
- const TagChange emitted = applied.first().at(0).value<TagChange>();
- QCOMPARE(emitted.messageIds, QStringList{ QStringLiteral("a2@example.org") });
- QCOMPARE(emitted.description, QStringLiteral("Apply rule"));
- QVERIFY(tagsOf(QStringLiteral("a2@example.org"))
- .contains(QStringLiteral("backfill")));
+ NotmuchWorker restore(m_fixture.configPath());
+ restore.applyTags(TagChange{ { a1, a2 }, {}, { QStringLiteral("ruleadd") },
+ QStringLiteral("restore") });
+}
+
+void TestNotmuchWorker::applyRuleResolvesItsQueryOnceForEveryTag()
+{
+ // A rule whose query names a tag it removes. Re-resolving the query per
+ // operation would let `-src` empty the match before `-other` ran, writing
+ // half the rule. The hook runs a rule once over one query, so the ids are
+ // resolved once.
+ const QString threadId = threadIdOf(QStringLiteral("Release notes"));
+ QVERIFY(!threadId.isEmpty());
+ const QString a1 = QStringLiteral("a1@example.org");
+ const QString a2 = QStringLiteral("a2@example.org");
+
+ NotmuchWorker setup(m_fixture.configPath());
+ setup.applyTags(TagChange{ { a1, a2 },
+ { QStringLiteral("backfill-src"),
+ QStringLiteral("backfill-other") }, {},
+ QStringLiteral("seed") });
+
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy ruled(&worker, &NotmuchWorker::ruleApplied);
+ worker.applyRule(
+ QStringLiteral("thread:%1 and tag:backfill-src").arg(threadId), {},
+ { QStringLiteral("backfill-src"), QStringLiteral("backfill-other") },
+ QStringLiteral("Apply rule"), 1);
+
+ QCOMPARE(ruled.size(), 1);
+ const auto changes = ruled.first().at(1).value<QVector<TagChange>>();
+ QCOMPARE(changes.size(), 2);
+ QCOMPARE(changes.at(0).messageIds.size(), 2);
+ QCOMPARE(changes.at(1).messageIds.size(), 2);
+ for (const QString &id : { a1, a2 }) {
+ QVERIFY(!tagsOf(id).contains(QStringLiteral("backfill-src")));
+ QVERIFY2(!tagsOf(id).contains(QStringLiteral("backfill-other")),
+ qPrintable(id));
+ }
+}
+
+void TestNotmuchWorker::applyRuleThatChangesNothingStillAnswers()
+{
+ // Unlike applyTags(), which stays silent on an empty write, a rule always
+ // answers: the window is waiting on its token.
+ const QString threadId = threadIdOf(QStringLiteral("Release notes"));
+ QVERIFY(!threadId.isEmpty());
+
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy applied(&worker, &NotmuchWorker::tagsApplied);
+ QSignalSpy ruled(&worker, &NotmuchWorker::ruleApplied);
+ worker.applyRule(QStringLiteral("thread:%1").arg(threadId), {},
+ { QStringLiteral("nobody-has-this") },
+ QStringLiteral("Apply rule"), 3);
+
+ QCOMPARE(applied.size(), 0);
+ QCOMPARE(ruled.size(), 1);
+ QCOMPARE(ruled.first().at(0).value<quint64>(), quint64(3));
+ const auto changes = ruled.first().at(1).value<QVector<TagChange>>();
+ for (const TagChange &change : changes)
+ QVERIFY(change.messageIds.isEmpty());
+}
+
+void TestNotmuchWorker::applyRuleAnswersAcrossAQueuedConnection()
+{
+ // QVector<TagChange> crosses from the worker thread to the UI, and an
+ // unregistered container is dropped there with only a warning. Received
+ // by a lambda living on THIS thread, so the delivery is genuinely queued.
+ const QString threadId = threadIdOf(QStringLiteral("Release notes"));
+ QVERIFY(!threadId.isEmpty());
+
+ NotmuchWorker worker(m_fixture.configPath());
+ QThread thread;
+ worker.moveToThread(&thread);
+ thread.start();
+
+ QObject receiver;
+ int answers = 0;
+ QVector<TagChange> received;
+ connect(&worker, &NotmuchWorker::ruleApplied, &receiver,
+ [&](quint64, const QVector<TagChange> &changes) {
+ ++answers;
+ received = changes;
+ });
+ QVERIFY(QMetaObject::invokeMethod(
+ &worker, "applyRule", Qt::QueuedConnection,
+ Q_ARG(QString, QStringLiteral("thread:%1").arg(threadId)),
+ Q_ARG(QStringList, QStringList{ QStringLiteral("crossed") }),
+ Q_ARG(QStringList, QStringList{}),
+ Q_ARG(QString, QStringLiteral("Apply rule")),
+ Q_ARG(quint64, 9)));
+
+ QTRY_COMPARE_WITH_TIMEOUT(answers, 1, 5000);
+ QCOMPARE(received.size(), 1);
+ QCOMPARE(received.at(0).messageIds.size(), 2);
+
+ thread.quit();
+ QVERIFY(thread.wait(5000));
NotmuchWorker restore(m_fixture.configPath());
- restore.applyTags(TagChange{ { QStringLiteral("a1@example.org"),
- QStringLiteral("a2@example.org") },
- {}, { QStringLiteral("backfill") },
+ restore.applyTags(TagChange{ received.at(0).messageIds, {},
+ { QStringLiteral("crossed") },
QStringLiteral("restore") });
}