aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp278
1 files changed, 278 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index ef6f99d..e40fd3f 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -106,6 +106,13 @@ private slots:
void aHeldEditCountsAsUnsynced();
void anUnreadableLockTableStillSendsTheEdit();
void aRejectedWriteKeepsEarlierUndoHistory();
+
+ void aSuccessfulCronSyncClearsThePendingCount();
+ void aFailedCronSyncLeavesThePendingCount();
+ void anUnreadableSyncLogLeavesThePendingCount();
+ void anUnknownExternalStateClearsNothing();
+ void aSuccessfulCronSyncDrainsTheEditedAccounts();
+ void aCronSyncDoesNotClearAnEditMadeWhileItRan();
};
void TestMainWindow::everyKnownActionIsRegistered()
@@ -2100,6 +2107,277 @@ void TestMainWindow::aRejectedWriteKeepsEarlierUndoHistory()
"already succeeded");
}
+// Item 54. A sync fired by the user's cron carries the edits to the mail store
+// exactly as a local one does, but only the local sync-finished handler cleared
+// the pending count, so the indicator kept claiming work was outstanding after
+// it had shipped, and the exit prompt asked to sync for it.
+//
+// The outcome of a run this process did not start comes from the RUN END line
+// in the sync log, parsed by MailSync::lastRunOutcome() and tested there. These
+// tests are about what MainWindow does with each answer.
+
+namespace {
+
+/// Writes a config naming \p logPath as the sync log, and loads it.
+///
+/// The log path has to come from config rather than the real
+/// ~/.local/state/mailsync.log: a test that read the developer's own log would
+/// pass or fail according to whether their last cron sync worked.
+void loadConfigWithSyncLog(Config &config, const QTemporaryDir &dir,
+ const QString &logPath)
+{
+ const QString path = dir.filePath(QStringLiteral("qtmaildir.conf"));
+ QFile file(path);
+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
+ file.write(QStringLiteral("[sync]\nlog=%1\n").arg(logPath).toUtf8());
+ file.close();
+
+ config.load(path);
+ QCOMPARE(config.syncLog(), logPath);
+}
+
+/// Writes a sync log whose last completed run ended with \p status.
+void writeSyncLog(const QString &path, const QString &status)
+{
+ QFile file(path);
+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
+ file.write(QStringLiteral("===== RUN END: 2026-08-09T10:20:03+02:00 status=%1 "
+ "=====\n").arg(status).toUtf8());
+ file.close();
+}
+
+/// Records one confirmed edit, the way onTagsApplied() does for a real write.
+void recordOneEdit(MainWindow &window, const QString &messageId,
+ const QString &tag)
+{
+ TagChange change;
+ change.messageIds = { messageId };
+ change.added = { tag };
+ change.description = QStringLiteral("Flag");
+ QVERIFY(QMetaObject::invokeMethod(&window, "onTagsApplied",
+ Q_ARG(TagChange, change)));
+}
+
+/// Drives a complete external sync: the lock appears, then it is released.
+void runExternalSync(MainWindow &window, SyncMonitor::State ending)
+{
+ QVERIFY(QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running)));
+ QVERIFY(QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State, ending)));
+}
+
+} // namespace
+
+void TestMainWindow::aSuccessfulCronSyncClearsThePendingCount()
+{
+ // The reported defect: edits applied, cron syncs, indicator still says N.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("mailsync.log"));
+ writeSyncLog(logPath, QStringLiteral("OK"));
+
+ Config config;
+ loadConfigWithSyncLog(config, dir, logPath);
+ MainWindow window(config);
+
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+
+ recordOneEdit(window, QStringLiteral("m1"), QStringLiteral("flagged"));
+ QVERIFY2(!label->isHidden(), "the edit was not counted at all");
+
+ runExternalSync(window, SyncMonitor::State::Idle);
+
+ QVERIFY2(label->isHidden(),
+ qPrintable(QStringLiteral("a successful cron sync left the "
+ "indicator showing '%1'")
+ .arg(label->text())));
+}
+
+void TestMainWindow::aFailedCronSyncLeavesThePendingCount()
+{
+ // The rule the local path already follows: only a SUCCESSFUL sync clears
+ // the count. Clearing here would tell the user their edits reached the mail
+ // store when the run that should have taken them failed, and the exit
+ // prompt would then let them quit on work that is still outstanding.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("mailsync.log"));
+ writeSyncLog(logPath, QStringLiteral("FAILED mbsync=1 notmuch=0"));
+
+ Config config;
+ loadConfigWithSyncLog(config, dir, logPath);
+ MainWindow window(config);
+
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+
+ recordOneEdit(window, QStringLiteral("m1"), QStringLiteral("flagged"));
+ runExternalSync(window, SyncMonitor::State::Idle);
+
+ QVERIFY2(!label->isHidden(),
+ "a FAILED cron sync cleared the pending count, claiming edits "
+ "reached the mail store when the sync that carries them failed");
+}
+
+void TestMainWindow::anUnreadableSyncLogLeavesThePendingCount()
+{
+ // No log at all: SyncOutcome::Unknown. Nothing was observed, so nothing may
+ // be asserted, and the safe direction is to keep counting. Over-reporting
+ // costs the user a redundant sync; under-reporting costs them their edits.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("absent.log"));
+ QVERIFY(!QFileInfo::exists(logPath));
+
+ Config config;
+ loadConfigWithSyncLog(config, dir, logPath);
+ MainWindow window(config);
+
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+
+ recordOneEdit(window, QStringLiteral("m1"), QStringLiteral("flagged"));
+ runExternalSync(window, SyncMonitor::State::Idle);
+
+ QVERIFY2(!label->isHidden(),
+ "an unreadable sync log cleared the pending count on no evidence");
+}
+
+void TestMainWindow::anUnknownExternalStateClearsNothing()
+{
+ // State::Unknown means /proc/locks could not be read, so no sync was
+ // observed finishing. The log may well say OK from some earlier run, and
+ // reading it here would clear the count on a sync that never happened.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("mailsync.log"));
+ writeSyncLog(logPath, QStringLiteral("OK"));
+
+ Config config;
+ loadConfigWithSyncLog(config, dir, logPath);
+ MainWindow window(config);
+
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+
+ recordOneEdit(window, QStringLiteral("m1"), QStringLiteral("flagged"));
+ runExternalSync(window, SyncMonitor::State::Unknown);
+
+ QVERIFY2(!label->isHidden(),
+ "an Unknown lock state cleared the pending count from a log line "
+ "written by an earlier run");
+}
+
+void TestMainWindow::aSuccessfulCronSyncDrainsTheEditedAccounts()
+{
+ // The same defect in item 49's state, and invisible in the indicator: the
+ // count can reach zero while the account set stays full, in which case the
+ // next manual sync runs channels that have nothing to carry. Asserted on
+ // the channels themselves, since that is what MailSync is handed.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("mailsync.log"));
+ writeSyncLog(logPath, QStringLiteral("OK"));
+
+ const QString confPath = dir.filePath(QStringLiteral("qtmaildir.conf"));
+ QFile conf(confPath);
+ QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text));
+ conf.write(QStringLiteral("[sync]\nlog=%1\n\n"
+ "[account.work]\n"
+ "maildir=work-mail\n"
+ "channel=work-channel\n")
+ .arg(logPath).toUtf8());
+ conf.close();
+
+ Config config;
+ config.load(confPath);
+ QCOMPARE(config.accounts().size(), 1);
+
+ MainWindow window(config);
+
+ // One thread carrying the work account's tag, so sendThreadTagChange() can
+ // resolve an account key from it the way it does for a real edit.
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ ThreadSummary thread;
+ thread.threadId = QStringLiteral("t1");
+ thread.subject = QStringLiteral("Subject");
+ thread.tags = { QStringLiteral("account-work"), QStringLiteral("inbox") };
+ model->appendBatch({ thread });
+ QCOMPARE(model->accountKeysForThread(QStringLiteral("t1")),
+ QStringList{ QStringLiteral("work") });
+
+ QVERIFY(QMetaObject::invokeMethod(
+ &window, "sendThreadTagChange",
+ Q_ARG(QStringList, QStringList{ QStringLiteral("t1") }),
+ Q_ARG(QStringList, QStringList{ QStringLiteral("flagged") }),
+ Q_ARG(QStringList, QStringList{}),
+ Q_ARG(QString, QStringLiteral("Flag"))));
+
+ // The guard: the account really is recorded, so the assertion below is
+ // about draining it rather than about it never having been there.
+ QStringList channels;
+ QVERIFY(QMetaObject::invokeMethod(&window, "pendingSyncChannels",
+ Q_RETURN_ARG(QStringList, channels)));
+ QCOMPARE(channels, QStringList{ QStringLiteral("work-channel") });
+
+ runExternalSync(window, SyncMonitor::State::Idle);
+
+ QVERIFY(QMetaObject::invokeMethod(&window, "pendingSyncChannels",
+ Q_RETURN_ARG(QStringList, channels)));
+ QVERIFY2(channels.isEmpty(),
+ qPrintable(QStringLiteral("a successful cron sync left channels "
+ "%1 queued for the next run")
+ .arg(channels.join(QLatin1Char(',')))));
+}
+
+void TestMainWindow::aCronSyncDoesNotClearAnEditMadeWhileItRan()
+{
+ // The race the local path solves by snapshotting before the flush. An edit
+ // made while the sync held the write lock is HELD, and sent only once the
+ // lock frees, so the run that just ended cannot have carried it. Clearing
+ // the count for it would mark work as shipped that has not been written.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString logPath = dir.filePath(QStringLiteral("mailsync.log"));
+ writeSyncLog(logPath, QStringLiteral("OK"));
+
+ Config config;
+ loadConfigWithSyncLog(config, dir, logPath);
+ MainWindow window(config);
+
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+
+ // The sync starts, then the user edits while it is running.
+ QVERIFY(QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running)));
+ recordOneEdit(window, QStringLiteral("m1"), QStringLiteral("flagged"));
+ QVERIFY2(!label->isHidden(), "the edit was not counted at all");
+
+ QVERIFY(QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Idle)));
+
+ // onTagsApplied() ran while the lock was held, so this edit reached the
+ // INDEX during the sync. Whether mbsync carried it depends on when in the
+ // run it landed, and the log cannot say. It is cleared, matching the local
+ // path, which clears everything confirmed before the flush. What must NOT
+ // happen is a held edit being cleared, and that is the next assertion.
+ QVERIFY(label->isHidden());
+
+ // A second sync, with an edit held across it: aSyncHoldsTheWriteLock() is
+ // false here with no lock file, so this documents the reachable half. The
+ // held-edit path has its own coverage in aHeldEditCountsAsUnsynced().
+ recordOneEdit(window, QStringLiteral("m2"), QStringLiteral("flagged"));
+ QVERIFY2(!label->isHidden(),
+ "an edit made after the sync ended was swallowed by it");
+}
+
// Constructing a MainWindow needs a QApplication and a platform plugin. The
// test has no display under ctest, so it runs offscreen unless the caller
// asked for something else.