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: 94.0% 237 / 0 / 252
Functions: 96.0% 24 / 0 / 25
Branches: 77.2% 233 / 0 / 302

controller/ServerSearchSession.cpp
Line Branch Exec Source
1 #include "ServerSearchSession.h"
2
3 #include "service/ImapService.h"
4 #include <QLoggingCategory>
5 #include <algorithm>
6 #include <utility>
7
8 // Same category as MailController (defined there): server-search logs keep
9 // their name and filtering (docs/LOGGING.md).
10 Q_DECLARE_LOGGING_CATEGORY(lcController)
11
12 1051 ServerSearchSession::ServerSearchSession(ImapService *const &searchImap,
13 const QStringList &subscribedFolders,
14 1051 Deps deps, QObject *parent)
15 1051 : QObject(parent), m_subscribedFolders(subscribedFolders), m_deps(std::move(deps)),
16
8/8
✓ Branch 6 → 7 taken 1051 times.
✓ Branch 7 → 8 taken 1051 times.
✓ Branch 8 → 9 taken 1051 times.
✓ Branch 9 → 10 taken 1051 times.
✓ Branch 10 → 11 taken 1051 times.
✓ Branch 11 → 12 taken 1051 times.
✓ Branch 12 → 13 taken 1051 times.
✓ Branch 13 → 14 taken 1051 times.
2102 m_searchImap(searchImap) {}
17
18 // Sprint 59 (S1): translate the local SearchFilter + free text into the
19 // server-mappable IMAP criteria. has:attachment has no standard SEARCH key, so
20 // it is intentionally dropped here and left to the local FTS/cache.
21 static ImapService::SearchCriteria
22 15 toImapCriteria(const QString &freeText,
23 const MailCache::SearchFilter &filter) {
24 using FTri = MailCache::SearchFilter::Tri;
25 using ITri = ImapService::SearchTri;
26 45 auto tri = [](FTri t) {
27
2/4
✓ Branch 2 → 3 taken 45 times.
✗ Branch 2 → 7 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 45 times.
45 return t == FTri::Yes ? ITri::Yes : t == FTri::No ? ITri::No : ITri::Any;
28 };
29
30 15 ImapService::SearchCriteria c;
31 15 c.text = freeText;
32 15 c.from = filter.fromFilter;
33 15 c.to = filter.toFilter;
34 15 c.listId = filter.listFilter;
35 15 c.subject = filter.subjectFilter;
36
2/3
✓ Branch 7 → 8 taken 15 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 15 times.
15 if (filter.dateFrom.isValid())
37 ✗ c.since = filter.dateFrom.date();
38
2/3
✓ Branch 10 → 11 taken 15 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 15 times.
15 if (filter.dateTo.isValid())
39 ✗ c.before = filter.dateTo.date().addDays(1); // inclusive UI → exclusive BEFORE
40 15 c.unread = tri(filter.unread);
41 15 c.flagged = tri(filter.flagged);
42 15 c.answered = tri(filter.answered);
43 15 c.keywords = filter.tags;
44 // Sprint 101 D-14: negations map to NOT …; excluded folders are skipped
45 // when the folder queue is built (serverSearch).
46 15 c.notText = filter.excludedTerms;
47 15 c.notFrom = filter.excludedFrom;
48 15 c.notTo = filter.excludedTo;
49 15 c.notListId = filter.excludedLists;
50 15 c.notSubject = filter.excludedSubjects;
51 15 c.notKeywords = filter.excludedTags;
52 15 return c;
53 ✗ }
54
55 1 void ServerSearchSession::setServerSearchExcludedFolders(
56 const QStringList &folders) {
57 1 m_serverSearchExcludedFolders.clear();
58
2/2
✓ Branch 18 → 5 taken 2 times.
✓ Branch 18 → 19 taken 1 time.
3 for (const QString &folder : folders) {
59
1/1
✓ Branch 6 → 7 taken 2 times.
2 const QString path = folder.trimmed();
60
2/4
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 12 not taken.
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
4 if (!path.isEmpty() &&
61
1/2
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 12 not taken.
2 !m_serverSearchExcludedFolders.contains(path, Qt::CaseInsensitive))
62
1/1
✓ Branch 14 → 15 taken 2 times.
2 m_serverSearchExcludedFolders.append(path);
63 2 }
64 1 }
65
66 20 void ServerSearchSession::serverSearch(const QString &freeText,
67 const MailCache::SearchFilter &filter, const QString &owner) {
68 20 m_deps.ensureConnection();
69 20 m_owner = owner;
70 20 m_paused = false;
71 20 m_totalFolders = 0;
72
73 // T-195: Disconnect any previous one-shot connections (re-entry safety).
74 // S106-188: that includes the previous scan's header fetch — its late
75 // headerFetchComplete would otherwise store the old folder's headers under
76 // the new scan's folder id and report them as the new scan's hits.
77 20 QObject::disconnect(m_searchStateConn);
78 20 QObject::disconnect(m_searchFolderConn);
79 20 QObject::disconnect(m_searchFailConn);
80 20 QObject::disconnect(m_searchResultConn);
81 20 QObject::disconnect(m_searchRefusedConn);
82 20 dropHeaderFetch();
83
84
6/6
✓ Branch 11 → 12 taken 17 times.
✓ Branch 11 → 15 taken 3 times.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 15 taken 4 times.
✓ Branch 16 → 17 taken 13 times.
✓ Branch 16 → 37 taken 7 times.
37 if (m_searchImap->state() != ImapService::State::Authenticated &&
85 17 m_searchImap->state() != ImapService::State::Selected) {
86 // Not ready yet — wait for authentication, then retry
87
5/5
✓ Branch 17 → 18 taken 13 times.
✓ Branch 21 → 22 taken 13 times.
✓ Branch 22 → 23 taken 13 times.
✓ Branch 29 → 19 taken 13 times.
✓ Branch 29 → 30 taken 13 times.
26 qCInfo(lcController) << "Search IMAP not ready (state:"
88
1/1
✓ Branch 24 → 25 taken 13 times.
13 << static_cast<int>(m_searchImap->state())
89
1/1
✓ Branch 25 → 26 taken 13 times.
13 << ") — waiting for Authenticated";
90 // S106-193: the scan starts from here, so a pause() requested while
91 // waiting holds it (serverSearch() would reset the pause).
92 13 m_searchStateConn = connect(
93 13 m_searchImap, &ImapService::stateChanged, this,
94
1/3
✓ Branch 32 → 33 taken 13 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
26 [this, freeText, filter](ImapService::State s) {
95
3/4
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2 times.
3 if (s == ImapService::State::Authenticated ||
96 s == ImapService::State::Selected) {
97 1 QObject::disconnect(m_searchStateConn);
98 1 beginScan(freeText, filter);
99 }
100 13 });
101 13 return;
102 }
103 7 beginScan(freeText, filter);
104 }
105
106 8 void ServerSearchSession::beginScan(const QString &freeText, const MailCache::SearchFilter &filter) {
107 8 m_searchQuery = freeText;
108 8 m_searchFilter = filter;
109
110 // If nothing maps to a server SEARCH key (e.g. a has:attachment-only search),
111 // skip the server scan entirely — the local FTS/cache already has the answer.
112 // Sprint 103 D-21: about:<name> names mails linked locally, which are in
113 // the cache; IMAP has no key for it.
114
8/12
✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 8 times.
✓ Branch 7 → 8 taken 8 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 7 times.
✓ Branch 11 → 12 taken 8 times.
✗ Branch 11 → 13 not taken.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 26 taken 7 times.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
8 if (!filter.aboutFilter.isEmpty() || toImapCriteria(freeText, filter).isEmpty()) {
115
4/4
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 23 → 16 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
2 qCInfo(lcController) << "Server search: no server-mappable criteria — "
116
1/1
✓ Branch 19 → 20 taken 1 time.
1 "serving from local cache only";
117
1/1
✓ Branch 24 → 25 taken 1 time.
1 emit serverSearchComplete();
118 1 return;
119 }
120
121 // Build folder search queue: all subscribed folders, skip special ones.
122 // Sprint 60 (S1): when folder filter(s) are given (from "folder:" prefixes),
123 // only folders whose path contains ANY of the patterns are searched (OR).
124 7 QStringList folderFilters;
125
2/2
✓ Branch 36 → 28 taken 3 times.
✓ Branch 36 → 37 taken 7 times.
10 for (const QString &p : filter.folderPatterns)
126
2/3
✓ Branch 29 → 30 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✗ Branch 32 → 34 not taken.
3 if (!p.trimmed().isEmpty())
127
1/1
✓ Branch 33 → 34 taken 3 times.
3 folderFilters.append(p);
128 7 const bool hasFolderFilter = !folderFilters.isEmpty();
129
1/1
✓ Branch 38 → 39 taken 7 times.
7 m_searchPendingFolders.clear();
130
2/2
✓ Branch 88 → 41 taken 18 times.
✓ Branch 88 → 89 taken 7 times.
25 for (const QString &folder : m_subscribedFolders) {
131
2/2
✓ Branch 42 → 43 taken 8 times.
✓ Branch 42 → 55 taken 10 times.
18 if (hasFolderFilter) {
132 8 bool matchesAny = false;
133
4/4
✓ Branch 43 → 44 taken 8 times.
✓ Branch 44 → 45 taken 8 times.
✓ Branch 52 → 46 taken 8 times.
✓ Branch 52 → 53 taken 5 times.
13 for (const QString &f : folderFilters) {
134
3/3
✓ Branch 47 → 48 taken 8 times.
✓ Branch 48 → 49 taken 3 times.
✓ Branch 48 → 50 taken 5 times.
8 if (folder.contains(f, Qt::CaseInsensitive)) {
135 3 matchesAny = true;
136 3 break;
137 }
138 }
139
2/2
✓ Branch 53 → 54 taken 5 times.
✓ Branch 53 → 55 taken 3 times.
8 if (!matchesAny)
140 5 continue;
141 }
142 13 bool skip = false;
143 // Sprint 101 D-14: "-folder:x" never scans a matching folder.
144
1/2
✗ Branch 71 → 57 not taken.
✓ Branch 71 → 72 taken 13 times.
13 for (const QString &excluded : filter.excludedFolders)
145 ✗ if (!excluded.trimmed().isEmpty() && folder.contains(excluded, Qt::CaseInsensitive)) skip = true;
146 // A folder explicitly targeted via folder: is searched even if it is one of
147 // the normally-skipped special folders (the user asked for it).
148
2/2
✓ Branch 72 → 73 taken 10 times.
✓ Branch 72 → 84 taken 3 times.
13 if (!hasFolderFilter) {
149
4/4
✓ Branch 73 → 74 taken 10 times.
✓ Branch 74 → 75 taken 10 times.
✓ Branch 82 → 76 taken 7 times.
✓ Branch 82 → 83 taken 8 times.
15 for (const QString &excluded : m_serverSearchExcludedFolders) {
150
2/2
✓ Branch 78 → 79 taken 2 times.
✓ Branch 78 → 80 taken 5 times.
7 if (folder.compare(excluded, Qt::CaseInsensitive) == 0) {
151 2 skip = true;
152 2 break;
153 }
154 }
155 }
156
2/2
✓ Branch 84 → 85 taken 11 times.
✓ Branch 84 → 86 taken 2 times.
13 if (!skip)
157
1/1
✓ Branch 85 → 86 taken 11 times.
11 m_searchPendingFolders.append(folder);
158 }
159
160 // Privacy (Sprint 101 rule 11, Sprint 98 G18): the query text is never logged.
161
7/7
✓ Branch 89 → 90 taken 7 times.
✓ Branch 93 → 94 taken 7 times.
✓ Branch 94 → 95 taken 7 times.
✓ Branch 96 → 97 taken 7 times.
✓ Branch 97 → 98 taken 7 times.
✓ Branch 101 → 91 taken 7 times.
✓ Branch 101 → 102 taken 7 times.
14 qCInfo(lcController) << "Server search: queued" << m_searchPendingFolders.size() << "folders";
162 7 m_totalFolders = int(m_searchPendingFolders.size());
163
1/1
✓ Branch 103 → 104 taken 7 times.
7 emit serverSearchProgress(0, m_totalFolders);
164
1/1
✓ Branch 104 → 105 taken 7 times.
7 searchNextFolder();
165 7 }
166
167 5 int ServerSearchSession::doneFolders() const {
168
1/2
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 6 not taken.
5 return qMax(0, m_totalFolders - int(m_searchPendingFolders.size()) - (m_searchCurrentFolder.isEmpty() ? 0 : 1));
169 }
170
171 // S101.1.7 (D-12): switching away from the owning search tab stops the scan
172 // after the folder in flight; resume() continues with the remaining queue.
173 1 void ServerSearchSession::pause() {
174
3/6
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 7 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (!running() && !awaitingConnection()) return;
175 1 m_paused = true;
176
7/7
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 12 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
2 qCInfo(lcController) << "Server search: paused (" << m_searchPendingFolders.size() << "folders remaining)";
177 }
178
179 1 void ServerSearchSession::resume() {
180
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (!m_paused) return;
181 1 m_paused = false;
182
7/7
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 16 → 6 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
2 qCInfo(lcController) << "Server search: resumed (" << m_searchPendingFolders.size() << "folders remaining)";
183
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
1 if (awaitingConnection()) return; // the scan starts once the connection is ready
184
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
1 if (m_searchCurrentFolder.isEmpty()) searchNextFolder();
185 }
186
187 16 void ServerSearchSession::searchNextFolder() {
188
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 15 times.
16 if (m_paused) {
189 1 m_searchCurrentFolder.clear();
190 1 m_searchCurrentFolderId = -1;
191 1 return;
192 }
193
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 20 taken 12 times.
15 if (m_searchPendingFolders.isEmpty()) {
194
5/5
✓ Branch 7 → 8 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 16 → 9 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
6 qCInfo(lcController) << "Server search: all folders searched";
195 3 m_searchCurrentFolder.clear();
196 3 m_searchCurrentFolderId = -1;
197 3 emit serverSearchComplete();
198 3 return;
199 }
200
201 // Disconnect previous one-shot connections
202 12 QObject::disconnect(m_searchFolderConn);
203 12 QObject::disconnect(m_searchFailConn);
204 12 QObject::disconnect(m_searchResultConn);
205 12 QObject::disconnect(m_searchRefusedConn);
206
207
1/1
✓ Branch 24 → 25 taken 12 times.
12 m_searchCurrentFolder = m_searchPendingFolders.takeFirst();
208 12 m_searchCurrentFolderId = m_deps.resolveFolderId(m_searchCurrentFolder);
209 12 m_searchCurrentUidValidity = 0;
210
211 // Sprint 101 G11: folder paths only at debug level.
212
7/7
✓ Branch 28 → 29 taken 12 times.
✓ Branch 32 → 33 taken 12 times.
✓ Branch 33 → 34 taken 12 times.
✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
✓ Branch 40 → 30 taken 12 times.
✓ Branch 40 → 41 taken 12 times.
24 qCInfo(lcController) << "Server search: SELECT next folder (" << m_searchPendingFolders.size() << "remaining)";
213
6/6
✓ Branch 41 → 42 taken 12 times.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 46 → 47 taken 12 times.
✓ Branch 47 → 48 taken 12 times.
✓ Branch 51 → 43 taken 12 times.
✓ Branch 51 → 52 taken 12 times.
24 qCDebug(lcController) << "Server search: SELECT" << m_searchCurrentFolder;
214
215 // Step 1: SELECT folder. We must react to BOTH outcomes:
216 // - folderSelected → folder is ready, send SEARCH
217 // - folderSelectFailed → folder cannot be selected (e.g. \Noselect
218 // container), skip it and continue. Without this the
219 // whole search would stall forever on that folder.
220 // NOTE: not Qt::SingleShotConnection — a mismatching path must NOT consume
221 // the connection, otherwise the real folderSelected would be missed.
222 12 m_searchFolderConn = connect(
223 12 m_searchImap, &ImapService::folderSelected, this,
224
1/1
✓ Branch 52 → 53 taken 12 times.
12 [this](const QString &path, int, quint32 uidValidity, quint64) {
225
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
7 if (path != m_searchCurrentFolder) return;
226 7 QObject::disconnect(m_searchFolderConn);
227 7 QObject::disconnect(m_searchFailConn);
228 7 m_searchCurrentUidValidity = uidValidity;
229
230 // Step 2: Connect result handler BEFORE sending SEARCH
231 7 m_currentFolderRefused = false;
232 7 m_searchRefusedConn = connect(m_searchImap, &ImapService::searchFailed, this,
233
1/1
✓ Branch 7 → 8 taken 7 times.
7 [this](const QString &) { m_currentFolderRefused = true; });
234 7 m_searchResultConn = connect(
235 7 m_searchImap, &ImapService::searchResultReceived, this,
236
1/1
✓ Branch 10 → 11 taken 7 times.
7 [this](const QList<qint64> &uids) {
237 6 QObject::disconnect(m_searchResultConn);
238 6 QObject::disconnect(m_searchRefusedConn);
239
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 17 taken 5 times.
6 if (std::exchange(m_currentFolderRefused, false)) {
240
5/5
✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 8 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
2 qCWarning(lcController) << "Server search: the server refused the search in a folder";
241 1 emit serverSearchFolderFailed(m_searchCurrentFolder);
242 }
243
7/7
✓ Branch 17 → 18 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
✓ Branch 24 → 25 taken 6 times.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 29 → 19 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
12 qCInfo(lcController) << "Server search:" << uids.size() << "results in the folder";
244
6/6
✓ Branch 30 → 31 taken 6 times.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 37 taken 6 times.
✓ Branch 40 → 32 taken 6 times.
✓ Branch 40 → 41 taken 6 times.
12 qCDebug(lcController) << "Server search: results in" << m_searchCurrentFolder;
245 6 fetchUncachedThenFinish(uids);
246 13 });
247
248 // Step 3: Send the composite SEARCH built from free text + facets.
249
2/2
✓ Branch 13 → 14 taken 7 times.
✓ Branch 14 → 15 taken 7 times.
7 m_searchImap->search(toImapCriteria(m_searchQuery, m_searchFilter));
250 12 });
251
252 12 m_searchFailConn = connect(
253 12 m_searchImap, &ImapService::folderSelectFailed, this,
254
1/1
✓ Branch 55 → 56 taken 12 times.
12 [this](const QString &path) {
255 ✗ if (path != m_searchCurrentFolder) return;
256 ✗ QObject::disconnect(m_searchFolderConn);
257 ✗ QObject::disconnect(m_searchFailConn);
258 ✗ qCWarning(lcController) << "Server search: SELECT failed — skipping the folder";
259 ✗ qCDebug(lcController) << "Server search: SELECT failed for" << path;
260 ✗ m_searchCurrentFolder.clear();
261 ✗ emit serverSearchProgress(doneFolders(), m_totalFolders);
262 ✗ searchNextFolder();
263 12 });
264
265 12 m_searchImap->selectFolder(m_searchCurrentFolder);
266 }
267
268 6 void ServerSearchSession::fetchUncachedThenFinish(const QList<qint64> &uids) {
269
3/4
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 9 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 9 taken 2 times.
12 QList<qint64> missing = m_deps.uncachedUids && m_searchCurrentFolderId >= 0 && !uids.isEmpty()
270
2/3
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 9 not taken.
✓ Branch 7 → 8 taken 4 times.
12 ? m_deps.uncachedUids(m_searchCurrentFolderId, uids) : QList<qint64>{};
271
6/8
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 15 taken 2 times.
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 4 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 20 taken 4 times.
6 if (missing.isEmpty() || !m_deps.storeHeaders || !m_searchImap) {
272
1/1
✓ Branch 18 → 19 taken 2 times.
2 finishFolder(uids);
273 2 return;
274 }
275
3/3
✓ Branch 20 → 21 taken 4 times.
✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
4 std::sort(missing.begin(), missing.end(), std::greater<>()); // the newest first
276
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 39 taken 4 times.
4 if (missing.size() > MaximumHeaderFetch) {
277 ✗ qCInfo(lcController) << "Server search:" << missing.size() - MaximumHeaderFetch
278 ✗ << "uncached hits beyond the fetch limit";
279 ✗ missing.resize(MaximumHeaderFetch);
280 }
281
7/7
✓ Branch 39 → 40 taken 4 times.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 51 → 41 taken 4 times.
✓ Branch 51 → 52 taken 4 times.
8 qCInfo(lcController) << "Server search: fetching" << missing.size() << "uncached headers";
282
1/1
✓ Branch 52 → 53 taken 4 times.
4 m_fetchedHeaders.clear();
283 4 m_headerConn = connect(m_searchImap, &ImapService::headersReceived, this,
284
1/1
✓ Branch 53 → 54 taken 4 times.
6 [this](const QList<MailHeader> &headers) { m_fetchedHeaders += headers; });
285
1/1
✓ Branch 57 → 58 taken 4 times.
8 m_headerDoneConn = connect(m_searchImap, &ImapService::headerFetchComplete, this, [this, uids] {
286 2 const QList<MailHeader> headers = m_fetchedHeaders;
287
1/1
✓ Branch 3 → 4 taken 2 times.
2 dropHeaderFetch();
288
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
2 if (!headers.isEmpty())
289
1/1
✓ Branch 6 → 7 taken 2 times.
2 m_deps.storeHeaders(m_searchCurrentFolderId, m_searchCurrentUidValidity, headers);
290
1/1
✓ Branch 7 → 8 taken 2 times.
2 finishFolder(uids);
291 6 });
292 // A failed fetch still reports the folder's cached hits.
293
1/1
✓ Branch 62 → 63 taken 4 times.
8 m_headerFailConn = connect(m_searchImap, &ImapService::errorOccurred, this, [this, uids](const QString &error) {
294
6/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 4 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
2 qCWarning(lcController) << "Server search: header fetch failed:" << error;
295 1 dropHeaderFetch();
296 1 finishFolder(uids);
297 5 });
298
1/1
✓ Branch 66 → 67 taken 4 times.
4 m_searchImap->fetchHeadersByUids(missing);
299 6 }
300
301 5 void ServerSearchSession::finishFolder(const QList<qint64> &uids) {
302
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 2 times.
5 if (!uids.isEmpty()) emit serverSearchResultReceived(uids, m_searchCurrentFolderId, m_searchCurrentFolder);
303 5 m_searchCurrentFolder.clear();
304 5 emit serverSearchProgress(doneFolders(), m_totalFolders);
305 5 searchNextFolder();
306 5 }
307
308 31 void ServerSearchSession::dropHeaderFetch() {
309 31 QObject::disconnect(m_headerConn);
310 31 QObject::disconnect(m_headerDoneConn);
311 31 QObject::disconnect(m_headerFailConn);
312 31 m_fetchedHeaders.clear();
313 31 }
314
315 6 void ServerSearchSession::cancelServerSearch() {
316
5/5
✓ Branch 2 → 3 taken 6 times.
✓ Branch 6 → 7 taken 6 times.
✓ Branch 7 → 8 taken 6 times.
✓ Branch 14 → 4 taken 6 times.
✓ Branch 14 → 15 taken 6 times.
12 qCInfo(lcController) << "Server search: cancelled ("
317
2/2
✓ Branch 9 → 10 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
6 << m_searchPendingFolders.size() << "folders remaining)";
318 6 m_searchPendingFolders.clear();
319 6 m_searchCurrentFolder.clear();
320 6 m_searchCurrentFolderId = -1;
321 6 m_searchQuery.clear();
322 6 m_searchFilter = {};
323 6 m_owner.clear();
324 6 m_paused = false;
325 6 m_totalFolders = 0;
326 6 QObject::disconnect(m_searchStateConn);
327 6 QObject::disconnect(m_searchFolderConn);
328 6 QObject::disconnect(m_searchFailConn);
329 6 QObject::disconnect(m_searchResultConn);
330 6 QObject::disconnect(m_searchRefusedConn);
331 6 dropHeaderFetch();
332 12 }
333
334 1043 void ServerSearchSession::abandon() {
335
5/6
✓ Branch 3 → 4 taken 1042 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 5 → 6 taken 1042 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1042 times.
✓ Branch 8 → 10 taken 1 time.
1043 if (!running() && !awaitingConnection())
336 1042 return;
337
5/5
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 12 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
2 qCInfo(lcController) << "Server search: ended by an account switch";
338 1 emit serverSearchComplete(); // still owned: the starting tab leaves "running"
339 1 cancelServerSearch();
340 }
341
342 // Formerly part of MailController::ensureSearchConnection()'s stateChanged
343 // handler (Authenticated branch); the controller calls it right after its
344 // "connection ready" log.
345 6 void ServerSearchSession::resumeAfterReconnect() {
346 // T-720: Active server-search recovery. If the connection
347 // died mid-search (m_searchCurrentFolder /
348 // m_searchPendingFolders non-empty), the one-shot SELECT/
349 // SEARCH handlers were disconnected by Error/Disconnected.
350 // Resume the search here so the user's query completes
351 // instead of hanging forever (the stall the sprint plan
352 // calls out at MailController.cpp:1295-1358).
353
4/4
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 6 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 27 taken 3 times.
9 if (!m_searchCurrentFolder.isEmpty() ||
354
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
3 !m_searchPendingFolders.isEmpty()) {
355
5/5
✓ Branch 9 → 10 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 21 → 11 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
6 qCInfo(lcController) << "T-720: Resuming server search after search-connection reconnect (pending="
356
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
3 << m_searchPendingFolders.size() << ")";
357 // Put the current folder back at the head of the queue so
358 // searchNextFolder() retries it, then drains the rest.
359
1/2
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
3 if (!m_searchCurrentFolder.isEmpty())
360 3 m_searchPendingFolders.prepend(m_searchCurrentFolder);
361 3 m_searchCurrentFolder.clear();
362 3 searchNextFolder();
363 }
364 6 }
365
366 // Formerly the Error/Disconnected branch of the same handler, called after
367 // the controller's warning log.
368 2 void ServerSearchSession::dropPendingHandlers() {
369 // T-720: Disconnect the one-shot SELECT/SEARCH handlers so
370 // they cannot fire against the wrong socket state after
371 // reconnect. searchNextFolder() reconnects them.
372 2 QObject::disconnect(m_searchFolderConn);
373 2 QObject::disconnect(m_searchFailConn);
374 2 QObject::disconnect(m_searchResultConn);
375 2 QObject::disconnect(m_searchRefusedConn);
376 2 dropHeaderFetch(); // the folder is searched again after the reconnect
377 2 }
378