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: 91.4% 266 / 0 / 291
Functions: 96.3% 26 / 0 / 27
Branches: 67.7% 272 / 0 / 402

service/MailSelectionMutationService.cpp
Line Branch Exec Source
1 #include "MailSelectionMutationService.h"
2 #include "data/MailCache.h"
3 #include "data/MailCacheSql.h"
4 #include <QCoreApplication>
5 #include <QFutureWatcher>
6 #include <QPointer>
7 #include <QScopeGuard>
8 #include <QSqlError>
9 #include <QtConcurrent>
10 #include <algorithm>
11 #include <memory>
12
13 namespace {
14 // The single durable-writer thread (m_flagPool, one thread) owns this
15 // connection. A burst of intents must not reopen SQLite or repeat schema
16 // admission per write; a profile switch replaces it before any request for
17 // the new database runs.
18 279 MailCache *durableWriter(const QString &databasePath, QString *error) {
19
2/2
✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 4 taken 263 times.
279 static thread_local std::unique_ptr<MailCache> writer;
20
2/2
✓ Branch 4 → 5 taken 16 times.
✓ Branch 4 → 6 taken 263 times.
279 static thread_local QString writerPath;
21
6/6
✓ Branch 7 → 8 taken 261 times.
✓ Branch 7 → 10 taken 18 times.
✓ Branch 9 → 10 taken 136 times.
✓ Branch 9 → 11 taken 125 times.
✓ Branch 12 → 13 taken 154 times.
✓ Branch 12 → 30 taken 125 times.
279 if (!writer || writerPath != databasePath) {
22 154 writer.reset();
23
1/1
✓ Branch 14 → 15 taken 154 times.
154 writer = std::make_unique<MailCache>();
24
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 29 taken 152 times.
154 if (!writer->openExistingWriter(databasePath)) {
25
2/3
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 26 not taken.
✓ Branch 22 → 23 taken 2 times.
2 if (error) *error = writer->lastError();
26 2 writer.reset();
27 2 writerPath.clear();
28 2 return nullptr;
29 }
30 152 writerPath = databasePath;
31 }
32 277 return writer.get();
33 }
34 } // namespace
35
36
2/2
✓ Branch 3 → 4 taken 27 times.
✓ Branch 4 → 5 taken 27 times.
27 MailSelectionMutationService::MailSelectionMutationService(QObject *parent) : QObject(parent) {
37
1/1
✓ Branch 5 → 6 taken 27 times.
27 m_pool.setMaxThreadCount(1);
38
1/1
✓ Branch 6 → 7 taken 27 times.
27 m_pool.setExpiryTimeout(1000);
39
1/1
✓ Branch 7 → 8 taken 27 times.
27 m_flagPool.setMaxThreadCount(1);
40
1/1
✓ Branch 8 → 9 taken 27 times.
27 m_flagPool.setExpiryTimeout(-1);
41
1/1
✓ Branch 10 → 11 taken 27 times.
27 connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
42 27 [this] { stop(); });
43 27 }
44 40 MailSelectionMutationService::~MailSelectionMutationService() { stop(); }
45 namespace {
46 QPointer<MailSelectionMutationService> sharedInstance;
47 } // namespace
48
49 // Runs first in ~QCoreApplication: the workers must finish while Qt's SQL
50 // and plugin state still exists. aboutToQuit covers a normal quit, but a
51 // process that ends without the event loop (tests, early exits) deleted the
52 // service as an application child only after that state was gone, while a
53 // worker could still be opening its SQLite writer (crash in
54 // QSqlDatabase::open, test_sprint79_crossfolder under load).
55 12 void MailSelectionMutationService::stopSharedBeforeApplicationTeardown() {
56
1/2
✓ Branch 3 → 4 taken 12 times.
✗ Branch 3 → 6 not taken.
12 if (sharedInstance) sharedInstance->stop();
57 12 }
58
59 298 MailSelectionMutationService *MailSelectionMutationService::shared() {
60
2/2
✓ Branch 3 → 4 taken 12 times.
✓ Branch 3 → 11 taken 286 times.
298 if (!sharedInstance) {
61
3/6
✓ Branch 6 → 7 taken 12 times.
✓ Branch 7 → 8 taken 12 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12 times.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
12 sharedInstance = new MailSelectionMutationService(QCoreApplication::instance());
62 12 qAddPostRoutine(stopSharedBeforeApplicationTeardown);
63 }
64 298 return sharedInstance;
65 }
66 39 void MailSelectionMutationService::stop() {
67 39 m_stopping = true;
68
1/1
✓ Branch 3 → 4 taken 39 times.
39 m_pool.waitForDone();
69
1/1
✓ Branch 5 → 6 taken 39 times.
39 m_flagPool.waitForDone();
70 39 }
71
72 161 void MailSelectionMutationService::setFlag(QObject *owner, QString databasePath,
73 MailId mailId, MutationKind kind, bool desiredState, FlagCallback callback) {
74 161 FlagResult rejected;
75
3/6
✓ Branch 4 → 5 taken 161 times.
✗ Branch 4 → 15 not taken.
✓ Branch 6 → 7 taken 161 times.
✗ Branch 6 → 15 not taken.
✓ Branch 8 → 9 taken 161 times.
✗ Branch 8 → 15 not taken.
322 if (!owner || !mailId.isValid() || databasePath.isEmpty() ||
76
3/4
✓ Branch 10 → 11 taken 161 times.
✗ Branch 10 → 15 not taken.
✓ Branch 11 → 12 taken 43 times.
✓ Branch 11 → 13 taken 118 times.
322 databasePath.size() > 4096 || mailId.accountId.size() > 256 ||
77
1/2
✓ Branch 12 → 13 taken 43 times.
✗ Branch 12 → 15 not taken.
43 (kind != MutationKind::Seen && kind != MutationKind::Flagged) ||
78
4/8
✓ Branch 2 → 3 taken 161 times.
✗ Branch 2 → 15 not taken.
✓ Branch 13 → 14 taken 161 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 161 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 29 taken 161 times.
322 m_stopping || m_flagPendingCount >= 256) {
79 ✗ rejected.error = QStringLiteral("Flag writer is busy or the identity is invalid");
80 ✗ callback(std::move(rejected));
81 ✗ return;
82 }
83 161 ++m_flagPendingCount;
84
1/1
✓ Branch 29 → 30 taken 161 times.
161 const QPointer<QObject> consumer(owner);
85
3/6
✓ Branch 30 → 31 taken 161 times.
✓ Branch 31 → 32 taken 161 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 161 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
161 auto *watcher = new QFutureWatcher<FlagResult>(this);
86 161 connect(watcher, &QFutureWatcher<FlagResult>::finished, this,
87
1/3
✓ Branch 37 → 38 taken 161 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
322 [this, watcher, consumer, callback = std::move(callback)]() mutable {
88 161 --m_flagPendingCount;
89
1/1
✓ Branch 2 → 3 taken 161 times.
161 auto result = watcher->result();
90
1/1
✓ Branch 3 → 4 taken 161 times.
161 watcher->deleteLater();
91
6/7
✓ Branch 4 → 5 taken 161 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 156 times.
✓ Branch 6 → 8 taken 5 times.
✓ Branch 9 → 10 taken 156 times.
✓ Branch 9 → 15 taken 5 times.
✓ Branch 12 → 13 taken 156 times.
161 if (!m_stopping && consumer) callback(std::move(result));
92 161 });
93
1/1
✓ Branch 45 → 46 taken 161 times.
161 watcher->setFuture(QtConcurrent::run(&m_flagPool,
94
1/5
✓ Branch 44 → 45 taken 161 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
322 [databasePath = std::move(databasePath), mailId = std::move(mailId), kind, desiredState]() {
95 161 FlagResult result;
96
1/1
✓ Branch 2 → 3 taken 161 times.
161 auto *writer = durableWriter(databasePath, &result.error);
97
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 161 times.
161 if (!writer) return result;
98
1/1
✓ Branch 5 → 6 taken 161 times.
161 const auto header = writer->header(mailId.folderId, mailId.uid);
99
1/1
✓ Branch 6 → 7 taken 161 times.
161 const auto folderPath = writer->folderPath(mailId.folderId);
100
5/6
✓ Branch 8 → 9 taken 158 times.
✓ Branch 8 → 13 taken 3 times.
✓ Branch 10 → 11 taken 158 times.
✗ Branch 10 → 13 not taken.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 23 taken 158 times.
319 if (!header || folderPath.isEmpty() ||
101
2/3
✓ Branch 11 → 12 taken 158 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 158 times.
158 writer->findFolderId(mailId.accountId, folderPath) != mailId.folderId) {
102 3 result.error = QStringLiteral("Flag target no longer belongs to its mailbox");
103 3 return result;
104 }
105 158 MutationRecord record;
106 158 record.mailId = mailId;
107 158 record.folderPath = folderPath;
108 158 record.kind = kind;
109 158 record.replayClass = MutationReplayClass::DesiredState;
110 158 record.phase = MutationPhase::Queued;
111
2/2
✓ Branch 25 → 26 taken 115 times.
✓ Branch 25 → 29 taken 43 times.
589 record.flag = kind == MutationKind::Seen ? QStringLiteral("\\Seen")
112
4/4
✓ Branch 34 → 35 taken 43 times.
✓ Branch 34 → 36 taken 115 times.
✓ Branch 36 → 37 taken 115 times.
✓ Branch 36 → 38 taken 43 times.
316 : QStringLiteral("\\Flagged");
113 158 record.desiredState = desiredState;
114 158 record.messageId = header->messageId;
115
1/1
✓ Branch 40 → 41 taken 158 times.
158 record.uidValidity = writer->uidValidity(mailId.folderId);
116
2/2
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 49 taken 157 times.
158 if (record.uidValidity == 0) {
117 1 result.error = QStringLiteral("Flag target has no confirmed UIDVALIDITY");
118 1 return result;
119 }
120
1/1
✓ Branch 49 → 50 taken 157 times.
157 const auto committed = writer->enqueueFlagMutationAndUpdate(record);
121
2/2
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 56 taken 156 times.
157 if (!committed) {
122
1/1
✓ Branch 52 → 53 taken 1 time.
1 result.error = writer->lastError();
123 1 return result;
124 }
125 156 record.token = committed->token;
126 156 result.record = record;
127 156 result.journaled = true;
128 156 result.flags = committed->flags;
129 156 return result;
130 161 }));
131 161 }
132
133 40 void MailSelectionMutationService::setKeyword(QObject *owner, QString databasePath,
134 MailId mailId, QString keyword, bool desiredState, FlagCallback callback) {
135 40 FlagResult rejected;
136
3/6
✓ Branch 4 → 5 taken 40 times.
✗ Branch 4 → 21 not taken.
✓ Branch 6 → 7 taken 40 times.
✗ Branch 6 → 21 not taken.
✓ Branch 8 → 9 taken 40 times.
✗ Branch 8 → 21 not taken.
80 if (!owner || !mailId.isValid() || databasePath.isEmpty() ||
137
3/4
✓ Branch 10 → 11 taken 40 times.
✗ Branch 10 → 21 not taken.
✓ Branch 12 → 13 taken 39 times.
✓ Branch 12 → 21 taken 1 time.
120 databasePath.size() > 4096 || mailId.accountId.size() > 256 ||
138
4/5
✓ Branch 14 → 15 taken 39 times.
✗ Branch 14 → 21 not taken.
✓ Branch 17 → 18 taken 39 times.
✓ Branch 18 → 19 taken 38 times.
✓ Branch 18 → 21 taken 1 time.
79 keyword.isEmpty() || keyword.size() > 256 || keyword.startsWith(QLatin1Char('\\')) ||
139
5/8
✓ Branch 2 → 3 taken 40 times.
✗ Branch 2 → 21 not taken.
✓ Branch 19 → 20 taken 38 times.
✗ Branch 19 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 38 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 35 taken 38 times.
80 m_stopping || m_flagPendingCount >= 256) {
140 2 rejected.error = QStringLiteral("Label writer is busy or the identity is invalid");
141
1/1
✓ Branch 32 → 33 taken 2 times.
2 callback(std::move(rejected));
142 2 return;
143 }
144 38 ++m_flagPendingCount;
145
1/1
✓ Branch 35 → 36 taken 38 times.
38 const QPointer<QObject> consumer(owner);
146
3/6
✓ Branch 36 → 37 taken 38 times.
✓ Branch 37 → 38 taken 38 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 38 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
38 auto *watcher = new QFutureWatcher<FlagResult>(this);
147 38 connect(watcher, &QFutureWatcher<FlagResult>::finished, this,
148
1/3
✓ Branch 43 → 44 taken 38 times.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
76 [this, watcher, consumer, callback = std::move(callback)]() mutable {
149 38 --m_flagPendingCount;
150
1/1
✓ Branch 2 → 3 taken 38 times.
38 auto result = watcher->result();
151
1/1
✓ Branch 3 → 4 taken 38 times.
38 watcher->deleteLater();
152
6/7
✓ Branch 4 → 5 taken 38 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 32 times.
✓ Branch 6 → 8 taken 6 times.
✓ Branch 9 → 10 taken 32 times.
✓ Branch 9 → 15 taken 6 times.
✓ Branch 12 → 13 taken 32 times.
38 if (!m_stopping && consumer) callback(std::move(result));
153 38 });
154
1/1
✓ Branch 53 → 54 taken 38 times.
38 watcher->setFuture(QtConcurrent::run(&m_flagPool,
155
1/7
✓ Branch 52 → 53 taken 38 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
114 [databasePath = std::move(databasePath), mailId = std::move(mailId),
156 38 keyword = std::move(keyword), desiredState]() {
157 38 FlagResult result;
158
1/1
✓ Branch 2 → 3 taken 38 times.
38 auto *writer = durableWriter(databasePath, &result.error);
159
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 36 times.
38 if (!writer) return result;
160
1/1
✓ Branch 5 → 6 taken 36 times.
36 const auto header = writer->header(mailId.folderId, mailId.uid);
161
1/1
✓ Branch 6 → 7 taken 36 times.
36 const auto folderPath = writer->folderPath(mailId.folderId);
162
5/6
✓ Branch 8 → 9 taken 35 times.
✓ Branch 8 → 13 taken 1 time.
✓ Branch 10 → 11 taken 35 times.
✗ Branch 10 → 13 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 23 taken 35 times.
71 if (!header || folderPath.isEmpty() ||
163
2/3
✓ Branch 11 → 12 taken 35 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 35 times.
35 writer->findFolderId(mailId.accountId, folderPath) != mailId.folderId) {
164 1 result.error = QStringLiteral("Label target no longer belongs to its mailbox");
165 1 return result;
166 }
167 35 MutationRecord record;
168 35 record.mailId = mailId;
169 35 record.folderPath = folderPath;
170 35 record.kind = MutationKind::Keyword;
171 35 record.replayClass = MutationReplayClass::DesiredState;
172 35 record.phase = MutationPhase::Queued;
173 35 record.flag = keyword;
174 35 record.desiredState = desiredState;
175 35 record.messageId = header->messageId;
176
1/1
✓ Branch 28 → 29 taken 35 times.
35 record.uidValidity = writer->uidValidity(mailId.folderId);
177
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 37 taken 34 times.
35 if (record.uidValidity == 0) {
178 1 result.error = QStringLiteral("Label target has no confirmed UIDVALIDITY");
179 1 return result;
180 }
181
1/1
✓ Branch 37 → 38 taken 34 times.
34 const auto token = writer->enqueueKeywordMutationAndUpdate(record);
182
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 44 taken 34 times.
34 if (!token) {
183 ✗ result.error = writer->lastError();
184 ✗ return result;
185 }
186 34 record.token = *token;
187 34 result.record = record;
188 34 result.journaled = true;
189 34 return result;
190 36 }));
191 40 }
192
193 24 void MailSelectionMutationService::setSelectionState(QObject *owner, QString databasePath,
194 MailSelectionStateRequest request, StateCallback callback) {
195
5/10
✓ Branch 2 → 3 taken 24 times.
✗ Branch 2 → 9 not taken.
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 9 not taken.
✓ Branch 6 → 7 taken 24 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 24 times.
✗ Branch 7 → 9 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 24 taken 24 times.
48 if (!owner || databasePath.isEmpty() || databasePath.size() > 4096 || m_stopping ||
196
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 24 times.
24 m_flagPendingCount >= 256) {
197 ✗ MailSelectionStateResult rejected;
198 ✗ rejected.error = QStringLiteral("Flag writer is busy or the request is invalid");
199 ✗ callback(std::move(rejected));
200 ✗ return;
201 ✗ }
202 24 ++m_flagPendingCount;
203
1/1
✓ Branch 24 → 25 taken 24 times.
24 const QPointer<QObject> consumer(owner);
204
3/6
✓ Branch 25 → 26 taken 24 times.
✓ Branch 26 → 27 taken 24 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 24 times.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
24 auto *watcher = new QFutureWatcher<MailSelectionStateResult>(this);
205 24 connect(watcher, &QFutureWatcher<MailSelectionStateResult>::finished, this,
206
1/3
✓ Branch 32 → 33 taken 24 times.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
48 [this, watcher, consumer, callback = std::move(callback)]() mutable {
207 24 --m_flagPendingCount;
208
1/1
✓ Branch 2 → 3 taken 24 times.
24 auto result = watcher->result();
209
1/1
✓ Branch 3 → 4 taken 24 times.
24 watcher->deleteLater();
210
4/7
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 24 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 24 times.
✗ Branch 9 → 15 not taken.
✓ Branch 12 → 13 taken 24 times.
24 if (!m_stopping && consumer) callback(std::move(result));
211 24 });
212
1/1
✓ Branch 40 → 41 taken 24 times.
24 watcher->setFuture(QtConcurrent::run(&m_flagPool,
213
1/3
✓ Branch 39 → 40 taken 24 times.
✗ Branch 66 → 67 not taken.
✗ Branch 66 → 68 not taken.
48 [databasePath = std::move(databasePath), request = std::move(request)]() mutable {
214 24 MailSelectionStateResult result;
215
1/1
✓ Branch 2 → 3 taken 24 times.
24 auto *writer = durableWriter(databasePath, &result.error);
216
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 24 times.
24 if (!writer) return result;
217
1/1
✓ Branch 8 → 9 taken 24 times.
48 return writer->enqueueSelectionState(std::move(request));
218 24 }));
219 24 }
220
221 59 void MailSelectionMutationService::enqueueMoves(QObject *owner, QString databasePath,
222 QList<MutationRecord> records, MoveJournalCallback callback) {
223 59 MoveJournalResult rejected;
224
1/1
✓ Branch 4 → 5 taken 59 times.
59 const bool valid = std::all_of(records.cbegin(), records.cend(), [](const MutationRecord &record) {
225
3/4
✓ Branch 2 → 3 taken 147 times.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 4 → 5 taken 147 times.
✗ Branch 4 → 6 not taken.
148 return record.kind == MutationKind::Move && record.mailId.isValid();
226 });
227
4/6
✓ Branch 7 → 8 taken 58 times.
✗ Branch 7 → 15 not taken.
✓ Branch 9 → 10 taken 58 times.
✗ Branch 9 → 15 not taken.
✓ Branch 11 → 12 taken 57 times.
✓ Branch 11 → 15 taken 1 time.
116 if (!owner || databasePath.isEmpty() || databasePath.size() > 4096 ||
228
8/10
✓ Branch 5 → 6 taken 58 times.
✓ Branch 5 → 15 taken 1 time.
✓ Branch 12 → 13 taken 56 times.
✓ Branch 12 → 15 taken 1 time.
✓ Branch 13 → 14 taken 56 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 56 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 29 taken 56 times.
175 records.isEmpty() || !valid || m_stopping || m_flagPendingCount >= 256) {
229 3 rejected.error = QStringLiteral("Move writer is busy or the request is invalid");
230
1/1
✓ Branch 26 → 27 taken 3 times.
3 callback(std::move(rejected));
231 3 return;
232 }
233 56 ++m_flagPendingCount;
234
1/1
✓ Branch 29 → 30 taken 56 times.
56 const QPointer<QObject> consumer(owner);
235
3/6
✓ Branch 30 → 31 taken 56 times.
✓ Branch 31 → 32 taken 56 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 56 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
56 auto *watcher = new QFutureWatcher<MoveJournalResult>(this);
236 56 connect(watcher, &QFutureWatcher<MoveJournalResult>::finished, this,
237
1/3
✓ Branch 37 → 38 taken 56 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
112 [this, watcher, consumer, callback = std::move(callback)]() mutable {
238 56 --m_flagPendingCount;
239
1/1
✓ Branch 2 → 3 taken 56 times.
56 auto result = watcher->result();
240
1/1
✓ Branch 3 → 4 taken 56 times.
56 watcher->deleteLater();
241
6/7
✓ Branch 4 → 5 taken 56 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 52 times.
✓ Branch 6 → 8 taken 4 times.
✓ Branch 9 → 10 taken 52 times.
✓ Branch 9 → 15 taken 4 times.
✓ Branch 12 → 13 taken 52 times.
56 if (!m_stopping && consumer) callback(std::move(result));
242 56 });
243
1/1
✓ Branch 45 → 46 taken 56 times.
56 watcher->setFuture(QtConcurrent::run(&m_flagPool,
244
1/3
✓ Branch 44 → 45 taken 56 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
112 [databasePath = std::move(databasePath), records = std::move(records)]() {
245 56 MoveJournalResult result;
246
1/1
✓ Branch 2 → 3 taken 56 times.
56 auto *writer = durableWriter(databasePath, &result.error);
247
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 56 times.
56 if (!writer) return result;
248 // A mail with a journaled, unconfirmed move must not be queued twice.
249 // The GUI answers "already moving" from loaded rows and this session's
250 // moves only; the journal itself is checked here, off the GUI thread.
251 // S106-119: check and insert run in ONE write transaction — the
252 // selection move (MailCache::moveSelection) journals on another pool
253 // thread, and two transactions let both pass the check.
254 56 QSqlDatabase &db = writer->database();
255
2/3
✓ Branch 6 → 7 taken 56 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 56 times.
56 if (!MailCacheSql::beginWriteTransaction(db, &result.error)) return result;
256 57 auto rollback = qScopeGuard([&db] { db.rollback(); });
257 56 QHash<QPair<QString, qint64>, QSet<qint64>> pending;
258
2/2
✓ Branch 48 → 13 taken 145 times.
✓ Branch 48 → 49 taken 55 times.
200 for (const auto &record : std::as_const(records)) {
259 145 const QPair<QString, qint64> folder{record.mailId.accountId, record.mailId.folderId};
260
2/2
✓ Branch 16 → 17 taken 56 times.
✓ Branch 16 → 30 taken 89 times.
145 if (!pending.contains(folder)) {
261 56 bool ok = false;
262
2/2
✓ Branch 18 → 19 taken 56 times.
✓ Branch 19 → 20 taken 56 times.
56 pending.insert(folder, writer->journal().pendingMoveUids(folder.first, folder.second, &ok));
263
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 29 taken 56 times.
56 if (!ok) {
264 ✗ result.error = QStringLiteral("Move journal could not be read");
265 ✗ return result;
266 }
267 }
268
2/2
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 41 taken 144 times.
145 if (pending.value(folder).contains(record.mailId.uid)) {
269 1 result.error = QStringLiteral("A message already has a pending move");
270 1 return result;
271 }
272 145 }
273
1/1
✓ Branch 50 → 51 taken 55 times.
55 const auto tokens = writer->journal().enqueueMutationsInTransaction(records);
274
3/6
✓ Branch 52 → 53 taken 55 times.
✗ Branch 52 → 57 not taken.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 55 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 71 taken 55 times.
55 if (!tokens || tokens->size() != records.size()) {
275 ✗ result.error = tokens ? QStringLiteral("Move journal returned an incomplete receipt")
276 ✗ : writer->lastError();
277 ✗ return result;
278 }
279
2/3
✓ Branch 71 → 72 taken 55 times.
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 79 taken 55 times.
55 if (!db.commit()) {
280 ✗ result.error = db.lastError().text();
281 ✗ return result;
282 }
283 55 rollback.dismiss();
284 55 result.tokens = *tokens;
285 55 result.journaled = true;
286 55 return result;
287 56 }));
288 59 }
289
290 13 void MailSelectionMutationService::cancelQueuedMove(QObject *owner, QString databasePath,
291 MailSelectionMoveResult action, CancelCallback callback) {
292 13 MailSelectionCancelResult rejected;
293
9/10
✓ Branch 2 → 3 taken 12 times.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 4 → 5 taken 11 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 6 → 7 taken 11 times.
✗ Branch 6 → 9 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 10 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 23 taken 10 times.
13 if (!owner || databasePath.isEmpty() || databasePath.size() > 4096 || !action.selection) {
294 3 rejected.error = QStringLiteral("Invalid selection cancellation request");
295
1/1
✓ Branch 20 → 21 taken 3 times.
3 callback(std::move(rejected));
296 3 return;
297 }
298
2/4
✓ Branch 23 → 24 taken 10 times.
✗ Branch 23 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 36 taken 10 times.
10 if (m_stopping || m_pendingCount >= MaximumRequests) {
299 ✗ rejected.outcome = MailSelectionCancelResult::Outcome::Busy;
300 ✗ rejected.error = QStringLiteral("Selection writer is busy or stopping");
301 ✗ callback(std::move(rejected));
302 ✗ return;
303 }
304 10 ++m_pendingCount;
305
1/1
✓ Branch 36 → 37 taken 10 times.
10 const QPointer<QObject> consumer(owner);
306
3/6
✓ Branch 37 → 38 taken 10 times.
✓ Branch 38 → 39 taken 10 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 10 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
10 auto *watcher = new QFutureWatcher<MailSelectionCancelResult>(this);
307 10 connect(watcher, &QFutureWatcher<MailSelectionCancelResult>::finished, this,
308
1/3
✓ Branch 44 → 45 taken 10 times.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
20 [this, watcher, consumer, callback = std::move(callback)]() mutable {
309 9 --m_pendingCount;
310
1/1
✓ Branch 2 → 3 taken 9 times.
9 auto result = watcher->result();
311
1/1
✓ Branch 3 → 4 taken 9 times.
9 watcher->deleteLater();
312
4/7
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 9 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 9 times.
✗ Branch 9 → 15 not taken.
✓ Branch 12 → 13 taken 9 times.
9 if (!m_stopping && consumer) callback(std::move(result));
313 9 });
314
1/1
✓ Branch 52 → 53 taken 10 times.
10 watcher->setFuture(QtConcurrent::run(&m_pool,
315
1/3
✓ Branch 51 → 52 taken 10 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
20 [databasePath = std::move(databasePath), action = std::move(action)] {
316
1/1
✓ Branch 2 → 3 taken 10 times.
10 MailCache writer;
317
3/3
✓ Branch 3 → 4 taken 10 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 11 taken 9 times.
10 if (!writer.openExistingWriter(databasePath)) {
318 ✗ MailSelectionCancelResult failed;
319
1/1
✓ Branch 5 → 6 taken 1 time.
1 failed.error = writer.lastError();
320
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 return failed;
321 ✗ }
322
1/1
✓ Branch 11 → 12 taken 9 times.
9 return writer.cancelQueuedSelectionMove(action);
323 10 }));
324 13 }
325 36 void MailSelectionMutationService::move(QObject *owner, QString databasePath,
326 std::shared_ptr<const MailSelectionSnapshot> selection, QString targetFolder,
327 Callback callback) {
328 36 MailSelectionMoveResult rejected;
329
4/4
✓ Branch 4 → 5 taken 34 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 6 → 7 taken 33 times.
✓ Branch 6 → 11 taken 1 time.
35 if (!owner || databasePath.isEmpty() || databasePath.size() > 4096 ||
330
8/8
✓ Branch 2 → 3 taken 35 times.
✓ Branch 2 → 11 taken 1 time.
✓ Branch 8 → 9 taken 32 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 31 times.
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 25 taken 31 times.
71 !selection || targetFolder.size() > 1024) {
331 5 rejected.error = QStringLiteral("Invalid selection writer request");
332
1/1
✓ Branch 22 → 23 taken 5 times.
5 callback(std::move(rejected));
333 5 return;
334 }
335
3/4
✓ Branch 25 → 26 taken 31 times.
✗ Branch 25 → 27 not taken.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 38 taken 30 times.
31 if (m_stopping || m_pendingCount >= MaximumRequests) {
336 1 rejected.outcome = MailSelectionMoveResult::Outcome::Busy;
337 1 rejected.error = QStringLiteral("Selection writer is busy or stopping");
338
1/1
✓ Branch 35 → 36 taken 1 time.
1 callback(std::move(rejected));
339 1 return;
340 }
341 30 ++m_pendingCount;
342
1/1
✓ Branch 38 → 39 taken 30 times.
30 const QPointer<QObject> consumer(owner);
343
3/6
✓ Branch 39 → 40 taken 30 times.
✓ Branch 40 → 41 taken 30 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 30 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
30 auto *watcher = new QFutureWatcher<MailSelectionMoveResult>(this);
344 30 connect(watcher, &QFutureWatcher<MailSelectionMoveResult>::finished, this,
345
1/3
✓ Branch 46 → 47 taken 30 times.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
60 [this, watcher, consumer, callback = std::move(callback)]() mutable {
346 25 --m_pendingCount;
347
1/1
✓ Branch 2 → 3 taken 25 times.
25 auto result = watcher->result();
348
1/1
✓ Branch 3 → 4 taken 25 times.
25 watcher->deleteLater();
349
6/7
✓ Branch 4 → 5 taken 25 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 22 times.
✓ Branch 6 → 8 taken 3 times.
✓ Branch 9 → 10 taken 22 times.
✓ Branch 9 → 15 taken 3 times.
✓ Branch 12 → 13 taken 22 times.
25 if (!m_stopping && consumer) callback(std::move(result));
350 25 });
351
1/1
✓ Branch 56 → 57 taken 30 times.
30 watcher->setFuture(QtConcurrent::run(&m_pool,
352
1/5
✓ Branch 55 → 56 taken 30 times.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
90 [databasePath = std::move(databasePath), selection = std::move(selection),
353 30 targetFolder = std::move(targetFolder)]() mutable {
354
1/1
✓ Branch 2 → 3 taken 30 times.
30 MailCache writer;
355
3/3
✓ Branch 3 → 4 taken 30 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 11 taken 29 times.
30 if (!writer.openExistingWriter(databasePath)) {
356 ✗ MailSelectionMoveResult failed;
357
1/1
✓ Branch 5 → 6 taken 1 time.
1 failed.error = writer.lastError();
358
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 return failed;
359 ✗ }
360
1/1
✓ Branch 13 → 14 taken 29 times.
29 return writer.enqueueSelectionMove(std::move(selection), targetFolder);
361 30 }));
362 36 }
363