diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-04 18:57:20 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 18:57:20 +0200 |
| commit | 0b3869c758f4dc54017ff91b5b69bfc263c0da52 (patch) | |
| tree | a202a583a6f794c743cc838b33da2d1a79cfc07b /tests/test_syncmonitor.cpp | |
| parent | 1407f70352c05d59fa2e1bb7c59b048b767972e5 (diff) | |
| parent | ce0753bcb6263dec5a6acb53647ca7b64950f8fd (diff) | |
| download | qtmaildir-0b3869c758f4dc54017ff91b5b69bfc263c0da52.tar.gz qtmaildir-0b3869c758f4dc54017ff91b5b69bfc263c0da52.zip | |
Merge branch 'multiselect-discoverability'
Multi-select discoverability (items 24, 25) and awareness of syncs this
window did not start (item 27).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests/test_syncmonitor.cpp')
| -rw-r--r-- | tests/test_syncmonitor.cpp | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/tests/test_syncmonitor.cpp b/tests/test_syncmonitor.cpp new file mode 100644 index 0000000..c120085 --- /dev/null +++ b/tests/test_syncmonitor.cpp @@ -0,0 +1,180 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <QtTest> + +#include <QSignalSpy> +#include <QTemporaryDir> +#include <QTemporaryFile> + +#include "syncmonitor.h" + +/// SyncMonitor answers one question: is a sync running that this process did +/// not start? The parsing of /proc/locks is the testable part and is kept +/// separate from the polling for exactly that reason. +class TestSyncMonitor : public QObject +{ + Q_OBJECT +private slots: + void anFlockOnTheWatchedInodeIsHeld(); + void anFlockOnAnotherInodeIsIgnored(); + void aPosixLockOnTheWatchedInodeIsIgnored(); + void emptyContentMeansNotHeld(); + void garbageLinesAreSkippedRatherThanMisread(); + void anUnreadableLockTableIsUnknownNotIdle(); + void aMissingLockFileIsNotHeld(); + void theStateChangeSignalFiresOnlyOnTransitions(); +}; + +void TestSyncMonitor::anFlockOnTheWatchedInodeIsHeld() +{ + // The real shape of a held lock, taken verbatim from /proc/locks while + // mailsync.sh held /tmp/mbsync.lock: the inode is the last colon-separated + // field of the device:inode column, not the whole column. + const QString content = + QStringLiteral("82: FLOCK ADVISORY WRITE 9051 fc:00:12058676 0 EOF\n"); + QVERIFY(SyncMonitor::lockHeldIn(content, 12058676)); +} + +void TestSyncMonitor::anFlockOnAnotherInodeIsIgnored() +{ + // A busy machine has many flocks. Matching anything but our own inode would + // report a sync whenever some unrelated program took a lock. + const QString content = + QStringLiteral("82: FLOCK ADVISORY WRITE 9051 fc:00:99999999 0 EOF\n"); + QVERIFY(!SyncMonitor::lockHeldIn(content, 12058676)); +} + +void TestSyncMonitor::aPosixLockOnTheWatchedInodeIsIgnored() +{ + // flock(2) and fcntl(2) are separate namespaces in the kernel and cannot + // see each other; mailsync.sh uses flock(2). A POSIX lock on the same file + // is somebody else's, and treating it as ours would report a sync that is + // not running. Verified: fcntl(F_OFD_GETLK) reports UNLOCKED against a + // held flock, which is why this distinction is not academic. + const QString content = + QStringLiteral("1: POSIX ADVISORY WRITE 1234 fc:00:12058676 0 EOF\n"); + QVERIFY(!SyncMonitor::lockHeldIn(content, 12058676)); +} + +void TestSyncMonitor::emptyContentMeansNotHeld() +{ + QVERIFY(!SyncMonitor::lockHeldIn(QString(), 12058676)); +} + +void TestSyncMonitor::garbageLinesAreSkippedRatherThanMisread() +{ + // /proc/locks gains fields across kernel versions, and a line can be + // truncated as it is read. A short line must not match by accident, and + // must not stop the lines after it from being read. + const QString content = QStringLiteral( + "not a lock line at all\n" + "3: FLOCK\n" + "82: FLOCK ADVISORY WRITE 9051 fc:00:12058676 0 EOF\n"); + QVERIFY(SyncMonitor::lockHeldIn(content, 12058676)); + + const QString onlyGarbage = QStringLiteral("nonsense\n3: FLOCK\n"); + QVERIFY(!SyncMonitor::lockHeldIn(onlyGarbage, 12058676)); +} + +void TestSyncMonitor::anUnreadableLockTableIsUnknownNotIdle() +{ + // /proc/locks is Linux-only. Where it cannot be read the honest answer is + // "unknown", and the indicator stays hidden. Reporting idle would be a + // claim the monitor cannot support, and it is the claim that matters: + // "no sync is running" is what lets the window quit. + SyncMonitor monitor(QStringLiteral("/nonexistent/mbsync.lock"), + QStringLiteral("/nonexistent/proc/locks")); + QCOMPARE(monitor.state(), SyncMonitor::State::Unknown); + monitor.poll(); + QCOMPARE(monitor.state(), SyncMonitor::State::Unknown); +} + +void TestSyncMonitor::aMissingLockFileIsNotHeld() +{ + // Before the first sync ever runs there is no lock file. That is not a + // sync in progress, and it must not read as unknown either: the lock table + // is perfectly readable, there is simply nothing holding anything. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + QTemporaryFile locks; + QVERIFY(locks.open()); + locks.write("82: FLOCK ADVISORY WRITE 9051 fc:00:12058676 0 EOF\n"); + locks.flush(); + + SyncMonitor monitor(dir.filePath(QStringLiteral("never-created.lock")), + locks.fileName()); + monitor.poll(); + QCOMPARE(monitor.state(), SyncMonitor::State::Idle); +} + +void TestSyncMonitor::theStateChangeSignalFiresOnlyOnTransitions() +{ + // The UI reacts to a sync starting and finishing, so a signal on every + // poll would repaint the status bar every two seconds forever and would + // stamp over whatever else had been written there. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString lockPath = dir.filePath(QStringLiteral("mbsync.lock")); + { + QFile lock(lockPath); + QVERIFY(lock.open(QIODevice::WriteOnly)); + } + + const qint64 inode = SyncMonitor::inodeOf(lockPath); + QVERIFY(inode > 0); + + // A stand-in for /proc/locks whose contents the test controls. + const QString locksPath = dir.filePath(QStringLiteral("locks")); + auto writeLocks = [&locksPath](const QString &text) { + QFile f(locksPath); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); + f.write(text.toUtf8()); + }; + writeLocks(QString()); + + SyncMonitor monitor(lockPath, locksPath); + QSignalSpy spy(&monitor, &SyncMonitor::stateChanged); + + monitor.poll(); + QCOMPARE(monitor.state(), SyncMonitor::State::Idle); + QCOMPARE(spy.count(), 1); // Unknown -> Idle is a real transition. + + monitor.poll(); + monitor.poll(); + QCOMPARE(spy.count(), 1); // Still idle: no further signals. + + writeLocks(QStringLiteral("82: FLOCK ADVISORY WRITE 9051 fc:00:%1 0 EOF\n") + .arg(inode)); + monitor.poll(); + QCOMPARE(monitor.state(), SyncMonitor::State::Running); + QCOMPARE(spy.count(), 2); + + monitor.poll(); + QCOMPARE(spy.count(), 2); // Still running. + + writeLocks(QString()); + monitor.poll(); + QCOMPARE(monitor.state(), SyncMonitor::State::Idle); + QCOMPARE(spy.count(), 3); +} + +QTEST_MAIN(TestSyncMonitor) + +#include "test_syncmonitor.moc" |
