diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_mainwindow.cpp | 70 | ||||
| -rw-r--r-- | tests/test_notmuchworker.cpp | 75 | ||||
| -rw-r--r-- | tests/test_tagdialog.cpp | 4 |
3 files changed, 108 insertions, 41 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 9a38a95..a92b3d1 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -11981,7 +11981,7 @@ void TestMainWindow::deleteRecordsWhereTheMessageCameFrom() // would report the tag whether or not the write ever landed. // Re-queried by id and asserted on the TAG LIST the database returns. // - // Not with `tag:"deleted-from:inbox"` in the query: notmuch's parser does + // Not with `tag:"moved-from:inbox"` in the query: notmuch's parser does // not match a quoted tag containing a colon that way, so such a query // returns nothing against a perfectly tagged message and reads as the // feature being broken. Asking for the message and inspecting its tags @@ -11998,7 +11998,7 @@ void TestMainWindow::deleteRecordsWhereTheMessageCameFrom() QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000); const QStringList tags = model->threadAt(0).tags; tagged = tags.contains(QStringLiteral("deleted")) - && tags.contains(QStringLiteral("deleted-from:inbox")); + && tags.contains(QStringLiteral("moved-from:inbox")); if (!tagged) QTest::qWait(200); } @@ -12017,8 +12017,8 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind() // onMessagesMoved() resolved the origin placeholder from the folder the // WORKER reported, which is where the message came FROM. On a delete that // is the inbox and correct. On a restore it is the TRASH, so the restore - // asked to remove `deleted-from:Trash`, a tag that had never been written, - // while the real `deleted-from:inbox` was never named and stayed on the + // asked to remove `moved-from:Trash`, a tag that had never been written, + // while the real `moved-from:inbox` was never named and stayed on the // message. It came home still claiming to have been deleted from // somewhere, which makes Restore offer to move a message already at home. // @@ -12066,7 +12066,7 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind() QTRY_VERIFY_WITH_TIMEOUT( notmuchCount(backed.fixture().configPath(), QStringLiteral("id:twice@example.org and " - "tag:\"deleted-from:inbox\"")) == 1, + "tag:\"moved-from:inbox\"")) == 1, 15000); // Second press on the same message, which restores it. @@ -12096,11 +12096,11 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind() == 0, 15000); - // BOTH tags gone, asked of the database. `deleted-from:` left behind is + // BOTH tags gone, asked of the database. `moved-from:` left behind is // the defect this covers, and it survived a green suite before. // The origin tag specifically, asserted on its OWN query. // - // A combined `tag:deleted or tag:"deleted-from:inbox"` query is NOT + // A combined `tag:deleted or tag:"moved-from:inbox"` query is NOT // equivalent and passed against the bug: `deleted` is removed correctly // and promptly, so the disjunction went to zero on that term alone while // the origin tag was still on the message. Split, so the assertion can @@ -12120,18 +12120,18 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind() QCOMPARE(notmuchCount(cfg, QStringLiteral("id:twice@example.org")), 1); // The origin tag is gone. This is the defect: it used to survive the - // restore, because the placeholder resolved to `deleted-from:Trash`, the + // restore, because the placeholder resolved to `moved-from:Trash`, the // folder the message was coming FROM, and stripped a tag that had never // been written. QCOMPARE(notmuchCount(cfg, QStringLiteral("id:twice@example.org and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 0); // And no tag naming the trash was invented in its place. QCOMPARE(notmuchCount(cfg, QStringLiteral("id:twice@example.org and " - "tag:\"deleted-from:Trash\"")), + "tag:\"moved-from:Trash\"")), 0); // `deleted` itself, so a fix that dropped this one instead cannot hide. @@ -12151,7 +12151,7 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo() // placeholder for the tags it wrote to the database, but handed the undo // command the raw list. Undo then asked to remove a tag by the // placeholder's literal name, which no message carries, so the removal - // was a silent no-op and `deleted-from:inbox` survived. The message came + // was a silent no-op and `moved-from:inbox` survived. The message came // home still claiming to have been deleted from somewhere, which makes // Restore offer to move a message that is already at home. // @@ -12193,7 +12193,7 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo() // about it being REMOVED rather than never having existed. QTRY_VERIFY_WITH_TIMEOUT( notmuchCount(cfg, QStringLiteral("id:undotag@example.org and " - "tag:\"deleted-from:inbox\"")) == 1, + "tag:\"moved-from:inbox\"")) == 1, 15000); window.findChild<QAction *>(QStringLiteral("undo"))->trigger(); @@ -12217,11 +12217,11 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo() QCOMPARE(notmuchCount(cfg, QStringLiteral("id:undotag@example.org")), 1); QCOMPARE(notmuchCount(cfg, QStringLiteral("id:undotag@example.org and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 0); QCOMPARE(notmuchCount(cfg, QStringLiteral("id:undotag@example.org and " - "tag:\"deleted-from:Trash\"")), + "tag:\"moved-from:Trash\"")), 0); } @@ -12237,8 +12237,8 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt() // from the mismatch: the toggle asked a thread ROW about its thread's // tags, which notmuch gives as a UNION, so deleting the root left the // union carrying no `deleted` and a second press ran Delete AGAIN, - // trash-to-trash, producing `deleted-from:inbox` and - // `deleted-from:Trash` at once with no way back. Item 177 dissolves the + // trash-to-trash, producing `moved-from:inbox` and + // `moved-from:Trash` at once with no way back. Item 177 dissolves the // mismatch rather than patching it: the row and the write now agree about // what they are for. The trash-to-trash assertions stay, because they are // what proves a delete cannot run twice on one message. @@ -12286,7 +12286,7 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt() QTRY_VERIFY_WITH_TIMEOUT(folderHasMessageFile(trash, stem), 15000); QTRY_VERIFY_WITH_TIMEOUT( notmuchCount(cfg, QStringLiteral("id:tlone@example.org and " - "tag:\"deleted-from:inbox\"")) == 1, + "tag:\"moved-from:inbox\"")) == 1, 15000); // There is no second press to make any more, and that is the point. @@ -12330,12 +12330,12 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt() // query bar passes against any state of the database. QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org")), 1); QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 0); // The tag a re-delete would invent. Its presence is the signature of a // trash-to-trash move rather than a variation on the origin-tag defects. QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org and " - "tag:\"deleted-from:Trash\"")), + "tag:\"moved-from:Trash\"")), 0); QVERIFY2(!folderHasMessageFile(trash, stem), "the message was left in the trash"); @@ -12349,7 +12349,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard() // when Delete became a move, so a whole conversation stayed in the inbox // wearing a `deleted` chip, which is the half-deleted state item 103 // existed to remove. It moves every message now, each carrying its own - // `deleted-from:` origin so a thread spanning folders reassembles. + // `moved-from:` origin so a thread spanning folders reassembles. // // And the ROOT card did not repaint until it was clicked, while its // replies did. A thread-scoped move updated each message's node; @@ -12418,7 +12418,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard() // Each with its own origin, which is what makes the move reversible. QCOMPARE(notmuchCount(cfg, thread + QStringLiteral(" and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 3); // The ROOT CARD's own state, which is what the user watches. Read from the @@ -12441,7 +12441,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard() QCOMPARE(notmuchCount(cfg, thread), 3); QCOMPARE(notmuchCount(cfg, thread + QStringLiteral(" and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 0); QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt0.example.org"))); QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt1.example.org"))); @@ -12452,8 +12452,8 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip() { // A notmuch tag MAY contain a space, and a Maildir folder name may too. // The worker reported each message's tags as one space-joined string, so - // `deleted-from:Inbox/SlackBuilds users` was split back into - // "deleted-from:Inbox/SlackBuilds" and "users", and Restore moved the + // `moved-from:Inbox/SlackBuilds users` was split back into + // "moved-from:Inbox/SlackBuilds" and "users", and Restore moved the // messages to the truncated folder, CREATING it. On the user's real // Maildir that put four messages into a directory mbsync does not sync, // beside the real folder of 808, and they read as missing. @@ -12505,7 +12505,7 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip() // The origin tag carries the WHOLE folder name, space included. QCOMPARE(notmuchCount(cfg, thread - + QStringLiteral(" and tag:\"deleted-from:" + + QStringLiteral(" and tag:\"moved-from:" "Inbox/SlackBuilds users\"")), 2); @@ -12521,13 +12521,13 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip() // could see, could not type, and could not remove. QCOMPARE(notmuchCount(cfg, thread - + QStringLiteral(" and tag:\"deleted-from:" + + QStringLiteral(" and tag:\"moved-from:" "Inbox/SlackBuilds users\"")), 0); // Nor a truncated one, which is what a space-split would have written. QCOMPARE(notmuchCount(cfg, thread - + QStringLiteral(" and tag:\"deleted-from:" + + QStringLiteral(" and tag:\"moved-from:" "Inbox/SlackBuilds\"")), 0); @@ -12754,7 +12754,7 @@ void TestMainWindow::restoreReturnsAMessageToItsOriginFolder() // removes the race rather than papering over it with a longer timeout. QTRY_VERIFY_WITH_TIMEOUT( notmuchCount(cfg, QStringLiteral("id:ro1@example.org and " - "tag:\"deleted-from:inbox\"")) == 0, + "tag:\"moved-from:inbox\"")) == 0, 15000); QTRY_VERIFY_WITH_TIMEOUT( notmuchCount(cfg, @@ -12784,7 +12784,7 @@ void TestMainWindow::restoreReturnsAMessageToItsOriginFolder() void TestMainWindow::restoreFallsBackToInboxWithoutAnOriginTag() { // A message trashed by ANOTHER client: it sits in the trash folder and - // carries no `deleted-from:` tag, because nothing here put it there. The + // carries no `moved-from:` tag, because nothing here put it there. The // real Maildir has such messages, which is why the trash view is path // based rather than tag based. // @@ -12814,7 +12814,7 @@ void TestMainWindow::restoreFallsBackToInboxWithoutAnOriginTag() // The guard this test needs: no origin tag, so the fallback is what is // under test rather than an ordinary restore. QCOMPARE(notmuchCount(cfg, QStringLiteral("id:foreign@example.org and " - "tag:\"deleted-from:inbox\"")), + "tag:\"moved-from:inbox\"")), 0); queryEdit->setText(QStringLiteral("path:\"acct/Trash/**\"")); @@ -12882,10 +12882,10 @@ void TestMainWindow::undoMovesTheMessageBack() QVERIFY2(!folderHasMessageFile(trash, stem), "undo restored the file and left a copy in the trash"); - // Both tags gone, asked of the database. `deleted-from:` left behind would + // Both tags gone, asked of the database. `moved-from:` left behind would // make Restore offer to move a message that is already home. queryEdit->setText(QStringLiteral( - "id:del3@example.org and (tag:deleted or tag:\"deleted-from:inbox\")")); + "id:del3@example.org and (tag:deleted or tag:\"moved-from:inbox\")")); queryEdit->returnPressed(); QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 0, 15000); // The guard the assertion above needs: a query that matches nothing @@ -13101,7 +13101,7 @@ void TestMainWindow::twoDeletesToOneTrashBothGetTheirTags() // Deletes in one account before the first confirmation arrived both named // `acct/Trash`: the second insert overwrote the first and the second // confirmation took an empty entry. That file reached the trash carrying - // neither `deleted` nor `deleted-from:`, which makes it unrestorable by + // neither `deleted` nor `moved-from:`, which makes it unrestorable by // Restore and invisible to a `tag:deleted` query. WorkerBackedWindow backed; QVERIFY(backed.fixture().addMessage( @@ -13156,7 +13156,7 @@ void TestMainWindow::twoDeletesToOneTrashBothGetTheirTags() // the defect was a write that never happened, and the model would have // shown the optimistic state either way. queryEdit->setText(QStringLiteral( - "tag:deleted and tag:\"deleted-from:inbox\" and " + "tag:deleted and tag:\"moved-from:inbox\" and " "(id:two1@example.org or id:two2@example.org)")); queryEdit->returnPressed(); QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 2, 15000); @@ -13385,7 +13385,7 @@ void TestMainWindow::aMoveThatRelocatesNothingWritesNoTag() QCOMPARE(notmuchCount(cfg, QStringLiteral("id:nomove@example.org and " "tag:deleted")), 0); QCOMPARE(notmuchCount(cfg, QStringLiteral("id:nomove@example.org and " - "tag:\"deleted-from:inbox\"")), 0); + "tag:\"moved-from:inbox\"")), 0); // And the file never left. QVERIFY(folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"), diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index 970f51d..2877894 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -54,6 +54,7 @@ private slots: void applyTagsWithNoIdsDoesNothing(); void applyTagsReportsOnlyTheMessagesItChanged(); void applyTagsThatChangeNothingEmitNothing(); + void applyTagsKeepsOnlyTheNewestOriginTag(); void queryStillWorksAfterWrite(); void aThreadCarriesItsCardMessagesOwnTags(); @@ -142,8 +143,10 @@ private: QString fileOf(const QString &messageId, const QString &configPath = QString()); - /// Tags of one message, read back through a fresh worker query. - QStringList tagsOf(const QString &messageId); + /// Tags of one message, read back through a fresh worker query. Defaults to + /// the shared fixture; a test with its own fixture passes its own path. + QStringList tagsOf(const QString &messageId, + const QString &configPath = QString()); QVector<MessageRef> messagesOfThread(const QString &threadId, const QString &matchQuery = QString(), bool matchedOnly = false); @@ -401,9 +404,11 @@ QVector<MessageRef> TestNotmuchWorker::messagesOfThread(const QString &threadId, return loaded.first().at(0).value<QVector<MessageRef>>(); } -QStringList TestNotmuchWorker::tagsOf(const QString &messageId) +QStringList TestNotmuchWorker::tagsOf(const QString &messageId, + const QString &configPath) { - NotmuchWorker worker(m_fixture.configPath()); + NotmuchWorker worker(configPath.isEmpty() ? m_fixture.configPath() + : configPath); QSignalSpy loaded(&worker, &NotmuchWorker::threadLoaded); worker.loadThread(QStringLiteral("{id:%1}").arg(messageId), QString(), 1); if (loaded.isEmpty()) @@ -1117,6 +1122,68 @@ void TestNotmuchWorker::applyTagsThatChangeNothingEmitNothing() QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString())); } +void TestNotmuchWorker::applyTagsKeepsOnlyTheNewestOriginTag() +{ + // A message that travelled inbox -> spam -> trash must hold exactly ONE + // `moved-from:` tag, the newest, or Restore's first-match scan picks an + // origin arbitrarily. Its own fixture, so the tags this test leaves behind + // cannot move the shared one's thread counts. + NotmuchFixture fixture; + QVERIFY(fixture.addMessage(QStringLiteral("inbox"), + QStringLiteral("origin1@example.org"), + QStringLiteral("Travelled"), + QStringLiteral("Erin <erin@example.org>"), + QStringLiteral("Sun, 7 Jun 2026 10:00:00 +0000"), + QStringLiteral("body"), false)); + QVERIFY(fixture.addMessage(QStringLiteral("inbox"), + QStringLiteral("origin2@example.org"), + QStringLiteral("Fresh"), + QStringLiteral("Erin <erin@example.org>"), + QStringLiteral("Sun, 7 Jun 2026 11:00:00 +0000"), + QStringLiteral("body"), false)); + QVERIFY(fixture.index()); + + NotmuchWorker worker(fixture.configPath()); + QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred); + + // An earlier move left an origin behind. + worker.applyTags(TagChange{ { QStringLiteral("origin1@example.org") }, + { QStringLiteral("moved-from:inbox") }, + {}, + QStringLiteral("Earlier move") }); + QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString())); + QVERIFY(tagsOf(QStringLiteral("origin1@example.org"), fixture.configPath()) + .contains(QStringLiteral("moved-from:inbox"))); + + // The move under test: a new origin lands while the old one is still there. + worker.applyTags(TagChange{ + { QStringLiteral("origin1@example.org"), + QStringLiteral("origin2@example.org") }, + { QStringLiteral("moved-from:Spam"), QStringLiteral("spam") }, + { QStringLiteral("inbox"), QStringLiteral("unread") }, + QStringLiteral("Mark spam") }); + QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString())); + + const QStringList travelled = + tagsOf(QStringLiteral("origin1@example.org"), fixture.configPath()); + QVERIFY(!travelled.contains(QStringLiteral("moved-from:inbox"))); + QVERIFY(travelled.contains(QStringLiteral("moved-from:Spam"))); + int origins = 0; + for (const QString &tag : travelled) { + if (tag.startsWith(QStringLiteral("moved-from:"))) + ++origins; + } + QCOMPARE(origins, 1); + + // The message that never carried an origin keeps exactly the new one, and + // the move still removed what a move removes. + const QStringList fresh = + tagsOf(QStringLiteral("origin2@example.org"), fixture.configPath()); + QVERIFY(fresh.contains(QStringLiteral("moved-from:Spam"))); + QVERIFY(fresh.contains(QStringLiteral("spam"))); + QVERIFY(!fresh.contains(QStringLiteral("inbox"))); +} + void TestNotmuchWorker::queryStillWorksAfterWrite() { // applyTags closes the read-only handle to take the write lock. The same diff --git a/tests/test_tagdialog.cpp b/tests/test_tagdialog.cpp index 9b12109..93a7229 100644 --- a/tests/test_tagdialog.cpp +++ b/tests/test_tagdialog.cpp @@ -176,14 +176,14 @@ void TestTagDialog::aTagWithASpaceCanStillBeRemoved() // they can see and cannot get rid of. // // Reached by a real Maildir: a folder named "Inbox/SlackBuilds users" - // produced `deleted-from:Inbox/SlackBuilds users`, and the one dialog that + // produced `moved-from:Inbox/SlackBuilds users`, and the one dialog that // could have cleared it refused the only text that names it. // // Only the TYPED route was blocked. Unchecking appends to the removal list // after validation has run, so it worked throughout; that asymmetry is why // both routes are asserted here rather than just the one that failed. const QString spaced = - QStringLiteral("deleted-from:Inbox/SlackBuilds users"); + QStringLiteral("moved-from:Inbox/SlackBuilds users"); QHash<QString, int> current; current.insert(spaced, 1); |
