MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - service - ConnectionHealthMonitor.h (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 100.0 % 19 19
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 8 8
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 86.4 % 22 19

             Branch data     Line data    Source code
       1                 :             : #pragma once
       2                 :             : 
       3                 :             : #include <QObject>
       4                 :             : #include <QNetworkInformation>
       5                 :             : #include <QString>
       6                 :             : #include <QTimer>
       7                 :             : 
       8                 :             : #include "data/AccountConfig.h"
       9                 :             : 
      10                 :             : class ImapService;
      11                 :             : 
      12                 :             : // ConnectionHealthMonitor (Sprint 72) wraps a single ImapService with three
      13                 :             : // responsibilities that previously lived fragmented across MainWindow,
      14                 :             : // MailController and SettingsSyncService:
      15                 :             : //
      16                 :             : //   1. Periodic liveness probes (15 s) — calls
      17                 :             : //      ImapService::requestLivenessProbe(); ImapService decides whether
      18                 :             : //      that is a NOOP (Authenticated/Selected) or an IDLE DONE/OK
      19                 :             : //      round-trip (Idling) and runs a 15 s watchdog. Combined worst-case
      20                 :             : //      dead-socket detection ≈ 30 s (vs. ~40 min before Sprint 72).
      21                 :             : //   2. Exponential-backoff reconnect — 5, 10, 20, 40, 80, 160, 300, 300, …
      22                 :             : //      seconds (corrected from SettingsSyncService, whose pre-increment
      23                 :             : //      started at 10 s). The Bug-24/T-405 overflow guard is preserved.
      24                 :             : //   3. System suspend/resume + network-change reactivity — on
      25                 :             : //      applicationSuspended → applicationActive and on offline → online /
      26                 :             : //      transport change it calls forceReconnect() so recovery starts in
      27                 :             : //      the first backoff window instead of waiting for a probe timeout.
      28                 :             : //
      29                 :             : // The monitor never touches private ImapService internals; it goes through
      30                 :             : // the two public methods added in T-72.2 (requestLivenessProbe /
      31                 :             : // abortForReconnect). One monitor instance per ImapService.
      32                 :             : class ConnectionHealthMonitor : public QObject {
      33                 :             :   Q_OBJECT
      34                 :             : #ifdef MAILJD_UNIT_TEST
      35                 :             :   friend class TestConnectionHealth;
      36                 :             : #endif
      37                 :             : 
      38                 :             : public:
      39                 :             :   // systemWatchEnabled=true (default) marks this monitor as the single
      40                 :             :   // "primary" instance that wires the system-wide suspend + network-change
      41                 :             :   // hooks. Secondary monitors (body/search/sync) pass false so one OS event
      42                 :             :   // does not trigger N simultaneous forceReconnect() calls (Sprint 72
      43                 :             :   // post-review fix). Secondary monitors still run their own liveness
      44                 :             :   // probes and reconnect on real socket failures / state errors.
      45                 :             :   explicit ConnectionHealthMonitor(bool systemWatchEnabled = true,
      46                 :             :                                    QObject *parent = nullptr);
      47                 :             :   ~ConnectionHealthMonitor() override;
      48                 :             : 
      49                 :             :   // Wire the monitor to an ImapService. Safe to call again after detach().
      50                 :             :   void attach(ImapService *imap);
      51                 :             :   // Disconnect signal/slot wiring from the previously-attached ImapService.
      52                 :             :   // Call BEFORE the service is destroyed or replaced so monitor slots
      53                 :             :   // cannot react to the dying socket's stateChanged/errorOccurred.
      54                 :             :   void detach();
      55                 :             : 
      56                 :             :   // Account configuration for reconnect attempts. Empty host = invalid;
      57                 :             :   // scheduleReconnect() becomes a no-op until a real config is set.
      58                 :             :   void setReconnectConfig(const ImapConfig &cfg);
      59                 :             :   ImapConfig reconnectConfig() const { return m_config; }
      60                 :             : 
      61                 :             :   // false during shutdown / account switch — stops both timers and
      62                 :             :   // suppresses scheduleReconnect(). True re-enables probing in
      63                 :             :   // Authenticated/Selected/Idling.
      64                 :             :   void setActive(bool active);
      65                 :           3 :   bool isActive() const { return m_active; }
      66                 :             : 
      67                 :             :   // Probe interval (default PROBE_DEFAULT_MS). A shorter interval makes
      68                 :             :   // dead-socket detection faster at the cost of more NOOP traffic.
      69                 :             :   void setProbeInterval(int ms);
      70                 :             : 
      71                 :             :   // Force immediate teardown + reconnect (suspend, network change).
      72                 :             :   // Resets backoff, sets the outage flag, and asks the ImapService to
      73                 :             :   // abort — the resulting State::Error fires the monitor's own reconnect
      74                 :             :   // arm with the first 5 s delay.
      75                 :             :   void forceReconnect(const QString &reason);
      76                 :             : 
      77                 :             :   // T-72.7 unit-test seams — direct hooks for the suspend/network logic.
      78                 :             :   // Inline so tests do not need a QGuiApplication event loop.
      79                 :             :   // NOTE: only ApplicationSuspended counts as "was suspended". On Linux
      80                 :             :   // desktops ApplicationInactive simply means "window lost focus"; treating
      81                 :             :   // it as resume tore down healthy connections on every focus change
      82                 :             :   // (Sprint 72 post-review fix).
      83                 :             :   void handleApplicationState(Qt::ApplicationState current);
      84                 :          42 :   static bool detectTimerSkew(qint64 elapsedMs, qint64 intervalMs) {
      85                 :             :     // Treat only a clear multi-minute gap as resume. A busy UI thread can
      86                 :             :     // delay a 15 s timer by 30+ seconds without any suspend/network change.
      87   [ +  +  +  - ]:          42 :     return elapsedMs >= 5 * 60 * 1000 && elapsedMs >= 10 * intervalMs;
      88                 :             :   }
      89                 :          12 :   static bool isReachabilityReconnectTrigger(
      90                 :             :       QNetworkInformation::Reachability reachability) {
      91         [ +  + ]:          10 :     return reachability == QNetworkInformation::Reachability::Local ||
      92   [ +  +  +  + ]:          22 :            reachability == QNetworkInformation::Reachability::Site ||
      93                 :          12 :            reachability == QNetworkInformation::Reachability::Online;
      94                 :             :   }
      95                 :           5 :   static bool isReachabilityReconnectTransition(
      96                 :             :       QNetworkInformation::Reachability previous,
      97                 :             :       QNetworkInformation::Reachability current) {
      98   [ +  +  +  - ]:           7 :     return !isReachabilityReconnectTrigger(previous) &&
      99                 :           7 :            isReachabilityReconnectTrigger(current);
     100                 :             :   }
     101                 :           4 :   static bool isTransportReconnectTransition(
     102                 :             :       QNetworkInformation::TransportMedium previous,
     103                 :             :       QNetworkInformation::TransportMedium current) {
     104         [ +  + ]:           3 :     return previous != QNetworkInformation::TransportMedium::Unknown &&
     105   [ +  +  +  + ]:           7 :            current != QNetworkInformation::TransportMedium::Unknown &&
     106                 :           4 :            previous != current;
     107                 :             :   }
     108                 :             : 
     109                 :             :   // Test introspection.
     110                 :           3 :   int reconnectAttempts() const { return m_reconnectAttempts; }
     111                 :          12 :   bool isReconnectScheduled() const { return m_reconnectTimer.isActive(); }
     112                 :           4 :   int reconnectIntervalSeconds() const {
     113         [ +  - ]:           4 :     return m_reconnectTimer.isActive() ? m_reconnectTimer.interval() / 1000
     114                 :           4 :                                        : -1;
     115                 :             :   }
     116                 :             : 
     117                 :             : signals:
     118                 :             :   void statusMessage(const QString &message);
     119                 :             :   void reconnectScheduled(int delaySeconds);
     120                 :             :   void connectionRestored();
     121                 :             : 
     122                 :             : private slots:
     123                 :             :   void onStateChanged(int newState);
     124                 :             :   void onErrorOccurred(const QString &error);
     125                 :             :   void onProbeTimer();
     126                 :             :   void onReconnect();
     127                 :             : 
     128                 :             : private:
     129                 :             :   void scheduleReconnect();
     130                 :             :   int backoffDelaySeconds() const;
     131                 :             :   void stopAllTimers();
     132                 :             :   void setupNetworkHooks();
     133                 :             : 
     134                 :             :   ImapService *m_imap = nullptr;
     135                 :             :   ImapConfig m_config;
     136                 :             :   bool m_active = false;
     137                 :             :   // ctor param: only the primary monitor wires the suspend + network hooks.
     138                 :             :   bool m_systemWatchEnabled = true;
     139                 :             :   bool m_hadOutage = false;
     140                 :             : 
     141                 :             :   QTimer m_probeTimer;
     142                 :             :   QTimer m_reconnectTimer;
     143                 :             :   int m_reconnectAttempts = 0;
     144                 :             : 
     145                 :             :   // T-72.7: suspend/resume detection (QGuiApplication::applicationStateChanged).
     146                 :             :   Qt::ApplicationState m_lastAppState = Qt::ApplicationActive;
     147                 :             :   // T-72.7: timer-skew fallback for Linux desktops where
     148                 :             :   // applicationState is unreliable. Wall-clock at the last probe fire.
     149                 :             :   qint64 m_lastProbeMsecsSinceEpoch = 0;
     150                 :             : 
     151                 :             :   // T-72.7: optional network-change hooks. Null if no backend available
     152                 :             :   // (e.g. minimal Linux container) — probes + timer-skew still cover us.
     153                 :             :   QNetworkInformation *m_netInfo = nullptr;
     154                 :             :   QNetworkInformation::Reachability m_lastReachability =
     155                 :             :       QNetworkInformation::Reachability::Unknown;
     156                 :             :   QNetworkInformation::TransportMedium m_lastTransportMedium =
     157                 :             :       QNetworkInformation::TransportMedium::Unknown;
     158                 :             : 
     159                 :             :   static constexpr int MAX_RECONNECT_DELAY = 300;   // 5 min cap (Bug-24 guard)
     160                 :             :   static constexpr int PROBE_DEFAULT_MS = 15000;     // 15 s + ImapService 15 s watchdog = ~30 s
     161                 :             : };
        

Generated by: LCOV version 2.0-1