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: 84.4% 211 / 0 / 250
Functions: 93.3% 14 / 0 / 15
Branches: 77.0% 281 / 25 / 390

data/PayloadCacheLimits.cpp
Line Branch Exec Source
1 #include "data/PayloadCacheLimits.h"
2
3 #include "data/MailCacheSql.h"
4 #include <QLoggingCategory>
5 #include <QMutexLocker>
6 #include <QScopeGuard>
7 #include <QSqlError>
8 #include <QSqlQuery>
9 #include <QVariant>
10
11 // Same category as MailCache (defined there), see docs/LOGGING.md.
12 Q_DECLARE_LOGGING_CATEGORY(lcCache)
13
14 using MailCacheSql::beginWriteTransaction;
15
16 5035 PayloadCacheLimits::PayloadCacheLimits(QSqlDatabase &db, QString &lastError,
17 const std::shared_ptr<QRecursiveMutex> &writeMutex,
18 5035 Deps deps)
19 5035 : m_db(db), m_lastError(lastError), m_writeMutex(writeMutex), m_deps(std::move(deps)) {}
20
21 60 bool PayloadCacheLimits::purgeFolderInTransaction(qint64 folderId) {
22 // Caller owns the transaction (F-63's atomic epoch transition). CASCADE
23 // deletes bodies, attachments, and the canonical search document; its
24 // delete trigger removes the FTS entry in the same transaction.
25
1/1
✓ Branch 2 → 3 taken 60 times.
60 QSqlQuery q(m_db);
26
1/1
✓ Branch 6 → 7 taken 60 times.
60 q.prepare(QStringLiteral("DELETE FROM headers WHERE folder_id = :fid"));
27
1/1
✓ Branch 14 → 15 taken 60 times.
120 q.bindValue(QStringLiteral(":fid"), folderId);
28
3/3
✓ Branch 18 → 19 taken 60 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 37 taken 58 times.
60 if (!q.exec()) {
29
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
2 m_lastError = q.lastError().text();
30
6/6
✓ Branch 25 → 26 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 35 → 27 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
4 qCWarning(lcCache) << "Failed to purge folder:" << m_lastError;
31 2 return false;
32 }
33 58 return true;
34 60 }
35
36 15 void PayloadCacheLimits::purgeFolder(qint64 folderId) {
37 15 const auto writeMutex = m_writeMutex;
38 15 QMutexLocker publication(writeMutex.get());
39
3/3
✓ Branch 5 → 6 taken 15 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 23 taken 12 times.
15 if (!beginWriteTransaction(m_db)) {
40 6 qCWarning(lcCache) << "Failed to start folder purge transaction:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; transaction result remains measured.
41
3/3
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
3 << m_db.lastError().text();
42 3 return;
43 }
44
3/3
✓ Branch 23 → 24 taken 12 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 11 times.
12 if (!purgeFolderInTransaction(folderId)) {
45
1/1
✓ Branch 25 → 26 taken 1 time.
1 m_db.rollback();
46 1 return;
47 }
48
2/3
✓ Branch 27 → 28 taken 11 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 46 taken 11 times.
11 if (!m_db.commit()) {
49 ✗ m_db.rollback();
50 ✗ qCWarning(lcCache) << "Failed to commit folder purge:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; commit result remains measured.
51 ✗ << m_db.lastError().text();
52 ✗ return;
53 }
54
55
1/1
✓ Branch 46 → 47 taken 11 times.
11 m_deps.folderHeadersRemoved(folderId);
56
6/6
✓ Branch 47 → 48 taken 11 times.
✓ Branch 51 → 52 taken 11 times.
✓ Branch 52 → 53 taken 11 times.
✓ Branch 53 → 54 taken 11 times.
✓ Branch 57 → 49 taken 11 times.
✓ Branch 57 → 58 taken 11 times.
22 qCInfo(lcCache) << "Purged cache for folder" << folderId;
57 19 }
58
59 1228 int PayloadCacheLimits::cachedHeaderCount(const QString &account,
60 const QString &folderPath, QString *error) {
61
2/2
✓ Branch 2 → 3 taken 1047 times.
✓ Branch 2 → 4 taken 181 times.
1228 if (error) error->clear();
62
1/1
✓ Branch 4 → 5 taken 1228 times.
1228 QSqlQuery query(m_db);
63
1/1
✓ Branch 8 → 9 taken 1228 times.
1228 query.prepare(QStringLiteral(
64 "SELECT coalesce(c.total,0) FROM folders f "
65 "LEFT JOIN header_counts c ON c.folder_id=f.id WHERE f.account=? AND f.path=?"));
66
1/1
✓ Branch 13 → 14 taken 1228 times.
1228 query.addBindValue(account);
67
1/1
✓ Branch 17 → 18 taken 1228 times.
1228 query.addBindValue(folderPath);
68
3/3
✓ Branch 19 → 20 taken 1228 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 45 taken 1225 times.
1228 if (!query.exec()) {
69 // Read-only consumers may still open a legacy cache before migration.
70
1/1
✓ Branch 24 → 25 taken 3 times.
3 query.prepare(QStringLiteral(
71 "SELECT count(*) FROM headers h JOIN folders f ON f.id=h.folder_id "
72 "WHERE f.account=? AND f.path=?"));
73
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
3 query.addBindValue(account); query.addBindValue(folderPath);
74
3/3
✓ Branch 35 → 36 taken 3 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 45 taken 1 time.
3 if (!query.exec()) {
75
1/4
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 44 taken 2 times.
✗ Branch 38 → 39 not taken.
✗ Branch 39 → 40 not taken.
2 if (error) *error = query.lastError().text();
76 2 return 0;
77 }
78 }
79
3/3
✓ Branch 45 → 46 taken 1226 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 55 taken 1222 times.
1226 if (!query.next()) {
80
1/4
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 54 taken 4 times.
✗ Branch 48 → 49 not taken.
✗ Branch 49 → 50 not taken.
4 if (error) *error = query.lastError().text();
81 4 return 0;
82 }
83
2/2
✓ Branch 55 → 56 taken 1222 times.
✓ Branch 56 → 57 taken 1222 times.
1222 return query.value(0).toInt();
84 1228 }
85
86 175 int PayloadCacheLimits::cachedBodyCount(const QString &account,
87 const QString &folderPath) {
88
1/1
✓ Branch 2 → 3 taken 175 times.
175 const qint64 fid = m_deps.findFolderId(account, folderPath);
89
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 171 times.
175 if (fid <= 0)
90 4 return 0;
91
92
1/1
✓ Branch 5 → 6 taken 171 times.
171 QSqlQuery q(m_db);
93
1/1
✓ Branch 9 → 10 taken 171 times.
171 q.prepare(QStringLiteral(
94 "SELECT COUNT(*) FROM bodies WHERE header_id IN "
95 "(SELECT id FROM headers WHERE folder_id = :fid)"));
96
1/1
✓ Branch 17 → 18 taken 171 times.
342 q.bindValue(QStringLiteral(":fid"), fid);
97
5/8
✓ Branch 21 → 22 taken 171 times.
✓ Branch 22 → 23 taken 171 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 171 times.
✓ Branch 24 → 25 taken 171 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 171 times.
✗ Branch 27 → 33 not taken.
171 if (q.exec() && q.next())
98
2/2
✓ Branch 28 → 29 taken 171 times.
✓ Branch 29 → 30 taken 171 times.
171 return q.value(0).toInt();
99 ✗ return 0;
100 171 }
101
102 8 void PayloadCacheLimits::purgeFolderByPath(const QString &account,
103 const QString &folderPath) {
104 // S106-122: a path that is not cached has nothing to purge — looking it
105 // up must not create an empty folders row.
106 8 const qint64 fid = m_deps.findFolderId(account, folderPath);
107
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 4 times.
8 if (fid > 0)
108 4 purgeFolder(fid);
109 8 }
110
111 // T-286: Total cached disk usage (bodies + attachments) in bytes
112 11 qint64 PayloadCacheLimits::cachedDiskUsage(const QString &account,
113 const QString &folderPath) {
114
1/1
✓ Branch 2 → 3 taken 11 times.
11 const qint64 fid = m_deps.findFolderId(account, folderPath);
115
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 7 times.
11 if (fid <= 0)
116 4 return 0;
117
118
1/1
✓ Branch 5 → 6 taken 7 times.
7 QSqlQuery q(m_db);
119
1/1
✓ Branch 9 → 10 taken 7 times.
7 q.prepare(QStringLiteral(
120 // T-406/Bug 21: Use COALESCE per field — LENGTH(NULL) returns NULL.
121 // F-65 (SPRINT-104): CAST to BLOB so decoded text contributes its
122 // UTF-8 BYTES, matching the transactional payload accounting —
123 // LENGTH() on TEXT counts characters and understated Unicode
124 // storage (ä漢😀 reported 3 instead of 9).
125 "SELECT "
126 "COALESCE((SELECT SUM(COALESCE(length(CAST(b.text_plain AS BLOB)),0) "
127 "+ COALESCE(length(CAST(b.text_html AS BLOB)),0) "
128 "+ COALESCE(length(b.raw_body),0)) "
129 "FROM bodies b JOIN headers h ON b.header_id = h.id "
130 "WHERE h.folder_id = :body_fid), 0) "
131 "+ COALESCE((SELECT SUM(COALESCE(LENGTH(a.data),0)) "
132 "FROM attachments a JOIN headers h ON a.header_id = h.id "
133 "WHERE h.folder_id = :attachment_fid), 0)"));
134
1/1
✓ Branch 17 → 18 taken 7 times.
14 q.bindValue(QStringLiteral(":body_fid"), fid);
135
1/1
✓ Branch 26 → 27 taken 7 times.
14 q.bindValue(QStringLiteral(":attachment_fid"), fid);
136
5/8
✓ Branch 30 → 31 taken 7 times.
✓ Branch 31 → 32 taken 7 times.
✗ Branch 31 → 35 not taken.
✓ Branch 32 → 33 taken 7 times.
✓ Branch 33 → 34 taken 7 times.
✗ Branch 33 → 35 not taken.
✓ Branch 36 → 37 taken 7 times.
✗ Branch 36 → 42 not taken.
7 if (q.exec() && q.next())
137
2/2
✓ Branch 37 → 38 taken 7 times.
✓ Branch 38 → 39 taken 7 times.
7 return q.value(0).toLongLong();
138 ✗ return 0;
139 7 }
140
141 // T-286: Total server-side size (SUM of RFC822.SIZE from headers)
142 7 qint64 PayloadCacheLimits::totalServerSize(const QString &account,
143 const QString &folderPath) {
144
1/1
✓ Branch 2 → 3 taken 7 times.
7 const qint64 fid = m_deps.findFolderId(account, folderPath);
145
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 3 times.
7 if (fid <= 0)
146 4 return 0;
147
148
1/1
✓ Branch 5 → 6 taken 3 times.
3 QSqlQuery q(m_db);
149
1/1
✓ Branch 9 → 10 taken 3 times.
3 q.prepare(QStringLiteral(
150 "SELECT COALESCE(SUM(size), 0) FROM headers WHERE folder_id = :fid"));
151
1/1
✓ Branch 17 → 18 taken 3 times.
6 q.bindValue(QStringLiteral(":fid"), fid);
152
5/8
✓ Branch 21 → 22 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 33 not taken.
3 if (q.exec() && q.next())
153
2/2
✓ Branch 28 → 29 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
3 return q.value(0).toLongLong();
154 ✗ return 0;
155 3 }
156
157 // T-286: Average mail size (server-side)
158 7 qint64 PayloadCacheLimits::averageMailSize(const QString &account,
159 const QString &folderPath) {
160
1/1
✓ Branch 2 → 3 taken 7 times.
7 const qint64 fid = m_deps.findFolderId(account, folderPath);
161
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 3 times.
7 if (fid <= 0)
162 4 return 0;
163
164
1/1
✓ Branch 5 → 6 taken 3 times.
3 QSqlQuery q(m_db);
165
1/1
✓ Branch 9 → 10 taken 3 times.
3 q.prepare(QStringLiteral(
166 "SELECT COALESCE(AVG(size), 0) FROM headers WHERE folder_id = :fid"));
167
1/1
✓ Branch 17 → 18 taken 3 times.
6 q.bindValue(QStringLiteral(":fid"), fid);
168
5/8
✓ Branch 21 → 22 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 33 not taken.
3 if (q.exec() && q.next())
169
2/2
✓ Branch 28 → 29 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
3 return q.value(0).toLongLong();
170 ✗ return 0;
171 3 }
172
173 // T-286: Purge only body cache (keep headers). F-64 (SPRINT-104): the
174 // body-derived search text leaves with the bodies — every indexed message
175 // is reprojected to a header-only search document inside the same
176 // transaction (as budget eviction has done via indexHeaderById since
177 // Sprint 91), and the caller learns whether the clear succeeded.
178 10 bool PayloadCacheLimits::purgeBodyCache(const QString &account,
179 const QString &folderPath) {
180
1/1
✓ Branch 2 → 3 taken 10 times.
10 const qint64 fid = m_deps.findFolderId(account, folderPath);
181
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 6 times.
10 if (fid <= 0)
182 4 return true; // not cached: no bodies to clear (S106-122)
183
184
3/3
✓ Branch 5 → 6 taken 6 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 23 taken 5 times.
6 if (!beginWriteTransaction(m_db)) {
185
5/5
✓ Branch 7 → 8 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 21 → 9 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 qCWarning(lcCache) << "Failed to start body purge transaction:"
186
3/3
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 << m_db.lastError().text();
187 1 return false;
188 }
189
190
1/1
✓ Branch 23 → 24 taken 5 times.
5 QSqlQuery attachments(m_db);
191
1/1
✓ Branch 27 → 28 taken 5 times.
5 attachments.prepare(QStringLiteral(
192 "DELETE FROM attachments WHERE header_id IN "
193 "(SELECT id FROM headers WHERE folder_id = :fid)"));
194
1/1
✓ Branch 35 → 36 taken 5 times.
10 attachments.bindValue(QStringLiteral(":fid"), fid);
195
3/3
✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 59 taken 3 times.
5 if (!attachments.exec()) {
196
1/1
✓ Branch 41 → 42 taken 2 times.
2 m_db.rollback();
197
2/2
✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
2 m_lastError = attachments.lastError().text();
198
5/5
✓ Branch 47 → 48 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 57 → 49 taken 2 times.
✓ Branch 57 → 58 taken 2 times.
4 qCWarning(lcCache) << "Failed to purge attachment cache:"
199
1/1
✓ Branch 53 → 54 taken 2 times.
2 << m_lastError;
200 2 return false;
201 }
202
1/1
✓ Branch 59 → 60 taken 3 times.
3 const int attachmentsDeleted = attachments.numRowsAffected();
203
204
1/1
✓ Branch 60 → 61 taken 3 times.
3 QSqlQuery q(m_db);
205
1/1
✓ Branch 64 → 65 taken 3 times.
3 q.prepare(QStringLiteral(
206 "DELETE FROM bodies WHERE header_id IN "
207 "(SELECT id FROM headers WHERE folder_id = :fid)"));
208
1/1
✓ Branch 72 → 73 taken 3 times.
6 q.bindValue(QStringLiteral(":fid"), fid);
209
3/3
✓ Branch 76 → 77 taken 3 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 96 taken 2 times.
3 if (!q.exec()) {
210
1/1
✓ Branch 78 → 79 taken 1 time.
1 m_db.rollback();
211
2/2
✓ Branch 79 → 80 taken 1 time.
✓ Branch 80 → 81 taken 1 time.
1 m_lastError = q.lastError().text();
212 2 qCWarning(lcCache) << "Failed to purge body cache:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; SQL result remains measured.
213
1/1
✓ Branch 90 → 91 taken 1 time.
1 << m_lastError;
214 1 return false;
215 }
216
1/1
✓ Branch 96 → 97 taken 2 times.
2 const int bodiesDeleted = q.numRowsAffected();
217
218 // Reproject the folder's indexed messages to header-only documents; the
219 // projection reads the (now empty) body columns.
220
1/1
✓ Branch 97 → 98 taken 2 times.
2 QSqlQuery indexed(m_db);
221
1/1
✓ Branch 101 → 102 taken 2 times.
2 indexed.prepare(QStringLiteral(
222 "SELECT s.header_id FROM search_documents s "
223 "JOIN headers h ON h.id = s.header_id WHERE h.folder_id = :fid"));
224
1/1
✓ Branch 109 → 110 taken 2 times.
4 indexed.bindValue(QStringLiteral(":fid"), fid);
225
2/3
✓ Branch 113 → 114 taken 2 times.
✗ Branch 114 → 115 not taken.
✓ Branch 114 → 133 taken 2 times.
2 if (!indexed.exec()) {
226 ✗ m_db.rollback();
227 ✗ m_lastError = indexed.lastError().text();
228 ✗ qCWarning(lcCache) << "Failed to list indexed messages of the folder:"
229 ✗ << m_lastError;
230 ✗ return false;
231 }
232 2 QList<qint64> indexedIds;
233
3/3
✓ Branch 139 → 140 taken 4 times.
✓ Branch 140 → 134 taken 2 times.
✓ Branch 140 → 141 taken 2 times.
4 while (indexed.next())
234
3/3
✓ Branch 134 → 135 taken 2 times.
✓ Branch 135 → 136 taken 2 times.
✓ Branch 136 → 137 taken 2 times.
2 indexedIds.append(indexed.value(0).toLongLong());
235
1/1
✓ Branch 141 → 142 taken 2 times.
2 indexed.finish();
236
4/4
✓ Branch 142 → 143 taken 2 times.
✓ Branch 143 → 144 taken 2 times.
✓ Branch 164 → 145 taken 2 times.
✓ Branch 164 → 165 taken 2 times.
4 for (qint64 rowId : indexedIds) {
237
2/3
✓ Branch 146 → 147 taken 2 times.
✗ Branch 147 → 148 not taken.
✓ Branch 147 → 162 taken 2 times.
2 if (!m_deps.indexHeaderById(rowId)) {
238 ✗ m_db.rollback();
239 ✗ m_payloadCacheBytes = calculatePayloadCacheSize();
240 ✗ qCWarning(lcCache) << "Failed to reproject search documents while"
241 ✗ << "clearing the body cache";
242 ✗ return false;
243 }
244 }
245
246
2/3
✓ Branch 165 → 166 taken 2 times.
✗ Branch 166 → 167 not taken.
✓ Branch 166 → 185 taken 2 times.
2 if (!m_db.commit()) {
247 ✗ m_db.rollback();
248 ✗ m_lastError = m_db.lastError().text();
249 ✗ qCWarning(lcCache) << "Failed to commit body purge:"
250 ✗ << m_lastError;
251 ✗ return false;
252 }
253
1/1
✓ Branch 185 → 186 taken 2 times.
2 m_payloadCacheBytes = calculatePayloadCacheSize();
254
255
6/6
✓ Branch 186 → 187 taken 2 times.
✓ Branch 190 → 191 taken 2 times.
✓ Branch 191 → 192 taken 2 times.
✓ Branch 192 → 193 taken 2 times.
✓ Branch 204 → 188 taken 2 times.
✓ Branch 204 → 205 taken 2 times.
4 qCInfo(lcCache) << "Purged body cache for folder" << folderPath
256
3/3
✓ Branch 193 → 194 taken 2 times.
✓ Branch 194 → 195 taken 2 times.
✓ Branch 195 → 196 taken 2 times.
2 << "(" << bodiesDeleted << "bodies,"
257
2/2
✓ Branch 196 → 197 taken 2 times.
✓ Branch 197 → 198 taken 2 times.
2 << attachmentsDeleted << "attachments,"
258
2/2
✓ Branch 199 → 200 taken 2 times.
✓ Branch 200 → 201 taken 2 times.
2 << indexedIds.size() << "search documents reprojected)";
259 2 return true;
260 5 }
261
262 // S91.0.7: the single writer of the payload byte count outside the limit
263 // enforcement itself (open, body and attachment storage call it after their
264 // commit or rollback). Owned by the payload-limit store once extracted.
265 17477 void PayloadCacheLimits::refreshPayloadCacheBytes() {
266 17477 m_payloadCacheBytes = calculatePayloadCacheSize();
267 17477 }
268
269 34957 qint64 PayloadCacheLimits::calculatePayloadCacheSize() const {
270
1/1
✓ Branch 2 → 3 taken 34957 times.
34957 QSqlQuery q(m_db);
271
3/8
✓ Branch 14 → 15 taken 34957 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 16 → 18 taken 34953 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
69914 if (!q.exec(QStringLiteral(
272 "SELECT CAST(value AS INTEGER) FROM cache_meta "
273
4/5
✓ Branch 6 → 7 taken 34957 times.
✓ Branch 7 → 8 taken 34953 times.
✓ Branch 7 → 10 taken 4 times.
✓ Branch 12 → 13 taken 34957 times.
✗ Branch 12 → 14 not taken.
104867 "WHERE key='payload_cache_bytes'")) ||
274
2/3
✓ Branch 8 → 9 taken 34953 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 34953 times.
34953 !q.next()) {
275 4 return 0;
276 }
277
2/2
✓ Branch 18 → 19 taken 34953 times.
✓ Branch 19 → 20 taken 34953 times.
34953 return q.value(0).toLongLong();
278 34957 }
279
280 1641 bool PayloadCacheLimits::enforcePayloadCacheLimit(qint64 protectedHeaderId,
281 qint64 reserveBytes) {
282
7/7
✓ Branch 2 → 3 taken 1641 times.
✓ Branch 3 → 4 taken 1640 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1639 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 1639 times.
1641 if (!m_db.isOpen() || m_payloadCacheLimitBytes < 0)
283 2 return true;
284
2/3
✓ Branch 9 → 10 taken 1639 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 17 taken 1639 times.
1639 if (!beginWriteTransaction(m_db)) {
285 ✗ m_lastError = m_db.lastError().text();
286 ✗ return false;
287 }
288 3278 auto rollback = qScopeGuard([this]() {
289 ✗ m_db.rollback();
290 ✗ m_payloadCacheBytes = calculatePayloadCacheSize();
291 1639 });
292
2/3
✓ Branch 18 → 19 taken 1639 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 1639 times.
1639 if (!enforcePayloadCacheLimitInTransaction(protectedHeaderId, reserveBytes))
293 ✗ return false;
294
2/3
✓ Branch 21 → 22 taken 1639 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 29 taken 1639 times.
1639 if (!m_db.commit()) {
295 ✗ m_lastError = m_db.lastError().text();
296 ✗ return false;
297 }
298 1639 rollback.dismiss();
299 1639 return true;
300 1639 }
301
302 17465 bool PayloadCacheLimits::enforcePayloadCacheLimitInTransaction(qint64 protectedHeaderId,
303 qint64 reserveBytes) {
304
6/7
✓ Branch 2 → 3 taken 17465 times.
✓ Branch 3 → 4 taken 17465 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 17463 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 17463 times.
17465 if (!m_db.isOpen() || m_payloadCacheLimitBytes < 0)
305 2 return true;
306 17463 reserveBytes = qMax<qint64>(0, reserveBytes);
307
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 18 taken 17459 times.
17463 if (reserveBytes > m_payloadCacheLimitBytes) {
308 4 m_lastError = QStringLiteral("Payload reservation exceeds the cache budget");
309 4 return false;
310 }
311
1/1
✓ Branch 18 → 19 taken 17459 times.
17459 m_payloadCacheBytes = calculatePayloadCacheSize();
312
2/2
✓ Branch 19 → 20 taken 17448 times.
✓ Branch 19 → 21 taken 11 times.
17459 if (m_payloadCacheBytes <= m_payloadCacheLimitBytes - reserveBytes)
313 17448 return true;
314
315 struct Candidate {
316 qint64 headerId;
317 qint64 bytes;
318 };
319 11 QList<Candidate> candidates;
320 11 qint64 bytesToFree =
321 11 m_payloadCacheBytes + reserveBytes - m_payloadCacheLimitBytes;
322
323
1/1
✓ Branch 21 → 22 taken 11 times.
11 QSqlQuery select(m_db);
324
1/1
✓ Branch 25 → 26 taken 11 times.
11 select.prepare(QStringLiteral(
325 "SELECT h.id, "
326 "COALESCE(length(CAST(b.text_plain AS BLOB)), 0) + "
327 "COALESCE(length(CAST(b.text_html AS BLOB)), 0) + "
328 "COALESCE(length(b.raw_body), 0) + "
329 "COALESCE((SELECT SUM(COALESCE(length(a.data), 0)) "
330 " FROM attachments a WHERE a.header_id = h.id), 0) AS bytes "
331 "FROM headers h "
332 "LEFT JOIN bodies b ON b.header_id = h.id "
333 "WHERE b.header_id IS NOT NULL "
334 " OR EXISTS (SELECT 1 FROM attachments a "
335 " WHERE a.header_id = h.id AND a.data IS NOT NULL) "
336 "ORDER BY CASE WHEN h.id = :protected THEN 1 ELSE 0 END, "
337 " COALESCE(b.fetched_at, 0), h.id"));
338
1/1
✓ Branch 33 → 34 taken 11 times.
22 select.bindValue(QStringLiteral(":protected"), protectedHeaderId);
339
2/3
✓ Branch 37 → 38 taken 11 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 60 taken 11 times.
11 if (!select.exec()) {
340 ✗ m_lastError = select.lastError().text();
341 ✗ qCWarning(lcCache) << "Failed to select payload cache eviction candidates:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; query result remains measured.
342 ✗ << select.lastError().text();
343 ✗ return false;
344 }
345
346 11 qint64 selectedBytes = 0;
347
7/7
✓ Branch 72 → 73 taken 26 times.
✓ Branch 73 → 74 taken 16 times.
✓ Branch 73 → 76 taken 10 times.
✓ Branch 74 → 75 taken 15 times.
✓ Branch 74 → 76 taken 1 time.
✓ Branch 77 → 61 taken 15 times.
✓ Branch 77 → 78 taken 11 times.
26 while (select.next() && selectedBytes < bytesToFree) {
348
2/2
✓ Branch 61 → 62 taken 15 times.
✓ Branch 62 → 63 taken 15 times.
15 const qint64 headerId = select.value(0).toLongLong();
349
2/2
✓ Branch 64 → 65 taken 15 times.
✓ Branch 65 → 66 taken 15 times.
15 const qint64 bytes = select.value(1).toLongLong();
350
3/4
✓ Branch 67 → 68 taken 13 times.
✓ Branch 67 → 69 taken 2 times.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 70 taken 13 times.
15 if (headerId == protectedHeaderId || bytes <= 0)
351 2 continue;
352
1/1
✓ Branch 70 → 71 taken 13 times.
13 candidates.append({headerId, bytes});
353 13 selectedBytes += bytes;
354 }
355
1/1
✓ Branch 78 → 79 taken 11 times.
11 select.finish();
356
357
5/6
✓ Branch 80 → 81 taken 9 times.
✓ Branch 80 → 82 taken 2 times.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 83 taken 9 times.
✓ Branch 84 → 85 taken 2 times.
✓ Branch 84 → 86 taken 9 times.
11 if (candidates.isEmpty() || selectedBytes < bytesToFree)
358 2 return false;
359
360
1/1
✓ Branch 86 → 87 taken 9 times.
9 QSqlQuery indexed(m_db);
361
1/1
✓ Branch 90 → 91 taken 9 times.
9 indexed.prepare(
362 18 QStringLiteral("SELECT 1 FROM mail_fts WHERE rowid = :rowid LIMIT 1"));
363
1/1
✓ Branch 93 → 94 taken 9 times.
9 QSqlQuery deleteAttachments(m_db);
364
1/1
✓ Branch 97 → 98 taken 9 times.
9 deleteAttachments.prepare(
365 18 QStringLiteral("DELETE FROM attachments WHERE header_id = :headerId"));
366
1/1
✓ Branch 100 → 101 taken 9 times.
9 QSqlQuery deleteBody(m_db);
367
1/1
✓ Branch 104 → 105 taken 9 times.
9 deleteBody.prepare(
368 18 QStringLiteral("DELETE FROM bodies WHERE header_id = :headerId"));
369
370
4/4
✓ Branch 107 → 108 taken 9 times.
✓ Branch 108 → 109 taken 9 times.
✓ Branch 172 → 110 taken 13 times.
✓ Branch 172 → 173 taken 9 times.
22 for (const auto &candidate : candidates) {
371
1/1
✓ Branch 116 → 117 taken 13 times.
26 indexed.bindValue(QStringLiteral(":rowid"), candidate.headerId);
372
2/3
✓ Branch 120 → 121 taken 13 times.
✗ Branch 121 → 122 not taken.
✓ Branch 121 → 128 taken 13 times.
13 if (!indexed.exec()) {
373 ✗ m_lastError = indexed.lastError().text();
374 ✗ return false;
375 }
376
1/1
✓ Branch 128 → 129 taken 13 times.
13 const bool wasIndexed = indexed.next();
377
1/1
✓ Branch 129 → 130 taken 13 times.
13 indexed.finish();
378
379
1/1
✓ Branch 135 → 136 taken 13 times.
13 deleteAttachments.bindValue(QStringLiteral(":headerId"),
380 13 candidate.headerId);
381
1/1
✓ Branch 144 → 145 taken 13 times.
26 deleteBody.bindValue(QStringLiteral(":headerId"), candidate.headerId);
382
6/10
✓ Branch 148 → 149 taken 13 times.
✓ Branch 149 → 150 taken 13 times.
✗ Branch 149 → 155 not taken.
✓ Branch 150 → 151 taken 13 times.
✓ Branch 151 → 152 taken 13 times.
✗ Branch 151 → 155 not taken.
✓ Branch 152 → 153 taken 13 times.
✗ Branch 152 → 156 not taken.
✗ Branch 157 → 158 not taken.
✓ Branch 157 → 170 taken 13 times.
26 if (!deleteAttachments.exec() || !deleteBody.exec() ||
383
2/3
✓ Branch 153 → 154 taken 13 times.
✗ Branch 154 → 155 not taken.
✓ Branch 154 → 156 taken 13 times.
13 (wasIndexed && !m_deps.indexHeaderById(candidate.headerId))) {
384 ✗ m_payloadCacheBytes = calculatePayloadCacheSize();
385 ✗ qCWarning(lcCache) << "Failed to evict cached mail payload"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; eviction result remains measured.
386 ✗ return false;
387 }
388 }
389
390
1/1
✓ Branch 173 → 175 taken 9 times.
9 m_payloadCacheBytes = calculatePayloadCacheSize();
391
6/6
✓ Branch 175 → 176 taken 9 times.
✓ Branch 179 → 180 taken 9 times.
✓ Branch 180 → 181 taken 9 times.
✓ Branch 182 → 183 taken 9 times.
✓ Branch 188 → 177 taken 9 times.
✓ Branch 188 → 189 taken 9 times.
18 qCInfo(lcCache) << "Evicted" << candidates.size()
392
1/1
✓ Branch 183 → 184 taken 9 times.
9 << "cached mail payloads; remaining bytes:"
393
1/1
✓ Branch 184 → 185 taken 9 times.
9 << m_payloadCacheBytes;
394 9 return m_payloadCacheBytes <= m_payloadCacheLimitBytes - reserveBytes;
395 11 }
396