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.5% 220 / 0 / 228
Functions: 100.0% 22 / 0 / 22
Branches: 86.9% 306 / 0 / 352

service/ImapPushSession.cpp
Line Branch Exec Source
1 #include "ImapPushSession.h"
2
3 #include "ImapCommandTable.h"
4
5 #include <QLoggingCategory>
6 #include <QSslSocket>
7 #include <QTimer>
8
9 Q_DECLARE_LOGGING_CATEGORY(lcImap)
10
11 1401 ImapPushSession::ImapPushSession(QObject *owner, QSslSocket *socket,
12 ImapCommandTable &commands, const State &state,
13 1401 Deps deps)
14 1401 : m_socket(socket), m_commands(commands), m_state(state), m_deps(std::move(deps)),
15
3/6
✓ Branch 4 → 5 taken 1401 times.
✓ Branch 5 → 6 taken 1401 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1401 times.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
1401 m_idleRenewTimer(new QTimer(owner)),
16
3/6
✓ Branch 8 → 9 taken 1401 times.
✓ Branch 9 → 10 taken 1401 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1401 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
1401 m_livenessProbeWatchdog(new QTimer(owner)),
17
3/6
✓ Branch 12 → 13 taken 1401 times.
✓ Branch 13 → 14 taken 1401 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1401 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
1401 m_idleRenewWatchdog(new QTimer(owner)) {}
18
19 110 void ImapPushSession::startIdle() {
20
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 14 taken 96 times.
110 if (m_state != State::Selected) {
21
5/5
✓ Branch 3 → 4 taken 14 times.
✓ Branch 7 → 8 taken 14 times.
✓ Branch 8 → 9 taken 14 times.
✓ Branch 12 → 5 taken 14 times.
✓ Branch 12 → 13 taken 14 times.
28 qCWarning(lcImap) << "Cannot start IDLE: not in Selected state";
22 97 return;
23 }
24
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 26 taken 95 times.
96 if (m_isIdling) {
25
5/5
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 24 → 17 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
2 qCWarning(lcImap) << "Already idling";
26 1 return;
27 }
28
3/3
✓ Branch 26 → 27 taken 95 times.
✓ Branch 27 → 28 taken 78 times.
✓ Branch 27 → 39 taken 17 times.
95 if (!hasIdleCapability()) {
29
5/5
✓ Branch 28 → 29 taken 78 times.
✓ Branch 32 → 33 taken 78 times.
✓ Branch 33 → 34 taken 78 times.
✓ Branch 37 → 30 taken 78 times.
✓ Branch 37 → 38 taken 78 times.
156 qCWarning(lcImap) << "Server does not support IDLE";
30 78 return;
31 }
32
33 // A completion observer may request push before the tagged handler has
34 // drained user commands. Keep Selected until that work has completed.
35
7/7
✓ Branch 39 → 40 taken 17 times.
✓ Branch 40 → 41 taken 15 times.
✓ Branch 40 → 47 taken 2 times.
✓ Branch 43 → 44 taken 13 times.
✓ Branch 43 → 47 taken 2 times.
✓ Branch 49 → 50 taken 4 times.
✓ Branch 49 → 51 taken 13 times.
30 if (m_commands.hasStatefulCommandInFlight() || !m_commands.serializedCommands().isEmpty() ||
36
1/2
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 13 times.
13 !m_commands.deferredCommands().isEmpty()) {
37 4 m_idleStartPending = true;
38 4 return;
39 }
40 13 m_idleStartPending = false;
41 13 m_isIdling = true;
42
1/1
✓ Branch 51 → 52 taken 13 times.
13 m_idleTag = m_commands.nextTag();
43
1/5
✓ Branch 57 → 58 taken 13 times.
✗ Branch 101 → 102 not taken.
✗ Branch 101 → 103 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
26 m_commands.insertRecord(m_idleTag, ImapPendingCommand{QStringLiteral("IDLE")});
44
1/1
✓ Branch 60 → 61 taken 13 times.
13 auto cmd = m_idleTag + " IDLE\r\n";
45
7/7
✓ Branch 61 → 62 taken 13 times.
✓ Branch 65 → 66 taken 13 times.
✓ Branch 66 → 67 taken 13 times.
✓ Branch 67 → 68 taken 13 times.
✓ Branch 68 → 69 taken 13 times.
✓ Branch 73 → 63 taken 13 times.
✓ Branch 73 → 74 taken 13 times.
26 qCInfo(lcImap) << ">>>" << cmd.trimmed();
46 #ifdef MAILJD_E2E_TESTING
47
1/1
✓ Branch 77 → 78 taken 13 times.
13 reportE2eTransportPhase(QStringLiteral("command-write:IDLE"));
48 #endif
49
2/4
✗ Branch 74 → 75 not taken.
✗ Branch 75 → 76 not taken.
✓ Branch 80 → 81 taken 13 times.
✓ Branch 81 → 82 taken 13 times.
13 m_socket->write(cmd.toUtf8());
50
1/2
✗ Branch 77 → 78 not taken.
✓ Branch 83 → 84 taken 13 times.
13 setState(State::Idling);
51
1/2
✗ Branch 78 → 79 not taken.
✓ Branch 84 → 85 taken 13 times.
13 m_idleRenewTimer->start(IDLE_RENEW_MS);
52 13 }
53
54 20 void ImapPushSession::stopIdle() {
55
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 19 times.
20 if (!m_isIdling) {
56 1 return;
57 }
58
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 17 taken 17 times.
19 if (m_idleRenewWatchdog->isActive()) {
59
5/5
✓ Branch 6 → 7 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 15 → 8 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
4 qCDebug(lcImap) << "IDLE DONE/OK already pending — not sending DONE again";
60 2 return;
61 }
62
63
5/5
✓ Branch 17 → 18 taken 17 times.
✓ Branch 21 → 22 taken 17 times.
✓ Branch 22 → 23 taken 17 times.
✓ Branch 26 → 19 taken 17 times.
✓ Branch 26 → 27 taken 17 times.
34 qCInfo(lcImap) << ">>> DONE";
64 #ifdef MAILJD_E2E_TESTING
65
1/1
✓ Branch 30 → 31 taken 15 times.
15 reportE2eTransportPhase(QStringLiteral("command-write:IDLE_DONE"));
66 #endif
67 17 m_socket->write("DONE\r\n");
68 17 m_idleRenewTimer->stop();
69 // T-720: Arm a watchdog for the IDLE tagged OK that proves the
70 // DONE actually reached the server and was ACKed. Covers renew
71 // (onIdleRenew), executeAfterIdle and liveness-probe paths. The
72 // handleTagged() IDLE branch stops this timer on OK arrival.
73 // S92.5.3: the budget follows the measured round-trip time.
74 17 m_doneSent.start();
75 17 m_idleRenewWatchdog->start(probeWatchdogMs());
76 // m_isIdling will be cleared when we receive the tagged OK for IDLE
77 }
78
79 1 void ImapPushSession::onIdleRenew() {
80
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
1 if (!m_isIdling) {
81 1 return;
82 }
83 ✗ qCInfo(lcImap) << "IDLE renew: sending DONE + re-IDLE";
84 // Stop current IDLE, will re-start after receiving tagged OK
85 ✗ stopIdle();
86 // The re-start happens in handleTagged when IDLE OK is received
87 }
88
89 // T-320: Centralized push restart — prefers NOTIFY over IDLE.
90 // Called from every handler that previously did "if (m_autoIdle) startIdle()".
91 143 void ImapPushSession::restartPush() {
92
4/4
✓ Branch 3 → 4 taken 44 times.
✓ Branch 3 → 8 taken 11 times.
✓ Branch 4 → 5 taken 43 times.
✓ Branch 4 → 8 taken 1 time.
55 if (!m_autoIdle || m_state != State::Selected || m_isIdling ||
93
8/8
✓ Branch 2 → 3 taken 55 times.
✓ Branch 2 → 8 taken 88 times.
✓ Branch 5 → 6 taken 42 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 39 times.
✓ Branch 10 → 11 taken 104 times.
✓ Branch 10 → 12 taken 39 times.
198 m_isNotifying || m_commands.hasStatefulCommandInFlight())
94 104 return;
95
96 // T-84.C1: a tagged command is not complete from the protocol scheduler's
97 // point of view until work queued by its terminal signal has been observed.
98 // Drain commands while remaining in Selected; only the first command that
99 // actually starts a round trip stops this loop. This also handles harmless
100 // no-op callbacks without starting push between two queued mutations.
101
5/6
✓ Branch 33 → 34 taken 43 times.
✓ Branch 33 → 36 taken 9 times.
✓ Branch 34 → 35 taken 43 times.
✗ Branch 34 → 36 not taken.
✓ Branch 37 → 13 taken 43 times.
✓ Branch 37 → 38 taken 9 times.
52 while (!m_commands.hasStatefulCommandInFlight() && m_state == State::Selected) {
102
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 22 taken 41 times.
43 if (!m_commands.deferredCommands().isEmpty()) {
103
1/1
✓ Branch 17 → 18 taken 2 times.
2 auto command = m_commands.deferredCommands().dequeue();
104
1/1
✓ Branch 18 → 19 taken 2 times.
2 command();
105 2 continue;
106 2 }
107
2/2
✓ Branch 24 → 25 taken 11 times.
✓ Branch 24 → 31 taken 30 times.
41 if (!m_commands.serializedCommands().isEmpty()) {
108
1/1
✓ Branch 26 → 27 taken 11 times.
11 auto command = m_commands.serializedCommands().dequeue();
109
1/1
✓ Branch 27 → 28 taken 11 times.
11 command();
110 11 continue;
111 11 }
112 30 break;
113 }
114
115
1/2
✓ Branch 40 → 41 taken 30 times.
✗ Branch 40 → 47 not taken.
69 if (m_commands.hasStatefulCommandInFlight() || m_state != State::Selected ||
116
6/8
✓ Branch 39 → 40 taken 30 times.
✓ Branch 39 → 47 taken 9 times.
✓ Branch 43 → 44 taken 30 times.
✗ Branch 43 → 47 not taken.
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 30 times.
✓ Branch 49 → 50 taken 9 times.
✓ Branch 49 → 51 taken 30 times.
69 !m_commands.deferredCommands().isEmpty() || !m_commands.serializedCommands().isEmpty())
117 9 return;
118
119
5/6
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 56 taken 29 times.
✓ Branch 54 → 55 taken 1 time.
✗ Branch 54 → 56 not taken.
✓ Branch 57 → 58 taken 1 time.
✓ Branch 57 → 59 taken 29 times.
30 if (usesNotifyPush() && !m_notifyFolders.isEmpty()) {
120 1 startNotify(m_notifyFolders);
121 } else {
122 29 startIdle();
123 }
124 }
125
126 10 void ImapPushSession::startNotify(const QStringList &subscribedFolders) {
127
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 14 taken 8 times.
10 if (m_state != State::Selected) {
128
5/5
✓ Branch 3 → 4 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 12 → 5 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
4 qCWarning(lcImap) << "Cannot start NOTIFY: not in Selected state";
129 2 return;
130 }
131
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 26 taken 6 times.
8 if (m_isNotifying) {
132
5/5
✓ Branch 15 → 16 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 24 → 17 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
4 qCWarning(lcImap) << "Already notifying";
133 2 return;
134 }
135
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 39 taken 5 times.
6 if (!hasNotifyCapability()) {
136
5/5
✓ Branch 28 → 29 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 37 → 30 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
2 qCWarning(lcImap) << "Server does not support NOTIFY";
137 1 return;
138 }
139
140 5 m_isNotifying = true;
141 5 m_notifyFolders = subscribedFolders;
142 // T-720: Use m_commands.sendTaggedCommand() so NOTIFY SET goes through the regular
143 // command timeout machinery (a server that never ACKs the SET can no
144 // longer stall push setup — it gets failed after COMMAND_TIMEOUT_MS).
145
1/1
✓ Branch 46 → 47 taken 5 times.
15 m_notifyTag = m_commands.sendTaggedCommand(
146 10 QStringLiteral("NOTIFY"),
147 10 QStringLiteral("NOTIFY SET STATUS"
148 " (selected (MessageNew MessageExpunge FlagChange))"
149 5 " (subscribed (MessageNew MessageExpunge FlagChange))"));
150 5 setState(State::Selected); // NOTIFY doesn't change state like IDLE does
151 }
152
153 6 void ImapPushSession::stopNotify() {
154
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 4 times.
6 if (!m_isNotifying) {
155 2 return;
156 }
157
158 // T-720: Use m_commands.sendTaggedCommand() so NOTIFY NONE has command-timeout
159 // coverage symmetric to NOTIFY SET.
160
1/1
✓ Branch 10 → 11 taken 4 times.
16 m_notifyTag = m_commands.sendTaggedCommand(QStringLiteral("NOTIFY_NONE"),
161 12 QStringLiteral("NOTIFY NONE"));
162 // m_isNotifying cleared when NOTIFY_NONE OK arrives in handleTagged
163 }
164
165 1 void ImapPushSession::executeAfterNotify(std::function<void()> command) {
166 // T-320: NOTIFY is NOT a blocking state like IDLE — commands can be
167 // sent freely while NOTIFY is active (RFC 5465). Just execute directly.
168 1 command();
169 1 }
170
171 // T-540: NOOP command for keep-alive (body connection)
172 5 void ImapPushSession::sendNoop() {
173
4/4
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
5 if (m_state != State::Authenticated && m_state != State::Selected) {
174 1 return; // silently skip if not connected
175 }
176
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 11 taken 2 times.
4 if (m_commands.hasStatefulCommandInFlight()) {
177
1/1
✓ Branch 8 → 9 taken 2 times.
3 m_commands.enqueueSerializedCommand([this]() { sendNoop(); });
178 2 return;
179 }
180
3/3
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
2 m_commands.sendCommand("NOOP", "NOOP");
181 }
182
183 14 void ImapPushSession::sendProbeNoop() {
184 // Caller ensures state is Authenticated/Selected and no stateful command
185 // is in flight. Tag is remembered so handleTagged() can clear it on the
186 // matching tagged response.
187
3/3
✓ Branch 2 → 3 taken 14 times.
✓ Branch 3 → 4 taken 14 times.
✓ Branch 4 → 5 taken 14 times.
14 m_probeTag = m_commands.sendTaggedCommand("NOOP", "NOOP");
188 14 m_livenessProbeWatchdog->start(probeWatchdogMs());
189
6/6
✓ Branch 12 → 13 taken 14 times.
✓ Branch 16 → 17 taken 14 times.
✓ Branch 17 → 18 taken 14 times.
✓ Branch 18 → 19 taken 14 times.
✓ Branch 27 → 14 taken 14 times.
✓ Branch 27 → 28 taken 14 times.
28 qCInfo(lcImap) << "Liveness probe sent (NOOP" << m_probeTag
190
2/2
✓ Branch 19 → 20 taken 14 times.
✓ Branch 20 → 21 taken 14 times.
14 << ") reason:" << m_lastProbeReason
191
3/3
✓ Branch 21 → 22 taken 14 times.
✓ Branch 22 → 23 taken 14 times.
✓ Branch 23 → 24 taken 14 times.
14 << "watchdog ms:" << m_livenessProbeWatchdog->interval();
192 14 }
193
194 25 void ImapPushSession::requestLivenessProbe(const QString &reason) {
195 // Idempotent while a probe is already running.
196
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 16 taken 21 times.
25 if (!m_probeTag.isEmpty()) {
197
5/5
✓ Branch 4 → 5 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 14 → 6 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
8 qCDebug(lcImap) << "Liveness probe already in flight — ignoring"
198
1/1
✓ Branch 10 → 11 taken 4 times.
4 << reason;
199 4 return;
200 }
201 21 m_lastProbeReason = reason;
202
203
3/4
✓ Branch 17 → 18 taken 12 times.
✓ Branch 17 → 34 taken 4 times.
✓ Branch 17 → 47 taken 5 times.
✗ Branch 17 → 48 not taken.
21 switch (m_state) {
204 12 case State::Authenticated:
205 case State::Selected: {
206 // NOOP is the canonical probe. If a stateful command is in flight,
207 // the live round-trip already proves liveness — reset the probe state
208 // and rely on the command timeout to catch a stuck command.
209
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 32 taken 12 times.
12 if (m_commands.hasStatefulCommandInFlight()) {
210 ✗ qCDebug(lcImap) << "Liveness probe skipped — stateful command"
211 ✗ << "already in flight (command timeout covers it)";
212 ✗ return;
213 }
214 12 sendProbeNoop();
215 12 return;
216 }
217 4 case State::Idling: {
218 // NOOP is illegal during IDLE (RFC 2177). DONE → tagged OK is the
219 // round-trip that proves liveness; restartPush() re-arms push after.
220
6/6
✓ Branch 34 → 35 taken 4 times.
✓ Branch 38 → 39 taken 4 times.
✓ Branch 39 → 40 taken 4 times.
✓ Branch 40 → 41 taken 4 times.
✓ Branch 44 → 36 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
8 qCInfo(lcImap) << "Liveness probe via IDLE DONE/OK — reason:" << reason;
221 4 stopIdle();
222 4 return;
223 }
224 5 case State::Connecting:
225 case State::Connected:
226 case State::Greeting:
227 case State::Capability:
228 case State::StartingTLS:
229 case State::Authenticating:
230 case State::Disconnected:
231 case State::Error:
232 // These states are either making progress (handled by the connect
233 // timeout / command timeout) or already terminal. The monitor's
234 // reconnect arm will pick them up.
235 5 return;
236 }
237 }
238
239 4 void ImapPushSession::verifyAfterPathFailure(const QString &reason) {
240 4 const qint64 quiet = m_deps.quietMs();
241
4/4
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 18 taken 2 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 18 taken 1 time.
4 if (quiet >= 0 && quiet < PATH_ALIVE_MS) {
242
5/5
✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 16 → 7 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
2 qCInfo(lcImap) << "Path failure elsewhere — connection received data"
243
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
1 << quiet << "ms ago, kept";
244 1 return;
245 }
246 // A socket closed underneath the session cannot answer a probe.
247
5/6
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 25 taken 2 times.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 37 taken 2 times.
4 if (quiet >= 0 && m_socket->state() != QAbstractSocket::ConnectedState &&
248
2/4
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 24 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 1 time.
1 (m_state == State::Authenticated || m_state == State::Selected ||
249 ✗ m_state == State::Idling)) {
250
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
2 failConnection(QStringLiteral("IMAP socket closed after path failure: %1").arg(reason),
251 DisconnectReason::SocketError);
252 1 return;
253 }
254
4/6
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 42 not taken.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 54 taken 1 time.
4 if ((m_state == State::Authenticated || m_state == State::Selected) &&
255
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 42 taken 1 time.
2 m_commands.hasStatefulCommandInFlight()) {
256 // The command was written into the path the other connection found
257 // dead; waiting for its 30 s command timeout would stall the work
258 // queued behind it.
259
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
2 failConnection(QStringLiteral("IMAP command stuck after path failure: %1").arg(reason),
260 DisconnectReason::CommandTimeout);
261 1 return;
262 }
263
2/2
✓ Branch 58 → 59 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
2 requestLivenessProbe(QStringLiteral("path failure: %1").arg(reason));
264 }
265
266 3 void ImapPushSession::onLivenessProbeTimeout() {
267
1/1
✓ Branch 7 → 8 taken 3 times.
3 failConnection(
268
1/1
✓ Branch 6 → 7 taken 3 times.
9 QStringLiteral("IMAP liveness probe timeout: %1").arg(m_lastProbeReason),
269 DisconnectReason::CommandTimeout);
270 3 }
271
272 1 void ImapPushSession::onIdleRenewWatchdogTimeout() {
273
1/1
✓ Branch 7 → 8 taken 1 time.
3 failConnection(QStringLiteral(
274 "IDLE DONE/OK round-trip timeout (probe reason: %1)")
275
1/1
✓ Branch 6 → 7 taken 1 time.
2 .arg(m_lastProbeReason),
276 DisconnectReason::CommandTimeout);
277 1 }
278 10 void ImapPushSession::handleTaggedIdle(const TaggedResponse &response) {
279 10 m_isIdling = false;
280 10 setState(State::Selected);
281
2/2
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 31 taken 1 time.
10 if (response.status == "OK") {
282 // T-720: The DONE/OK round-trip (renew, executeAfterIdle, or liveness
283 // probe) proved the connection is alive — disarm its watchdog.
284 9 m_idleRenewWatchdog->stop();
285
2/2
✓ Branch 7 → 8 taken 8 times.
✓ Branch 7 → 10 taken 1 time.
9 if (m_doneSent.isValid())
286 8 noteRoundTrip(m_doneSent.elapsed());
287 9 m_doneSent.invalidate();
288 // S106-004: the renew timer is still armed only when the server ended
289 // IDLE on its own (every client DONE stops it in stopIdle()). That case
290 // used to be taken for a "manual stop" and push stayed off; now push
291 // restarts in every case. restartPush() itself refuses when push is off
292 // (autoIdle) or the mailbox is no longer selected.
293 9 const bool serverEnded = m_idleRenewTimer->isActive();
294 9 m_idleRenewTimer->stop();
295
7/7
✓ Branch 13 → 14 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 8 times.
✓ Branch 21 → 22 taken 9 times.
✓ Branch 25 → 15 taken 9 times.
✓ Branch 25 → 26 taken 9 times.
18 qCInfo(lcImap) << (serverEnded ? "IDLE ended by the server" : "IDLE ended normally");
296 // Process deferred commands first
297
2/2
✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 30 taken 5 times.
9 if (!m_commands.deferredCommands().isEmpty())
298 4 m_commands.runNextDeferredCommand();
299 else
300 5 restartPush();
301 } else {
302 // T-720: Treat a non-OK IDLE response as a dead connection. The old
303 // code only drained deferred commands and left the socket nominally
304 // open — failing the sprint's dead-socket detection goal and leaving
305 // the watchdog disarmed.
306
6/6
✓ Branch 31 → 32 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 42 → 33 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
2 qCWarning(lcImap) << "IDLE rejected by server:" << response.message
307
1/1
✓ Branch 38 → 39 taken 1 time.
1 << "— failing connection";
308 1 m_idleRenewWatchdog->stop();
309
1/1
✓ Branch 49 → 50 taken 1 time.
1 failConnection(
310
1/1
✓ Branch 48 → 49 taken 1 time.
3 QStringLiteral("IDLE rejected by server: %1").arg(response.message));
311 1 return;
312 }
313 }
314
315 6 void ImapPushSession::handleTaggedNotify(const TaggedResponse &response) {
316
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 18 taken 3 times.
6 if (response.status == "OK") {
317
5/5
✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 16 → 6 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
6 qCInfo(lcImap) << "NOTIFY active — watching"
318
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
3 << m_notifyFolders.size() << "folders";
319 } else {
320
6/6
✓ Branch 18 → 19 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 28 → 20 taken 3 times.
✓ Branch 28 → 29 taken 3 times.
6 qCWarning(lcImap) << "NOTIFY failed:" << response.message;
321 3 m_isNotifying = false;
322 // Fallback: try IDLE instead
323
6/6
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 33 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 46 taken 2 times.
3 if (m_autoIdle && hasIdleCapability()) {
324
5/5
✓ Branch 35 → 36 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 44 → 37 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
2 qCInfo(lcImap) << "Falling back to IDLE";
325 1 startIdle();
326 }
327 }
328 6 }
329
330 7 void ImapPushSession::handleTaggedNotifyNone(const TaggedResponse &response) {
331 7 m_isNotifying = false;
332
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 18 taken 3 times.
7 if (response.status == "OK") {
333
5/5
✓ Branch 4 → 5 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 13 → 6 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
8 qCInfo(lcImap) << "NOTIFY stopped";
334
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 33 taken 2 times.
4 if (!m_commands.deferredCommands().isEmpty()) {
335 2 m_commands.runNextDeferredCommand();
336 }
337 } else {
338
6/6
✓ Branch 18 → 19 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 28 → 20 taken 3 times.
✓ Branch 28 → 29 taken 3 times.
6 qCWarning(lcImap) << "NOTIFY NONE failed:" << response.message;
339
2/2
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 1 time.
3 if (!m_commands.deferredCommands().isEmpty()) {
340 2 m_commands.runNextDeferredCommand();
341 }
342 }
343 7 }
344
345 // S92.5.3: smoothed round-trip time of the minimal commands (probe NOOP and
346 // the IDLE DONE/OK exchange), RFC 6298 weighting (alpha 1/8).
347 138 void ImapPushSession::noteRoundTrip(qint64 elapsedMs) {
348 138 const double sample = double(qMax<qint64>(0, elapsedMs));
349
2/2
✓ Branch 4 → 5 taken 124 times.
✓ Branch 4 → 7 taken 14 times.
138 m_smoothedRttMs = m_smoothedRttMs ? *m_smoothedRttMs * 7.0 / 8.0 + sample / 8.0 : sample;
350
8/8
✓ Branch 9 → 10 taken 138 times.
✓ Branch 13 → 14 taken 138 times.
✓ Branch 14 → 15 taken 138 times.
✓ Branch 15 → 16 taken 138 times.
✓ Branch 16 → 17 taken 138 times.
✓ Branch 18 → 19 taken 138 times.
✓ Branch 26 → 11 taken 138 times.
✓ Branch 26 → 27 taken 138 times.
276 qCDebug(lcImap) << "Round trip" << elapsedMs << "ms, smoothed" << *m_smoothedRttMs
351
4/4
✓ Branch 19 → 20 taken 138 times.
✓ Branch 20 → 21 taken 138 times.
✓ Branch 21 → 22 taken 138 times.
✓ Branch 22 → 23 taken 138 times.
138 << "ms, probe watchdog" << probeWatchdogMs() << "ms";
352 138 }
353
354 // S92.5.3: a healthy NOOP or DONE/OK answers within a few round trips, so the
355 // liveness watchdog is clamp(10 x SRTT, 3 s, 15 s); before the first sample of
356 // a session it keeps the conservative 15 s.
357 180 int ImapPushSession::probeWatchdogMs() const {
358
2/2
✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 5 taken 152 times.
180 if (!m_smoothedRttMs)
359 28 return PROBE_WATCHDOG_MS;
360 152 const double scaled = *m_smoothedRttMs * PROBE_WATCHDOG_RTT_FACTOR;
361
1/1
✓ Branch 6 → 7 taken 152 times.
152 return int(qBound(double(PROBE_WATCHDOG_MIN_MS), scaled, double(PROBE_WATCHDOG_MS)));
362 }
363
364 74 void ImapPushSession::resetRoundTrip() {
365 74 m_smoothedRttMs.reset();
366 74 m_doneSent.invalidate();
367 74 }
368
369 // S92.5.2: probe before use. A command the user waits for (folder open, body
370 // demand, mutation, APPEND) on a connection that received nothing for
371 // PROBE_BEFORE_USE_QUIET_MS first sends the liveness probe. The command
372 // queues behind it (a probe NOOP is a stateful command) and runs on its
373 // tagged OK; a dead connection fails after the RTT-aware watchdog instead of
374 // the 30 s command timeout, and the queued command is cancelled before
375 // dispatch (journal tokens retry after the reconnect). While idling, the
376 // DONE/OK exchange in front of the command is the probe. Returns false when
377 // it failed the connection; the caller then re-evaluates the command, which
378 // takes its regular not-connected path.
379 246 bool ImapPushSession::probeBeforeUse(const char *command) {
380
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 246 times.
246 if (!m_probeTag.isEmpty())
381 ✗ return true;
382
3/4
✓ Branch 5 → 6 taken 165 times.
✓ Branch 5 → 8 taken 81 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 165 times.
246 if (m_state != State::Authenticated && m_state != State::Selected)
383 ✗ return true;
384
2/2
✓ Branch 9 → 10 taken 47 times.
✓ Branch 9 → 11 taken 199 times.
246 if (m_commands.hasStatefulCommandInFlight())
385 47 return true;
386 199 const qint64 quiet = m_deps.quietMs();
387
2/2
✓ Branch 12 → 13 taken 89 times.
✓ Branch 12 → 14 taken 110 times.
199 if (quiet < 0)
388 89 return true; // nothing received yet in this session
389 // A socket that was closed underneath the session without an error
390 // signal cannot answer at all: reconnect now instead of writing the
391 // command into it and waiting for the command timeout.
392
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 27 taken 108 times.
110 if (m_socket->state() != QAbstractSocket::ConnectedState) {
393
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
4 failConnection(QStringLiteral("IMAP socket closed before %1").arg(QLatin1String(command)),
394 DisconnectReason::SocketError);
395 2 return false;
396 }
397
2/2
✓ Branch 27 → 28 taken 106 times.
✓ Branch 27 → 29 taken 2 times.
108 if (quiet < PROBE_BEFORE_USE_QUIET_MS)
398 106 return true;
399 4 m_lastProbeReason = QStringLiteral("before %1 after %2 ms quiet")
400
1/1
✓ Branch 34 → 35 taken 2 times.
4 .arg(QLatin1String(command))
401
1/1
✓ Branch 36 → 37 taken 2 times.
2 .arg(quiet);
402 2 sendProbeNoop();
403 2 return true;
404 }
405