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.8% 323 / 0 / 327
Functions: 100.0% 36 / 0 / 36
Branches: 85.5% 390 / 0 / 456

service/MailBodyWriteService.cpp
Line Branch Exec Source
1 #include "MailBodyWriteService.h"
2 #include "util/Utf8Digest.h"
3 #include "data/MailCache.h"
4 #include <QCoreApplication>
5 #include <QFutureWatcher>
6 #include <QFileInfo>
7 #include <QPointer>
8 #include <QPromise>
9 #include <QtConcurrent>
10 #include <algorithm>
11 #include <utility>
12
13
2/2
✓ Branch 5 → 6 taken 56 times.
✓ Branch 6 → 7 taken 56 times.
56 MailBodyWriteService::MailBodyWriteService(QObject *parent) : QObject(parent) {
14
1/1
✓ Branch 7 → 8 taken 56 times.
56 m_pool.setMaxThreadCount(1);
15
1/1
✓ Branch 8 → 9 taken 56 times.
56 m_pool.setExpiryTimeout(1000);
16
1/1
✓ Branch 10 → 11 taken 56 times.
56 connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
17 57 this, [this] { stop(); });
18 56 }
19
20 91 MailBodyWriteService::~MailBodyWriteService() { stop(); }
21
22 namespace {
23 QPointer<MailBodyWriteService> sharedMailBodyWriteService;
24 // S106-120: runs first in ~QCoreApplication, while Qt's SQL and plugin state
25 // still exists — aboutToQuit misses a process that ends without the event
26 // loop, and the application-child deletion came only after that state was
27 // gone, with a worker possibly inside QSqlDatabase::open (the crash fixed
28 // for MailSelectionMutationService).
29 } // namespace
30
31 34 void MailBodyWriteService::stopSharedBeforeTeardown() {
32
1/2
✓ Branch 3 → 4 taken 34 times.
✗ Branch 3 → 6 not taken.
34 if (sharedMailBodyWriteService) sharedMailBodyWriteService->stop();
33 34 }
34
35 3773 MailBodyWriteService *MailBodyWriteService::shared() {
36
2/2
✓ Branch 3 → 4 taken 34 times.
✓ Branch 3 → 11 taken 3739 times.
3773 if (!sharedMailBodyWriteService) {
37
3/6
✓ Branch 6 → 7 taken 34 times.
✓ Branch 7 → 8 taken 34 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 34 times.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
34 sharedMailBodyWriteService = new MailBodyWriteService(QCoreApplication::instance());
38 34 qAddPostRoutine(stopSharedBeforeTeardown);
39 }
40 3773 return sharedMailBodyWriteService;
41 }
42
43 94 void MailBodyWriteService::stop() {
44 94 m_stopping = true;
45 {
46 94 QMutexLocker lock(&m_fencesMutex);
47
6/6
✓ Branch 3 → 4 taken 94 times.
✓ Branch 4 → 5 taken 94 times.
✓ Branch 9 → 10 taken 16 times.
✓ Branch 9 → 12 taken 69 times.
✓ Branch 15 → 6 taken 85 times.
✓ Branch 15 → 16 taken 94 times.
179 for (const auto &weak : m_fences) if (const auto fence = weak.lock()) fence->cancel();
48 94 }
49 // Every admitted write is already on this pool; finishing queued work never
50 // depends on a GUI callback starting the next task.
51
1/1
✓ Branch 18 → 19 taken 94 times.
94 m_pool.waitForDone();
52 94 }
53
54 39 void MailBodyWriteService::write(QObject *owner, const QString &databasePath,
55 const MailId &mailId, quint32 uidValidity,
56 QByteArray raw, MimeMessage parsed,
57 Callback callback, Priority priority) {
58 39 qint64 bytes = raw.size();
59 39 bytes += 2LL * (parsed.textPlain.size() + parsed.textHtml.size());
60
4/4
✓ Branch 5 → 6 taken 39 times.
✓ Branch 6 → 7 taken 39 times.
✓ Branch 17 → 8 taken 278 times.
✓ Branch 17 → 18 taken 39 times.
317 for (const auto &part : parsed.attachments)
61 278 bytes += part.body.size() + 2LL * (part.filename.size() + part.contentType.size() + part.contentId.size() +
62 278 part.charset.size() + part.transferEncoding.size());
63 39 MailBodyWriteResult rejected;
64
4/4
✓ Branch 19 → 20 taken 37 times.
✓ Branch 19 → 26 taken 1 time.
✓ Branch 21 → 22 taken 36 times.
✓ Branch 21 → 26 taken 1 time.
75 if (parsed.limitExceeded || bytes > MaximumRequestBytes ||
65
8/8
✓ Branch 18 → 19 taken 38 times.
✓ Branch 18 → 26 taken 1 time.
✓ Branch 23 → 24 taken 35 times.
✓ Branch 23 → 26 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 34 times.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 40 taken 34 times.
149 parsed.attachments.size() > 256 || databasePath.size() > 4096 ||
66 35 mailId.accountId.size() > 4096) {
67 5 rejected.outcome = MailBodyWriteResult::Outcome::Limited;
68 5 rejected.error = QStringLiteral("Prepared payload exceeds writer limits");
69
1/1
✓ Branch 37 → 38 taken 5 times.
5 callback(std::move(rejected));
70 5 return;
71 }
72 // Speculation may use spare capacity, but cannot consume the final body
73 // request slot or the bytes needed by one ordinary foreground payload.
74 34 const int requestLimit = priority == Priority::Speculative
75
1/2
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 34 times.
34 ? MaximumRequests - 1 : MaximumRequests;
76 34 const qint64 byteLimit = priority == Priority::Speculative
77
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 34 times.
34 ? MaximumQueuedBytes - ForegroundReservedBytes : MaximumQueuedBytes;
78
4/4
✓ Branch 46 → 47 taken 33 times.
✓ Branch 46 → 49 taken 1 time.
✓ Branch 47 → 48 taken 32 times.
✓ Branch 47 → 49 taken 1 time.
34 if (m_stopping || m_pendingCount >= requestLimit ||
79
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 60 taken 31 times.
32 bytes > byteLimit - m_pendingBytes) {
80 3 rejected.outcome = MailBodyWriteResult::Outcome::Busy;
81 3 rejected.error = QStringLiteral("Payload writer is busy or stopping");
82
1/1
✓ Branch 57 → 58 taken 3 times.
3 callback(std::move(rejected));
83 3 return;
84 }
85 31 ++m_pendingCount;
86 31 m_pendingBytes += bytes;
87
1/1
✓ Branch 60 → 61 taken 31 times.
31 const QPointer<QObject> consumer(owner);
88
3/6
✓ Branch 61 → 62 taken 31 times.
✓ Branch 62 → 63 taken 31 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 31 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
31 auto *watcher = new QFutureWatcher<MailBodyWriteResult>(this);
89 31 connect(watcher, &QFutureWatcher<MailBodyWriteResult>::finished, this,
90
1/3
✓ Branch 68 → 69 taken 31 times.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
62 [this, watcher, consumer, bytes, callback = std::move(callback)]() mutable {
91 30 --m_pendingCount;
92 30 m_pendingBytes -= bytes;
93
2/2
✓ Branch 2 → 3 taken 30 times.
✓ Branch 3 → 4 taken 30 times.
30 auto result = watcher->future().takeResult();
94
1/1
✓ Branch 5 → 6 taken 30 times.
30 watcher->deleteLater();
95
6/7
✓ Branch 6 → 7 taken 30 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 8 times.
✓ Branch 11 → 12 taken 22 times.
✓ Branch 11 → 17 taken 8 times.
✓ Branch 14 → 15 taken 22 times.
30 if (!m_stopping && consumer) callback(std::move(result));
96 30 });
97 31 const auto bodyStoreRevision = m_bodyStoreRevision;
98 31 auto work = [databasePath, mailId, uidValidity, bodyStoreRevision,
99 62 raw = std::move(raw), parsed = std::move(parsed)]() mutable {
100 31 MailBodyWriteResult result;
101
1/1
✓ Branch 2 → 3 taken 31 times.
31 MailCache writer;
102
3/3
✓ Branch 3 → 4 taken 31 times.
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 9 taken 27 times.
31 if (!writer.openExistingWriter(databasePath)) {
103
1/1
✓ Branch 5 → 6 taken 4 times.
4 result.error = writer.lastError();
104 4 return result;
105 }
106 27 MailBody body;
107 27 body.uid = mailId.uid;
108 27 body.cacheUidValidity = uidValidity;
109 27 body.cacheAccountId = mailId.accountId;
110 27 body.textPlain = std::move(parsed.textPlain);
111 27 body.textHtml = std::move(parsed.textHtml);
112
2/6
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 27 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 29 taken 27 times.
27 if (!body.textHtml.isEmpty() && body.textHtml.size() <= 1024 * 1024) {
113 ✗ const auto digest = MailJD::sha256Utf8HexUpTo(body.textHtml, 1024 * 1024);
114 ✗ if (digest) body.htmlDigest = *digest;
115 ✗ }
116 27 body.rawSource = std::move(raw);
117 27 QList<Attachment> attachments;
118 27 QList<QByteArray> blobs;
119
1/1
✓ Branch 32 → 33 taken 27 times.
27 attachments.reserve(parsed.attachments.size());
120
1/1
✓ Branch 34 → 35 taken 27 times.
27 blobs.reserve(parsed.attachments.size());
121
4/4
✓ Branch 35 → 36 taken 27 times.
✓ Branch 36 → 37 taken 27 times.
✓ Branch 57 → 38 taken 20 times.
✓ Branch 57 → 58 taken 27 times.
47 for (auto &part : parsed.attachments) {
122 20 Attachment attachment;
123 20 attachment.filename = std::move(part.filename);
124 20 attachment.contentType = std::move(part.contentType);
125 20 attachment.contentId = std::move(part.contentId);
126 20 attachment.presentation = part.presentation;
127 20 attachment.size = part.body.size();
128
1/1
✓ Branch 47 → 48 taken 20 times.
20 attachments.append(std::move(attachment));
129
1/1
✓ Branch 49 → 50 taken 20 times.
20 blobs.append(std::move(part.body));
130
2/2
✓ Branch 50 → 51 taken 3 times.
✓ Branch 50 → 52 taken 17 times.
23 result.hasVisibleAttachment |= part.presentation == AttachmentPresentation::Regular ||
131
2/2
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 2 times.
3 part.presentation == AttachmentPresentation::ExplicitAttachmentWithContentId;
132 20 result.inlineImageCount += part.presentation == AttachmentPresentation::InlineCandidate;
133 20 }
134
3/3
✓ Branch 58 → 59 taken 27 times.
✓ Branch 59 → 60 taken 5 times.
✓ Branch 59 → 64 taken 22 times.
27 if (!writer.storeMessageContentForIdentity(mailId, uidValidity, body, attachments, blobs)) {
135
1/1
✓ Branch 60 → 61 taken 5 times.
5 result.error = writer.lastError();
136 5 return result;
137 }
138 22 bodyStoreRevision->fetch_add(1, std::memory_order_release);
139 // IDs are assigned by the transaction. Read them on the same worker; raw
140 // MIME and BLOBs are released before publishing the display result.
141
1/1
✓ Branch 68 → 69 taken 22 times.
22 body.attachments = writer.attachments().attachments(mailId.folderId, mailId.uid);
142 // The display callback needs this header before its first content frame.
143 // Resolve it here on the writer lane instead of querying SQLite on the
144 // GUI thread after the completed payload is handed back.
145
1/1
✓ Branch 71 → 72 taken 22 times.
22 result.header = writer.header(mailId.folderId, mailId.uid);
146
2/3
✓ Branch 74 → 75 taken 22 times.
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 77 taken 22 times.
22 if (writer.uidValidity(mailId.folderId) != uidValidity)
147 ✗ result.header.reset();
148
1/1
✓ Branch 77 → 78 taken 22 times.
22 body.rawSource.clear();
149 22 result.body = std::move(body);
150 22 result.outcome = MailBodyWriteResult::Outcome::Stored;
151 22 return result;
152 62 };
153 // QThreadPool orders queued tasks by priority. Its single worker preserves
154 // the one-writer contract; a foreground body can pass queued header batches
155 // and speculative bodies, while an active SQLite write runs to completion.
156
1/1
✓ Branch 79 → 80 taken 31 times.
31 auto promise = std::make_shared<QPromise<MailBodyWriteResult>>();
157
1/1
✓ Branch 81 → 82 taken 31 times.
31 promise->start();
158
2/2
✓ Branch 83 → 84 taken 31 times.
✓ Branch 84 → 85 taken 31 times.
31 watcher->setFuture(promise->future());
159
1/3
✓ Branch 89 → 90 taken 31 times.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
31 m_pool.start([promise, work = std::move(work)]() mutable {
160
2/2
✓ Branch 3 → 4 taken 31 times.
✓ Branch 4 → 5 taken 31 times.
31 (void)promise->addResult(work());
161 31 promise->finish();
162 31 }, static_cast<int>(priority));
163 39 }
164
165 1404 MailHeaderStateFence::~MailHeaderStateFence() { QObject::disconnect(destroyed); }
166
167 2722 void MailHeaderStateFence::amend(qint64 uid, const std::function<void(Change &)> &change) {
168
7/8
✓ Branch 2 → 3 taken 2722 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 315 times.
✓ Branch 4 → 8 taken 2407 times.
✓ Branch 6 → 7 taken 207 times.
✓ Branch 6 → 8 taken 108 times.
✓ Branch 9 → 10 taken 207 times.
✓ Branch 9 → 11 taken 2515 times.
3075 if (uid <= 0 || (!uids.isEmpty() && !uids.contains(uid))) return;
169 2515 QMutexLocker lock(&mutex);
170
5/6
✓ Branch 13 → 14 taken 2163 times.
✓ Branch 13 → 16 taken 352 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2163 times.
✓ Branch 18 → 19 taken 352 times.
✓ Branch 18 → 20 taken 2163 times.
2515 if (m_cancelled.load() || !error.isEmpty()) return;
171
6/6
✓ Branch 21 → 22 taken 2137 times.
✓ Branch 21 → 25 taken 26 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 2136 times.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 34 taken 2162 times.
2163 if (!changes.contains(uid) && changes.size() >= 2048) {
172 1 error = QStringLiteral("Header state history exceeds its identity budget");
173 1 return;
174 }
175 4324 auto size = [](const Change &value) {
176 4324 qint64 result = 128;
177
4/4
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 13 taken 4316 times.
✓ Branch 11 → 7 taken 1 time.
✓ Branch 11 → 12 taken 8 times.
4325 if (value.labels) for (const auto &label : *value.labels) result += 2LL * label.size() + 32;
178
4/4
✓ Branch 13 → 14 taken 4324 times.
✓ Branch 18 → 19 taken 4353 times.
✓ Branch 20 → 15 taken 29 times.
✓ Branch 20 → 21 taken 4324 times.
4353 for (auto it = value.labelChanges.cbegin(); it != value.labelChanges.cend(); ++it)
179 29 result += 2LL * it.key().size() + 64;
180 4324 return result;
181 };
182
1/1
✓ Branch 34 → 35 taken 2162 times.
2162 auto &value = changes[uid];
183
1/1
✓ Branch 35 → 36 taken 2162 times.
2162 const auto before = size(value);
184
1/1
✓ Branch 36 → 37 taken 2162 times.
2162 change(value);
185
1/1
✓ Branch 37 → 38 taken 2162 times.
2162 bytes += size(value) - before;
186 // Include fixed ownership for every entry, even one containing only flags.
187
2/2
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 48 taken 2161 times.
2162 if (bytes + 128LL * changes.size() > 2LL * 1024 * 1024) {
188 1 changes.clear();
189 1 bytes = 0;
190 1 error = QStringLiteral("Header state history exceeds its byte budget");
191 }
192 2515 }
193
194 874 QString MailHeaderStateFence::apply(QList<MailHeader> &headers) const {
195 874 QMutexLocker lock(&mutex);
196
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 10 taken 871 times.
877 if (m_cancelled.load()) return QStringLiteral("Header stream is no longer active");
197
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 868 times.
871 if (!error.isEmpty()) return error;
198
4/4
✓ Branch 13 → 14 taken 868 times.
✓ Branch 59 → 60 taken 36144 times.
✓ Branch 61 → 15 taken 35276 times.
✓ Branch 61 → 62 taken 868 times.
36144 for (auto it = headers.begin(); it != headers.end();) {
199 35276 const auto found = changes.constFind(it->uid);
200
2/2
✓ Branch 19 → 20 taken 35267 times.
✓ Branch 19 → 22 taken 9 times.
35278 if (found == changes.cend()) { ++it; continue; }
201 9 const auto &change = found.value();
202
3/3
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 27 taken 7 times.
✓ Branch 25 → 26 taken 2 times.
9 if (change.removed) { it = headers.erase(it); continue; }
203
2/2
✓ Branch 28 → 29 taken 6 times.
✓ Branch 28 → 32 taken 1 time.
7 if (change.flags) it->flags = *change.flags;
204
2/2
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 37 taken 6 times.
7 if (change.spam) it->isSpam = *change.spam;
205
2/2
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 42 taken 6 times.
7 if (change.labels) it->labels = *change.labels;
206
4/4
✓ Branch 42 → 43 taken 7 times.
✓ Branch 53 → 54 taken 10 times.
✓ Branch 55 → 44 taken 3 times.
✓ Branch 55 → 56 taken 7 times.
10 for (auto label = change.labelChanges.cbegin(); label != change.labelChanges.cend(); ++label) {
207
1/1
✓ Branch 46 → 47 taken 3 times.
3 it->labels.removeAll(label.key());
208
3/3
✓ Branch 48 → 49 taken 2 times.
✓ Branch 48 → 52 taken 1 time.
✓ Branch 51 → 52 taken 2 times.
3 if (label.value()) it->labels.append(label.key());
209 }
210 7 ++it;
211 }
212 868 return {};
213 874 }
214
215 46805 void MailBodyWriteService::amendHeaderState(
216 const QString &databasePath, qint64 folderId, qint64 uid,
217 const std::function<void(MailHeaderStateFence::Change &)> &change) {
218 46805 QMutexLocker lock(&m_fencesMutex);
219
4/4
✓ Branch 3 → 4 taken 46805 times.
✓ Branch 29 → 30 taken 53000 times.
✓ Branch 31 → 5 taken 6195 times.
✓ Branch 31 → 32 taken 46805 times.
53000 for (auto it = m_fences.begin(); it != m_fences.end();) {
220 6195 const auto fence = it->lock();
221
3/3
✓ Branch 8 → 9 taken 613 times.
✓ Branch 8 → 12 taken 5582 times.
✓ Branch 10 → 11 taken 613 times.
6195 if (!fence) { it = m_fences.erase(it); continue; }
222
6/6
✓ Branch 14 → 15 taken 2731 times.
✓ Branch 14 → 18 taken 2851 times.
✓ Branch 16 → 17 taken 2721 times.
✓ Branch 16 → 18 taken 10 times.
✓ Branch 19 → 20 taken 2721 times.
✓ Branch 19 → 22 taken 2861 times.
5582 if (fence->writeDomain == databasePath && fence->identity.folderId == folderId)
223
1/1
✓ Branch 21 → 22 taken 2721 times.
2721 fence->amend(uid, change);
224 5582 ++it;
225 6195 }
226 46805 }
227
228 1055 void MailBodyWriteService::observeCache(MailCache *cache) {
229 1055 connect(cache, &MailCache::headerStateReplaced, this,
230
1/1
✓ Branch 2 → 3 taken 1055 times.
1055 [this, cache](qint64 folderId, const MailHeader &header) {
231
1/1
✓ Branch 4 → 5 taken 44405 times.
44405 amendHeaderState(cache->writeDomain(), folderId, header.uid, [&](auto &change) {
232 4 change.flags = header.flags; change.spam = header.isSpam;
233 4 change.labels = header.labels; change.labelChanges.clear(); change.removed = false;
234 4 });
235 44405 }, Qt::DirectConnection);
236 1055 connect(cache, &MailCache::headerFlagsChanged, this,
237
1/1
✓ Branch 4 → 5 taken 1055 times.
1055 [this, cache](qint64 folderId, qint64 uid, quint32 flags) {
238
1/1
✓ Branch 4 → 5 taken 2316 times.
4455 amendHeaderState(cache->writeDomain(), folderId, uid, [flags](auto &change) { change.flags = flags; });
239 2316 }, Qt::DirectConnection);
240 1055 connect(cache, &MailCache::headerLabelChanged, this,
241
1/1
✓ Branch 6 → 7 taken 1055 times.
1055 [this, cache](qint64 folderId, qint64 uid, const QString &label, bool present) {
242
1/1
✓ Branch 4 → 5 taken 59 times.
59 amendHeaderState(cache->writeDomain(), folderId, uid,
243 74 [&](auto &change) { change.labelChanges[label] = present; });
244 59 }, Qt::DirectConnection);
245 1055 connect(cache, &MailCache::headerRemoved, this,
246
1/1
✓ Branch 8 → 9 taken 1055 times.
1055 [this, cache](qint64 folderId, qint64 uid) {
247
1/1
✓ Branch 4 → 5 taken 25 times.
25 amendHeaderState(cache->writeDomain(), folderId, uid, [](auto &change) { change.removed = true; });
248 25 }, Qt::DirectConnection);
249
1/1
✓ Branch 10 → 11 taken 1055 times.
1055 connect(cache, &MailCache::folderHeadersRemoved, this, [this, cache](qint64 folderId) {
250 30 QMutexLocker lock(&m_fencesMutex);
251
4/4
✓ Branch 3 → 4 taken 30 times.
✓ Branch 4 → 5 taken 30 times.
✓ Branch 26 → 6 taken 22 times.
✓ Branch 26 → 27 taken 30 times.
52 for (const auto &weak : m_fences)
252
8/8
✓ Branch 9 → 10 taken 15 times.
✓ Branch 9 → 17 taken 7 times.
✓ Branch 13 → 14 taken 14 times.
✓ Branch 13 → 17 taken 1 time.
✓ Branch 18 → 19 taken 15 times.
✓ Branch 18 → 20 taken 7 times.
✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 23 taken 11 times.
36 if (const auto fence = weak.lock(); fence && fence->writeDomain == cache->writeDomain() &&
253
2/2
✓ Branch 15 → 16 taken 11 times.
✓ Branch 15 → 17 taken 3 times.
36 fence->identity.folderId == folderId) fence->cancel();
254 30 }, Qt::DirectConnection);
255 1055 }
256
257 1417 std::shared_ptr<MailHeaderStateFence> MailBodyWriteService::beginHeaderStream(
258 QObject *owner, const MailHeaderStreamIdentity &identity, const QList<qint64> &uids) {
259
10/10
✓ Branch 3 → 4 taken 1415 times.
✓ Branch 3 → 19 taken 1 time.
✓ Branch 5 → 6 taken 1414 times.
✓ Branch 5 → 19 taken 1 time.
✓ Branch 7 → 8 taken 1413 times.
✓ Branch 7 → 19 taken 1 time.
✓ Branch 8 → 9 taken 1412 times.
✓ Branch 8 → 19 taken 1 time.
✓ Branch 10 → 11 taken 1411 times.
✓ Branch 10 → 19 taken 1 time.
2828 if (!owner || m_stopping || identity.databasePath.isEmpty() || identity.databasePath.size() > 4096 || identity.folderId <= 0 ||
260
4/4
✓ Branch 12 → 13 taken 1410 times.
✓ Branch 12 → 19 taken 1 time.
✓ Branch 14 → 15 taken 1409 times.
✓ Branch 14 → 19 taken 1 time.
4233 identity.account.isEmpty() || identity.folder.isEmpty() ||
261
8/8
✓ Branch 2 → 3 taken 1416 times.
✓ Branch 2 → 19 taken 1 time.
✓ Branch 16 → 17 taken 1408 times.
✓ Branch 16 → 19 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 1407 times.
✓ Branch 21 → 22 taken 10 times.
✓ Branch 21 → 23 taken 1407 times.
4243 identity.account.size() > 4096 || identity.folder.size() > 4096 || uids.size() > 2048)
262 10 return {};
263 1407 QMutexLocker lock(&m_fencesMutex);
264
4/4
✓ Branch 24 → 25 taken 1407 times.
✓ Branch 33 → 34 taken 2709 times.
✓ Branch 35 → 26 taken 1302 times.
✓ Branch 35 → 36 taken 1407 times.
2709 for (auto it = m_fences.begin(); it != m_fences.end();) {
265
3/3
✓ Branch 28 → 29 taken 742 times.
✓ Branch 28 → 32 taken 560 times.
✓ Branch 30 → 31 taken 742 times.
1302 if (it->expired()) it = m_fences.erase(it); else ++it;
266 }
267
2/2
✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 39 taken 1404 times.
1407 if (m_fences.size() >= 16) return {};
268
1/1
✓ Branch 39 → 40 taken 1404 times.
1404 auto fence = std::make_shared<MailHeaderStateFence>();
269 1404 fence->identity = identity;
270
1/1
✓ Branch 42 → 43 taken 1404 times.
1404 const QFileInfo file(identity.databasePath);
271
1/1
✓ Branch 43 → 44 taken 1404 times.
1404 fence->writeDomain = file.canonicalFilePath();
272
3/3
✓ Branch 49 → 50 taken 20 times.
✓ Branch 49 → 55 taken 1384 times.
✓ Branch 50 → 51 taken 20 times.
1404 if (fence->writeDomain.isEmpty()) fence->writeDomain = file.absoluteFilePath();
273
2/2
✓ Branch 64 → 57 taken 505 times.
✓ Branch 64 → 65 taken 1403 times.
1908 for (qint64 uid : uids) {
274
2/2
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 504 times.
505 if (uid <= 0) return {};
275
1/1
✓ Branch 61 → 62 taken 504 times.
504 fence->uids.insert(uid);
276 }
277 1403 const std::weak_ptr<MailHeaderStateFence> weak = fence;
278
1/1
✓ Branch 67 → 69 taken 1403 times.
2806 fence->destroyed = connect(owner, &QObject::destroyed, this, [weak] {
279
1/2
✓ Branch 4 → 5 taken 47 times.
✗ Branch 4 → 7 not taken.
47 if (const auto fence = weak.lock()) fence->cancel();
280 1403 }, Qt::DirectConnection);
281
1/1
✓ Branch 74 → 75 taken 1403 times.
1403 m_fences.append(fence);
282 1403 return fence;
283 1407 }
284
285 1 void MailBodyWriteService::preserveHeaderFlags(
286 const std::shared_ptr<MailHeaderStateFence> &fence, qint64 uid, quint32 flags) {
287
2/3
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 9 not taken.
✓ Branch 6 → 7 taken 1 time.
2 if (fence) fence->amend(uid, [flags](auto &change) { change.flags = flags; });
288 1 }
289
290 124 bool MailBodyWriteService::widenHeaderStream(
291 const std::shared_ptr<MailHeaderStateFence> &fence,
292 const MailHeaderStreamIdentity &identity, const QList<qint64> &uids) {
293 // The fence mutex list serializes this with amendHeaderState(), which
294 // reads the accepted UIDs from worker threads.
295 124 QMutexLocker lock(&m_fencesMutex);
296
6/8
✓ Branch 4 → 5 taken 124 times.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 124 times.
✗ Branch 5 → 9 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 123 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 123 times.
124 if (!fence || m_stopping || fence->m_cancelled.load()) return false;
297 123 const auto &open = fence->identity;
298
1/2
✓ Branch 17 → 18 taken 123 times.
✗ Branch 17 → 22 not taken.
246 if (open.databasePath != identity.databasePath || open.account != identity.account ||
299
6/8
✓ Branch 15 → 16 taken 123 times.
✗ Branch 15 → 22 not taken.
✓ Branch 19 → 20 taken 122 times.
✓ Branch 19 → 22 taken 1 time.
✓ Branch 20 → 21 taken 122 times.
✗ Branch 20 → 22 not taken.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 26 taken 121 times.
368 open.folder != identity.folder || open.folderId != identity.folderId ||
300
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 121 times.
122 open.uidValidity != identity.uidValidity)
301 2 return false;
302
2/2
✓ Branch 28 → 29 taken 13 times.
✓ Branch 28 → 30 taken 108 times.
121 if (fence->uids.isEmpty()) return true; // already accepts every UID
303
5/6
✓ Branch 31 → 32 taken 107 times.
✓ Branch 31 → 36 taken 1 time.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 107 times.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 42 taken 107 times.
108 if (uids.isEmpty() || fence->uids.size() + uids.size() > 2048) {
304 1 fence->uids.clear();
305 1 return true;
306 }
307
2/2
✓ Branch 51 → 44 taken 109 times.
✓ Branch 51 → 52 taken 107 times.
216 for (qint64 uid : uids) {
308
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 109 times.
109 if (uid <= 0) return false;
309
1/1
✓ Branch 48 → 49 taken 109 times.
109 fence->uids.insert(uid);
310 }
311 107 return true;
312 124 }
313
314 67 static qint64 headerBatchBytes(const QList<MailHeader> &headers) {
315 67 qint64 bytes = 0;
316
2/2
✓ Branch 32 → 4 taken 94 times.
✓ Branch 32 → 33 taken 67 times.
161 for (const auto &header : headers) {
317 94 bytes += 256 + 2LL * (header.subject.size() + header.from.size() + header.to.size() +
318 94 header.messageId.size() + header.inReplyTo.size());
319
2/2
✓ Branch 23 → 15 taken 282 times.
✓ Branch 23 → 24 taken 94 times.
658 for (const auto &list : {header.labels, header.references, header.inReplyToIds})
320
4/4
✓ Branch 21 → 17 taken 5 times.
✓ Branch 21 → 22 taken 282 times.
✓ Branch 25 → 26 taken 282 times.
✓ Branch 25 → 29 taken 94 times.
663 for (const auto &value : list) bytes += 32 + 2LL * value.size();
321 }
322 67 return bytes;
323 }
324
325 44 void MailBodyWriteService::writeHeaders(
326 QObject *owner, QList<MailHeader> headers,
327 std::shared_ptr<MailHeaderStateFence> fence, HeaderCallback callback) {
328
10/10
✓ Branch 2 → 3 taken 43 times.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 4 → 5 taken 42 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 6 → 7 taken 41 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 40 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 24 taken 40 times.
44 if (!owner || !fence || headers.isEmpty() || headers.size() > 512) {
329 12 callback({false, QStringLiteral("Header batch exceeds writer identity or byte limits"
330 " (%1 headers, limit 512)").arg(headers.size()), {}});
331 10 return;
332 }
333
1/1
✓ Branch 24 → 25 taken 40 times.
40 const qint64 bytes = headerBatchBytes(headers);
334
1/1
✓ Branch 27 → 28 taken 40 times.
40 const auto foreign = std::find_if(headers.cbegin(), headers.cend(), [&](const auto &header) {
335
6/6
✓ Branch 2 → 3 taken 53 times.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 5 → 6 taken 17 times.
✓ Branch 5 → 10 taken 36 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 16 times.
54 return header.uid <= 0 || (!fence->uids.isEmpty() && !fence->uids.contains(header.uid));
336 });
337
6/6
✓ Branch 28 → 29 taken 39 times.
✓ Branch 28 → 32 taken 1 time.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 37 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 34 → 67 taken 37 times.
40 if (bytes > 8LL * 1024 * 1024 || foreign != headers.cend()) {
338 6 callback({false, foreign != headers.cend()
339
2/6
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 66 taken 1 time.
✗ Branch 145 → 146 not taken.
✗ Branch 145 → 147 not taken.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
5 ? QStringLiteral("Header batch exceeds writer identity or byte limits"
340 " (UID %1 outside the stream's %2 UIDs)")
341
3/5
✓ Branch 43 → 44 taken 2 times.
✓ Branch 62 → 63 taken 2 times.
✓ Branch 62 → 64 taken 1 time.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
7 .arg(foreign->uid).arg(fence->uids.size())
342
2/6
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 2 times.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 137 not taken.
4 : QStringLiteral("Header batch exceeds writer identity or byte limits"
343 " (%1 bytes, limit 8 MiB)").arg(bytes), {}});
344 3 return;
345 }
346
6/6
✓ Branch 67 → 68 taken 36 times.
✓ Branch 67 → 70 taken 1 time.
✓ Branch 68 → 69 taken 35 times.
✓ Branch 68 → 70 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 78 taken 34 times.
37 if (m_stopping || m_pendingCount >= MaximumRequests || bytes > MaximumQueuedBytes - m_pendingBytes) {
347 3 callback({false, QStringLiteral("Cache writer is busy or stopping"), {}});
348 3 return;
349 }
350 34 ++m_pendingCount;
351 34 m_pendingBytes += bytes;
352
1/1
✓ Branch 78 → 79 taken 34 times.
34 const QPointer<QObject> consumer(owner);
353
3/6
✓ Branch 79 → 80 taken 34 times.
✓ Branch 80 → 81 taken 34 times.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 83 taken 34 times.
✗ Branch 169 → 170 not taken.
✗ Branch 169 → 171 not taken.
34 auto *watcher = new QFutureWatcher<MailHeaderWriteResult>(this);
354 34 connect(watcher, &QFutureWatcher<MailHeaderWriteResult>::finished, this,
355
1/3
✓ Branch 86 → 87 taken 34 times.
✗ Branch 174 → 175 not taken.
✗ Branch 174 → 176 not taken.
68 [this, watcher, consumer, bytes, callback = std::move(callback)]() mutable {
356 34 --m_pendingCount; m_pendingBytes -= bytes;
357
2/2
✓ Branch 2 → 3 taken 34 times.
✓ Branch 3 → 4 taken 34 times.
34 auto result = watcher->result(); watcher->deleteLater();
358
6/7
✓ Branch 4 → 5 taken 34 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 33 times.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 9 → 10 taken 33 times.
✓ Branch 9 → 15 taken 1 time.
✓ Branch 12 → 13 taken 33 times.
34 if (!m_stopping && consumer) callback(std::move(result));
359 34 });
360
1/1
✓ Branch 93 → 94 taken 34 times.
34 watcher->setFuture(QtConcurrent::run(&m_pool,
361
1/3
✓ Branch 92 → 93 taken 34 times.
✗ Branch 183 → 184 not taken.
✗ Branch 183 → 185 not taken.
68 [headers = std::move(headers), fence]() mutable {
362 34 const auto &identity = fence->identity;
363 34 MailHeaderWriteResult result;
364 34 QList<MailHeader> check;
365
3/3
✓ Branch 5 → 6 taken 34 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 31 times.
34 if (!(result.error = fence->apply(check)).isEmpty()) return result;
366 // Reuse the serial worker's connection within one fenced header stream.
367 // A large import otherwise reopens SQLite and rechecks schema for every
368 // bounded batch. A new stream (including a same-path profile reset),
369 // changed database path or failed write drops the previous handle.
370
2/2
✓ Branch 11 → 12 taken 10 times.
✓ Branch 11 → 13 taken 21 times.
31 static thread_local std::unique_ptr<MailCache> writer;
371
2/2
✓ Branch 13 → 14 taken 10 times.
✓ Branch 13 → 15 taken 21 times.
31 static thread_local QString writerPath;
372
2/2
✓ Branch 15 → 16 taken 10 times.
✓ Branch 15 → 17 taken 21 times.
31 static thread_local std::weak_ptr<MailHeaderStateFence> writerFence;
373
9/10
✓ Branch 18 → 19 taken 16 times.
✓ Branch 18 → 25 taken 15 times.
✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 24 taken 9 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 7 times.
✓ Branch 26 → 27 taken 7 times.
✓ Branch 26 → 28 taken 24 times.
✓ Branch 28 → 29 taken 9 times.
✓ Branch 28 → 32 taken 22 times.
31 if (writer && (writerPath != identity.databasePath || writerFence.lock() != fence)) {
374 9 writer.reset();
375 9 writerPath.clear();
376 9 writerFence.reset();
377 }
378
2/2
✓ Branch 33 → 34 taken 24 times.
✓ Branch 33 → 48 taken 7 times.
31 if (!writer) {
379
1/1
✓ Branch 34 → 35 taken 24 times.
24 writer = std::make_unique<MailCache>();
380
3/3
✓ Branch 38 → 39 taken 24 times.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 46 taken 23 times.
24 if (!writer->openExistingWriter(identity.databasePath)) {
381
1/1
✓ Branch 41 → 42 taken 1 time.
1 result.error = writer->lastError();
382 1 writer.reset();
383 1 return result;
384 }
385 23 writerPath = identity.databasePath;
386 23 writerFence = fence;
387 }
388
1/1
✓ Branch 49 → 50 taken 30 times.
30 const qint64 previousRevision = writer->threadRevision(identity.folderId);
389 // Another connection can hold the write lock beyond busy_timeout (5 s);
390 // a single SQLITE_BUSY used to abort the whole folder backfill until the
391 // next open. The fence preparation is idempotent, so the same batch is
392 // retried a bounded number of times.
393 30 bool stored = false;
394
2/4
✓ Branch 81 → 82 taken 31 times.
✗ Branch 81 → 83 not taken.
✓ Branch 82 → 51 taken 31 times.
✗ Branch 82 → 83 not taken.
31 for (int attempt = 1; attempt <= MaximumBusyAttempts && !stored; ++attempt) {
395 62 stored = writer->storeStreamingHeaders(identity.account, identity.folder, identity.folderId,
396
1/1
✓ Branch 53 → 54 taken 31 times.
31 identity.uidValidity, headers,
397 31 [&](auto &batch) {
398
1/1
✓ Branch 3 → 4 taken 27 times.
27 const auto error = fence->apply(batch);
399
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 27 times.
27 if (!error.isEmpty()) return error;
400
1/1
✓ Branch 8 → 9 taken 27 times.
27 return headerBatchBytes(batch) > 8LL * 1024 * 1024
401
4/4
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 13 taken 26 times.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 26 times.
55 ? QStringLiteral("Updated header batch exceeds writer byte limits") : QString{};
402 27 });
403
14/20
✓ Branch 55 → 56 taken 7 times.
✓ Branch 55 → 63 taken 24 times.
✓ Branch 57 → 58 taken 7 times.
✓ Branch 61 → 62 taken 7 times.
✓ Branch 62 → 63 taken 6 times.
✓ Branch 62 → 64 taken 1 time.
✓ Branch 65 → 66 taken 7 times.
✓ Branch 65 → 67 taken 24 times.
✓ Branch 67 → 68 taken 7 times.
✓ Branch 67 → 69 taken 24 times.
✓ Branch 69 → 70 taken 7 times.
✓ Branch 69 → 71 taken 24 times.
✓ Branch 71 → 72 taken 30 times.
✓ Branch 71 → 73 taken 1 time.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
✗ Branch 132 → 133 not taken.
✗ Branch 132 → 134 not taken.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
38 if (stored || !writer->lastError().contains(QStringLiteral("database is locked"))) break;
404
4/4
✓ Branch 74 → 75 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
2 qWarning() << "Header write found the database locked, attempt" << attempt << "of"
405
1/1
✓ Branch 78 → 79 taken 1 time.
1 << MaximumBusyAttempts;
406 }
407
2/2
✓ Branch 83 → 84 taken 6 times.
✓ Branch 83 → 92 taken 24 times.
30 if (!stored) {
408
1/1
✓ Branch 85 → 86 taken 6 times.
6 result.error = writer->lastError();
409 6 writer.reset();
410 6 writerPath.clear();
411 6 writerFence.reset();
412 6 return result;
413 }
414
1/1
✓ Branch 93 → 94 taken 24 times.
24 const auto committedState = writer->folderPresentationState(identity.folderId);
415 // A failed revision read must cause a conservative refresh. Both reads
416 // happen on the writer lane, where successful no-op batches keep the
417 // derived projection revision byte-for-byte unchanged.
418
2/4
✓ Branch 94 → 95 taken 24 times.
✗ Branch 94 → 97 not taken.
✓ Branch 95 → 96 taken 24 times.
✗ Branch 95 → 97 not taken.
48 result.projectionChanged = previousRevision < 0 || committedState.threadRevision < 0 ||
419
2/2
✓ Branch 96 → 97 taken 22 times.
✓ Branch 96 → 98 taken 2 times.
24 committedState.threadRevision != previousRevision;
420
1/2
✓ Branch 99 → 100 taken 24 times.
✗ Branch 99 → 101 not taken.
24 if (committedState.uidValidity == identity.uidValidity) {
421 24 result.totalCount = committedState.headerCount;
422 24 result.uidValidity = identity.uidValidity;
423 }
424
1/1
✓ Branch 103 → 104 taken 24 times.
24 const auto pendingMoves = writer->journal().pendingMoveUids(identity.account, identity.folderId);
425
2/2
✓ Branch 114 → 107 taken 35 times.
✓ Branch 114 → 115 taken 24 times.
59 for (const auto &header : std::as_const(headers))
426
1/3
✗ Branch 109 → 110 not taken.
✓ Branch 109 → 112 taken 35 times.
✗ Branch 110 → 111 not taken.
35 if (pendingMoves.contains(header.uid)) result.pendingMoveUids.insert(header.uid);
427 24 result.headers = std::move(headers); result.stored = true;
428 24 return result;
429 34 }));
430 69 }
431