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: 74.8% 95 / 0 / 127
Functions: 86.7% 13 / 0 / 15
Branches: 52.6% 132 / 0 / 251

app/core/FolderPredictionSession.cpp
Line Branch Exec Source
1 #include "FolderPredictionSession.h"
2 #include "AccountSession.h"
3 #include "MailSessionCore.h"
4 #include "controller/MailController.h"
5 #include "data/FolderEvidenceModel.h"
6 #include "data/MailCache.h"
7 #include "performance/BackgroundPerformanceTracker.h"
8 #include "performance/PerformanceTrace.h"
9 #include "performance/StartupPerformanceTracker.h"
10 #include "service/FolderPredictionService.h"
11 #include <QDir>
12 #include <QLoggingCategory>
13 #include <QTimer>
14
15
4/7
✓ Branch 2 → 3 taken 26 times.
✓ Branch 2 → 8 taken 1507 times.
✓ Branch 4 → 5 taken 26 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 26 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
1533 Q_LOGGING_CATEGORY(lcPredictionSession, "mailjd.predictor.session")
16
17 854 FolderPredictionSession::FolderPredictionSession(MailSessionCore &session, QObject *parent)
18 854 : QObject(parent), m_session(session) {
19
1/1
✓ Branch 7 → 8 taken 854 times.
854 connect(session.accounts(), &AccountSession::accountsLoaded, this, &FolderPredictionSession::reset);
20
1/1
✓ Branch 10 → 11 taken 854 times.
854 connect(session.controller(), &MailController::headersStored, this, &FolderPredictionSession::trainHeaders);
21 854 }
22 1708 FolderPredictionSession::~FolderPredictionSession() {
23
2/2
✓ Branch 3 → 4 taken 724 times.
✓ Branch 3 → 6 taken 130 times.
854 if (m_training) m_training->cancel(this);
24 854 finishMeasurement(false, 0);
25 1708 }
26 21 bool FolderPredictionSession::busy() const {
27
5/6
✓ Branch 2 → 3 taken 20 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 9 not taken.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 16 times.
21 return m_startedNs != 0 || (m_training && m_training->hasPendingHeaders(this));
28 }
29 56 bool FolderPredictionSession::excludedFolder(const QString &path) {
30 56 return FolderEvidenceModel::excludedFolder(path);
31 }
32 866 void FolderPredictionSession::reset() {
33
3/3
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 6 taken 844 times.
✓ Branch 5 → 6 taken 22 times.
866 if (m_training) m_training->cancel(this);
34
1/1
✓ Branch 6 → 7 taken 866 times.
866 finishMeasurement(false, 0);
35 866 const auto generation = ++m_generation;
36 866 ++m_revision;
37
2/4
✓ Branch 13 → 14 taken 811 times.
✓ Branch 13 → 19 taken 41 times.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
2570 const auto databasePath = m_session.accounts()->primary() && !m_session.profileDirectory().isEmpty()
38
13/19
✓ Branch 10 → 11 taken 852 times.
✓ Branch 10 → 19 taken 14 times.
✓ Branch 15 → 16 taken 811 times.
✓ Branch 16 → 17 taken 811 times.
✓ Branch 17 → 18 taken 811 times.
✓ Branch 20 → 21 taken 811 times.
✓ Branch 20 → 22 taken 55 times.
✓ Branch 22 → 23 taken 811 times.
✓ Branch 22 → 24 taken 55 times.
✓ Branch 24 → 25 taken 811 times.
✓ Branch 24 → 26 taken 55 times.
✓ Branch 26 → 27 taken 852 times.
✓ Branch 26 → 28 taken 14 times.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 86 not taken.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
3395 ? QDir(m_session.profileDirectory()).filePath("folder_suggestions.db") : QString{};
39 // An account reload (e.g. saving account settings) keeps the persisted
40 // model answering while the bootstrap refreshes it; only a different or
41 // missing classifier file withdraws suggestions.
42
5/6
✓ Branch 28 → 29 taken 15 times.
✓ Branch 28 → 34 taken 851 times.
✓ Branch 30 → 31 taken 11 times.
✓ Branch 30 → 34 taken 4 times.
✓ Branch 32 → 33 taken 11 times.
✗ Branch 32 → 34 not taken.
866 const bool keepReady = m_ready && !databasePath.isEmpty() && databasePath == m_databasePath;
43 866 m_ready = keepReady;
44 866 m_error.clear();
45 866 m_databasePath = databasePath;
46
4/4
✓ Branch 37 → 38 taken 866 times.
✓ Branch 41 → 42 taken 866 times.
✓ Branch 58 → 39 taken 866 times.
✓ Branch 58 → 59 taken 866 times.
1732 qCInfo(lcPredictionSession).noquote()
47
3/3
✓ Branch 47 → 48 taken 866 times.
✓ Branch 49 → 50 taken 866 times.
✓ Branch 50 → 51 taken 866 times.
2598 << QStringLiteral("reset generation=%1 keepReady=%2").arg(generation).arg(keepReady);
48
1/1
✓ Branch 59 → 60 taken 866 times.
866 const QPointer<FolderPredictionSession> guard(this);
49
1/1
✓ Branch 60 → 61 taken 866 times.
866 emit changed();
50
3/6
✓ Branch 62 → 63 taken 866 times.
✗ Branch 62 → 64 not taken.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 866 times.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 866 times.
866 if (!guard || generation != m_generation) return;
51 // Local account publication and restoration finish before auxiliary work.
52 // Reentrant account reloads invalidate this queued demand as well.
53
1/1
✓ Branch 68 → 69 taken 866 times.
1648 QTimer::singleShot(0, this, [this, generation] { bootstrap(generation); });
54 866 }
55 782 void FolderPredictionSession::bootstrap(quint64 generation) {
56
8/9
✓ Branch 2 → 3 taken 758 times.
✓ Branch 2 → 8 taken 24 times.
✓ Branch 4 → 5 taken 733 times.
✓ Branch 4 → 8 taken 25 times.
✓ Branch 6 → 7 taken 733 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 733 times.
✓ Branch 10 → 11 taken 49 times.
✓ Branch 10 → 12 taken 733 times.
783 if (generation != m_generation || m_databasePath.isEmpty() || !m_session.cache()->isOpen()) return;
57
1/1
✓ Branch 12 → 13 taken 733 times.
733 m_startedNs = MailJD::Performance::PerformanceTrace::monotonicNowNs();
58
1/1
✓ Branch 13 → 14 taken 733 times.
733 auto &tracker = MailJD::Performance::BackgroundPerformanceTracker::instance();
59
3/5
✓ Branch 19 → 20 taken 733 times.
✓ Branch 22 → 23 taken 2932 times.
✓ Branch 22 → 24 taken 733 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
3665 tracker.beginBackground({"predictor_initial", "predictor_incremental", "search_during_predictor", "typing_during_predictor"}, m_startedNs);
60
2/2
✓ Branch 24 → 25 taken 733 times.
✓ Branch 25 → 26 taken 733 times.
733 tracker.backgroundPhaseStarted("predictor_training");
61
2/2
✓ Branch 27 → 28 taken 733 times.
✓ Branch 28 → 29 taken 733 times.
733 m_training = PredictorTrainingService::shared();
62
1/1
✓ Branch 29 → 30 taken 733 times.
733 const QPointer<FolderPredictionSession> guard(this);
63
1/1
✓ Branch 30 → 31 taken 733 times.
733 emit trainingStarted();
64
5/6
✓ Branch 32 → 33 taken 733 times.
✗ Branch 32 → 34 not taken.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 732 times.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 732 times.
733 if (!guard || generation != m_generation) return;
65
1/1
✓ Branch 48 → 49 taken 732 times.
2196 m_training->train(this, m_session.cache()->databasePath(), m_databasePath,
66
1/1
✓ Branch 41 → 42 taken 732 times.
2196 m_session.controller()->accountId(), FolderEvidenceModel::excludedFolders(), [this, generation](PredictorTrainingResult result) {
67
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 664 times.
664 if (generation != m_generation) return;
68
1/1
✓ Branch 4 → 5 taken 664 times.
664 finishMeasurement(result.completed, result.trainedDocuments);
69 // A failed or rejected refresh never withdraws a model that can answer:
70 // the classifier file keeps every batch committed before the failure.
71
1/6
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 664 times.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
664 m_ready = result.completed || result.documents > 0 || m_ready;
72 664 m_error = result.error;
73
1/2
✓ Branch 11 → 12 taken 664 times.
✗ Branch 11 → 35 not taken.
664 if (result.completed)
74
4/4
✓ Branch 12 → 13 taken 664 times.
✓ Branch 16 → 17 taken 664 times.
✓ Branch 33 → 14 taken 664 times.
✓ Branch 33 → 34 taken 664 times.
1328 qCInfo(lcPredictionSession).noquote()
75 1328 << QStringLiteral("bootstrap finished documents=%1 trained=%2")
76
3/3
✓ Branch 22 → 23 taken 664 times.
✓ Branch 24 → 25 taken 664 times.
✓ Branch 25 → 26 taken 664 times.
1992 .arg(result.documents).arg(result.trainedDocuments);
77 else
78 ✗ qCWarning(lcPredictionSession).noquote()
79 ✗ << QStringLiteral("bootstrap failed ready=%1 documents=%2 error=%3")
80 ✗ .arg(m_ready).arg(result.documents).arg(result.error);
81 664 ++m_revision;
82
1/1
✓ Branch 61 → 62 taken 664 times.
664 const QPointer<FolderPredictionSession> guard(this);
83
1/1
✓ Branch 62 → 63 taken 664 times.
664 emit changed();
84
4/7
✓ Branch 64 → 65 taken 664 times.
✗ Branch 64 → 67 not taken.
✓ Branch 65 → 66 taken 664 times.
✗ Branch 65 → 67 not taken.
✓ Branch 68 → 69 taken 664 times.
✗ Branch 68 → 70 not taken.
✓ Branch 69 → 70 taken 664 times.
664 if (guard && generation == m_generation) emit trainingFinished(result.completed, result.documents, result.error);
85 1396 }, [this, generation](int documents) {
86 // F108-13: the classifier file from the last run answers at once; the
87 // pass over the cache (minutes for a large profile, mostly nothing new)
88 // refreshes it in the background.
89
2/4
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
3 if (generation != m_generation || m_ready) return;
90 3 m_ready = true;
91 3 ++m_revision;
92
6/6
✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
✓ Branch 23 → 7 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
9 qCInfo(lcPredictionSession).noquote() << QStringLiteral("stored model answers documents=%1").arg(documents);
93 3 emit changed();
94 });
95 1466 }
96 2384 void FolderPredictionSession::finishMeasurement(bool completed, int documents) {
97
2/2
✓ Branch 2 → 3 taken 1651 times.
✓ Branch 2 → 4 taken 733 times.
2384 if (!m_startedNs) return;
98
2/2
✓ Branch 6 → 7 taken 733 times.
✓ Branch 7 → 8 taken 733 times.
733 MailJD::Performance::StartupPerformanceTracker::instance().recordPhase("background_predictor_training", m_startedNs,
99 MailJD::Performance::PerformanceTrace::monotonicNowNs());
100 733 m_startedNs = 0;
101 733 auto &tracker = MailJD::Performance::BackgroundPerformanceTracker::instance();
102 733 tracker.addBackgroundWork(documents);
103
2/2
✓ Branch 11 → 12 taken 733 times.
✓ Branch 12 → 13 taken 733 times.
733 tracker.backgroundPhaseFinished("predictor_training", documents);
104 733 tracker.backgroundFinished(completed);
105 }
106 3 void FolderPredictionSession::trainHeaders(const QString &folder, qint64 folderId,
107 quint32 uidValidity,
108 const QList<MailHeader> &headers) {
109
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
3 if (folder.compare(QStringLiteral("INBOX"), Qt::CaseInsensitive) == 0)
110 ✗ classifyArrivals(folder, folderId, uidValidity, headers);
111
4/7
✓ Branch 13 → 14 taken 3 times.
✗ Branch 13 → 18 not taken.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 18 not taken.
6 if (m_databasePath.isEmpty() || headers.isEmpty() || excludedFolder(folder) ||
112
3/6
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 3 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 3 times.
6 folderId <= 0 || uidValidity == 0) return;
113 // The writer's confirmed mailbox identity owns the batch. Resolving it
114 // again here would issue two GUI SQLite reads before header publication.
115 3 const auto account = m_session.controller()->accountId();
116 3 QList<qint64> uids;
117 // Preserve the training service's global 1024-identity admission budget
118 // (S106-210: at most 1024 — 1025 made the service refuse the whole batch).
119
2/2
✓ Branch 33 → 26 taken 1027 times.
✓ Branch 33 → 34 taken 2 times.
1029 for (const auto &header : headers) {
120
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 1026 times.
1027 if (uids.size() >= 1024) break;
121
1/1
✓ Branch 30 → 31 taken 1026 times.
1026 uids.append(header.uid);
122 }
123 3 const auto generation = m_generation;
124
2/2
✓ Branch 34 → 35 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
3 m_training = PredictorTrainingService::shared();
125
1/1
✓ Branch 43 → 44 taken 3 times.
6 m_training->trainHeaders(this, m_session.cache()->databasePath(), m_databasePath,
126 3 account, folderId, folder, uidValidity, uids, [this, generation](PredictorTrainingResult result) {
127
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
3 if (generation != m_generation) return;
128 3 m_error = result.error;
129
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 26 taken 3 times.
3 if (!result.completed)
130 ✗ qCWarning(lcPredictionSession).noquote()
131 ✗ << QStringLiteral("incremental training failed error=%1").arg(result.error);
132
1/2
✓ Branch 26 → 27 taken 3 times.
✗ Branch 26 → 28 not taken.
3 if (result.completed) ++m_revision;
133
1/1
✓ Branch 28 → 29 taken 3 times.
3 const QPointer<FolderPredictionSession> guard(this);
134
1/1
✓ Branch 29 → 30 taken 3 times.
3 emit changed();
135
4/7
✓ Branch 31 → 32 taken 3 times.
✗ Branch 31 → 34 not taken.
✓ Branch 32 → 33 taken 3 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 3 times.
✗ Branch 35 → 37 not taken.
✓ Branch 36 → 37 taken 3 times.
3 if (guard && generation == m_generation) emit trainingFinished(result.completed, result.documents, result.error);
136 3 });
137 3 }
138
139 ✗ void FolderPredictionSession::classifyArrivals(const QString &folder, qint64 folderId, quint32 uidValidity,
140 const QList<MailHeader> &headers) {
141 ✗ if (!m_ready || m_databasePath.isEmpty() || headers.isEmpty() || folderId <= 0 || uidValidity == 0) return;
142 ✗ FolderPredictionRequest request;
143 ✗ request.cachePath = m_session.cache()->databasePath();
144 ✗ request.predictorPath = m_databasePath;
145 ✗ request.account = m_session.controller()->accountId();
146 ✗ request.folder = folder;
147 ✗ request.folderId = folderId;
148 ✗ request.uidValidity = uidValidity;
149 // Bounded per batch; the newest mails first (the top of the list).
150 ✗ for (auto it = headers.crbegin(); it != headers.crend() && request.uids.size() < FolderPredictionService::MaxRows; ++it)
151 ✗ if (it->uid > 0) request.uids.append(it->uid);
152 ✗ if (request.uids.isEmpty()) return;
153 ✗ if (FolderPredictionService::shared()->pendingCount() >= FolderPredictionService::MaxPending / 2) {
154 ✗ qCInfo(lcPredictionSession).noquote()
155 ✗ << QStringLiteral("arrival batch dropped mails=%1 reason=queue busy").arg(request.uids.size());
156 ✗ return;
157 }
158 // One owner per batch: a later batch never replaces an earlier one.
159 ✗ auto *owner = new QObject(this);
160 ✗ const int count = int(request.uids.size());
161 ✗ FolderPredictionService::shared()->predict(owner, request, [owner, count](FolderPredictionResult result) {
162 ✗ if (!result.error.isEmpty())
163 ✗ qCInfo(lcPredictionSession).noquote()
164 ✗ << QStringLiteral("arrival batch dropped mails=%1 reason=%2").arg(count).arg(result.error);
165 else
166 ✗ qCDebug(lcPredictionSession).noquote() << QStringLiteral("arrival batch classified mails=%1").arg(count);
167 ✗ owner->deleteLater();
168 ✗ });
169 ✗ }
170