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: 95.9% 118 / 0 / 123
Functions: 100.0% 6 / 0 / 6
Branches: 94.8% 147 / 18 / 173

data/AttachmentStore.cpp
Line Branch Exec Source
1 #include "data/AttachmentStore.h"
2
3 #include "data/MailCacheSql.h"
4 #include <QLoggingCategory>
5 #include <QScopeGuard>
6 #include <QSqlError>
7 #include <QSqlQuery>
8 #include <QVariant>
9 #include <limits>
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 AttachmentStore::AttachmentStore(QSqlDatabase &db, QString &lastError, Deps deps)
17 5035 : m_db(db), m_lastError(lastError), m_deps(std::move(deps)) {}
18
19 49 void AttachmentStore::storeAttachments(qint64 folderId, qint64 uid,
20 const QList<Attachment> &attachments,
21 const QList<QByteArray> &blobs) {
22 49 m_lastError.clear();
23
3/3
✓ Branch 3 → 4 taken 49 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 11 taken 46 times.
49 if (!beginWriteTransaction(m_db)) {
24
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
3 m_lastError = m_db.lastError().text();
25 11 return;
26 }
27 100 auto rollback = qScopeGuard([this]() {
28 8 m_db.rollback();
29 8 m_deps.refreshPayloadCacheBytes();
30 46 });
31
1/1
✓ Branch 12 → 13 taken 46 times.
46 qint64 hid = m_deps.headerRowId(folderId, uid);
32
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 26 taken 45 times.
46 if (hid < 0) {
33
5/5
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 24 → 16 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
2 qCWarning(lcCache) << "Cannot store attachments: header not found for UID"
34
1/1
✓ Branch 20 → 21 taken 1 time.
1 << uid;
35 1 return;
36 }
37
38
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 45 taken 44 times.
45 if (attachments.size() != blobs.size()) {
39 2 qCWarning(lcCache) << "Attachment/BLOB count mismatch:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; invariant check remains measured.
40
3/3
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
1 << attachments.size() << "vs" << blobs.size();
41 1 return;
42 }
43
44 44 qint64 previousBytes = 0;
45
1/1
✓ Branch 45 → 46 taken 44 times.
44 QSqlQuery previous(m_db);
46
1/1
✓ Branch 49 → 50 taken 44 times.
44 previous.prepare(QStringLiteral(
47 "SELECT COALESCE(SUM(length(data)), 0) "
48 "FROM attachments WHERE header_id = :hid"));
49
1/1
✓ Branch 57 → 58 taken 44 times.
88 previous.bindValue(QStringLiteral(":hid"), hid);
50
7/8
✓ Branch 61 → 62 taken 44 times.
✓ Branch 62 → 63 taken 43 times.
✓ Branch 62 → 65 taken 1 time.
✓ Branch 63 → 64 taken 43 times.
✗ Branch 64 → 65 not taken.
✓ Branch 64 → 66 taken 43 times.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 74 taken 43 times.
44 if (!previous.exec() || !previous.next()) {
51
2/2
✓ Branch 68 → 69 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
1 m_lastError = previous.lastError().text();
52 1 return;
53 }
54
2/2
✓ Branch 74 → 75 taken 43 times.
✓ Branch 75 → 76 taken 43 times.
43 previousBytes = previous.value(0).toLongLong();
55
1/1
✓ Branch 77 → 78 taken 43 times.
43 previous.finish();
56
57 43 qint64 currentBytes = 0;
58
2/2
✓ Branch 99 → 80 taken 50 times.
✓ Branch 99 → 100 taken 43 times.
93 for (const auto &blob : blobs) {
59
1/2
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 96 taken 50 times.
50 if (blob.size() > std::numeric_limits<qint64>::max() - currentBytes) {
60 ✗ qCWarning(lcCache) << "Attachment payload size overflow for UID" << uid; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; overflow guard remains measured.
61 ✗ return;
62 }
63 50 currentBytes += blob.size();
64 }
65 43 const qint64 additionalBytes = qMax<qint64>(0, currentBytes - previousBytes);
66
3/3
✓ Branch 101 → 102 taken 43 times.
✓ Branch 102 → 103 taken 2 times.
✓ Branch 102 → 117 taken 41 times.
43 if (!m_deps.enforcePayloadCacheLimitInTransaction(hid, additionalBytes)) {
67
4/4
✓ Branch 103 → 104 taken 2 times.
✓ Branch 107 → 108 taken 2 times.
✓ Branch 115 → 105 taken 2 times.
✓ Branch 115 → 116 taken 2 times.
4 qCWarning(lcCache)
68
1/1
✓ Branch 108 → 109 taken 2 times.
2 << "Refusing to store attachments above payload cache budget for UID"
69
3/3
✓ Branch 109 → 110 taken 2 times.
✓ Branch 110 → 111 taken 2 times.
✓ Branch 111 → 112 taken 2 times.
2 << uid << "bytes" << currentBytes;
70 2 return;
71 }
72
73
3/3
✓ Branch 117 → 118 taken 41 times.
✓ Branch 118 → 119 taken 3 times.
✓ Branch 118 → 120 taken 38 times.
41 if (!replaceAttachmentsInTransaction(hid, attachments, blobs)) {
74 3 return;
75 }
76
2/3
✓ Branch 120 → 121 taken 38 times.
✗ Branch 121 → 122 not taken.
✓ Branch 121 → 128 taken 38 times.
38 if (!m_db.commit()) {
77 ✗ m_lastError = m_db.lastError().text();
78 ✗ return;
79 }
80 38 rollback.dismiss();
81
1/1
✓ Branch 129 → 130 taken 38 times.
38 m_deps.refreshPayloadCacheBytes();
82 52 }
83
84 439 bool AttachmentStore::replaceAttachmentsInTransaction(
85 qint64 hid, const QList<Attachment> &attachments,
86 const QList<QByteArray> &blobs) {
87
1/1
✓ Branch 2 → 3 taken 439 times.
439 QSqlQuery deleteExisting(m_db);
88
1/1
✓ Branch 6 → 7 taken 439 times.
439 deleteExisting.prepare(QStringLiteral(
89 "DELETE FROM attachments WHERE header_id = :hid"));
90
1/1
✓ Branch 14 → 15 taken 439 times.
878 deleteExisting.bindValue(QStringLiteral(":hid"), hid);
91
3/3
✓ Branch 18 → 19 taken 439 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 43 taken 438 times.
439 if (!deleteExisting.exec()) {
92
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
1 m_lastError = deleteExisting.lastError().text();
93
6/6
✓ Branch 25 → 26 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 41 → 27 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
2 qCWarning(lcCache) << "Failed to replace attachments for UID" << hid
94
4/4
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
1 << ":" << deleteExisting.lastError().text();
95 1 return false;
96 }
97
98
1/1
✓ Branch 43 → 44 taken 438 times.
438 QSqlQuery q(m_db);
99
1/1
✓ Branch 47 → 48 taken 438 times.
438 q.prepare(QStringLiteral(
100 "INSERT INTO attachments (header_id, filename, content_type, size, "
101 "content_id, presentation, data) "
102 "VALUES (:hid, :fn, :ct, :sz, :cid, :presentation, :data)"));
103
104
2/2
✓ Branch 143 → 51 taken 1198 times.
✓ Branch 143 → 144 taken 434 times.
1632 for (int i = 0; i < attachments.size(); ++i) {
105 1198 const auto &att = attachments[i];
106
1/1
✓ Branch 57 → 58 taken 1198 times.
2396 q.bindValue(QStringLiteral(":hid"), hid);
107
1/1
✓ Branch 66 → 67 taken 1198 times.
2396 q.bindValue(QStringLiteral(":fn"), att.filename);
108
1/1
✓ Branch 75 → 76 taken 1198 times.
2396 q.bindValue(QStringLiteral(":ct"), att.contentType);
109
1/1
✓ Branch 84 → 85 taken 1198 times.
2396 q.bindValue(QStringLiteral(":sz"), att.size);
110
1/1
✓ Branch 93 → 94 taken 1198 times.
2396 q.bindValue(QStringLiteral(":cid"), att.contentId);
111
1/1
✓ Branch 102 → 103 taken 1198 times.
1198 q.bindValue(QStringLiteral(":presentation"),
112 1198 static_cast<int>(att.presentation));
113
1/1
✓ Branch 112 → 113 taken 1198 times.
2396 q.bindValue(QStringLiteral(":data"), blobs[i]);
114
115
3/3
✓ Branch 116 → 117 taken 1198 times.
✓ Branch 117 → 118 taken 4 times.
✓ Branch 117 → 141 taken 1194 times.
1198 if (!q.exec()) {
116
2/2
✓ Branch 118 → 119 taken 4 times.
✓ Branch 119 → 120 taken 4 times.
4 m_lastError = q.lastError().text();
117 8 qCWarning(lcCache) << "Failed to insert attachment" << att.filename << ":" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; insert result remains measured.
118
3/3
✓ Branch 131 → 132 taken 4 times.
✓ Branch 132 → 133 taken 4 times.
✓ Branch 133 → 134 taken 4 times.
4 << q.lastError().text();
119 4 return false;
120 }
121 }
122
123
1/1
✓ Branch 144 → 145 taken 434 times.
434 QSqlQuery uq(m_db);
124
1/1
✓ Branch 148 → 149 taken 434 times.
434 uq.prepare(QStringLiteral(
125 "UPDATE headers SET has_attachments = :has WHERE id = :hid"));
126 434 bool hasVisibleAttachment = false;
127
2/2
✓ Branch 159 → 153 taken 1189 times.
✓ Branch 159 → 160 taken 298 times.
1487 for (const auto &attachment : attachments) {
128
2/2
✓ Branch 154 → 155 taken 1057 times.
✓ Branch 154 → 156 taken 132 times.
1189 if (attachment.presentation == AttachmentPresentation::Regular ||
129
2/2
✓ Branch 155 → 156 taken 4 times.
✓ Branch 155 → 157 taken 1053 times.
1057 attachment.presentation ==
130 AttachmentPresentation::ExplicitAttachmentWithContentId) {
131 136 hasVisibleAttachment = true;
132 136 break;
133 }
134 }
135
3/3
✓ Branch 161 → 162 taken 136 times.
✓ Branch 161 → 163 taken 298 times.
✓ Branch 168 → 169 taken 434 times.
868 uq.bindValue(QStringLiteral(":has"), hasVisibleAttachment ? 1 : 0);
136
1/1
✓ Branch 177 → 178 taken 434 times.
868 uq.bindValue(QStringLiteral(":hid"), hid);
137
3/3
✓ Branch 181 → 182 taken 434 times.
✓ Branch 182 → 183 taken 2 times.
✓ Branch 182 → 189 taken 432 times.
434 if (!uq.exec()) {
138
2/2
✓ Branch 183 → 184 taken 2 times.
✓ Branch 184 → 185 taken 2 times.
2 m_lastError = uq.lastError().text();
139 2 return false;
140 }
141 432 return true;
142 439 }
143
144 132 QList<Attachment> AttachmentStore::attachments(qint64 folderId, qint64 uid) const {
145 132 QList<Attachment> result;
146
1/1
✓ Branch 2 → 3 taken 132 times.
132 qint64 hid = m_deps.headerRowId(folderId, uid);
147
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 130 times.
132 if (hid < 0)
148 2 return result;
149
150
1/1
✓ Branch 5 → 6 taken 130 times.
130 QSqlQuery q(m_db);
151 // Intentionally omit 'data' column for lazy loading
152
1/1
✓ Branch 9 → 10 taken 130 times.
130 q.prepare(
153 260 QStringLiteral("SELECT id, filename, content_type, size, content_id, "
154 "presentation "
155 "FROM attachments WHERE header_id = :hid ORDER BY id"));
156
1/1
✓ Branch 17 → 18 taken 130 times.
260 q.bindValue(QStringLiteral(":hid"), hid);
157
158
2/3
✓ Branch 21 → 22 taken 130 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 130 times.
130 if (!q.exec())
159 ✗ return result;
160
161
3/3
✓ Branch 55 → 56 taken 236 times.
✓ Branch 56 → 25 taken 106 times.
✓ Branch 56 → 57 taken 130 times.
236 while (q.next()) {
162 106 Attachment att;
163
2/2
✓ Branch 25 → 26 taken 106 times.
✓ Branch 26 → 27 taken 106 times.
106 att.id = q.value(0).toLongLong();
164
2/2
✓ Branch 28 → 29 taken 106 times.
✓ Branch 29 → 30 taken 106 times.
106 att.filename = q.value(1).toString();
165
2/2
✓ Branch 33 → 34 taken 106 times.
✓ Branch 34 → 35 taken 106 times.
106 att.contentType = q.value(2).toString();
166
2/2
✓ Branch 38 → 39 taken 106 times.
✓ Branch 39 → 40 taken 106 times.
106 att.size = q.value(3).toLongLong();
167
2/2
✓ Branch 41 → 42 taken 106 times.
✓ Branch 42 → 43 taken 106 times.
106 att.contentId = q.value(4).toString();
168
2/2
✓ Branch 46 → 47 taken 106 times.
✓ Branch 47 → 48 taken 106 times.
106 const int presentation = q.value(5).toInt();
169
2/4
✓ Branch 49 → 50 taken 106 times.
✗ Branch 49 → 52 not taken.
✓ Branch 50 → 51 taken 106 times.
✗ Branch 50 → 52 not taken.
106 if (presentation >= static_cast<int>(AttachmentPresentation::Regular) &&
170 presentation <=
171 static_cast<int>(AttachmentPresentation::RejectedInline)) {
172 106 att.presentation = static_cast<AttachmentPresentation>(presentation);
173 }
174
1/1
✓ Branch 52 → 53 taken 106 times.
106 result.append(att);
175 106 }
176
177 130 return result;
178 130 }
179
180 30 QByteArray AttachmentStore::attachmentData(qint64 attachmentId) const {
181
1/1
✓ Branch 2 → 3 taken 30 times.
30 QSqlQuery q(m_db);
182
1/1
✓ Branch 6 → 7 taken 30 times.
30 q.prepare(QStringLiteral("SELECT data FROM attachments WHERE id = :id"));
183
1/1
✓ Branch 14 → 15 taken 30 times.
60 q.bindValue(QStringLiteral(":id"), attachmentId);
184
185
8/8
✓ Branch 18 → 19 taken 30 times.
✓ Branch 19 → 20 taken 28 times.
✓ Branch 19 → 23 taken 2 times.
✓ Branch 20 → 21 taken 28 times.
✓ Branch 21 → 22 taken 27 times.
✓ Branch 21 → 23 taken 1 time.
✓ Branch 24 → 25 taken 27 times.
✓ Branch 24 → 30 taken 3 times.
30 if (q.exec() && q.next()) {
186
2/2
✓ Branch 25 → 26 taken 27 times.
✓ Branch 26 → 27 taken 27 times.
27 return q.value(0).toByteArray();
187 }
188 3 return {};
189 30 }
190