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: 98.1% 610 / 0 / 622
Functions: 98.9% 94 / 0 / 95
Branches: 82.3% 831 / 0 / 1010

presentation/MailSearchSession.cpp
Line Branch Exec Source
1 #include "MailSearchSession.h"
2
3 #include "SearchLog.h"
4 #include "WorkspaceState.h"
5 #include "app/SearchCoordinator.h"
6 #include "app/SearchQuery.h"
7 #include "app/TimeHistogram.h"
8 #include "controller/ServerSearchSession.h"
9 #include "data/MailSearchIndex.h"
10 #include "service/MailSearchReadService.h"
11
12 namespace {
13
14 8 QString containsFolded(const QString &value) { return MailSearchIndex::foldForSearch(value); }
15
16 215 bool containsCi(const QString &haystack, const QString &needle) {
17
4/4
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 6 taken 205 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 5 times.
215 return needle.isEmpty() || haystack.contains(needle, Qt::CaseInsensitive);
18 }
19
20 474 bool anyContains(const QString &haystack, const QStringList &needles) {
21
2/2
✓ Branch 18 → 4 taken 16 times.
✓ Branch 18 → 19 taken 465 times.
481 for (const QString &needle : needles)
22
8/12
✓ Branch 5 → 6 taken 16 times.
✓ Branch 7 → 8 taken 16 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 16 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 11 taken 7 times.
✓ Branch 12 → 13 taken 16 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 9 times.
✓ Branch 14 → 16 taken 7 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
16 if (!needle.trimmed().isEmpty() && haystack.contains(needle, Qt::CaseInsensitive)) return true;
23 465 return false;
24 }
25
26 398 bool triMatches(MailCache::SearchFilter::Tri tri, bool value) {
27 using Tri = MailCache::SearchFilter::Tri;
28
4/4
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 4 taken 388 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 5 times.
398 return tri == Tri::Any || (tri == Tri::Yes) == value;
29 }
30
31 } // namespace
32
33 110 MailSearchSession::MailSearchSession(QString tabId, QString accountId, Deps deps, QObject *parent)
34 110 : QObject(parent), m_tabId(std::move(tabId)), m_accountId(std::move(accountId)), m_deps(std::move(deps)),
35
8/8
✓ Branch 13 → 14 taken 110 times.
✓ Branch 15 → 16 taken 110 times.
✓ Branch 18 → 19 taken 110 times.
✓ Branch 19 → 20 taken 110 times.
✓ Branch 20 → 21 taken 110 times.
✓ Branch 21 → 22 taken 110 times.
✓ Branch 30 → 31 taken 110 times.
✓ Branch 31 → 32 taken 110 times.
220 m_results(&m_rows, m_accountId) {
36 // D-21: newest first. The proxy keeps the date order when server hits
37 // arrive (SB-01); ties keep the SQL order (date, id descending).
38
1/1
✓ Branch 35 → 36 taken 110 times.
110 m_results.setDynamicSortFilter(true);
39
1/1
✓ Branch 39 → 40 taken 110 times.
220 m_results.sortByColumn(QStringLiteral("date"), false);
40 110 m_results.setSelectionProvider([this](qint64 folderId, qint64 uid) {
41
6/6
✓ Branch 2 → 3 taken 9922 times.
✓ Branch 2 → 4 taken 5303 times.
✓ Branch 3 → 4 taken 9376 times.
✓ Branch 3 → 7 taken 546 times.
✓ Branch 6 → 7 taken 831 times.
✓ Branch 6 → 8 taken 13848 times.
15225 return (m_selected.folderId == folderId && m_selected.uid == uid) || m_marked.contains({folderId, uid});
42 });
43
1/1
✓ Branch 45 → 46 taken 110 times.
110 m_localDebounce.setSingleShot(true);
44
1/1
✓ Branch 46 → 47 taken 110 times.
110 connect(&m_localDebounce, &QTimer::timeout, this, [this] {
45
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 run(SearchTermModel::withFieldText(m_query, m_pendingFieldText), true, true);
46 1 });
47
1/1
✓ Branch 48 → 49 taken 110 times.
110 m_serverDelay.setSingleShot(true);
48
1/1
✓ Branch 49 → 50 taken 110 times.
110 m_serverDelay.setInterval(ServerDelayMs);
49
1/1
✓ Branch 50 → 51 taken 110 times.
110 connect(&m_serverDelay, &QTimer::timeout, this, &MailSearchSession::startServerSearch);
50 110 m_editClock.start();
51
7/7
✓ Branch 53 → 54 taken 110 times.
✓ Branch 57 → 58 taken 110 times.
✓ Branch 58 → 59 taken 110 times.
✓ Branch 59 → 60 taken 110 times.
✓ Branch 60 → 61 taken 110 times.
✓ Branch 65 → 55 taken 110 times.
✓ Branch 65 → 66 taken 110 times.
220 qCInfo(lcSearch) << "Search session created for tab" << m_tabId.left(8);
52 110 }
53
54 199 MailSearchSession::~MailSearchSession() {
55 110 cancelReads();
56
8/10
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 9 taken 109 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 9 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 109 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 109 times.
110 if (m_serverRunning && m_deps.server && m_deps.server->owner() == m_tabId) m_deps.server->cancelServerSearch();
57
2/2
✓ Branch 26 → 16 taken 110 times.
✓ Branch 26 → 27 taken 110 times.
220 qCInfo(lcSearch) << "Search session closed for tab" << m_tabId.left(8);
58 199 }
59
60 417 QString MailSearchSession::fieldText() const { return SearchTermModel::fieldText(m_query); }
61
62 381 QVariantList MailSearchSession::terms() const {
63
4/9
✓ Branch 2 → 3 taken 381 times.
✓ Branch 4 → 5 taken 381 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 9 taken 381 times.
✓ Branch 9 → 10 taken 381 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 20 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
381 return SearchTermModel::termMaps(m_query, m_deps.lookups ? m_deps.lookups() : SearchTermModel::Lookups{});
64 }
65
66 295 QString MailSearchSession::title() const {
67
5/9
✓ Branch 2 → 3 taken 295 times.
✓ Branch 4 → 5 taken 293 times.
✓ Branch 4 → 6 taken 2 times.
✓ Branch 5 → 9 taken 293 times.
✓ Branch 9 → 10 taken 295 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 20 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
295 return SearchTermModel::title(m_query, m_deps.lookups ? m_deps.lookups() : SearchTermModel::Lookups{});
68 }
69
70 1 QString MailSearchSession::dateFromIso() const {
71
3/4
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
1 return m_filter.dateFrom.isValid() ? m_filter.dateFrom.date().toString(Qt::ISODate) : QString();
72 }
73
74 1 QString MailSearchSession::dateToIso() const {
75
3/4
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
1 return m_filter.dateTo.isValid() ? m_filter.dateTo.date().toString(Qt::ISODate) : QString();
76 }
77
78 2044 QString MailSearchSession::state() const {
79
7/8
✓ Branch 2 → 3 taken 31 times.
✓ Branch 2 → 8 taken 852 times.
✓ Branch 2 → 13 taken 1141 times.
✓ Branch 2 → 18 taken 4 times.
✓ Branch 2 → 23 taken 11 times.
✓ Branch 2 → 28 taken 2 times.
✓ Branch 2 → 33 taken 3 times.
✗ Branch 2 → 38 not taken.
2044 switch (m_state) {
80 62 case State::Empty: return QStringLiteral("empty");
81 1704 case State::Searching: return QStringLiteral("searching");
82 2282 case State::Ready: return QStringLiteral("ready");
83 8 case State::Limited: return QStringLiteral("limited");
84 22 case State::Failed: return QStringLiteral("failed");
85 4 case State::Indexing: return QStringLiteral("indexing");
86 6 case State::Busy: return QStringLiteral("busy");
87 }
88 ✗ return {};
89 }
90
91 2534 qint64 MailSearchSession::total() const {
92
2/2
✓ Branch 2 → 3 taken 23 times.
✓ Branch 2 → 4 taken 2511 times.
2534 if (m_state == State::Empty) return 0;
93
4/4
✓ Branch 4 → 5 taken 1979 times.
✓ Branch 4 → 8 taken 532 times.
✓ Branch 5 → 6 taken 1972 times.
✓ Branch 5 → 8 taken 7 times.
2511 if (m_summaryPending || m_summary.outcome == MailSearchSummary::Outcome::Failed ||
94
2/4
✓ Branch 6 → 7 taken 1972 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1972 times.
1972 m_summary.outcome == MailSearchSummary::Outcome::Cancelled || m_summary.outcome == MailSearchSummary::Outcome::Busy)
95 539 return -1;
96 1972 return m_summary.total + m_serverAdded;
97 }
98
99 2131 QString MailSearchSession::summaryState() const {
100
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 8 taken 2121 times.
2141 if (m_state == State::Empty) return QStringLiteral("empty");
101
2/2
✓ Branch 8 → 9 taken 384 times.
✓ Branch 8 → 14 taken 1737 times.
2505 if (m_summaryPending) return QStringLiteral("pending");
102
2/3
✓ Branch 14 → 15 taken 1732 times.
✗ Branch 14 → 20 not taken.
✓ Branch 14 → 25 taken 5 times.
1737 switch (m_summary.outcome) {
103 3464 case MailSearchSummary::Outcome::Ready: return QStringLiteral("ready");
104 ✗ case MailSearchSummary::Outcome::Limited: return QStringLiteral("limited");
105 10 default: return QStringLiteral("failed");
106 }
107 }
108
109 461 void MailSearchSession::setStateValue(State state, const QString &text) {
110
5/6
✓ Branch 2 → 3 taken 42 times.
✓ Branch 2 → 6 taken 419 times.
✓ Branch 4 → 5 taken 42 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 42 times.
✓ Branch 7 → 9 taken 419 times.
461 if (m_state == state && m_stateText == text) return;
111 419 m_state = state;
112 419 m_stateText = text;
113 419 emit stateChanged();
114 }
115
116 // ── Running a query ──────────────────────────────────────────────────────
117
118 280 void MailSearchSession::setQuery(const QString &query, bool coalesce) {
119
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 8 taken 279 times.
280 if (query.size() > WorkspaceState::MaximumQueryCharacters) {
120
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
1 setStateValue(State::Limited, tr("The search is too long. Shorten it and retry."));
121 1 return;
122 }
123 279 m_localDebounce.stop();
124
2/2
✓ Branch 9 → 10 taken 279 times.
✓ Branch 10 → 11 taken 279 times.
279 run(SearchTermModel::canonical(query), coalesce, true);
125 }
126
127 9 void MailSearchSession::editFieldText(const QString &text) {
128 9 m_pendingFieldText = text;
129 // T-87.D3 policy shared with the command bar previews: quiet edits run
130 // promptly, a typing burst coalesces toward the cap.
131 9 const qint64 now = m_editClock.elapsed();
132
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 6 times.
9 m_localDebounce.start(SearchCoordinator::adaptiveDebounceMs(m_lastEdit < 0 ? -1 : now - m_lastEdit));
133 9 m_lastEdit = now;
134 9 }
135
136 4 void MailSearchSession::submitFieldText(const QString &text) {
137 4 m_localDebounce.stop();
138
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 4 → 5 taken 4 times.
4 run(SearchTermModel::withFieldText(m_query, text), m_lastStepTyped, true);
139 4 m_lastStepTyped = false;
140 4 m_serverDelay.stop();
141 4 startServerSearch(); // D-6: Enter runs the server too
142 4 }
143
144 156 QString MailSearchSession::normalizedFieldText(const QString &text) const {
145
2/2
✓ Branch 2 → 3 taken 156 times.
✓ Branch 3 → 4 taken 156 times.
156 return SearchTermModel::fieldText(SearchTermModel::withFieldText(m_query, text));
146 }
147
148 1 void MailSearchSession::revertFieldText() {
149 1 m_localDebounce.stop();
150 1 m_pendingFieldText.clear();
151 1 emit queryChanged(); // the field shows the query's text again
152 1 }
153
154 284 void MailSearchSession::run(const QString &canonical, bool coalesce, bool keepAnchor) {
155
7/10
✓ Branch 3 → 4 taken 90 times.
✓ Branch 3 → 9 taken 194 times.
✓ Branch 4 → 5 taken 90 times.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 90 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 90 times.
✗ Branch 6 → 9 not taken.
✓ Branch 10 → 11 taken 90 times.
✓ Branch 10 → 14 taken 194 times.
374 if (canonical == m_query && m_live && m_state != State::Failed && m_state != State::Busy &&
156
1/2
✓ Branch 7 → 8 taken 90 times.
✗ Branch 7 → 9 not taken.
90 m_state != State::Indexing) {
157
1/2
✓ Branch 11 → 12 taken 90 times.
✗ Branch 11 → 13 not taken.
90 if (!coalesce) m_lastStepTyped = false;
158 90 return;
159 }
160 194 pushHistory(canonical, coalesce);
161 194 m_lastStepTyped = coalesce;
162 194 m_query = canonical;
163 194 m_filter = {};
164
1/1
✓ Branch 20 → 21 taken 194 times.
194 m_text = parseSearchQuery(m_query, m_filter);
165 194 clearMarks(); // marks belong to the rows of the previous query
166 194 emit queryChanged();
167
10/10
✓ Branch 25 → 26 taken 194 times.
✓ Branch 29 → 30 taken 194 times.
✓ Branch 30 → 31 taken 194 times.
✓ Branch 31 → 32 taken 194 times.
✓ Branch 32 → 33 taken 194 times.
✓ Branch 33 → 34 taken 194 times.
✓ Branch 35 → 36 taken 194 times.
✓ Branch 36 → 37 taken 194 times.
✓ Branch 41 → 27 taken 194 times.
✓ Branch 41 → 42 taken 194 times.
388 qCDebug(lcSearch) << "Search tab" << m_tabId.left(8) << "runs a query of" << m_query.size() << "characters";
168
8/10
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 48 taken 193 times.
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 48 not taken.
✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 48 not taken.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 193 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 193 times.
194 if (m_serverRunning && m_deps.server && m_deps.server->owner() == m_tabId) m_deps.server->cancelServerSearch();
169 194 m_serverRunning = false;
170 194 m_serverAdded = 0;
171 194 m_serverFailed = 0;
172 194 emit serverChanged();
173 194 startLocal(keepAnchor);
174
7/8
✓ Branch 55 → 56 taken 192 times.
✓ Branch 55 → 61 taken 2 times.
✓ Branch 57 → 58 taken 192 times.
✗ Branch 57 → 61 not taken.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 61 taken 190 times.
✓ Branch 62 → 63 taken 2 times.
✓ Branch 62 → 64 taken 192 times.
194 if (m_state != State::Empty && m_deps.serverEnabled && m_deps.serverEnabled()) m_serverDelay.start();
175 194 }
176
177 194 void MailSearchSession::pushHistory(const QString &canonical, bool coalesce) {
178
6/6
✓ Branch 2 → 3 taken 89 times.
✓ Branch 2 → 7 taken 105 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 87 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 1 time.
194 if (m_historyIndex >= 0 && coalesce && m_lastStepTyped) {
179 1 m_history[m_historyIndex] = canonical; // one step per typing burst
180 } else {
181
1/1
✓ Branch 7 → 8 taken 193 times.
193 m_history = m_history.mid(0, m_historyIndex + 1);
182 193 m_history.append(canonical);
183
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 193 times.
193 if (m_history.size() > MaximumHistory) m_history.removeFirst();
184 193 m_historyIndex = int(m_history.size()) - 1;
185 }
186 194 emit historyChanged();
187 194 }
188
189 // F-27 (SPRINT-104): navigation owns the server lifecycle of a new query —
190 // the running search of the query left behind is cancelled and the restored
191 // query gets its own (delayed) server search, exactly like run().
192 59 bool MailSearchSession::navigateHistory(int step) {
193 59 m_historyIndex += step;
194 59 emit historyChanged();
195 59 m_lastStepTyped = false;
196 59 m_filter = {};
197 59 m_query = m_history.at(m_historyIndex);
198
1/1
✓ Branch 9 → 10 taken 59 times.
59 m_text = parseSearchQuery(m_query, m_filter);
199 59 emit queryChanged();
200
8/10
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 19 taken 57 times.
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 19 not taken.
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 19 not taken.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 57 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 57 times.
59 if (m_serverRunning && m_deps.server && m_deps.server->owner() == m_tabId) m_deps.server->cancelServerSearch();
201 59 m_serverRunning = false;
202 59 m_serverAdded = 0;
203 59 m_serverFailed = 0;
204 59 emit serverChanged();
205 59 startLocal(true);
206
6/8
✓ Branch 26 → 27 taken 59 times.
✗ Branch 26 → 32 not taken.
✓ Branch 28 → 29 taken 59 times.
✗ Branch 28 → 32 not taken.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 57 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 57 times.
59 if (m_state != State::Empty && m_deps.serverEnabled && m_deps.serverEnabled()) m_serverDelay.start();
207 59 return true;
208 59 }
209
210
3/4
✓ Branch 3 → 4 taken 41 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 5 → 6 taken 41 times.
✗ Branch 5 → 7 not taken.
42 bool MailSearchSession::back() { return canGoBack() && navigateHistory(-1); }
211
2/4
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 7 not taken.
18 bool MailSearchSession::forward() { return canGoForward() && navigateHistory(1); }
212
213 3 void MailSearchSession::retry() { startLocal(true); }
214
215 370 void MailSearchSession::cancelReads() {
216
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 370 times.
370 if (!m_deps.reader) return;
217 370 m_deps.reader->cancel(&m_pageOwner);
218 370 m_deps.reader->cancelSummary(&m_summaryOwner);
219 370 m_deps.reader->cancelSummary(&m_relaxationOwner);
220 }
221
222 258 void MailSearchSession::startLocal(bool keepAnchor) {
223 Q_UNUSED(keepAnchor)
224 258 cancelReads();
225 258 ++m_generation;
226 258 m_relaxationCounts.clear();
227 258 m_relaxationCandidates.clear();
228 258 m_runClock.start();
229
6/6
✓ Branch 7 → 8 taken 21 times.
✓ Branch 7 → 11 taken 237 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 19 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 25 taken 256 times.
258 if (m_text.isEmpty() && m_filter.isEmpty()) {
230
1/1
✓ Branch 14 → 15 taken 2 times.
2 m_rows.setHeaders({});
231 2 m_identities.clear();
232 2 m_summary = {};
233 2 m_summaryPending = false;
234 2 m_hasMore = false;
235 2 m_pagePending = false;
236
1/1
✓ Branch 20 → 21 taken 2 times.
2 setStateValue(State::Empty);
237 2 emit rowsChanged();
238 2 emit summaryChanged();
239 2 return;
240 }
241
5/6
✓ Branch 26 → 27 taken 256 times.
✗ Branch 26 → 30 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 254 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 44 taken 254 times.
256 if (m_deps.indexReady && !m_deps.indexReady()) {
242
1/1
✓ Branch 33 → 34 taken 2 times.
2 m_rows.setHeaders({});
243 2 m_identities.clear();
244 // S106-243: like the empty query — the previous query's counts, chips
245 // and "more" state must not stand next to this one's empty list.
246 2 m_summary = {};
247 2 m_summaryPending = false;
248 2 m_hasMore = false;
249 2 m_pagePending = false;
250
2/2
✓ Branch 38 → 39 taken 2 times.
✓ Branch 39 → 40 taken 2 times.
2 setStateValue(State::Indexing, tr("The search index is being built. Results appear when it is ready."));
251 2 emit rowsChanged();
252 2 emit summaryChanged();
253 2 return;
254 }
255 254 m_live = true;
256 254 m_offset = 0;
257
1/2
✓ Branch 45 → 46 taken 254 times.
✗ Branch 45 → 48 not taken.
254 m_indexGeneration = m_deps.indexGeneration ? m_deps.indexGeneration() : 0;
258 // §6.2.4 "refetch keeps the frame": the previous rows stay until the
259 // first page of the new query replaces them.
260
1/1
✓ Branch 50 → 51 taken 254 times.
254 setStateValue(State::Searching);
261 254 m_summaryPending = true;
262 254 emit summaryChanged();
263 254 requestPage(true);
264 254 requestSummary();
265 }
266
267 548 static MailSearchRequest searchRequest(MailCache *cache, const QString &text, const MailCache::SearchFilter &filter,
268 const QString &account, int limit, int offset) {
269
1/2
✓ Branch 2 → 3 taken 548 times.
✗ Branch 2 → 4 not taken.
548 MailSearchRequest request{cache ? cache->databasePath() : QString(), text, filter, account, limit, offset};
270 548 return request;
271 }
272
273 271 void MailSearchSession::requestPage(bool replace) {
274
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 271 times.
271 if (!m_deps.reader) return;
275 271 m_pagePending = true;
276 271 emit rowsChanged();
277 271 const quint64 generation = m_generation;
278
1/1
✓ Branch 7 → 8 taken 271 times.
271 m_deps.reader->read(&m_pageOwner, searchRequest(m_deps.cache, m_text, m_filter, m_accountId, PageSize + 1, m_offset),
279
1/1
✓ Branch 5 → 6 taken 271 times.
542 [this, replace, generation](MailSearchPage page) {
280
1/1
✓ Branch 4 → 5 taken 219 times.
219 pageArrived(std::move(page), replace, generation);
281 219 });
282 }
283
284 23906 QString MailSearchSession::identityKey(const MailHeader &header) const {
285 23906 return header.messageId.isEmpty()
286
5/11
✓ Branch 8 → 9 taken 2249 times.
✓ Branch 14 → 15 taken 2249 times.
✓ Branch 14 → 16 taken 21657 times.
✓ Branch 16 → 17 taken 2249 times.
✓ Branch 16 → 18 taken 21657 times.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
26155 ? QStringLiteral("row:%1:%2").arg(header.folderId).arg(header.uid)
287
5/5
✓ Branch 3 → 4 taken 2249 times.
✓ Branch 3 → 11 taken 21657 times.
✓ Branch 10 → 12 taken 2249 times.
✓ Branch 12 → 13 taken 2249 times.
✓ Branch 12 → 14 taken 21657 times.
50061 : header.messageId;
288 }
289
290 // D-11: one row per distinct mail (canonical Message-ID, else the row),
291 // only results of this account in folders it lists.
292 215 QList<MailHeader> MailSearchSession::admit(const QList<MailSearchHit> &hits) {
293 215 QList<MailHeader> headers;
294
2/2
✓ Branch 34 → 4 taken 23791 times.
✓ Branch 34 → 35 taken 215 times.
24006 for (const MailSearchHit &hit : hits) {
295 23791 const MailId identity{m_accountId, hit.header.folderId, hit.header.uid};
296
6/7
✓ Branch 7 → 8 taken 23791 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 23791 times.
✓ Branch 9 → 10 taken 42 times.
✓ Branch 9 → 11 taken 23749 times.
✓ Branch 12 → 13 taken 42 times.
✓ Branch 12 → 14 taken 23749 times.
23791 if (m_deps.acceptResult && !m_deps.acceptResult(identity, hit.folderPath)) continue;
297
1/1
✓ Branch 14 → 15 taken 23749 times.
23749 const QString key = identityKey(hit.header);
298
2/2
✓ Branch 16 → 17 taken 74 times.
✓ Branch 16 → 18 taken 23675 times.
23749 if (m_identities.contains(key)) continue;
299
1/1
✓ Branch 18 → 19 taken 23675 times.
23675 m_identities.insert(key);
300
1/1
✓ Branch 19 → 20 taken 23675 times.
23675 m_rows.setFolderBadge(hit.header.folderId, hit.folderPath);
301
1/1
✓ Branch 20 → 21 taken 23675 times.
23675 m_folderPaths.insert(hit.header.folderId, hit.folderPath);
302
1/1
✓ Branch 21 → 22 taken 23675 times.
23675 headers.append(hit.header);
303 23865 }
304 215 return headers;
305 ✗ }
306
307 219 void MailSearchSession::pageArrived(MailSearchPage page, bool replace, quint64 generation) {
308
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 219 times.
223 if (generation != m_generation) return;
309 219 m_pagePending = false;
310
5/9
✓ Branch 5 → 6 taken 219 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 219 times.
✓ Branch 7 → 8 taken 219 times.
✗ Branch 7 → 12 not taken.
✓ Branch 9 → 10 taken 219 times.
✗ Branch 9 → 13 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 219 times.
657 if ((m_deps.indexReady && !m_deps.indexReady()) ||
311
2/3
✓ Branch 10 → 11 taken 219 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 219 times.
438 (m_deps.indexGeneration && m_indexGeneration != m_deps.indexGeneration())) {
312 ✗ startLocal(true);
313 ✗ return;
314 }
315
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 48 taken 215 times.
219 if (page.outcome != MailSearchPage::Outcome::Ready) {
316 4 const bool busy = page.outcome == MailSearchPage::Outcome::Busy;
317
9/9
✓ Branch 18 → 19 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 27 → 28 taken 4 times.
✓ Branch 32 → 20 taken 4 times.
✓ Branch 32 → 33 taken 4 times.
8 qCInfo(lcSearch) << "Search page failed for tab" << m_tabId.left(8) << "outcome" << int(page.outcome);
318
5/5
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 3 times.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 42 taken 2 times.
✓ Branch 44 → 45 taken 4 times.
4 setStateValue(busy ? State::Busy : page.outcome == MailSearchPage::Outcome::Limited ? State::Limited : State::Failed,
319
5/5
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 3 times.
✓ Branch 34 → 38 taken 1 time.
✓ Branch 36 → 38 taken 1 time.
✓ Branch 37 → 38 taken 2 times.
11 busy ? tr("Search is busy. Try again.")
320 3 : page.outcome == MailSearchPage::Outcome::Limited
321
2/2
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 2 times.
3 ? tr("This search exceeds the processing limits. Narrow it and try again.")
322 : tr("The local search failed. Try again."));
323
1/1
✓ Branch 46 → 47 taken 4 times.
4 emit rowsChanged();
324 4 return;
325 }
326 215 m_hasMore = page.hits.size() > PageSize;
327
2/2
✓ Branch 49 → 50 taken 96 times.
✓ Branch 49 → 51 taken 119 times.
215 if (m_hasMore) page.hits.removeLast();
328 215 m_offset += int(page.hits.size());
329
2/2
✓ Branch 52 → 53 taken 198 times.
✓ Branch 52 → 54 taken 17 times.
215 if (replace) m_identities.clear();
330
1/1
✓ Branch 54 → 55 taken 215 times.
215 const QList<MailHeader> headers = admit(page.hits);
331 215 bool selectedSurvived = false;
332
2/2
✓ Branch 55 → 56 taken 198 times.
✓ Branch 55 → 75 taken 17 times.
215 if (replace) {
333
2/2
✓ Branch 64 → 58 taken 22147 times.
✓ Branch 64 → 65 taken 198 times.
22345 for (const MailHeader &header : headers)
334
4/4
✓ Branch 59 → 60 taken 3737 times.
✓ Branch 59 → 62 taken 18410 times.
✓ Branch 60 → 61 taken 12 times.
✓ Branch 60 → 62 taken 3725 times.
22147 if (header.folderId == m_selected.folderId && header.uid == m_selected.uid) selectedSurvived = true;
335
1/1
✓ Branch 65 → 66 taken 198 times.
198 m_rows.clearFolderBadges();
336
5/5
✓ Branch 66 → 67 taken 198 times.
✓ Branch 67 → 68 taken 198 times.
✓ Branch 70 → 71 taken 22198 times.
✓ Branch 73 → 69 taken 22198 times.
✓ Branch 73 → 74 taken 198 times.
22396 for (const MailSearchHit &hit : page.hits) m_rows.setFolderBadge(hit.header.folderId, hit.folderPath);
337
1/1
✓ Branch 74 → 78 taken 198 times.
198 m_rows.setHeaders(headers);
338
1/2
✓ Branch 76 → 77 taken 17 times.
✗ Branch 76 → 78 not taken.
17 } else if (!headers.isEmpty()) {
339
1/1
✓ Branch 77 → 78 taken 17 times.
17 m_rows.appendHeaders(headers);
340 }
341
10/10
✓ Branch 78 → 79 taken 215 times.
✓ Branch 82 → 83 taken 215 times.
✓ Branch 83 → 84 taken 215 times.
✓ Branch 84 → 85 taken 215 times.
✓ Branch 85 → 86 taken 215 times.
✓ Branch 86 → 87 taken 215 times.
✓ Branch 88 → 89 taken 215 times.
✓ Branch 89 → 90 taken 215 times.
✓ Branch 102 → 80 taken 215 times.
✓ Branch 102 → 103 taken 215 times.
430 qCInfo(lcSearch) << "Search page for tab" << m_tabId.left(8) << "rows" << headers.size() << "loaded"
342
6/6
✓ Branch 91 → 92 taken 215 times.
✓ Branch 92 → 93 taken 215 times.
✓ Branch 93 → 94 taken 215 times.
✓ Branch 94 → 95 taken 215 times.
✓ Branch 95 → 96 taken 215 times.
✓ Branch 97 → 98 taken 215 times.
215 << m_rows.rowCount() << "more" << m_hasMore << "ms" << m_runClock.elapsed();
343
5/7
✓ Branch 103 → 104 taken 17 times.
✓ Branch 103 → 106 taken 198 times.
✓ Branch 104 → 105 taken 17 times.
✗ Branch 104 → 106 not taken.
✗ Branch 105 → 106 not taken.
✓ Branch 105 → 110 taken 17 times.
✓ Branch 107 → 108 taken 198 times.
215 if (m_state == State::Searching || m_state == State::Busy || m_state == State::Failed) setStateValue(State::Ready);
344
1/1
✓ Branch 110 → 111 taken 215 times.
215 emit rowsChanged();
345
5/5
✓ Branch 111 → 112 taken 198 times.
✓ Branch 111 → 118 taken 17 times.
✓ Branch 112 → 113 taken 12 times.
✓ Branch 112 → 114 taken 186 times.
✓ Branch 115 → 116 taken 198 times.
215 if (replace) emit rowsReplaced(selectedSurvived ? m_selected : MailId{});
346 // SB-05: a page whose rows were all known already must not end the list.
347
4/7
✓ Branch 119 → 120 taken 14 times.
✓ Branch 119 → 122 taken 201 times.
✗ Branch 120 → 121 not taken.
✓ Branch 120 → 122 taken 14 times.
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 125 taken 215 times.
✗ Branch 124 → 125 not taken.
215 if (headers.isEmpty() && m_hasMore) requestPage(false);
348
9/10
✓ Branch 125 → 126 taken 198 times.
✓ Branch 125 → 131 taken 17 times.
✓ Branch 127 → 128 taken 198 times.
✓ Branch 128 → 129 taken 14 times.
✓ Branch 128 → 131 taken 184 times.
✓ Branch 129 → 130 taken 14 times.
✗ Branch 129 → 131 not taken.
✓ Branch 132 → 133 taken 14 times.
✓ Branch 132 → 134 taken 201 times.
✓ Branch 133 → 134 taken 14 times.
215 if (replace && m_rows.rowCount() == 0 && !m_hasMore) requestRelaxations();
349 215 }
350
351 242 void MailSearchSession::fetchMore() {
352
5/8
✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 6 taken 225 times.
✓ Branch 3 → 4 taken 17 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 17 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 17 times.
242 if (!m_hasMore || m_pagePending || !m_live || m_state == State::Empty) return;
353 17 requestPage(false);
354 }
355
356 // ── Summary and relaxations ──────────────────────────────────────────────
357
358 254 void MailSearchSession::requestSummary() {
359
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 254 times.
254 if (!m_deps.reader) return;
360 254 const quint64 generation = m_generation;
361 254 auto request = searchRequest(m_deps.cache, m_text, m_filter, m_accountId, 1, 0);
362
2/3
✓ Branch 6 → 7 taken 254 times.
✗ Branch 6 → 11 not taken.
✓ Branch 7 → 8 taken 254 times.
254 if (m_deps.allowedFolders) request.allowedFolders = m_deps.allowedFolders();
363
2/2
✓ Branch 12 → 13 taken 254 times.
✓ Branch 13 → 14 taken 254 times.
254 m_deps.reader->summarize(&m_summaryOwner, request, [this, generation](MailSearchSummary summary) {
364
1/1
✓ Branch 4 → 5 taken 190 times.
190 summaryArrived(std::move(summary), generation);
365 190 });
366 254 }
367
368 190 void MailSearchSession::summaryArrived(MailSearchSummary summary, quint64 generation) {
369
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 190 times.
190 if (generation != m_generation) return;
370 // S106-244: computed against an index that was rebuilt since — dropped
371 // like a stale page (the page path restarts the query).
372
3/6
✓ Branch 5 → 6 taken 190 times.
✗ Branch 5 → 9 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 190 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 190 times.
190 if (m_deps.indexGeneration && m_indexGeneration != m_deps.indexGeneration()) return;
373 190 m_summaryPending = false;
374 190 m_summary = std::move(summary);
375
9/9
✓ Branch 14 → 15 taken 190 times.
✓ Branch 18 → 19 taken 190 times.
✓ Branch 19 → 20 taken 190 times.
✓ Branch 20 → 21 taken 190 times.
✓ Branch 21 → 22 taken 190 times.
✓ Branch 22 → 23 taken 190 times.
✓ Branch 23 → 24 taken 190 times.
✓ Branch 35 → 16 taken 190 times.
✓ Branch 35 → 36 taken 190 times.
380 qCInfo(lcSearch) << "Search summary for tab" << m_tabId.left(8) << "outcome" << int(m_summary.outcome)
376
7/7
✓ Branch 24 → 25 taken 190 times.
✓ Branch 25 → 26 taken 190 times.
✓ Branch 26 → 27 taken 190 times.
✓ Branch 27 → 28 taken 190 times.
✓ Branch 28 → 29 taken 190 times.
✓ Branch 29 → 30 taken 190 times.
✓ Branch 30 → 31 taken 190 times.
190 << "total" << m_summary.total << "days" << m_summary.dayCounts.size() << "ms" << m_summary.elapsedMs;
377
1/5
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 42 taken 190 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 42 not taken.
✗ Branch 39 → 40 not taken.
190 if (m_summary.outcome == MailSearchSummary::Outcome::Limited && m_state == State::Ready) setStateValue(State::Limited);
378 190 emit summaryChanged();
379 }
380
381 // §6.2.5: after an empty result, one count per candidate relaxation (at
382 // most eight are counted, four are offered), one after the other on the
383 // summary lane, cancelled by the next run.
384 14 void MailSearchSession::requestRelaxations() {
385
4/8
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 5 taken 5 times.
✓ Branch 4 → 8 taken 9 times.
✓ Branch 8 → 9 taken 14 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 30 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
28 m_relaxationCandidates = SearchTermModel::relaxations(m_query, m_deps.lookups ? m_deps.lookups() : SearchTermModel::Lookups{})
386
1/1
✓ Branch 9 → 10 taken 14 times.
14 .mid(0, 8);
387
3/6
✓ Branch 15 → 16 taken 14 times.
✗ Branch 15 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 14 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 14 times.
14 if (m_relaxationCandidates.isEmpty() || !m_deps.reader) return;
388 14 relaxationStep(m_generation, 0);
389 }
390
391 // One relaxation count; the next one starts from its result. A member, not a
392 // self-capturing std::function (that leaked once per empty search, S102.4.1).
393 40 void MailSearchSession::relaxationStep(quint64 generation, int position) {
394
9/13
✓ Branch 2 → 3 taken 40 times.
✗ Branch 2 → 8 not taken.
✓ Branch 4 → 5 taken 27 times.
✓ Branch 4 → 8 taken 13 times.
✓ Branch 5 → 6 taken 27 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 27 times.
✓ Branch 10 → 11 taken 27 times.
✓ Branch 10 → 12 taken 13 times.
✓ Branch 12 → 13 taken 13 times.
✓ Branch 12 → 14 taken 27 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
44 if (generation != m_generation || position >= m_relaxationCandidates.size() || relaxations().size() >= 4) return;
395 27 MailCache::SearchFilter filter;
396
1/1
✓ Branch 16 → 17 taken 27 times.
27 const QString text = parseSearchQuery(m_relaxationCandidates.at(position).query, filter);
397
8/8
✓ Branch 18 → 19 taken 10 times.
✓ Branch 18 → 22 taken 17 times.
✓ Branch 19 → 20 taken 10 times.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 22 taken 6 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 26 taken 23 times.
✓ Branch 24 → 25 taken 4 times.
27 if (text.isEmpty() && filter.isEmpty()) { relaxationStep(generation, position + 1); return; }
398 23 auto request = searchRequest(m_deps.cache, text, filter, m_accountId, 1, 0);
399 23 request.detail = MailSearchSummary::Detail::CountOnly;
400
2/3
✓ Branch 28 → 29 taken 23 times.
✗ Branch 28 → 33 not taken.
✓ Branch 29 → 30 taken 23 times.
23 if (m_deps.allowedFolders) request.allowedFolders = m_deps.allowedFolders();
401
3/3
✓ Branch 33 → 34 taken 23 times.
✓ Branch 34 → 35 taken 23 times.
✓ Branch 35 → 36 taken 23 times.
23 m_deps.reader->summarize(&m_relaxationOwner, request, [this, generation, position](MailSearchSummary result) {
402
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 22 times.
22 if (generation != m_generation) return;
403
1/2
✓ Branch 4 → 5 taken 22 times.
✗ Branch 4 → 6 not taken.
22 if (result.outcome == MailSearchSummary::Outcome::Ready) m_relaxationCounts.insert(position, result.total);
404 22 emit summaryChanged();
405 22 relaxationStep(generation, position + 1);
406 });
407 31 }
408
409 1579 SearchMenuModel::Context MailSearchSession::menuContext() const {
410 1579 SearchMenuModel::Context context;
411 1579 context.query = m_query;
412
4/4
✓ Branch 4 → 5 taken 1300 times.
✓ Branch 4 → 7 taken 279 times.
✓ Branch 5 → 6 taken 1284 times.
✓ Branch 5 → 7 taken 16 times.
1579 const bool summaryKnown = !m_summaryPending && m_summary.outcome == MailSearchSummary::Outcome::Ready;
413
3/4
✓ Branch 9 → 10 taken 16 times.
✓ Branch 9 → 12 taken 279 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 16 times.
295 context.summary = summaryKnown || (!m_summaryPending && m_summary.outcome == MailSearchSummary::Outcome::Limited)
414
2/2
✓ Branch 8 → 9 taken 295 times.
✓ Branch 8 → 11 taken 1284 times.
1874 ? &m_summary : nullptr;
415 1579 context.countsShown = summaryKnown; // D-27: no counts while limited
416
2/3
✓ Branch 14 → 15 taken 1579 times.
✗ Branch 14 → 19 not taken.
✓ Branch 15 → 16 taken 1579 times.
1579 if (m_deps.lookups) context.lookups = m_deps.lookups();
417 1579 return context;
418 ✗ }
419
420
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 3 → 4 taken 3 times.
3 QVariantList MailSearchSession::filterMenu() const { return SearchMenuModel::filterEntries(menuContext()); }
421
422 1 QVariantList MailSearchSession::chipMenu(int termIndex) const {
423
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 return SearchMenuModel::chipEntries(menuContext(), termIndex);
424 }
425
426 516 QVariantList MailSearchSession::folderFacet() const {
427
4/5
✓ Branch 3 → 4 taken 516 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 516 times.
✓ Branch 6 → 7 taken 516 times.
✓ Branch 7 → 8 taken 516 times.
516 return SearchMenuModel::folderFacets(menuContext(), m_deps.folderTree ? m_deps.folderTree() : QList<SearchMenuModel::FolderRow>{});
428 }
429
430 1032 QVariantList MailSearchSession::valueFacet(const QString &kind, int limit) const {
431
2/2
✓ Branch 3 → 4 taken 1032 times.
✓ Branch 4 → 5 taken 1032 times.
2064 return SearchMenuModel::sidebarValues(menuContext(), SearchTermModel::kindFromName(kind), limit);
432 }
433
434 27 QVariantList MailSearchSession::fieldCompletions(const QString &text) const {
435
2/2
✓ Branch 2 → 3 taken 27 times.
✓ Branch 3 → 4 taken 27 times.
27 return SearchMenuModel::personCompletions(menuContext(), text);
436 }
437
438 1 bool MailSearchSession::pickFieldCompletion(const QString &text, const QString &id) {
439
1/1
✓ Branch 2 → 3 taken 1 time.
1 const QString next = SearchMenuModel::applyPerson(m_query, text, id);
440
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (next.isEmpty()) return false;
441
1/1
✓ Branch 6 → 7 taken 1 time.
1 m_localDebounce.stop();
442 1 m_pendingFieldText.clear();
443
7/7
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 23 → 10 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
2 qCDebug(lcSearch) << "Search tab: field completion picked" << id.section(QLatin1Char(':'), 0, 1);
444
1/1
✓ Branch 24 → 25 taken 1 time.
1 setQuery(next);
445
1/1
✓ Branch 25 → 26 taken 1 time.
1 emit queryChanged(); // the field shows the shortened text even when the query is unchanged
446 1 return true;
447 1 }
448
449 10 bool MailSearchSession::applyChoice(const QString &id) {
450
1/1
✓ Branch 2 → 3 taken 10 times.
10 const QString next = SearchMenuModel::apply(m_query, id);
451
5/6
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 7 taken 1 time.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 9 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 9 times.
10 if (next.isNull() || next == m_query) return false;
452
7/7
✓ Branch 11 → 12 taken 9 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 16 → 17 taken 9 times.
✓ Branch 20 → 21 taken 9 times.
✓ Branch 21 → 22 taken 9 times.
✓ Branch 26 → 13 taken 9 times.
✓ Branch 26 → 27 taken 9 times.
18 qCDebug(lcSearch) << "Search tab: menu choice" << id.section(QLatin1Char(':'), 0, 0);
453
1/1
✓ Branch 27 → 28 taken 9 times.
9 setQuery(next);
454 9 return true;
455 10 }
456
457 595 QVariantList MailSearchSession::relaxations() const {
458 595 QVariantList result;
459
5/6
✓ Branch 38 → 39 taken 172 times.
✓ Branch 38 → 42 taken 595 times.
✓ Branch 40 → 41 taken 172 times.
✗ Branch 40 → 42 not taken.
✓ Branch 43 → 3 taken 172 times.
✓ Branch 43 → 44 taken 595 times.
767 for (int i = 0; i < m_relaxationCandidates.size() && result.size() < 4; ++i) {
460 172 const qint64 count = m_relaxationCounts.value(i, 0);
461
2/2
✓ Branch 4 → 5 taken 142 times.
✓ Branch 4 → 6 taken 30 times.
172 if (count <= 0) continue;
462
1/1
✓ Branch 22 → 23 taken 30 times.
90 result.append(QVariantMap{{QStringLiteral("label"), m_relaxationCandidates.at(i).label},
463 60 {QStringLiteral("query"), m_relaxationCandidates.at(i).query},
464
2/4
✓ Branch 26 → 27 taken 90 times.
✓ Branch 26 → 28 taken 30 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
180 {QStringLiteral("count"), count}});
465 }
466 595 return result;
467 120 }
468
469 // ── Chip edits ───────────────────────────────────────────────────────────
470
471
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
2 void MailSearchSession::removeTerm(int index) { setQuery(SearchTermModel::remove(m_query, index)); }
472
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 void MailSearchSession::replaceTerm(int index, const QString &value) { setQuery(SearchTermModel::replace(m_query, index, value)); }
473
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 void MailSearchSession::negateTerm(int index) { setQuery(SearchTermModel::negate(m_query, index)); }
474
475 2 void MailSearchSession::addTerm(const QString &kind, const QString &value, bool negated) {
476
3/3
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
2 setQuery(SearchTermModel::add(m_query, SearchTermModel::kindFromName(kind), value, negated));
477 2 }
478
479 4 void MailSearchSession::setState(const QString &kind, int value) {
480
3/3
✓ Branch 2 → 3 taken 4 times.
✓ Branch 3 → 4 taken 4 times.
✓ Branch 4 → 5 taken 4 times.
4 setQuery(SearchTermModel::setState(m_query, SearchTermModel::kindFromName(kind), value));
481 4 }
482
483 1 void MailSearchSession::clearStatusFilters() { setStatusFilters(false, false, false); }
484
485 1 void MailSearchSession::setStatusFilters(bool unread, bool flagged, bool attachment) {
486
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 setQuery(SearchTermModel::setStatusStates(m_query, unread, flagged, attachment));
487 1 }
488
489 514 int MailSearchSession::termState(const QString &kind) const {
490 514 return SearchTermModel::state(m_query, SearchTermModel::kindFromName(kind));
491 }
492
493 41 void MailSearchSession::setDateRange(const QString &from, const QString &to) {
494
4/4
✓ Branch 2 → 3 taken 41 times.
✓ Branch 3 → 4 taken 41 times.
✓ Branch 4 → 5 taken 41 times.
✓ Branch 5 → 6 taken 41 times.
41 setQuery(SearchTermModel::setDateRange(m_query, QDate::fromString(from, Qt::ISODate), QDate::fromString(to, Qt::ISODate)));
495 41 }
496
497 1 int MailSearchSession::rowForIdentity(const QVariantMap &identity) const { return m_results.rowForIdentity(identity); }
498 1 QVariantMap MailSearchSession::mailAt(int row) const { return m_results.mailAt(row); }
499
500 // ── Lifecycle ────────────────────────────────────────────────────────────
501
502 108 void MailSearchSession::activate() {
503 108 m_active = true;
504
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 107 times.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
108 if (!m_live && m_state != State::Empty) startLocal(true);
505
6/10
✓ Branch 5 → 6 taken 101 times.
✓ Branch 5 → 12 taken 7 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 101 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 101 times.
✓ Branch 13 → 15 taken 7 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 108 times.
108 if (m_deps.server && m_deps.server->owner() == m_tabId && m_deps.server->paused()) m_deps.server->resume();
506
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 108 times.
108 else if (m_serverWanted) startServerSearch();
507 108 }
508
509 40 void MailSearchSession::deactivate() {
510 40 m_active = false;
511 // D-12: one search connection; only the active tab scans the server.
512
3/10
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 8 taken 40 times.
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 8 not taken.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 40 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 40 times.
40 if (m_serverRunning && m_deps.server && m_deps.server->owner() == m_tabId) m_deps.server->pause();
513 40 }
514
515 8 void MailSearchSession::release() {
516
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 2 times.
8 if (!m_live) return;
517 2 cancelReads();
518 2 ++m_generation;
519 2 m_live = false;
520
1/1
✓ Branch 6 → 7 taken 2 times.
2 m_rows.setHeaders({});
521 2 m_identities.clear();
522 2 m_pagePending = false;
523
7/7
✓ Branch 9 → 10 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 21 → 11 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
4 qCInfo(lcSearch) << "Search session released its rows for tab" << m_tabId.left(8);
524 2 emit rowsChanged();
525 }
526
527 1 void MailSearchSession::refreshAfterIndexReady() {
528
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
1 if (m_state != State::Empty) startLocal(true);
529 1 }
530
531 321 void MailSearchSession::setSelected(const MailId &mail) {
532
2/2
✓ Branch 3 → 4 taken 200 times.
✓ Branch 3 → 5 taken 121 times.
321 if (m_selected == mail) return;
533 121 const MailSelectionKey previous{m_selected.folderId, m_selected.uid, {}};
534 121 m_selected = mail;
535
1/1
✓ Branch 9 → 10 taken 121 times.
121 m_results.singleSelectionChanged(previous, {mail.folderId, mail.uid, {}});
536
1/1
✓ Branch 12 → 13 taken 121 times.
121 emit selectionChanged();
537 242 }
538
539 8 void MailSearchSession::mark(int row, Qt::KeyboardModifiers modifiers) {
540
1/1
✓ Branch 2 → 3 taken 8 times.
8 const MailId mail = mailIdAt(row);
541
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 8 times.
8 if (!mail.isValid()) return;
542
7/9
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 13 taken 3 times.
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 13 not taken.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 18 taken 3 times.
8 if (m_marked.isEmpty() && m_selected.isValid() && contains(m_selected)) {
543
1/1
✓ Branch 16 → 17 taken 5 times.
5 m_marked.insert({m_selected.folderId, m_selected.uid});
544 5 m_markAnchor = m_selected;
545 }
546
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 40 taken 7 times.
8 if (modifiers & Qt::ShiftModifier) {
547
2/3
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 25 not taken.
✓ Branch 23 → 24 taken 1 time.
1 const int anchor = m_markAnchor.isValid() ? rowOf(m_markAnchor) : row;
548 1 m_marked.clear();
549
2/2
✓ Branch 38 → 29 taken 4 times.
✓ Branch 38 → 39 taken 1 time.
5 for (int index = qMin(anchor, row); index <= qMax(anchor, row); ++index) {
550
1/1
✓ Branch 29 → 30 taken 4 times.
4 const MailId id = mailIdAt(index);
551
2/3
✓ Branch 31 → 32 taken 4 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 4 times.
4 if (id.isValid()) m_marked.insert({id.folderId, id.uid});
552 4 }
553 } else {
554 7 const QPair<qint64, qint64> key{mail.folderId, mail.uid};
555
4/4
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 44 taken 6 times.
✓ Branch 43 → 46 taken 1 time.
✓ Branch 44 → 45 taken 6 times.
7 if (m_marked.contains(key)) m_marked.remove(key); else m_marked.insert(key);
556 7 m_markAnchor = mail;
557 }
558
1/1
✓ Branch 48 → 49 taken 8 times.
8 m_results.selectionChanged();
559
1/1
✓ Branch 49 → 50 taken 8 times.
8 emit markedChanged();
560 8 }
561
562 745 void MailSearchSession::setPeriodUnit(const QString &unit) {
563
2/2
✓ Branch 3 → 4 taken 742 times.
✓ Branch 3 → 5 taken 3 times.
746 if (m_periodUnit == unit) return;
564 3 m_periodUnit = unit;
565
5/6
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 11 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 18 taken 2 times.
3 if (unit.isEmpty() || unit == QLatin1String("hour")) {
566
1/1
✓ Branch 15 → 16 taken 1 time.
1 m_results.setPeriodProvider({});
567 1 return;
568 }
569
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 2 times.
4 const TimeHistogram::Unit value = unit == QLatin1String("year") ? TimeHistogram::Unit::Year
570
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 1 time.
3 : unit == QLatin1String("month") ? TimeHistogram::Unit::Month
571
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 1 time.
1 : unit == QLatin1String("week") ? TimeHistogram::Unit::Week : TimeHistogram::Unit::Day;
572
1/1
✓ Branch 33 → 34 taken 2 times.
2 const QLocale locale;
573
1/1
✓ Branch 34 → 35 taken 2 times.
2 const Qt::DayOfWeek weekStart = locale.firstDayOfWeek();
574
2/4
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
2 m_results.setPeriodProvider([value, locale, weekStart](const QDateTime &date) {
575
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 88 times.
88 if (!date.isValid()) return QString();
576
4/4
✓ Branch 5 → 6 taken 88 times.
✓ Branch 6 → 7 taken 88 times.
✓ Branch 7 → 8 taken 88 times.
✓ Branch 8 → 9 taken 88 times.
88 return TimeHistogram::rangeLabel(TimeHistogram::unitRange(date.toLocalTime().date(), value, weekStart), locale);
577 });
578 2 }
579
580 4 void MailSearchSession::markAllLoaded() {
581
2/2
✓ Branch 12 → 3 taken 615 times.
✓ Branch 12 → 13 taken 4 times.
619 for (int row = 0; row < loadedCount(); ++row) {
582
1/1
✓ Branch 3 → 4 taken 615 times.
615 const MailId id = mailIdAt(row);
583
2/3
✓ Branch 5 → 6 taken 615 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 615 times.
615 if (id.isValid()) m_marked.insert({id.folderId, id.uid});
584 615 }
585 4 m_results.selectionChanged();
586 4 emit markedChanged();
587 4 }
588
589 206 void MailSearchSession::clearMarks() {
590
2/2
✓ Branch 3 → 4 taken 201 times.
✓ Branch 3 → 5 taken 5 times.
206 if (m_marked.isEmpty()) return;
591 5 m_marked.clear();
592 5 m_markAnchor = {};
593 5 m_results.selectionChanged();
594 5 emit markedChanged();
595 }
596
597 8 QList<MailId> MailSearchSession::markedIds() const {
598 8 QList<MailId> ids;
599
3/3
✓ Branch 10 → 11 taken 1240 times.
✓ Branch 11 → 3 taken 1232 times.
✓ Branch 11 → 12 taken 8 times.
1240 for (int row = 0; row < loadedCount(); ++row) {
600
1/1
✓ Branch 3 → 4 taken 1232 times.
1232 const MailId id = mailIdAt(row);
601
3/3
✓ Branch 6 → 7 taken 1180 times.
✓ Branch 6 → 8 taken 52 times.
✓ Branch 7 → 8 taken 1180 times.
1232 if (m_marked.contains({id.folderId, id.uid})) ids.append(id);
602 1232 }
603 8 return ids;
604 ✗ }
605
606 2 int MailSearchSession::rowForHour(int hour) const {
607
2/2
✓ Branch 23 → 3 taken 2 times.
✓ Branch 23 → 24 taken 1 time.
3 for (int row = 0; row < loadedCount(); ++row) {
608
1/1
✓ Branch 3 → 4 taken 2 times.
2 const QDateTime date = dateTimeAt(row);
609
10/14
✓ Branch 4 → 5 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 1 time.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
2 if (date.isValid() && date.toLocalTime().time().hour() == hour) return row;
610 2 }
611 1 return -1;
612 }
613
614 1611 QDateTime MailSearchSession::dateOf(const MailId &mail) const {
615 1611 const int source = m_rows.rowForUid(mail.uid, mail.folderId);
616
2/2
✓ Branch 3 → 4 taken 1607 times.
✓ Branch 3 → 6 taken 4 times.
1611 const MailHeader *header = source >= 0 ? m_rows.headerAt(source) : nullptr;
617
2/2
✓ Branch 7 → 8 taken 1607 times.
✓ Branch 7 → 9 taken 4 times.
1611 return header ? header->date : QDateTime();
618 }
619
620 2368 QDateTime MailSearchSession::dateTimeAt(int row) const {
621
9/11
✓ Branch 2 → 3 taken 1042 times.
✓ Branch 2 → 8 taken 1326 times.
✓ Branch 3 → 4 taken 1042 times.
✓ Branch 4 → 5 taken 1006 times.
✓ Branch 4 → 8 taken 36 times.
✓ Branch 5 → 6 taken 1006 times.
✓ Branch 6 → 7 taken 1006 times.
✓ Branch 10 → 11 taken 1006 times.
✓ Branch 10 → 12 taken 1362 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
4736 return row >= 0 && row < loadedCount() ? dateOf(mailIdAt(row)) : QDateTime();
622 }
623
624
2/2
✓ Branch 3 → 4 taken 605 times.
✓ Branch 3 → 5 taken 287 times.
892 QDateTime MailSearchSession::selectedDateTime() const { return m_selected.isValid() ? dateOf(m_selected) : QDateTime(); }
625
626 6 bool MailSearchSession::contains(const MailId &mail) const {
627
2/4
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 7 not taken.
6 return mail.accountId == m_accountId && m_rows.rowForUid(mail.uid, mail.folderId) >= 0;
628 }
629
630 4 void MailSearchSession::removeRows(qint64 folderId, const QList<qint64> &uids) {
631 4 int removed = 0;
632 4 const qsizetype marked = m_marked.size();
633
3/3
✓ Branch 7 → 8 taken 90 times.
✓ Branch 10 → 5 taken 90 times.
✓ Branch 10 → 11 taken 4 times.
94 for (const qint64 uid : uids) m_marked.remove({folderId, uid});
634
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 2 times.
4 if (m_marked.size() != marked) emit markedChanged();
635
2/2
✓ Branch 34 → 16 taken 90 times.
✓ Branch 34 → 35 taken 4 times.
94 for (const qint64 uid : uids) {
636
1/1
✓ Branch 17 → 18 taken 90 times.
90 const int row = m_rows.rowForUid(uid, folderId);
637
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 89 times.
90 if (row < 0) continue;
638
1/1
✓ Branch 20 → 21 taken 89 times.
89 const MailHeader header = *m_rows.headerAt(row);
639
2/2
✓ Branch 22 → 23 taken 89 times.
✓ Branch 23 → 24 taken 89 times.
89 m_identities.remove(identityKey(header));
640
1/1
✓ Branch 25 → 26 taken 89 times.
89 m_rows.removeByUid(uid, folderId);
641 // The counts follow the list (G4): the mail left this result.
642
1/2
✓ Branch 26 → 27 taken 89 times.
✗ Branch 26 → 28 not taken.
89 if (m_summary.total > 0) --m_summary.total;
643
1/1
✓ Branch 28 → 29 taken 89 times.
89 adjustSummary(header, -1);
644 89 ++removed;
645 89 }
646
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 4 times.
4 if (!removed) return;
647 4 emit rowsChanged();
648 4 emit summaryChanged();
649 }
650
651 250 void MailSearchSession::updateFlags(qint64 folderId, qint64 uid, quint32 flags) {
652
1/1
✓ Branch 2 → 3 taken 250 times.
250 const auto before = m_rows.flagsFor(uid, folderId);
653
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 250 times.
250 if (!before) return;
654
1/1
✓ Branch 6 → 7 taken 250 times.
250 m_rows.updateFlags(uid, flags, folderId);
655 // S106-245: the state counts follow a flag change of a shown mail.
656 750 const auto moved = [&](quint32 bit, qint64 &count, bool countsWhenSet) {
657 750 const bool was = (*before & bit) != 0, now = (flags & bit) != 0;
658
2/2
✓ Branch 3 → 4 taken 500 times.
✓ Branch 3 → 5 taken 250 times.
750 if (was == now) return;
659
2/2
✓ Branch 5 → 6 taken 246 times.
✓ Branch 5 → 7 taken 4 times.
250 count = std::max<qint64>(0, count + ((now == countsWhenSet) ? 1 : -1));
660 250 };
661 250 qint64 unread = m_summary.unread;
662
1/1
✓ Branch 7 → 8 taken 250 times.
250 moved(MailFlag::Seen, unread, false);
663
1/1
✓ Branch 8 → 9 taken 250 times.
250 moved(MailFlag::Flagged, m_summary.flagged, true);
664
1/1
✓ Branch 9 → 10 taken 250 times.
250 moved(MailFlag::Answered, m_summary.answered, true);
665
4/6
✓ Branch 10 → 11 taken 243 times.
✓ Branch 10 → 13 taken 7 times.
✓ Branch 12 → 13 taken 243 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 250 times.
✗ Branch 15 → 17 not taken.
250 if (unread != m_summary.unread || (*before ^ flags) & (MailFlag::Flagged | MailFlag::Answered)) {
666 250 m_summary.unread = unread;
667
1/1
✓ Branch 16 → 17 taken 250 times.
250 emit summaryChanged();
668 }
669 }
670
671 249 void MailSearchSession::setFlag(qint64 folderId, qint64 uid, quint32 flag, bool on) {
672
1/1
✓ Branch 2 → 3 taken 249 times.
249 const auto flags = m_rows.flagsFor(uid, folderId);
673
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 248 times.
249 if (!flags) return;
674
3/3
✓ Branch 6 → 7 taken 247 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 11 → 12 taken 248 times.
248 updateFlags(folderId, uid, on ? (*flags | flag) : (*flags & ~flag));
675 }
676
677 24 int MailSearchSession::rowOf(const MailId &mail) const {
678
4/8
✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 24 times.
✓ Branch 5 → 6 taken 24 times.
✓ Branch 9 → 10 taken 24 times.
✗ Branch 9 → 11 not taken.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
24 return mail.accountId == m_accountId ? m_results.rowForIdentity(WorkspaceState::encode(mail)) : -1;
679 }
680
681 4038 MailId MailSearchSession::mailIdAt(int row) const {
682
4/4
✓ Branch 2 → 3 taken 4038 times.
✓ Branch 7 → 8 taken 4038 times.
✓ Branch 8 → 9 taken 4038 times.
✓ Branch 9 → 10 taken 4038 times.
8076 return WorkspaceState::decode(m_results.mailAt(row).value(QStringLiteral("identity")).toMap());
683 }
684
685 // ── Server search (D-12) ─────────────────────────────────────────────────
686
687 8 void MailSearchSession::startServerSearch() {
688 8 m_serverDelay.stop();
689
8/10
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 9 not taken.
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 4 times.
8 if (m_state == State::Empty || !m_deps.server || (m_deps.serverEnabled && !m_deps.serverEnabled())) return;
690
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 4 times.
4 if (!m_active) { m_serverWanted = true; return; }
691 4 m_serverWanted = false;
692 4 m_serverAdded = 0;
693 4 m_serverFailed = 0;
694 4 m_serverDone = 0;
695 4 m_serverTotal = 0;
696 4 m_serverRunning = true;
697 4 emit serverChanged();
698 4 m_deps.server->serverSearch(m_text, m_filter, m_tabId);
699 }
700
701 110 bool MailSearchSession::matchesHeader(const MailHeader &header, const QString &folderPath,
702 const MailCache::SearchFilter &f) {
703
6/6
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 107 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 2 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 109 times.
110 if (!f.folderPatterns.isEmpty() && !anyContains(folderPath, f.folderPatterns)) return false;
704
4/6
✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 16 taken 100 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 9 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 109 times.
109 if (f.dateFrom.isValid() && header.date < f.dateFrom) return false;
705
6/6
✓ Branch 20 → 21 taken 10 times.
✓ Branch 20 → 25 taken 99 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 25 taken 7 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 106 times.
109 if (f.dateTo.isValid() && header.date > f.dateTo) return false;
706
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 31 taken 105 times.
106 if (!containsCi(header.from, f.fromFilter)) return false;
707
8/8
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 38 taken 102 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 38 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 104 times.
105 if (!f.toFilter.isEmpty() && !containsCi(header.to, f.toFilter) && !containsCi(header.cc, f.toFilter)) return false;
708
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 44 taken 103 times.
104 if (!containsCi(header.listId, f.listFilter)) return false;
709
13/17
✓ Branch 45 → 46 taken 2 times.
✓ Branch 45 → 51 taken 101 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
✓ Branch 48 → 49 taken 2 times.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 1 time.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 54 taken 101 times.
✓ Branch 54 → 55 taken 2 times.
✓ Branch 54 → 56 taken 101 times.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 58 taken 102 times.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
✗ Branch 162 → 163 not taken.
✗ Branch 162 → 164 not taken.
103 if (!f.subjectFilter.isEmpty() && !containsFolded(header.subject).contains(containsFolded(f.subjectFilter))) return false;
710
2/2
✓ Branch 74 → 60 taken 2 times.
✓ Branch 74 → 75 taken 101 times.
103 for (const QString &tag : f.tags)
711
7/11
✓ Branch 61 → 62 taken 2 times.
✓ Branch 63 → 64 taken 2 times.
✗ Branch 63 → 67 not taken.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 67 taken 1 time.
✓ Branch 68 → 69 taken 2 times.
✗ Branch 68 → 70 not taken.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 70 → 72 taken 1 time.
✗ Branch 166 → 167 not taken.
✗ Branch 166 → 168 not taken.
2 if (!tag.trimmed().isEmpty() && !header.labels.contains(tag)) return false;
712
2/2
✓ Branch 81 → 82 taken 99 times.
✓ Branch 81 → 86 taken 1 time.
201 if (!triMatches(f.unread, !header.isSeen()) || !triMatches(f.flagged, header.isFlagged()) ||
713
8/8
✓ Branch 77 → 79 taken 100 times.
✓ Branch 77 → 86 taken 1 time.
✓ Branch 83 → 84 taken 98 times.
✓ Branch 83 → 86 taken 1 time.
✓ Branch 85 → 86 taken 2 times.
✓ Branch 85 → 87 taken 96 times.
✓ Branch 88 → 89 taken 5 times.
✓ Branch 88 → 90 taken 96 times.
201 !triMatches(f.answered, header.flags & MailFlag::Answered) || !triMatches(f.hasAttachment, header.hasAttachments))
714 5 return false;
715
2/2
✓ Branch 93 → 94 taken 94 times.
✓ Branch 93 → 100 taken 1 time.
191 if (anyContains(header.from, f.excludedFrom) || anyContains(header.to, f.excludedTo) ||
716
8/8
✓ Branch 91 → 92 taken 95 times.
✓ Branch 91 → 100 taken 1 time.
✓ Branch 95 → 96 taken 93 times.
✓ Branch 95 → 100 taken 1 time.
✓ Branch 97 → 98 taken 91 times.
✓ Branch 97 → 100 taken 2 times.
✓ Branch 102 → 103 taken 6 times.
✓ Branch 102 → 104 taken 90 times.
282 anyContains(header.cc, f.excludedTo) || anyContains(folderPath, f.excludedFolders) ||
717
2/2
✓ Branch 99 → 100 taken 1 time.
✓ Branch 99 → 101 taken 90 times.
91 anyContains(header.listId, f.excludedLists))
718 6 return false;
719
2/2
✓ Branch 116 → 106 taken 2 times.
✓ Branch 116 → 117 taken 89 times.
91 for (const QString &subject : f.excludedSubjects)
720
5/5
✓ Branch 107 → 108 taken 2 times.
✓ Branch 108 → 109 taken 2 times.
✓ Branch 109 → 110 taken 2 times.
✓ Branch 112 → 113 taken 1 time.
✓ Branch 112 → 114 taken 1 time.
2 if (containsFolded(header.subject).contains(containsFolded(subject))) return false;
721
2/2
✓ Branch 126 → 120 taken 2 times.
✓ Branch 126 → 127 taken 88 times.
90 for (const QString &tag : f.excludedTags)
722
2/2
✓ Branch 122 → 123 taken 1 time.
✓ Branch 122 → 124 taken 1 time.
2 if (header.labels.contains(tag)) return false;
723
2/2
✓ Branch 154 → 130 taken 2 times.
✓ Branch 154 → 155 taken 87 times.
89 for (const QString &term : f.excludedTerms)
724
9/11
✓ Branch 137 → 138 taken 2 times.
✓ Branch 138 → 139 taken 2 times.
✓ Branch 139 → 140 taken 2 times.
✓ Branch 140 → 141 taken 2 times.
✓ Branch 141 → 142 taken 2 times.
✓ Branch 148 → 149 taken 2 times.
✓ Branch 148 → 150 taken 2 times.
✓ Branch 150 → 151 taken 1 time.
✓ Branch 150 → 152 taken 1 time.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
4 if (anyContains(header.subject + QLatin1Char(' ') + header.from + QLatin1Char(' ') + header.to, {term})) return false;
725 87 return true;
726 2 }
727
728 66 void MailSearchSession::countServerHit(const MailHeader &header, const QString &folderPath) {
729 66 m_folderPaths.insert(header.folderId, folderPath);
730 66 adjustSummary(header, +1);
731 66 }
732
733 namespace {
734 // A facet entry moves by `delta`; it disappears at zero. A new value joins
735 // only a list with room (the "top" lists are bounded).
736 794 void adjustFacet(QList<MailSearchFacetValue> &facet, const QString &value, const QString &label, int delta,
737 bool bounded) {
738
2/2
✓ Branch 3 → 4 taken 148 times.
✓ Branch 3 → 5 taken 646 times.
794 if (value.isEmpty()) return;
739
2/2
✓ Branch 17 → 6 taken 685 times.
✓ Branch 17 → 18 taken 19 times.
704 for (qsizetype i = 0; i < facet.size(); ++i) {
740
2/2
✓ Branch 8 → 9 taken 58 times.
✓ Branch 8 → 10 taken 627 times.
685 if (facet[i].value != value) continue;
741 627 facet[i].count += delta;
742
2/2
✓ Branch 12 → 13 taken 6 times.
✓ Branch 12 → 14 taken 621 times.
627 if (facet[i].count <= 0) facet.removeAt(i);
743 627 return;
744 }
745
5/8
✓ Branch 18 → 19 taken 19 times.
✗ Branch 18 → 23 not taken.
✓ Branch 19 → 20 taken 15 times.
✓ Branch 19 → 22 taken 4 times.
✓ Branch 21 → 22 taken 15 times.
✗ Branch 21 → 23 not taken.
✓ Branch 24 → 25 taken 19 times.
✗ Branch 24 → 34 not taken.
19 if (delta > 0 && (!bounded || facet.size() < MailSearchSummary::TopValues))
746 19 facet.append({value, label.isEmpty() ? value : label, delta});
747 19 }
748 } // namespace
749
750 155 void MailSearchSession::adjustSummary(const MailHeader &header, int delta) {
751 245 const auto step = [delta](qint64 &count) { count = std::max<qint64>(0, count + delta); };
752
2/3
✓ Branch 2 → 3 taken 155 times.
✓ Branch 3 → 4 taken 155 times.
✗ Branch 3 → 14 not taken.
155 if (header.date.isValid()) {
753
2/2
✓ Branch 4 → 5 taken 155 times.
✓ Branch 5 → 6 taken 155 times.
155 const QDate day = header.date.toLocalTime().date();
754
1/1
✓ Branch 7 → 8 taken 155 times.
155 qint64 count = m_summary.dayCounts.value(day);
755
1/1
✓ Branch 8 → 9 taken 155 times.
155 step(count);
756
4/4
✓ Branch 9 → 10 taken 66 times.
✓ Branch 9 → 12 taken 89 times.
✓ Branch 10 → 11 taken 66 times.
✓ Branch 12 → 13 taken 89 times.
155 if (count > 0) m_summary.dayCounts[day] = count; else m_summary.dayCounts.remove(day);
757 } else {
758 ✗ step(m_summary.undated);
759 }
760
3/3
✓ Branch 16 → 17 taken 36 times.
✓ Branch 16 → 18 taken 119 times.
✓ Branch 17 → 18 taken 36 times.
155 if (!header.isSeen()) step(m_summary.unread);
761
3/3
✓ Branch 19 → 20 taken 28 times.
✓ Branch 19 → 21 taken 127 times.
✓ Branch 20 → 21 taken 28 times.
155 if (header.isFlagged()) step(m_summary.flagged);
762
1/3
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 155 times.
✗ Branch 22 → 23 not taken.
155 if (header.flags & MailFlag::Answered) step(m_summary.answered);
763
3/3
✓ Branch 23 → 24 taken 26 times.
✓ Branch 23 → 25 taken 129 times.
✓ Branch 24 → 25 taken 26 times.
155 if (header.hasAttachments) step(m_summary.withAttachment);
764
1/1
✓ Branch 25 → 26 taken 155 times.
155 const auto senders = MailSearchSummaryAccumulator::addresses(header.from);
765
2/3
✓ Branch 27 → 28 taken 155 times.
✗ Branch 27 → 31 not taken.
✓ Branch 30 → 31 taken 155 times.
155 if (!senders.isEmpty()) adjustFacet(m_summary.topSenders, senders.first().first, senders.first().second, delta, true);
766 155 QSet<QString> recipients;
767
7/7
✓ Branch 35 → 36 taken 155 times.
✓ Branch 36 → 37 taken 155 times.
✓ Branch 37 → 38 taken 155 times.
✓ Branch 42 → 43 taken 155 times.
✓ Branch 43 → 44 taken 155 times.
✓ Branch 55 → 45 taken 155 times.
✓ Branch 55 → 56 taken 155 times.
465 for (const auto &[address, name] : MailSearchSummaryAccumulator::addresses(header.to + QStringLiteral(", ") + header.cc)) {
768
1/2
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 155 times.
155 if (recipients.contains(address)) continue;
769
1/1
✓ Branch 51 → 52 taken 155 times.
155 recipients.insert(address);
770
1/1
✓ Branch 52 → 53 taken 155 times.
155 adjustFacet(m_summary.topRecipients, address, name, delta, true);
771 155 }
772
3/3
✓ Branch 60 → 61 taken 19 times.
✓ Branch 63 → 59 taken 19 times.
✓ Branch 63 → 64 taken 155 times.
174 for (const QString &label : header.labels) adjustFacet(m_summary.topLabels, label, label, delta, true);
773
1/1
✓ Branch 64 → 65 taken 155 times.
155 QString list = header.listId.trimmed();
774
3/9
✓ Branch 67 → 68 taken 155 times.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 74 taken 155 times.
✗ Branch 71 → 72 not taken.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 81 taken 155 times.
✗ Branch 77 → 78 not taken.
155 if (list.startsWith(QLatin1Char('<')) && list.endsWith(QLatin1Char('>'))) list = list.mid(1, list.size() - 2);
775
1/1
✓ Branch 81 → 82 taken 155 times.
155 adjustFacet(m_summary.topLists, list, list, delta, true);
776 155 const QString folder = m_folderPaths.value(header.folderId);
777
1/1
✓ Branch 83 → 84 taken 155 times.
155 adjustFacet(m_summary.topFolders, folder, folder, delta, true);
778
1/1
✓ Branch 84 → 85 taken 155 times.
155 adjustFacet(m_summary.folders, folder, folder, delta, false);
779 155 }
780
781 7 void MailSearchSession::serverHits(const QList<qint64> &uids, qint64 folderId, const QString &folderPath) {
782
2/4
✓ Branch 2 → 3 taken 7 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
8 if (!m_deps.cache || !m_live) return;
783 7 QList<MailId> ids;
784
1/1
✓ Branch 6 → 7 taken 7 times.
7 ids.reserve(uids.size());
785
3/5
✓ Branch 11 → 12 taken 71 times.
✓ Branch 15 → 9 taken 71 times.
✓ Branch 15 → 16 taken 7 times.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
78 for (const qint64 uid : uids) ids.append(MailId{m_accountId, folderId, uid});
786 7 QList<MailHeader> added;
787
5/5
✓ Branch 16 → 17 taken 7 times.
✓ Branch 17 → 18 taken 7 times.
✓ Branch 18 → 19 taken 7 times.
✓ Branch 52 → 20 taken 71 times.
✓ Branch 52 → 53 taken 7 times.
78 for (const MailHeader &header : m_deps.cache->headersByIds(ids)) {
788 71 const MailId identity{m_accountId, header.folderId, header.uid};
789
4/7
✓ Branch 23 → 24 taken 71 times.
✗ Branch 23 → 27 not taken.
✓ Branch 24 → 25 taken 71 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 71 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 71 times.
71 if (m_deps.acceptResult && !m_deps.acceptResult(identity, folderPath)) continue;
790
3/3
✓ Branch 30 → 31 taken 71 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 31 → 33 taken 68 times.
71 if (!matchesHeader(header, folderPath, m_filter)) continue;
791
1/1
✓ Branch 33 → 34 taken 68 times.
68 const QString key = identityKey(header);
792
2/2
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 37 taken 66 times.
68 if (m_identities.contains(key)) continue;
793
1/1
✓ Branch 37 → 38 taken 66 times.
66 m_identities.insert(key);
794
1/1
✓ Branch 38 → 39 taken 66 times.
66 countServerHit(header, folderPath);
795
1/1
✓ Branch 39 → 40 taken 66 times.
66 added.append(header);
796 80 }
797
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 57 taken 6 times.
7 if (added.isEmpty()) return;
798
1/1
✓ Branch 57 → 58 taken 6 times.
6 m_rows.setFolderBadge(folderId, folderPath);
799
1/1
✓ Branch 58 → 59 taken 6 times.
6 m_rows.appendHeaders(added); // the date-sorted proxy places them (SB-01)
800 6 m_serverAdded += int(added.size());
801
9/9
✓ Branch 60 → 61 taken 6 times.
✓ Branch 64 → 65 taken 6 times.
✓ Branch 65 → 66 taken 6 times.
✓ Branch 66 → 67 taken 6 times.
✓ Branch 67 → 68 taken 6 times.
✓ Branch 68 → 69 taken 6 times.
✓ Branch 70 → 71 taken 6 times.
✓ Branch 75 → 62 taken 6 times.
✓ Branch 75 → 76 taken 6 times.
12 qCInfo(lcSearch) << "Server hits for tab" << m_tabId.left(8) << "added" << added.size();
802
1/1
✓ Branch 76 → 77 taken 6 times.
6 emit rowsChanged();
803
1/1
✓ Branch 77 → 78 taken 6 times.
6 emit serverChanged();
804
1/1
✓ Branch 78 → 79 taken 6 times.
6 emit summaryChanged();
805 8 }
806
807 1 void MailSearchSession::serverProgress(int done, int total) {
808 1 m_serverDone = done;
809 1 m_serverTotal = total;
810 1 emit serverChanged();
811 1 }
812
813 ✗ void MailSearchSession::serverFolderFailed() {
814 ✗ ++m_serverFailed;
815 ✗ emit serverChanged();
816 ✗ }
817
818 1 void MailSearchSession::serverFinished() {
819 1 m_serverRunning = false;
820
9/9
✓ 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 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 16 → 4 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
2 qCInfo(lcSearch) << "Server search finished for tab" << m_tabId.left(8) << "added" << m_serverAdded;
821 1 emit serverChanged();
822 1 }
823