summaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp199
1 files changed, 199 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index d133cfb..c409d1a 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -89,6 +89,11 @@ private slots:
void anEditUndoneNettsBackToZero();
void aDifferentTagOnTheSameMessageStillCounts();
void anEditWithNoMessageIdsStillCounts();
+ void anEditDuringABackgroundSyncIsNotSentYet();
+ void aHeldEditIsSentWhenTheBackgroundSyncEnds();
+ void aHeldEditCountsAsUnsynced();
+ void anUnreadableLockTableStillSendsTheEdit();
+ void aRejectedWriteKeepsEarlierUndoHistory();
};
void TestMainWindow::everyKnownActionIsRegistered()
@@ -1273,6 +1278,200 @@ void TestMainWindow::anEditWithNoMessageIdsStillCounts()
"an edit with no message ids was not counted at all");
}
+// Item 37. A tag edit made while a background sync holds notmuch's write lock
+// used to stall the worker: the read-write open BLOCKS until the lock frees
+// (measured 9.158s against a 12s hold, returning NOTMUCH_STATUS_SUCCESS), so
+// every later query and thread load queued behind it. These cases pin the fix:
+// do not send the write while a sync is running, send it when the sync ends.
+
+void TestMainWindow::anEditDuringABackgroundSyncIsNotSentYet()
+{
+ // The defect. Sending during the sync is what stalls the worker, so the
+ // edit is held instead. The rows still show it: it is what the user asked
+ // for and it is going to be applied.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("flag"));
+ QVERIFY2(action, "no flag action registered");
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"), {}) });
+ view->selectRow(0);
+
+ // A cron sync takes the lock.
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running));
+
+ action->trigger();
+
+ QVERIFY2(window.hasEditAwaitingSend(),
+ "the edit was sent straight into a running sync, which is the "
+ "blocking open that stalls the worker");
+ QVERIFY2(model->threadAt(0).tags.contains(QStringLiteral("flagged")),
+ "holding the edit also dropped it from the rows");
+}
+
+void TestMainWindow::aHeldEditIsSentWhenTheBackgroundSyncEnds()
+{
+ // The release. SyncMonitor already reports this transition for item 27, so
+ // the held edit rides a signal that exists rather than a timer.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("flag"));
+ QVERIFY(action);
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"), {}) });
+ view->selectRow(0);
+
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running));
+ action->trigger();
+ QVERIFY(window.hasEditAwaitingSend());
+
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Idle));
+
+ QVERIFY2(!window.hasEditAwaitingSend(),
+ "the sync ending did not send the held edit");
+ QVERIFY2(model->threadAt(0).tags.contains(QStringLiteral("flagged")),
+ "sending the held edit lost the tag from the rows");
+}
+
+void TestMainWindow::aHeldEditCountsAsUnsynced()
+{
+ // A held edit has not reached the index, so onTagsApplied() never counted
+ // it. It must still count here, because this is what the exit prompt reads:
+ // quitting on a held edit loses it outright, which is the whole failure the
+ // prompt exists to prevent.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("flag"));
+ QVERIFY(action);
+ auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits"));
+ QVERIFY(label);
+ QVERIFY2(label->isHidden(), "the indicator starts hidden at zero");
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"), {}) });
+ view->selectRow(0);
+
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running));
+ action->trigger();
+
+ QVERIFY2(!label->isHidden(),
+ "an edit held for a running sync was not counted as unsynced, so "
+ "the exit prompt would let the user quit on it");
+}
+
+void TestMainWindow::anUnreadableLockTableStillSendsTheEdit()
+{
+ // State::Unknown means /proc/locks could not be read, so nothing is
+ // observed. Holding writes there would strand every edit forever on a
+ // platform that cannot see the lock at all. Unknown is not "running".
+ //
+ // Driven from Running, not from a fresh window: the guard is that Unknown
+ // CLEARS the busy flag, and a window that was never busy would pass this
+ // whatever Unknown did. Reaching Unknown by way of Running is also the only
+ // way a real monitor gets there, when /proc/locks becomes unreadable
+ // mid-session.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("flag"));
+ QVERIFY(action);
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"), {}),
+ makeThread(QStringLiteral("t2"), {}) });
+
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running));
+ view->selectRow(0);
+ action->trigger();
+ QVERIFY2(window.hasEditAwaitingSend(),
+ "the edit was not held during a running sync, so this test is not "
+ "exercising the Unknown transition it claims to");
+
+ // The lock table becomes unreadable. That is not evidence of a sync, so
+ // writing must resume and the held edit must go out.
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Unknown));
+ QVERIFY2(!window.hasEditAwaitingSend(),
+ "an unreadable lock table kept the edit held, stranding it on any "
+ "platform without /proc/locks");
+
+ // And a NEW edit is sent rather than held.
+ view->selectRow(1);
+ action->trigger();
+ QVERIFY2(!window.hasEditAwaitingSend(),
+ "an unreadable lock table held a new edit, so writes never resume");
+}
+
+void TestMainWindow::aRejectedWriteKeepsEarlierUndoHistory()
+{
+ // revertPendingTagChange() used to undo the failed command and then CLEAR
+ // the whole stack, so one rejected write threw away every undo step the
+ // user had built up. Undoing the failed command is enough: it is already
+ // off the stack afterwards.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *flag = window.findChild<QAction *>(QStringLiteral("flag"));
+ QVERIFY(flag);
+ auto *archive = window.findChild<QAction *>(QStringLiteral("archive"));
+ QVERIFY2(archive, "no archive action registered");
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"),
+ { QStringLiteral("inbox") }) });
+ view->selectRow(0);
+
+ // One edit that succeeds, so there is history worth keeping.
+ archive->trigger();
+ TagChange applied;
+ applied.messageIds = { QStringLiteral("m1") };
+ applied.removed = { QStringLiteral("inbox") };
+ applied.description = QStringLiteral("Archive");
+ QVERIFY(QMetaObject::invokeMethod(&window, "onTagsApplied",
+ Q_ARG(TagChange, applied)));
+
+ // A second edit that the worker rejects outright.
+ flag->trigger();
+ QVERIFY(QMetaObject::invokeMethod(
+ &window, "onWorkerError",
+ Q_ARG(QString, QStringLiteral("Cannot resolve threads"))));
+
+ QVERIFY2(window.canUndo(),
+ "a rejected write cleared the undo history of edits that had "
+ "already succeeded");
+}
+
// 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.