MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 96.8% 150 / 0 / 155
Functions: 95.2% 20 / 0 / 21
Branches: 91.1% 185 / 0 / 203

app/core/DavSyncScheduler.cpp
Line Branch Exec Source
1 #include "DavSyncScheduler.h"
2
3 #include "data/DavPushModels.h"
4
5 #include <QElapsedTimer>
6 #include <QLoggingCategory>
7 #include <QRandomGenerator>
8 #include <climits>
9
10
4/7
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 8 taken 169 times.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
175 Q_LOGGING_CATEGORY(lcDavScheduler, "mailjd.calendarsync.scheduler")
11
12 namespace {
13 constexpr unsigned kImmediate = DavSyncReasonManual | DavSyncReasonInitial | DavSyncReasonRecovery | DavSyncReasonPoll;
14 constexpr unsigned kUnlocking = DavSyncReasonManual | DavSyncReasonInitial | DavSyncReasonPoll;
15
16 130 bool lockingFailure(const QString &kind) {
17
4/4
✓ Branch 7 → 8 taken 126 times.
✓ Branch 7 → 14 taken 1 time.
✓ Branch 10 → 11 taken 125 times.
✓ Branch 10 → 14 taken 1 time.
383 return kind == QLatin1String("authentication") || kind == QLatin1String("password") ||
18
3/4
✓ Branch 4 → 5 taken 127 times.
✓ Branch 4 → 14 taken 3 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 125 times.
383 kind == QLatin1String("authorization") || kind == QLatin1String("tls");
19 }
20 } // namespace
21
22 957 DavSyncScheduler::DavSyncScheduler(Executor executor, QObject *parent)
23
2/2
✓ Branch 18 → 19 taken 957 times.
✓ Branch 19 → 20 taken 957 times.
957 : QObject(parent), m_executor(std::move(executor)) {
24
1/1
✓ Branch 20 → 21 taken 957 times.
957 m_wake.setSingleShot(true);
25
1/1
✓ Branch 21 → 22 taken 957 times.
957 m_pollTimer.setSingleShot(true);
26
1/1
✓ Branch 22 → 23 taken 957 times.
957 connect(&m_wake, &QTimer::timeout, this, &DavSyncScheduler::evaluate);
27
1/1
✓ Branch 24 → 25 taken 957 times.
957 connect(&m_pollTimer, &QTimer::timeout, this, &DavSyncScheduler::onPollTimer);
28 1787 m_jitter = [] { return QRandomGenerator::global()->generateDouble(); };
29 957 }
30
31 1568 qint64 DavSyncScheduler::now() const {
32
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1568 times.
1568 if (m_clock) return m_clock();
33 static QElapsedTimer monotonic;
34
2/2
✓ Branch 7 → 8 taken 34 times.
✓ Branch 7 → 9 taken 1534 times.
1568 if (!monotonic.isValid()) monotonic.start();
35 1568 return monotonic.elapsed();
36 }
37
38 833 void DavSyncScheduler::setPollIntervalMs(qint64 intervalMs) {
39 833 m_stopped = false;
40 833 m_pollIntervalMs = qMax<qint64>(0, intervalMs);
41
1/1
✓ Branch 3 → 4 taken 833 times.
833 m_pollTimer.stop();
42 833 m_nextPollAt = 0;
43
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 832 times.
833 if (m_pollIntervalMs <= 0) return;
44 // Earlier-only jitter: the configured maximum interval never grows.
45
2/2
✓ Branch 6 → 7 taken 832 times.
✓ Branch 7 → 8 taken 832 times.
832 const qint64 delay = m_pollIntervalMs - qint64(m_pollIntervalMs * 0.1 * qBound(0.0, m_jitter(), 0.999));
46
1/1
✓ Branch 8 → 9 taken 832 times.
832 m_nextPollAt = now() + delay;
47
1/1
✓ Branch 10 → 11 taken 832 times.
832 m_pollTimer.start(int(qMin<qint64>(delay, INT_MAX)));
48 }
49
50 1 void DavSyncScheduler::onPollTimer() {
51 1 requestAll(DavSyncReasonPoll);
52 1 setPollIntervalMs(m_pollIntervalMs);
53 1 }
54
55 89 void DavSyncScheduler::requestAll(unsigned reasons) {
56
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 88 times.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
89 if (m_stopped && !(reasons & DavSyncReasonManual)) return;
57
6/7
✓ Branch 6 → 7 taken 88 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 87 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 87 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 11 → 12 taken 87 times.
88 if (m_roundHook && (reasons & kUnlocking)) m_roundHook(reasons);
58
2/3
✓ Branch 13 → 14 taken 88 times.
✗ Branch 13 → 15 not taken.
✓ Branch 14 → 16 taken 88 times.
88 const QStringList accounts = m_accounts ? m_accounts() : QStringList();
59
3/3
✓ Branch 20 → 21 taken 93 times.
✓ Branch 24 → 18 taken 93 times.
✓ Branch 24 → 25 taken 88 times.
181 for (const QString &accountId : accounts) mark(accountId, true, {}, reasons);
60
1/1
✓ Branch 25 → 26 taken 88 times.
88 evaluate();
61 88 }
62
63 13 void DavSyncScheduler::requestAccount(const QString &accountId, unsigned reasons) {
64
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 12 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
13 if (m_stopped && !(reasons & DavSyncReasonManual)) return;
65
1/1
✓ Branch 6 → 7 taken 13 times.
13 mark(accountId, true, {}, reasons);
66 13 evaluate();
67 }
68
69 181 void DavSyncScheduler::requestCollections(const QString &accountId, const QStringList &paths, unsigned reasons) {
70
6/8
✓ Branch 3 → 4 taken 181 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 180 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 180 times.
181 if (paths.isEmpty() || (m_stopped && !(reasons & DavSyncReasonManual))) return;
71 180 mark(accountId, false, paths, reasons);
72 180 evaluate();
73 }
74
75 296 void DavSyncScheduler::mark(const QString &accountId, bool full, const QStringList &paths, unsigned reasons) {
76
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 296 times.
296 if (accountId.isEmpty()) return;
77
2/2
✓ Branch 5 → 6 taken 91 times.
✓ Branch 5 → 8 taken 205 times.
296 if (reasons & DavSyncReasonManual) {
78
1/1
✓ Branch 6 → 7 taken 91 times.
91 m_locked.remove(accountId);
79
1/1
✓ Branch 7 → 8 taken 91 times.
91 m_holdUntil.remove(accountId);
80 }
81
1/1
✓ Branch 8 → 9 taken 296 times.
296 Pending &pending = m_pending[accountId];
82 296 const bool fresh = pending.reasons == 0;
83
4/4
✓ Branch 9 → 10 taken 293 times.
✓ Branch 9 → 11 taken 3 times.
✓ Branch 10 → 11 taken 113 times.
✓ Branch 10 → 12 taken 180 times.
296 pending.full = pending.full || full;
84 296 pending.reasons |= reasons;
85
2/2
✓ Branch 21 → 15 taken 181 times.
✓ Branch 21 → 22 taken 296 times.
477 for (const QString &path : paths)
86
3/3
✓ Branch 17 → 18 taken 74 times.
✓ Branch 17 → 19 taken 107 times.
✓ Branch 18 → 19 taken 74 times.
181 if (!pending.paths.contains(path)) pending.paths.append(path);
87
1/1
✓ Branch 22 → 23 taken 296 times.
296 const qint64 t = now();
88
2/2
✓ Branch 23 → 24 taken 172 times.
✓ Branch 23 → 29 taken 124 times.
296 if (reasons & kImmediate) {
89
2/2
✓ Branch 24 → 25 taken 159 times.
✓ Branch 24 → 26 taken 13 times.
172 pending.dueAt = fresh ? t : qMin(pending.dueAt, t);
90 } else {
91
2/2
✓ Branch 29 → 30 taken 42 times.
✓ Branch 29 → 31 taken 82 times.
124 if (pending.firstPushAt == 0) pending.firstPushAt = t;
92 124 const qint64 pushDue = qMin(t + m_timing.pushDebounceMs, pending.firstPushAt + m_timing.pushMaxDelayMs);
93 // Only push so far: the latest notice moves the start (coalescing).
94
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 35 taken 124 times.
124 pending.dueAt = (pending.reasons & kImmediate) ? qMin(pending.dueAt, pushDue) : pushDue;
95 }
96 }
97
98 288 qint64 DavSyncScheduler::earliestStart(const QString &accountId, const Pending &pending) const {
99 288 qint64 start = pending.dueAt;
100
2/2
✓ Branch 2 → 3 taken 195 times.
✓ Branch 2 → 6 taken 93 times.
288 if (!(pending.reasons & DavSyncReasonManual)) start = qMax(start, m_holdUntil.value(accountId, 0));
101
2/2
✓ Branch 6 → 7 taken 130 times.
✓ Branch 6 → 10 taken 158 times.
288 if (pending.reasons == DavSyncReasonPush)
102 130 start = qMax(start, m_lastStart.value(accountId, LLONG_MIN / 2) + m_timing.minBatchSpacingMs);
103 288 return start;
104 }
105
106 433 void DavSyncScheduler::evaluate() {
107
1/1
✓ Branch 2 → 3 taken 433 times.
433 const qint64 t = now();
108 // Fallback window after a push failure: regular short polls.
109
7/7
✓ Branch 3 → 4 taken 433 times.
✓ Branch 35 → 36 taken 446 times.
✓ Branch 35 → 40 taken 1 time.
✓ Branch 38 → 39 taken 14 times.
✓ Branch 38 → 40 taken 432 times.
✓ Branch 41 → 5 taken 14 times.
✓ Branch 41 → 42 taken 433 times.
447 for (auto it = m_fallbackUntil.begin(); !m_stopped && it != m_fallbackUntil.end();) {
110
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 24 taken 13 times.
14 if (t >= it.value()) {
111
6/6
✓ Branch 7 → 8 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 9 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
2 qCInfo(lcDavScheduler) << "Fallback window ended for" << it.key();
112
1/1
✓ Branch 20 → 21 taken 1 time.
1 m_nextFallbackPoll.remove(it.key());
113
1/1
✓ Branch 22 → 23 taken 1 time.
1 it = m_fallbackUntil.erase(it);
114 1 continue;
115 1 }
116
2/2
✓ Branch 26 → 27 taken 10 times.
✓ Branch 26 → 34 taken 3 times.
13 if (t >= m_nextFallbackPoll.value(it.key(), 0)) {
117
1/1
✓ Branch 28 → 29 taken 10 times.
10 m_nextFallbackPoll[it.key()] = t + m_timing.fallbackPollMs;
118
1/1
✓ Branch 31 → 32 taken 10 times.
10 mark(it.key(), true, {}, DavSyncReasonRecovery);
119 }
120 13 ++it;
121 }
122 433 qint64 wakeAt = LLONG_MAX;
123
2/2
✓ Branch 49 → 45 taken 13 times.
✓ Branch 49 → 50 taken 433 times.
446 for (qint64 next : std::as_const(m_nextFallbackPoll)) wakeAt = qMin(wakeAt, next);
124
4/5
✓ Branch 51 → 52 taken 433 times.
✗ Branch 51 → 54 not taken.
✓ Branch 52 → 53 taken 433 times.
✓ Branch 53 → 54 taken 431 times.
✓ Branch 53 → 55 taken 2 times.
433 const bool online = !m_networkGate || m_networkGate();
125
1/1
✓ Branch 56 → 57 taken 433 times.
433 const QStringList accounts = m_pending.keys();
126
2/2
✓ Branch 146 → 59 taken 343 times.
✓ Branch 146 → 147 taken 433 times.
776 for (const QString &accountId : accounts) {
127
6/6
✓ Branch 61 → 62 taken 293 times.
✓ Branch 61 → 63 taken 50 times.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 62 → 64 taken 290 times.
✓ Branch 65 → 66 taken 53 times.
✓ Branch 65 → 67 taken 290 times.
468 if (m_busy.contains(accountId) || !online) continue;
128 290 Pending pending = m_pending.value(accountId);
129
3/4
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 72 taken 289 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 72 taken 1 time.
290 if (m_stopped && !(pending.reasons & DavSyncReasonManual)) {
130 ✗ m_pending.remove(accountId); // background sync stopped: manual only
131 ✗ continue;
132 }
133
5/6
✓ Branch 73 → 74 taken 2 times.
✓ Branch 73 → 76 taken 288 times.
✓ Branch 74 → 75 taken 2 times.
✗ Branch 74 → 76 not taken.
✓ Branch 77 → 78 taken 2 times.
✓ Branch 77 → 91 taken 288 times.
290 if (m_locked.contains(accountId) && !(pending.reasons & kUnlocking)) {
134 // Broken credentials/TLS: no automatic retries until the next poll,
135 // a manual request or a configuration change.
136
6/6
✓ Branch 78 → 79 taken 2 times.
✓ Branch 82 → 83 taken 2 times.
✓ Branch 83 → 84 taken 2 times.
✓ Branch 84 → 85 taken 2 times.
✓ Branch 88 → 80 taken 2 times.
✓ Branch 88 → 89 taken 2 times.
4 qCInfo(lcDavScheduler) << "Account locked, dropping automatic request" << accountId;
137
1/1
✓ Branch 89 → 90 taken 2 times.
2 m_pending.remove(accountId);
138 2 continue;
139 2 }
140
1/1
✓ Branch 91 → 92 taken 288 times.
288 const qint64 start = earliestStart(accountId, pending);
141
2/2
✓ Branch 92 → 93 taken 120 times.
✓ Branch 92 → 95 taken 168 times.
288 if (start > t) {
142 120 wakeAt = qMin(wakeAt, start);
143 120 continue;
144 }
145
2/2
✓ Branch 96 → 97 taken 3 times.
✓ Branch 96 → 98 taken 165 times.
168 if (m_busy.size() >= m_timing.maxParallelAccounts) continue; // re-evaluated on finish
146
1/1
✓ Branch 98 → 99 taken 165 times.
165 m_pending.remove(accountId);
147 165 CalendarSyncRequest request;
148 165 request.accountId = accountId;
149 165 request.discover = pending.full;
150 165 request.forcedPaths = pending.paths;
151 165 request.reasons = pending.reasons;
152
1/1
✓ Branch 101 → 102 taken 165 times.
165 m_busy.insert(accountId);
153
1/1
✓ Branch 102 → 103 taken 165 times.
165 m_lastStart[accountId] = t;
154
9/9
✓ Branch 103 → 104 taken 165 times.
✓ Branch 107 → 108 taken 165 times.
✓ Branch 108 → 109 taken 165 times.
✓ Branch 109 → 110 taken 165 times.
✓ Branch 110 → 111 taken 165 times.
✓ Branch 111 → 112 taken 165 times.
✓ Branch 112 → 113 taken 165 times.
✓ Branch 120 → 105 taken 165 times.
✓ Branch 120 → 121 taken 165 times.
330 qCInfo(lcDavScheduler) << "Batch" << accountId << "full" << request.discover << "forced"
155
3/3
✓ Branch 114 → 115 taken 165 times.
✓ Branch 115 → 116 taken 165 times.
✓ Branch 116 → 117 taken 165 times.
165 << request.forcedPaths.size() << "reasons" << request.reasons;
156
1/1
✓ Branch 121 → 122 taken 165 times.
165 emit batchStarted(request);
157
3/3
✓ Branch 122 → 123 taken 165 times.
✓ Branch 123 → 124 taken 1 time.
✓ Branch 123 → 137 taken 164 times.
165 if (!m_executor(request)) {
158
1/1
✓ Branch 124 → 125 taken 1 time.
1 m_busy.remove(accountId);
159
6/6
✓ Branch 125 → 126 taken 1 time.
✓ Branch 129 → 130 taken 1 time.
✓ Branch 130 → 131 taken 1 time.
✓ Branch 131 → 132 taken 1 time.
✓ Branch 135 → 127 taken 1 time.
✓ Branch 135 → 136 taken 1 time.
2 qCInfo(lcDavScheduler) << "Batch not started for" << accountId;
160 }
161 290 }
162
3/3
✓ Branch 147 → 148 taken 133 times.
✓ Branch 147 → 151 taken 300 times.
✓ Branch 149 → 150 taken 133 times.
433 if (wakeAt != LLONG_MAX) m_wake.start(int(qBound(qint64(0), wakeAt - t, qint64(INT_MAX))));
163 433 }
164
165 130 void DavSyncScheduler::batchFinished(const CalendarSyncResult &result) {
166 130 m_busy.remove(result.accountId);
167
3/3
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 7 taken 125 times.
✓ Branch 5 → 6 taken 5 times.
130 if (lockingFailure(result.errorKind)) m_locked.insert(result.accountId);
168
6/6
✓ Branch 7 → 8 taken 122 times.
✓ Branch 7 → 11 taken 3 times.
✓ Branch 9 → 10 taken 115 times.
✓ Branch 9 → 11 taken 7 times.
✓ Branch 12 → 13 taken 115 times.
✓ Branch 12 → 14 taken 10 times.
125 else if (result.completed && result.errorKind.isEmpty()) m_locked.remove(result.accountId);
169
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 17 taken 129 times.
130 if (result.retryAfterSeconds > 0) holdUntil(result.accountId, now() + qint64(result.retryAfterSeconds) * 1000);
170 130 evaluate();
171 130 }
172
173 9 void DavSyncScheduler::enterFallback(const QString &accountId) {
174
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 6 times.
9 if (m_fallbackUntil.contains(accountId)) return;
175
6/6
✓ Branch 5 → 6 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
✓ Branch 15 → 7 taken 6 times.
✓ Branch 15 → 16 taken 6 times.
12 qCInfo(lcDavScheduler) << "Fallback polling for" << accountId;
176 6 m_fallbackUntil[accountId] = now() + m_timing.fallbackWindowMs;
177 6 m_nextFallbackPoll[accountId] = 0; // one immediate catch-up
178 6 evaluate();
179 }
180
181 2 void DavSyncScheduler::leaveFallback(const QString &accountId) {
182 2 m_fallbackUntil.remove(accountId);
183 2 m_nextFallbackPoll.remove(accountId);
184 2 }
185
186 474 bool DavSyncScheduler::inFallback(const QString &accountId) const {
187 474 return m_fallbackUntil.contains(accountId);
188 }
189
190 1 void DavSyncScheduler::holdUntil(const QString &accountId, qint64 at) {
191
1/1
✓ Branch 4 → 5 taken 1 time.
1 m_holdUntil[accountId] = qMax(m_holdUntil.value(accountId, 0), at);
192 1 }
193
194 ✗ void DavSyncScheduler::lockAccount(const QString &accountId) {
195 ✗ m_locked.insert(accountId);
196 ✗ }
197
198 1 void DavSyncScheduler::kick() {
199 1 evaluate();
200 1 }
201
202 12 void DavSyncScheduler::stop() {
203 12 m_stopped = true;
204 12 m_wake.stop();
205 12 m_pollTimer.stop();
206 12 m_nextPollAt = 0;
207 12 m_pending.clear();
208 12 m_busy.clear();
209 12 m_fallbackUntil.clear();
210 12 m_nextFallbackPoll.clear();
211 12 }
212