diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-07 11:39:31 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-07 11:39:31 +0200 |
| commit | 0a9ef3c77c7c593f3568f25761aad5d1f55e0e33 (patch) | |
| tree | 714f2a5f622a43477af50393007fd892434a7a65 /tests/test_mainwindow.cpp | |
| parent | 1a6007b4cdaacb22d013daeb58deb6ae72afda07 (diff) | |
| download | qtmaildir-0a9ef3c77c7c593f3568f25761aad5d1f55e0e33.tar.gz qtmaildir-0a9ef3c77c7c593f3568f25761aad5d1f55e0e33.zip | |
feat(sync): the status bar says which account is syncing
"Syncing..." was set once and never updated, so a run that takes over a
minute reported nothing about what it was doing.
The original diagnosis in the backlog was half wrong, and two further
wrong ones were made and discarded before the real cause: plain
"mbsync -a" prints NOTHING until it exits, then a single summary line.
Measured on a real run, one line at 11:11:08 then 73 within the second
11:11:33, at the end of a 46-second run. So there was no stream to read
for the part of a sync that takes time. It is not buffering, so stdbuf
changes nothing, and the account name is not unavailable either, which
was the second wrong conclusion.
mbsync -V is what changes both: it announces each channel as it reaches
it, which is at once the progress and the account name originally
asked for. The shipped script now passes it.
SyncPhaseTracker derives a short status from the output as it streams:
the channel being synced, the summary counts when mbsync ends, then the
notmuch reindex. It lives beside MailSync rather than in the window so
the matching rules are one testable thing, and it holds no widget.
Matching is loose and case-insensitive, since the wording varies by
version, and nothing in it decides success or failure: the exit status
remains the only authority on that.
Lines are reassembled in MainWindow before being fed, because
QProcess::readAll() splits wherever it happens to and a half-line would
match nothing. Every status is sanitised and truncated: the channel
name comes from a config file this app does not own, and a long one
must not stretch the status bar.
Two defects in existing code, fixed with it. setSyncBusy(true) ran
after start(), so a fast run's output arrived before the per-run reset
and wiped its own phase. And a first draft deferred phases while a
transient message showed, which let a "Background sync completed"
message armed before the sync began suppress the whole run: a running
sync's state outranks an expiring event message.
Verified by replaying real captured mbsync -V output through the
tracker, not only against fixtures. The MainWindow test paces its
script with sleeps, since a script that prints everything at once
arrives in one readyRead and makes every intermediate phase
unobservable.
Closes item 42.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests/test_mainwindow.cpp')
| -rw-r--r-- | tests/test_mainwindow.cpp | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 480be39..02490a5 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -80,6 +80,7 @@ private slots: void aLocalSyncsOwnLockIsNeverReportedAsBackground(); void aSkippedLocalSyncStillReportsTheOtherRunFinishing(); void anUnobservableLockTableLeavesTheSyncButtonUsable(); + void theStatusBarFollowsTheSyncPhase(); void theSyncActionIsDisabledWhileABackgroundSyncHoldsTheLock(); void escapeBlanksTheMessagePane(); void deleteTogglesOnAnAlreadyDeletedThread(); @@ -1031,6 +1032,122 @@ void TestMainWindow::theSyncActionIsDisabledWhileABackgroundSyncHoldsTheLock() MainWindow::setLocksPathForTesting(QStringLiteral("/proc/locks")); } +void TestMainWindow::theStatusBarFollowsTheSyncPhase() +{ + // Item 42: "Syncing..." said nothing about what was happening, while the + // script was already streaming its phase into the log pane and the app was + // throwing it away. + // + // Driven through a real script rather than by calling the tracker directly, + // because the defect this guards is in the wiring: the chunks QProcess + // hands over split mid-line, so a handler that fed them straight to the + // tracker would stall on the first partial line. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + QVERIFY(QDir().mkpath(dir.filePath(QStringLiteral("qtmaildir")))); + + const QString script = dir.filePath(QStringLiteral("fakesync.sh")); + { + QFile f(script); + QVERIFY(f.open(QIODevice::WriteOnly)); + // Shaped like the real thing: timestamped lines, the noise that makes + // up the bulk of a run, mbsync's one summary, then notmuch's output. + // Paced, not dumped. A script that prints everything at once is + // delivered in a single readyRead, so the tracker sees the whole run in + // one call and only its final phase is ever painted: the intermediate + // ones would be unobservable and the test would assert nothing. A real + // sync takes tens of seconds and arrives in separate chunks, which the + // sleeps stand in for. + f.write("#!/bin/sh\n" + "echo '===== RUN START: 2026-08-07T11:00:00+02:00 ====='\n" + "echo '11:00:01 Note: Ignoring non-mail file: /home/you/Mail/x/.uidvalidity'\n" + "sleep 0.2\n" + "echo '11:00:02 Channels: 5 Boxes: 39 Far: +0 *1 #0 -0 Near: +1 *0 #0 -0'\n" + "sleep 0.2\n" + "echo '11:00:03 Processed 79 total files in almost no time.'\n" + "echo '11:00:03 Added 1 new message to the database.'\n" + "sleep 0.2\n" + "echo '===== RUN END: 2026-08-07T11:00:03+02:00 status=OK ====='\n"); + f.close(); + QVERIFY(QFile::setPermissions(script, + QFile::ReadOwner | QFile::WriteOwner + | QFile::ExeOwner)); + } + + const QString conf = dir.filePath(QStringLiteral("qtmaildir/qtmaildir.conf")); + { + QSettings s(conf, QSettings::IniFormat); + s.setValue(QStringLiteral("sync/command"), script); + } + + const QString locks = dir.filePath(QStringLiteral("locks")); + { + QFile f(locks); + QVERIFY(f.open(QIODevice::WriteOnly)); + } + MainWindow::setLocksPathForTesting(locks); + + Config config; + config.load(conf); + QCOMPARE(config.syncCommand(), script); + MainWindow window(config); + + auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage")); + QVERIFY(status); + + // Every value the label takes, recorded as it changes. A run this small + // finishes in well under a second, so polling for an intermediate phase + // races the process and usually sees only "Sync complete": the sequence has + // to be captured, not sampled. + // QLabel has no textChanged signal, so the label is sampled on a fast timer + // rather than watched. Each distinct value is recorded once. + QStringList seen; + QTimer sampler; + sampler.setInterval(1); + connect(&sampler, &QTimer::timeout, &sampler, [&seen, status]() { + const QString text = status->text(); + if (seen.isEmpty() || seen.constLast() != text) + seen.append(text); + }); + sampler.start(); + + QVERIFY(QMetaObject::invokeMethod(&window, "startSync")); + + QTRY_VERIFY_WITH_TIMEOUT( + std::any_of(seen.cbegin(), seen.cend(), [](const QString &s) { + return s.contains(QStringLiteral("Sync complete")); + }), + 10000); + + const QString trace = seen.join(QStringLiteral(" | ")); + + // mbsync's summary is the only concrete thing the stream carries, since it + // names no channel unless run verbose. The counts must reach the label. + QVERIFY2(std::any_of(seen.cbegin(), seen.cend(), [](const QString &s) { + return s.contains(QStringLiteral("39")) + && s.contains(QStringLiteral("5")); + }), + qPrintable(QStringLiteral("the mbsync summary never reached the " + "status bar. Saw: ") + trace)); + + // Then the reindex phase, which is a different message entirely. Without + // the wiring the label went from "Syncing..." straight to "Sync complete", + // which is exactly what the defect looked like. + QVERIFY2(std::any_of(seen.cbegin(), seen.cend(), [](const QString &s) { + return s.contains(QStringLiteral("notmuch")); + }), + qPrintable(QStringLiteral("the notmuch phase never reached the " + "status bar. Saw: ") + trace)); + + // The banners are not a phase and must never appear in the status bar. + for (const QString &s : seen) { + QVERIFY2(!s.contains(QStringLiteral("RUN ")), qPrintable(s)); + QVERIFY2(!s.contains(QStringLiteral("status=")), qPrintable(s)); + } + + MainWindow::setLocksPathForTesting(QStringLiteral("/proc/locks")); +} + void TestMainWindow::anUnobservableLockTableLeavesTheSyncButtonUsable() { // Unknown means /proc/locks could not be read, so nothing was observed. A |
