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: 89.4% 110 / 0 / 123
Functions: 100.0% 8 / 0 / 8
Branches: 61.0% 169 / 0 / 277

data/MailAddressHistory.cpp
Line Branch Exec Source
1 #include "MailAddressHistory.h"
2
3 #include "MailAddressIndex.h"
4 #include "Models.h"
5
6 #include <QLoggingCategory>
7 #include <QHash>
8 #include <QSet>
9 #include <algorithm>
10 #include <QSqlError>
11 #include <QSqlQuery>
12 #include <QVariant>
13
14
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 3 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
4 Q_LOGGING_CATEGORY(lcAddressIndex, "mailjd.cache.addresses", QtWarningMsg)
15
16 namespace MailAddressHistory {
17 namespace {
18 // The backfill watermark; nullopt = no backfill needed (created at v26 or done).
19 154 std::optional<qint64> watermark(const QSqlDatabase &db) {
20
1/1
✓ Branch 2 → 3 taken 154 times.
154 QSqlQuery query(db);
21
9/16
✓ Branch 6 → 7 taken 154 times.
✓ Branch 7 → 8 taken 154 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 154 times.
✓ Branch 9 → 10 taken 149 times.
✓ Branch 9 → 11 taken 5 times.
✓ Branch 12 → 13 taken 154 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 154 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 149 times.
✓ Branch 16 → 20 taken 5 times.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
308 if (!query.exec(QStringLiteral("SELECT value FROM cache_meta WHERE key='address_index_watermark'")) || !query.next())
22 149 return std::nullopt;
23
2/2
✓ Branch 20 → 21 taken 5 times.
✓ Branch 21 → 22 taken 5 times.
5 return query.value(0).toLongLong();
24 154 }
25
26 1 qint64 newestHeader(const QSqlDatabase &db) {
27
1/1
✓ Branch 2 → 3 taken 1 time.
1 QSqlQuery query(db);
28
3/9
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 13 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
3 return query.exec(QStringLiteral("SELECT coalesce(max(id),0) FROM headers")) && query.next()
29
6/11
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 13 not taken.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 17 not taken.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 19 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
4 ? query.value(0).toLongLong() : 0;
30 1 }
31 } // namespace
32
33 143 bool complete(const QSqlDatabase &db) {
34
1/1
✓ Branch 2 → 3 taken 143 times.
143 const std::optional<qint64> mark = watermark(db);
35
4/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 142 times.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
286 return !mark || *mark >= newestHeader(db);
36 }
37
38 143 std::optional<QList<Mail>> read(const QSqlDatabase &db, const QStringList &addresses, int limit) {
39
3/3
✓ Branch 2 → 3 taken 143 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 142 times.
143 if (!complete(db)) return std::nullopt;
40 142 QStringList keys;
41
2/2
✓ Branch 20 → 7 taken 158 times.
✓ Branch 20 → 21 taken 142 times.
300 for (const QString &address : addresses) {
42
1/1
✓ Branch 8 → 9 taken 158 times.
158 const QString key = MailAddressIndex::normalize(address);
43
4/7
✓ Branch 10 → 11 taken 158 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 158 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 158 times.
✗ Branch 15 → 17 not taken.
✓ Branch 16 → 17 taken 158 times.
158 if (!key.isEmpty() && !keys.contains(key)) keys << key;
44 158 }
45 142 QList<Mail> result;
46
3/6
✓ Branch 22 → 23 taken 142 times.
✗ Branch 22 → 24 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 142 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 142 times.
142 if (keys.isEmpty() || limit <= 0) return result;
47 // Per address the newest rows straight from (address, date DESC) — a
48 // header has at most three rows per address (From, To, Cc), so 3 × limit
49 // rows per address always contain the overall newest `limit` headers.
50 struct Candidate {
51 qint64 date = 0;
52 qint64 id = 0;
53 };
54 142 QHash<qint64, Candidate> candidates;
55
1/1
✓ Branch 28 → 29 taken 142 times.
142 QSqlQuery top(db);
56
1/1
✓ Branch 32 → 33 taken 142 times.
142 top.prepare(QStringLiteral(
57 "SELECT header_id, coalesce(date, 0) FROM message_addresses WHERE address = ? ORDER BY date DESC LIMIT ?"));
58
2/2
✓ Branch 82 → 38 taken 158 times.
✓ Branch 82 → 83 taken 142 times.
300 for (const QString &key : std::as_const(keys)) {
59
1/1
✓ Branch 41 → 42 taken 158 times.
158 top.addBindValue(key);
60
1/1
✓ Branch 45 → 46 taken 158 times.
158 top.addBindValue(limit * 3);
61
2/3
✓ Branch 47 → 48 taken 158 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 66 taken 158 times.
158 if (!top.exec()) {
62 ✗ qCWarning(lcAddressIndex) << "History read failed:" << top.lastError().text();
63 ✗ return std::nullopt;
64 }
65
10/10
✓ Branch 67 → 68 taken 778 times.
✓ Branch 68 → 69 taken 778 times.
✓ Branch 69 → 70 taken 778 times.
✓ Branch 70 → 71 taken 778 times.
✓ Branch 71 → 72 taken 778 times.
✓ Branch 72 → 73 taken 778 times.
✓ Branch 73 → 74 taken 778 times.
✓ Branch 78 → 79 taken 936 times.
✓ Branch 79 → 67 taken 778 times.
✓ Branch 79 → 80 taken 158 times.
936 while (top.next()) candidates.insert(top.value(0).toLongLong(), {top.value(1).toLongLong(), top.value(0).toLongLong()});
66 }
67
1/1
✓ Branch 83 → 84 taken 142 times.
142 QList<Candidate> newest = candidates.values();
68
3/3
✓ Branch 84 → 85 taken 142 times.
✓ Branch 85 → 86 taken 142 times.
✓ Branch 86 → 87 taken 142 times.
142 std::sort(newest.begin(), newest.end(), [](const Candidate &a, const Candidate &b) {
69
1/2
✓ Branch 2 → 3 taken 6247 times.
✗ Branch 2 → 4 not taken.
6247 return a.date != b.date ? a.date > b.date : a.id > b.id;
70 });
71
1/1
✓ Branch 87 → 88 taken 142 times.
142 newest = newest.mid(0, limit);
72
2/2
✓ Branch 91 → 92 taken 117 times.
✓ Branch 91 → 93 taken 25 times.
142 if (newest.isEmpty()) return result;
73 25 QStringList marks;
74
3/3
✓ Branch 97 → 98 taken 189 times.
✓ Branch 102 → 94 taken 189 times.
✓ Branch 102 → 103 taken 25 times.
214 for (int i = 0; i < newest.size(); ++i) marks << QStringLiteral("?");
75
1/1
✓ Branch 103 → 104 taken 25 times.
25 QSqlQuery query(db);
76
1/1
✓ Branch 112 → 113 taken 25 times.
75 query.prepare(QStringLiteral(
77 "SELECT h.id, h.folder_id, h.uid, h.subject, h.from_addr, h.date, h.message_id, f.path "
78 "FROM headers h JOIN folders f ON f.id = h.folder_id WHERE h.id IN (%1)")
79
2/2
✓ Branch 110 → 111 taken 25 times.
✓ Branch 111 → 112 taken 25 times.
50 .arg(marks.join(QLatin1Char(','))));
80
3/3
✓ Branch 123 → 124 taken 189 times.
✓ Branch 127 → 120 taken 189 times.
✓ Branch 127 → 128 taken 25 times.
214 for (const Candidate &candidate : std::as_const(newest)) query.addBindValue(candidate.id);
81
2/3
✓ Branch 128 → 129 taken 25 times.
✗ Branch 129 → 130 not taken.
✓ Branch 129 → 146 taken 25 times.
25 if (!query.exec()) {
82 ✗ qCWarning(lcAddressIndex) << "History read failed:" << query.lastError().text();
83 ✗ return std::nullopt;
84 }
85 25 QHash<qint64, Mail> byId;
86
3/3
✓ Branch 196 → 197 taken 214 times.
✓ Branch 197 → 147 taken 189 times.
✓ Branch 197 → 198 taken 25 times.
214 while (query.next()) {
87 189 Mail mail;
88
2/2
✓ Branch 148 → 149 taken 189 times.
✓ Branch 149 → 150 taken 189 times.
189 mail.headerId = query.value(0).toLongLong();
89
2/2
✓ Branch 151 → 152 taken 189 times.
✓ Branch 152 → 153 taken 189 times.
189 mail.folderId = query.value(1).toLongLong();
90
2/2
✓ Branch 154 → 155 taken 189 times.
✓ Branch 155 → 156 taken 189 times.
189 mail.uid = query.value(2).toLongLong();
91
2/2
✓ Branch 157 → 158 taken 189 times.
✓ Branch 158 → 159 taken 189 times.
189 mail.subject = query.value(3).toString();
92
2/2
✓ Branch 162 → 163 taken 189 times.
✓ Branch 163 → 164 taken 189 times.
189 mail.from = query.value(4).toString();
93
6/7
✓ Branch 167 → 168 taken 189 times.
✓ Branch 168 → 169 taken 189 times.
✓ Branch 170 → 171 taken 189 times.
✗ Branch 170 → 178 not taken.
✓ Branch 171 → 172 taken 189 times.
✓ Branch 172 → 173 taken 189 times.
✓ Branch 173 → 174 taken 189 times.
189 if (!query.value(5).isNull()) mail.date = QDateTime::fromSecsSinceEpoch(query.value(5).toLongLong());
94
2/2
✓ Branch 178 → 179 taken 189 times.
✓ Branch 179 → 180 taken 189 times.
189 mail.messageId = query.value(6).toString();
95
2/2
✓ Branch 183 → 184 taken 189 times.
✓ Branch 184 → 185 taken 189 times.
189 mail.folderPath = query.value(7).toString();
96 // Sent = none of the person's addresses wrote it.
97
1/1
✓ Branch 188 → 189 taken 189 times.
189 const QStringList senders = MailAddressIndex::parse(mail.from);
98
1/1
✓ Branch 191 → 192 taken 189 times.
378 mail.sent = std::none_of(senders.cbegin(), senders.cend(), [&keys](const QString &s) { return keys.contains(s); });
99
1/1
✓ Branch 192 → 193 taken 189 times.
189 byId.insert(mail.headerId, mail);
100 189 }
101
2/2
✓ Branch 210 → 201 taken 189 times.
✓ Branch 210 → 211 taken 25 times.
214 for (const Candidate &candidate : std::as_const(newest))
102
2/3
✓ Branch 203 → 204 taken 189 times.
✗ Branch 203 → 208 not taken.
✓ Branch 205 → 206 taken 189 times.
189 if (byId.contains(candidate.id)) result.append(byId.value(candidate.id));
103 25 return result;
104 142 }
105
106 11 Backfill backfill(const QSqlDatabase &db, int chunk) {
107 11 Backfill result;
108
1/1
✓ Branch 2 → 3 taken 11 times.
11 QSqlDatabase connection = db;
109
1/1
✓ Branch 3 → 4 taken 11 times.
11 const std::optional<qint64> start = watermark(db);
110
2/2
✓ Branch 5 → 6 taken 7 times.
✓ Branch 5 → 7 taken 4 times.
11 if (!start) {
111 7 result.complete = true;
112 7 return result;
113 }
114 4 const qint64 from = *start;
115
1/1
✓ Branch 8 → 9 taken 4 times.
4 QSqlQuery rows(db);
116
1/1
✓ Branch 12 → 13 taken 4 times.
4 rows.prepare(QStringLiteral(
117 "SELECT id, from_addr, to_addr, cc_addr, date FROM headers WHERE id > ? ORDER BY id LIMIT ?"));
118
1/1
✓ Branch 17 → 18 taken 4 times.
4 rows.addBindValue(from);
119
1/1
✓ Branch 21 → 22 taken 4 times.
4 rows.addBindValue(chunk);
120
2/3
✓ Branch 23 → 24 taken 4 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 31 taken 4 times.
4 if (!connection.transaction()) {
121 ✗ result.error = connection.lastError().text();
122 ✗ return result;
123 }
124
3/3
✓ Branch 31 → 32 taken 4 times.
✓ Branch 32 → 33 taken 4 times.
✓ Branch 33 → 34 taken 4 times.
4 QSqlQuery remove(db), insert(db), mark(db);
125
1/1
✓ Branch 37 → 38 taken 4 times.
4 remove.prepare(QStringLiteral("DELETE FROM message_addresses WHERE header_id = ?"));
126
1/1
✓ Branch 43 → 44 taken 4 times.
4 insert.prepare(QStringLiteral(
127 "INSERT OR IGNORE INTO message_addresses (header_id, address, role, date) VALUES (?, ?, ?, ?)"));
128 4 qint64 last = from;
129
1/1
✓ Branch 46 → 47 taken 4 times.
4 bool ok = rows.exec();
130
6/7
✓ Branch 102 → 103 taken 304 times.
✗ Branch 102 → 106 not taken.
✓ Branch 103 → 104 taken 304 times.
✓ Branch 104 → 105 taken 300 times.
✓ Branch 104 → 106 taken 4 times.
✓ Branch 107 → 48 taken 300 times.
✓ Branch 107 → 108 taken 4 times.
304 while (ok && rows.next()) {
131 300 MailHeader header;
132
2/2
✓ Branch 49 → 50 taken 300 times.
✓ Branch 50 → 51 taken 300 times.
300 header.from = rows.value(1).toString();
133
2/2
✓ Branch 54 → 55 taken 300 times.
✓ Branch 55 → 56 taken 300 times.
300 header.to = rows.value(2).toString();
134
2/2
✓ Branch 59 → 60 taken 300 times.
✓ Branch 60 → 61 taken 300 times.
300 header.cc = rows.value(3).toString();
135
2/2
✓ Branch 64 → 65 taken 300 times.
✓ Branch 65 → 66 taken 300 times.
300 last = rows.value(0).toLongLong();
136
1/1
✓ Branch 69 → 70 taken 300 times.
300 remove.addBindValue(last);
137
1/1
✓ Branch 71 → 72 taken 300 times.
300 ok = remove.exec();
138
5/5
✓ Branch 72 → 73 taken 300 times.
✓ Branch 73 → 74 taken 300 times.
✓ Branch 74 → 75 taken 300 times.
✓ Branch 98 → 76 taken 643 times.
✓ Branch 98 → 99 taken 300 times.
943 for (const MailAddressIndex::Entry &entry : MailAddressIndex::entriesOf(header)) {
139
1/2
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 79 taken 643 times.
643 if (!ok) break;
140
1/1
✓ Branch 81 → 82 taken 643 times.
643 insert.addBindValue(last);
141
1/1
✓ Branch 85 → 86 taken 643 times.
643 insert.addBindValue(entry.address);
142
1/1
✓ Branch 89 → 90 taken 643 times.
643 insert.addBindValue(entry.role);
143
2/2
✓ Branch 92 → 93 taken 643 times.
✓ Branch 93 → 94 taken 643 times.
643 insert.addBindValue(rows.value(4));
144
1/1
✓ Branch 95 → 96 taken 643 times.
643 ok = insert.exec();
145 300 }
146 300 ++result.indexed;
147 300 }
148
1/2
✓ Branch 108 → 109 taken 4 times.
✗ Branch 108 → 129 not taken.
4 if (ok) {
149
2/2
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 117 taken 3 times.
4 if (result.indexed < chunk) {
150 // Reached the newest header: the writer indexes everything after it.
151
1/1
✓ Branch 113 → 114 taken 1 time.
1 ok = mark.exec(QStringLiteral("DELETE FROM cache_meta WHERE key='address_index_watermark'"));
152 } else {
153
1/1
✓ Branch 120 → 121 taken 3 times.
3 mark.prepare(QStringLiteral("UPDATE cache_meta SET value = ? WHERE key='address_index_watermark'"));
154
1/1
✓ Branch 125 → 126 taken 3 times.
3 mark.addBindValue(last);
155
1/1
✓ Branch 127 → 128 taken 3 times.
3 ok = mark.exec();
156 }
157 }
158
4/7
✓ Branch 129 → 130 taken 4 times.
✗ Branch 129 → 132 not taken.
✓ Branch 130 → 131 taken 4 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 133 taken 4 times.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 176 taken 4 times.
4 if (!ok || !connection.commit()) {
159 ✗ result.error = !rows.lastError().text().isEmpty() ? rows.lastError().text()
160 ✗ : !insert.lastError().text().isEmpty() ? insert.lastError().text()
161 ✗ : connection.lastError().text();
162 ✗ connection.rollback();
163 ✗ qCWarning(lcAddressIndex) << "Address backfill failed:" << result.error;
164 ✗ return result;
165 }
166 4 result.complete = result.indexed < chunk;
167
2/8
✓ Branch 176 → 177 taken 4 times.
✗ Branch 180 → 181 not taken.
✗ Branch 181 → 182 not taken.
✗ Branch 182 → 183 not taken.
✗ Branch 183 → 184 not taken.
✗ Branch 184 → 185 not taken.
✗ Branch 192 → 178 not taken.
✓ Branch 192 → 193 taken 4 times.
4 qCInfo(lcAddressIndex) << "Address backfill indexed" << result.indexed << "headers up to id" << last
168 ✗ << (result.complete ? "(complete)" : "");
169 4 return result;
170 11 }
171
172 } // namespace MailAddressHistory
173