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: 100.0% 37 / 0 / 37
Functions: 100.0% 24 / 0 / 24
Branches: 83.3% 20 / 0 / 24

service/ConnectionHealthMonitor.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QObject>
4 #include <QNetworkInformation>
5 #include <QString>
6 #include <QTimer>
7
8 #include <functional>
9
10 #include "data/AccountConfig.h"
11
12 class ImapService;
13 class NetworkLifecycleCoordinator;
14
15 // ConnectionHealthMonitor (Sprint 72) wraps a single ImapService with three
16 // responsibilities that previously lived fragmented across MainWindow,
17 // MailController and SettingsSyncService:
18 //
19 // 1. Periodic liveness probes — calls
20 // ImapService::requestLivenessProbe(); ImapService decides whether
21 // that is a NOOP (Authenticated/Selected) or an IDLE DONE/OK
22 // round-trip (Idling) and runs an RTT-aware 3–15 s watchdog. Since
23 // Sprint 92 the primary connection is probed every 7 s (15 s on a
24 // known metered link) and secondaries every 120 s plus before use, so
25 // the main connection detects a dead socket within ~10 s.
26 // 2. Exponential-backoff reconnect — 5, 10, 20, 40, 80, 160, 300, 300, …
27 // seconds (corrected from SettingsSyncService, whose pre-increment
28 // started at 10 s). The Bug-24/T-405 overflow guard is preserved.
29 // Sprint 92 S92.5.4: after a transport failure (socket error, probe or
30 // command timeout, forced recovery) on a reachable network one extra
31 // jittered attempt runs first — 0.5–1.5 s for the primary, 1.5–3 s for
32 // secondaries, so the main connection reconnects first and one outage
33 // never creates a burst. Server rejections (BYE, protocol error) and an
34 // unreachable network start at 5 s.
35 // 3. System suspend/resume + network-change reactivity — on
36 // applicationSuspended → applicationActive and on offline → online /
37 // transport change it calls forceReconnect() so recovery starts in
38 // the first backoff window instead of waiting for a probe timeout.
39 //
40 // The monitor never touches private ImapService internals; it goes through
41 // the two public methods added in T-72.2 (requestLivenessProbe /
42 // abortForReconnect). One monitor instance per ImapService.
43 class ConnectionHealthMonitor : public QObject {
44 Q_OBJECT
45
46 public:
47 // systemWatchEnabled=true (default) marks this monitor as the single
48 // "primary" instance that wires the system-wide suspend + network-change
49 // hooks. Secondary monitors (body/search/sync) pass false so one OS event
50 // does not trigger N simultaneous forceReconnect() calls (Sprint 72
51 // post-review fix). Secondary monitors still run their own liveness
52 // probes and reconnect on real socket failures / state errors.
53 explicit ConnectionHealthMonitor(bool systemWatchEnabled = true,
54 QObject *parent = nullptr);
55 ~ConnectionHealthMonitor() override;
56
57 // Wire the monitor to an ImapService. Safe to call again after detach().
58 void attach(ImapService *imap);
59 // Disconnect signal/slot wiring from the previously-attached ImapService.
60 // Call BEFORE the service is destroyed or replaced so monitor slots
61 // cannot react to the dying socket's stateChanged/errorOccurred.
62 void detach();
63
64 // Account configuration for reconnect attempts. Empty host = invalid;
65 // scheduleReconnect() becomes a no-op until a real config is set.
66 void setReconnectConfig(const ImapConfig &cfg);
67 1 ImapConfig reconnectConfig() const { return m_config; }
68
69 // false during shutdown / account switch — stops both timers and
70 // suppresses scheduleReconnect(). True re-enables probing in
71 // Authenticated/Selected/Idling.
72 void setActive(bool active);
73 11 bool isActive() const { return m_active; }
74 // S92.5.7: an outage of this connection is being recovered (a reconnect
75 // is scheduled or the attempt is still connecting). False while healthy,
76 // inactive, intentionally disconnected or waiting for a lifecycle resume.
77 bool isRecovering() const;
78 bool isTransportMediumMetered() const;
79
80 // Probe interval (PROBE_PRIMARY_MS / PROBE_SECONDARY_MS). A shorter interval makes
81 // dead-socket detection faster at the cost of more NOOP traffic.
82 void setProbeInterval(int ms);
83
84 // Force immediate teardown + reconnect (suspend, network change).
85 // Resets backoff, sets the outage flag, and asks the ImapService to
86 // abort — the resulting State::Error fires the monitor's own reconnect
87 // arm with the first 5 s delay. A recovery generation adopts an already
88 // pending/in-progress outage reconnect so a stale socket event delivered
89 // immediately before the OS resume event cannot create a second attempt.
90 void forceReconnect(const QString &reason);
91 void handleRecoveryGeneration(quint64 generation, int trigger);
92
93 // T-72.7 unit-test seams — direct hooks for the suspend/network logic.
94 // Inline so tests do not need a QGuiApplication event loop.
95 // NOTE: only ApplicationSuspended counts as "was suspended". On Linux
96 // desktops ApplicationInactive simply means "window lost focus"; treating
97 // it as resume tore down healthy connections on every focus change
98 // (Sprint 72 post-review fix).
99 void handleApplicationState(Qt::ApplicationState current);
100 8 static bool detectTimerSkew(qint64 elapsedMs, qint64 intervalMs) {
101 // Treat only a clear multi-minute gap as resume. A busy UI thread can
102 // delay a 15 s timer by 30+ seconds without any suspend/network change.
103
3/4
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 5 taken 4 times.
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 5 not taken.
8 return elapsedMs >= 5 * 60 * 1000 && elapsedMs >= 10 * intervalMs;
104 }
105 15 static bool isReachabilityReconnectTrigger(
106 QNetworkInformation::Reachability reachability) {
107
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 2 times.
12 return reachability == QNetworkInformation::Reachability::Local ||
108
4/4
✓ Branch 2 → 3 taken 12 times.
✓ Branch 2 → 5 taken 3 times.
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 5 times.
27 reachability == QNetworkInformation::Reachability::Site ||
109 15 reachability == QNetworkInformation::Reachability::Online;
110 }
111 7 static bool isReachabilityReconnectTransition(
112 QNetworkInformation::Reachability previous,
113 QNetworkInformation::Reachability current) {
114
3/4
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 4 times.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 7 not taken.
10 return !isReachabilityReconnectTrigger(previous) &&
115 10 isReachabilityReconnectTrigger(current);
116 }
117 6 static bool isTransportReconnectTransition(
118 QNetworkInformation::TransportMedium previous,
119 QNetworkInformation::TransportMedium current) {
120
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 6 taken 1 time.
4 return previous != QNetworkInformation::TransportMedium::Unknown &&
121
4/4
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
10 current != QNetworkInformation::TransportMedium::Unknown &&
122 6 previous != current;
123 }
124
125 // S92.5.4 first-attempt window after a transport failure.
126 static constexpr int FAST_RECONNECT_PRIMARY_MIN_MS = 500;
127 static constexpr int FAST_RECONNECT_PRIMARY_MAX_MS = 1500;
128 static constexpr int FAST_RECONNECT_SECONDARY_MIN_MS = 1500;
129 static constexpr int FAST_RECONNECT_SECONDARY_MAX_MS = 3000;
130
131 // Test introspection.
132 7 int reconnectAttempts() const { return m_reconnectAttempts; }
133 28 bool isReconnectScheduled() const { return m_reconnectTimer.isActive(); }
134 9 int reconnectIntervalSeconds() const {
135
1/2
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 6 not taken.
9 return m_reconnectTimer.isActive() ? m_reconnectTimer.interval() / 1000
136 9 : -1;
137 }
138 8 int reconnectIntervalMs() const {
139
1/2
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 6 not taken.
8 return m_reconnectTimer.isActive() ? m_reconnectTimer.interval() : -1;
140 }
141
142 // Test/diagnostic state contract (Sprint 92 S92.0.2 replaced the former
143 // test friend declaration, Sprint 91 rule 8). No product caller uses it;
144 // tests drive the monitor's handlers and timers deterministically.
145 14 int &mutableReconnectAttempts() { return m_reconnectAttempts; }
146 3 bool &hadOutage() { return m_hadOutage; }
147 1 bool &intentionallyDisconnected() { return m_intentionallyDisconnected; }
148 2 bool &systemWatchEnabled() { return m_systemWatchEnabled; }
149 20 QTimer &probeTimer() { return m_probeTimer; }
150 4 QTimer &reconnectTimer() { return m_reconnectTimer; }
151 4 Qt::ApplicationState &lastAppState() { return m_lastAppState; }
152 7 qint64 &lastProbeMsecsSinceEpoch() { return m_lastProbeMsecsSinceEpoch; }
153 8 QNetworkInformation *&networkInformation() { return m_netInfo; }
154 5 NetworkLifecycleCoordinator *&lifecycle() { return m_lifecycle; }
155 18 quint64 &lastRecoveryGeneration() { return m_lastRecoveryGeneration; }
156 5 QNetworkInformation::Reachability &lastReachability() { return m_lastReachability; }
157 7 QNetworkInformation::TransportMedium &lastTransportMedium() {
158 7 return m_lastTransportMedium;
159 }
160 // S92.5.4: jitter source, returns a value in [minMs, maxMs].
161 13 std::function<int(int minMs, int maxMs)> &jitterSource() { return m_jitter; }
162 bool &fastReconnectUsed() { return m_fastReconnectUsed; }
163 void scheduleReconnect();
164 int backoffDelaySeconds() const;
165 int fastReconnectDelayMs() const;
166 bool fastReconnectAllowed() const;
167 void applyProbeCadence();
168
169 signals:
170 void statusMessage(const QString &message);
171 void reconnectScheduled(int delaySeconds);
172 void connectionRestored();
173 // T-87.G2: true on cellular/bluetooth (and when no platform signal is
174 // available, as the deterministic conservative default).
175 void transportMediumMeteredChanged(bool metered);
176
177 // Public only for the test/diagnostic state contract above.
178 public slots:
179 void onConnectionAttemptStarted();
180 void onIntentionalDisconnect();
181 void onDisconnectReason(int reason);
182 void onStateChanged(int newState);
183 void onErrorOccurred(const QString &error);
184 void onProbeTimer();
185 void onReconnect();
186
187 private:
188 void stopAllTimers();
189 void setupNetworkHooks();
190
191 ImapService *m_imap = nullptr;
192 ImapConfig m_config;
193 bool m_active = false;
194 // ctor param: only the primary monitor wires the suspend + network hooks.
195 bool m_systemWatchEnabled = true;
196 bool m_hadOutage = false;
197 bool m_intentionallyDisconnected = false;
198
199 QTimer m_probeTimer;
200 QTimer m_reconnectTimer;
201 int m_reconnectAttempts = 0;
202 // S92.5.4: reason of the last failure (-1: none yet) and the one fast
203 // attempt per outage; the fast attempt does not count as a backoff step.
204 int m_lastDisconnectReason = -1;
205 bool m_fastReconnectUsed = false;
206 bool m_fastAttemptPending = false;
207 std::function<int(int, int)> m_jitter;
208
209 // T-72.7: suspend/resume detection (QGuiApplication::applicationStateChanged).
210 Qt::ApplicationState m_lastAppState = Qt::ApplicationActive;
211 // T-72.7: timer-skew fallback for Linux desktops where
212 // applicationState is unreliable. Wall-clock at the last probe fire.
213 qint64 m_lastProbeMsecsSinceEpoch = 0;
214
215 // T-72.7: optional network-change hooks. Null if no backend available
216 // (e.g. minimal Linux container) — probes + timer-skew still cover us.
217 QNetworkInformation *m_netInfo = nullptr;
218 NetworkLifecycleCoordinator *m_lifecycle = nullptr;
219 quint64 m_lastRecoveryGeneration = 0;
220 QNetworkInformation::Reachability m_lastReachability =
221 QNetworkInformation::Reachability::Unknown;
222 QNetworkInformation::TransportMedium m_lastTransportMedium =
223 QNetworkInformation::TransportMedium::Unknown;
224
225 static constexpr int MAX_RECONNECT_DELAY = 300; // 5 min cap (Bug-24 guard)
226 static bool transportMediumIsMetered(QNetworkInformation::TransportMedium medium);
227 // Probe cadence (Sprint 92 S92.5.2/S92.5.3): the primary connection every
228 // 7 s (with the RTT-aware 3–15 s watchdog: detection p95 below 10 s), 15 s
229 // on a known metered link, secondaries every 120 s plus a probe before a
230 // user-visible command (ImapService probe-before-use).
231 static constexpr int PROBE_PRIMARY_MS = 7000;
232 static constexpr int PROBE_METERED_MS = 15000;
233 static constexpr int PROBE_SECONDARY_MS = 120000;
234 };
235