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.7% 176 / 0 / 192
Functions: 100.0% 15 / 0 / 15
Branches: 77.3% 201 / 0 / 260

data/IdentityStore.cpp
Line Branch Exec Source
1 #include "IdentityStore.h"
2
3 #include "AccountConfig.h"
4
5 #include <QDir>
6 #include <QFile>
7 #include <QFileInfo>
8 #include <QJsonArray>
9 #include <QJsonDocument>
10 #include <QSaveFile>
11 #include <QUuid>
12
13 namespace {
14
15 228 QString bounded(const QJsonObject &object, const char *key, qsizetype limit) {
16
2/2
✓ Branch 3 → 4 taken 228 times.
✓ Branch 4 → 5 taken 228 times.
228 const QString value = object.value(QLatin1String(key)).toString();
17
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 228 times.
456 return value.size() > limit ? QString() : value;
18 228 }
19
20
2/2
✓ Branch 2 → 3 taken 775 times.
✓ Branch 3 → 4 taken 775 times.
775 QString newId() { return QUuid::createUuid().toString(QUuid::WithoutBraces); }
21
22 } // namespace
23
24 807 QString IdentityStore::pathFor(const QString &profileDirectory) {
25
2/2
✓ Branch 2 → 3 taken 807 times.
✓ Branch 6 → 7 taken 807 times.
1614 return QDir(profileDirectory).filePath(QStringLiteral("identities.json"));
26 }
27
28 800 QJsonObject IdentityStore::toJson(const Identity &identity) {
29
1/1
✓ Branch 2 → 3 taken 800 times.
800 QJsonArray signatures;
30
2/2
✓ Branch 46 → 5 taken 22 times.
✓ Branch 46 → 47 taken 800 times.
822 for (const Signature &signature : identity.signatures)
31
1/1
✓ Branch 28 → 29 taken 22 times.
66 signatures.append(QJsonObject{{QStringLiteral("id"), signature.id},
32 44 {QStringLiteral("name"), signature.name},
33 44 {QStringLiteral("html"), signature.html},
34 44 {QStringLiteral("text"), signature.text},
35
2/4
✓ Branch 32 → 33 taken 110 times.
✓ Branch 32 → 34 taken 22 times.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
176 {QStringLiteral("textOverridden"), signature.textOverridden}});
36 1600 return {{QStringLiteral("id"), identity.id},
37 1600 {QStringLiteral("accountId"), identity.accountId},
38 1600 {QStringLiteral("accountAddress"), identity.accountAddress},
39 1600 {QStringLiteral("accountServer"), identity.accountServer},
40 1600 {QStringLiteral("accountName"), identity.accountName},
41 1600 {QStringLiteral("name"), identity.name},
42 1600 {QStringLiteral("email"), identity.email},
43 1600 {QStringLiteral("replyTo"), identity.replyTo},
44 1600 {QStringLiteral("formattedByDefault"), identity.formattedByDefault},
45 1600 {QStringLiteral("bccSelf"), identity.bccSelf},
46 1600 {QStringLiteral("wrapPlainText"), identity.wrapPlainText},
47 1600 {QStringLiteral("signatures"), signatures},
48 1600 {QStringLiteral("newMailSignatureId"), identity.newMailSignatureId},
49 1600 {QStringLiteral("replySignatureId"), identity.replySignatureId},
50
2/4
✓ Branch 109 → 110 taken 12000 times.
✓ Branch 109 → 111 taken 800 times.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 194 not taken.
14400 {QStringLiteral("isDefault"), identity.isDefault}};
51 13732 }
52
53 20 std::optional<Identity> IdentityStore::fromJson(const QJsonObject &object) {
54 20 Identity identity;
55
1/1
✓ Branch 2 → 3 taken 20 times.
20 identity.id = bounded(object, "id", 64);
56
1/1
✓ Branch 5 → 6 taken 20 times.
20 identity.accountId = bounded(object, "accountId", 128);
57
1/1
✓ Branch 8 → 9 taken 20 times.
20 identity.accountAddress = bounded(object, "accountAddress", 320);
58
1/1
✓ Branch 11 → 12 taken 20 times.
20 identity.accountServer = bounded(object, "accountServer", 255);
59
1/1
✓ Branch 14 → 15 taken 20 times.
20 identity.accountName = bounded(object, "accountName", 256);
60
1/1
✓ Branch 17 → 18 taken 20 times.
20 identity.name = bounded(object, "name", 200);
61
1/1
✓ Branch 20 → 21 taken 20 times.
20 identity.email = bounded(object, "email", 320);
62
1/1
✓ Branch 23 → 24 taken 20 times.
20 identity.replyTo = bounded(object, "replyTo", 998);
63
2/2
✓ Branch 29 → 30 taken 20 times.
✓ Branch 30 → 31 taken 20 times.
20 identity.formattedByDefault = object.value(QStringLiteral("formattedByDefault")).toBool();
64
2/2
✓ Branch 37 → 38 taken 20 times.
✓ Branch 38 → 39 taken 20 times.
20 identity.bccSelf = object.value(QStringLiteral("bccSelf")).toBool();
65
2/2
✓ Branch 45 → 46 taken 20 times.
✓ Branch 46 → 47 taken 20 times.
20 identity.wrapPlainText = object.value(QStringLiteral("wrapPlainText")).toBool();
66
1/1
✓ Branch 50 → 51 taken 20 times.
20 identity.newMailSignatureId = bounded(object, "newMailSignatureId", 64);
67
1/1
✓ Branch 53 → 54 taken 20 times.
20 identity.replySignatureId = bounded(object, "replySignatureId", 64);
68
2/2
✓ Branch 59 → 60 taken 20 times.
✓ Branch 60 → 61 taken 20 times.
20 identity.isDefault = object.value(QStringLiteral("isDefault")).toBool();
69 // Header values: no line breaks, no NUL.
70
2/2
✓ Branch 83 → 66 taken 58 times.
✓ Branch 83 → 84 taken 19 times.
77 for (QString *value : {&identity.name, &identity.email, &identity.replyTo})
71
9/11
✓ Branch 68 → 69 taken 58 times.
✓ Branch 69 → 70 taken 58 times.
✗ Branch 69 → 77 not taken.
✓ Branch 72 → 73 taken 58 times.
✓ Branch 73 → 74 taken 57 times.
✓ Branch 73 → 77 taken 1 time.
✓ Branch 75 → 76 taken 57 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 57 times.
✓ Branch 79 → 80 taken 1 time.
✓ Branch 79 → 82 taken 57 times.
58 if (value->contains(QLatin1Char('\r')) || value->contains(QLatin1Char('\n')) || value->contains(QChar::Null))
72 1 return std::nullopt;
73
5/9
✓ Branch 85 → 86 taken 19 times.
✗ Branch 85 → 92 not taken.
✓ Branch 87 → 88 taken 19 times.
✗ Branch 87 → 92 not taken.
✓ Branch 90 → 91 taken 19 times.
✗ Branch 91 → 92 not taken.
✓ Branch 91 → 93 taken 19 times.
✗ Branch 94 → 95 not taken.
✓ Branch 94 → 96 taken 19 times.
19 if (identity.id.isEmpty() || identity.email.isEmpty() || !identity.email.contains(QLatin1Char('@')))
74 ✗ return std::nullopt;
75
2/2
✓ Branch 99 → 100 taken 19 times.
✓ Branch 100 → 101 taken 19 times.
19 const QJsonArray signatures = object.value(QStringLiteral("signatures")).toArray();
76
2/3
✓ Branch 104 → 105 taken 19 times.
✗ Branch 105 → 106 not taken.
✓ Branch 105 → 107 taken 19 times.
19 if (signatures.size() > MaxSignaturesPerIdentity) return std::nullopt;
77
3/3
✓ Branch 108 → 109 taken 19 times.
✓ Branch 153 → 110 taken 7 times.
✓ Branch 153 → 154 taken 19 times.
26 for (const QJsonValue &value : signatures) {
78
1/1
✓ Branch 112 → 113 taken 7 times.
7 const QJsonObject entry = value.toObject();
79 7 Signature signature;
80
1/1
✓ Branch 113 → 114 taken 7 times.
7 signature.id = bounded(entry, "id", 64);
81
1/1
✓ Branch 116 → 117 taken 7 times.
7 signature.name = bounded(entry, "name", 120);
82
1/1
✓ Branch 119 → 120 taken 7 times.
7 signature.html = bounded(entry, "html", MaxSignatureBytes);
83
1/1
✓ Branch 122 → 123 taken 7 times.
7 signature.text = bounded(entry, "text", MaxSignatureBytes);
84
2/2
✓ Branch 128 → 129 taken 7 times.
✓ Branch 129 → 130 taken 7 times.
7 signature.textOverridden = entry.value(QStringLiteral("textOverridden")).toBool();
85
1/2
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 137 taken 7 times.
7 if (signature.id.isEmpty()) return std::nullopt;
86
1/1
✓ Branch 137 → 138 taken 7 times.
7 identity.signatures.append(signature);
87 7 }
88 19 return identity;
89 20 }
90
91 804 bool IdentityStore::load(QString *error) {
92
1/1
✓ Branch 2 → 3 taken 804 times.
804 m_identities.clear();
93
1/1
✓ Branch 3 → 4 taken 804 times.
804 QFile file(m_path);
94
3/3
✓ Branch 4 → 5 taken 804 times.
✓ Branch 5 → 6 taken 786 times.
✓ Branch 5 → 7 taken 18 times.
804 if (!file.exists()) return true;
95
2/3
✓ Branch 7 → 8 taken 18 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 18 taken 18 times.
18 if (file.size() > MaxFileBytes) {
96 ✗ if (error) *error = QStringLiteral("identities file too large");
97 ✗ return false;
98 }
99
2/3
✓ Branch 19 → 20 taken 18 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 27 taken 18 times.
18 if (!file.open(QIODevice::ReadOnly)) {
100 ✗ if (error) *error = file.errorString();
101 ✗ return false;
102 }
103 18 QJsonParseError parseError;
104
2/2
✓ Branch 27 → 28 taken 18 times.
✓ Branch 28 → 29 taken 18 times.
18 const QJsonDocument document = QJsonDocument::fromJson(file.read(MaxFileBytes), &parseError);
105
4/7
✓ Branch 30 → 31 taken 18 times.
✗ Branch 30 → 33 not taken.
✓ Branch 31 → 32 taken 18 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 18 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 45 taken 18 times.
18 if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
106 ✗ if (error) *error = QStringLiteral("identities file is not valid JSON");
107 ✗ return false;
108 }
109
3/3
✓ Branch 45 → 46 taken 18 times.
✓ Branch 49 → 50 taken 18 times.
✓ Branch 50 → 51 taken 18 times.
36 const QJsonArray entries = document.object().value(QStringLiteral("identities")).toArray();
110
2/3
✓ Branch 55 → 56 taken 18 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 66 taken 18 times.
18 if (entries.size() > MaxIdentities) {
111 ✗ if (error) *error = QStringLiteral("too many identities");
112 ✗ return false;
113 }
114 18 QList<Identity> loaded;
115
3/3
✓ Branch 67 → 68 taken 18 times.
✓ Branch 98 → 69 taken 20 times.
✓ Branch 98 → 99 taken 17 times.
37 for (const QJsonValue &value : entries) {
116
2/2
✓ Branch 71 → 72 taken 20 times.
✓ Branch 72 → 73 taken 20 times.
20 const auto identity = fromJson(value.toObject());
117
2/2
✓ Branch 75 → 76 taken 1 time.
✓ Branch 75 → 85 taken 19 times.
20 if (!identity) {
118
1/2
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 84 not taken.
2 if (error) *error = QStringLiteral("invalid identity entry");
119 1 return false;
120 }
121
1/1
✓ Branch 86 → 87 taken 19 times.
19 loaded.append(*identity);
122 21 }
123 17 m_identities = loaded;
124 17 return true;
125 804 }
126
127 799 bool IdentityStore::save(QString *error) const {
128
1/1
✓ Branch 2 → 3 taken 799 times.
799 QJsonArray entries;
129
4/4
✓ Branch 6 → 7 taken 800 times.
✓ Branch 8 → 9 taken 800 times.
✓ Branch 13 → 5 taken 800 times.
✓ Branch 13 → 14 taken 799 times.
1599 for (const Identity &identity : m_identities) entries.append(toJson(identity));
130 const QByteArray bytes =
131
3/5
✓ Branch 23 → 24 taken 799 times.
✓ Branch 28 → 29 taken 1598 times.
✓ Branch 28 → 30 taken 799 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
4794 QJsonDocument(QJsonObject{{QStringLiteral("version"), 1}, {QStringLiteral("identities"), entries}})
132
1/1
✓ Branch 24 → 25 taken 799 times.
799 .toJson(QJsonDocument::Indented);
133
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 45 taken 799 times.
799 if (bytes.size() > MaxFileBytes) {
134 ✗ if (error) *error = QStringLiteral("identities too large to save");
135 ✗ return false;
136 }
137
4/4
✓ Branch 46 → 47 taken 799 times.
✓ Branch 47 → 48 taken 799 times.
✓ Branch 48 → 49 taken 799 times.
✓ Branch 49 → 50 taken 799 times.
799 QDir().mkpath(QFileInfo(m_path).absolutePath());
138
1/1
✓ Branch 54 → 55 taken 799 times.
799 QSaveFile file(m_path);
139
4/6
✓ Branch 56 → 57 taken 799 times.
✓ Branch 59 → 60 taken 799 times.
✓ Branch 60 → 61 taken 799 times.
✗ Branch 60 → 66 not taken.
✓ Branch 63 → 64 taken 799 times.
✗ Branch 63 → 66 not taken.
2397 if (!file.open(QIODevice::WriteOnly) || !file.setPermissions(QFile::ReadOwner | QFile::WriteOwner) ||
140
5/8
✓ Branch 57 → 58 taken 799 times.
✗ Branch 57 → 66 not taken.
✓ Branch 61 → 62 taken 799 times.
✓ Branch 64 → 65 taken 799 times.
✗ Branch 65 → 66 not taken.
✓ Branch 65 → 67 taken 799 times.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 75 taken 799 times.
2397 file.write(bytes) != bytes.size() || !file.commit()) {
141 ✗ if (error) *error = file.errorString();
142 ✗ return false;
143 }
144 799 return true;
145 3196 }
146
147 1603 bool IdentityStore::reconcile(const std::vector<AccountConfig> &accounts) {
148 1603 bool changed = false;
149 1603 QStringList known;
150
2/2
✓ Branch 69 → 4 taken 827 times.
✓ Branch 69 → 70 taken 1603 times.
2430 for (const AccountConfig &account : accounts) {
151
1/1
✓ Branch 5 → 6 taken 827 times.
827 known.append(account.id);
152 827 bool has = false;
153
4/4
✓ Branch 6 → 7 taken 827 times.
✓ Branch 7 → 8 taken 827 times.
✓ Branch 46 → 9 taken 79 times.
✓ Branch 46 → 47 taken 827 times.
906 for (Identity &identity : m_identities) {
154
2/2
✓ Branch 11 → 12 taken 27 times.
✓ Branch 11 → 13 taken 52 times.
79 if (identity.accountId != account.id) continue;
155 52 has = true;
156 // The account's own identity follows a changed account address.
157
6/8
✓ Branch 13 → 14 taken 52 times.
✗ Branch 13 → 19 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 19 taken 51 times.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 19 not taken.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 23 taken 51 times.
53 if (identity.isDefault && identity.accountAddress != account.email &&
158 1 identity.email.compare(identity.accountAddress, Qt::CaseInsensitive) == 0) {
159 1 identity.email = account.email;
160 1 changed = true;
161 }
162 // … and a changed "Your name" while its own name was not edited apart.
163
5/6
✓ Branch 23 → 24 taken 52 times.
✗ Branch 23 → 27 not taken.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 25 → 27 taken 46 times.
✓ Branch 28 → 29 taken 6 times.
✓ Branch 28 → 34 taken 46 times.
52 if (identity.isDefault && identity.accountName != account.name) {
164
2/2
✓ Branch 30 → 31 taken 4 times.
✓ Branch 30 → 32 taken 2 times.
6 if (identity.name == identity.accountName) identity.name = account.name;
165 6 identity.accountName = account.name;
166 6 changed = true;
167 }
168
6/6
✓ Branch 35 → 36 taken 51 times.
✓ Branch 35 → 38 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 50 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 44 taken 50 times.
52 if (identity.accountAddress != account.email || identity.accountServer != account.imap.host) {
169 2 identity.accountAddress = account.email;
170 2 identity.accountServer = account.imap.host;
171 2 changed = true;
172 }
173 }
174
5/6
✓ Branch 47 → 48 taken 775 times.
✓ Branch 47 → 50 taken 52 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 775 times.
✓ Branch 52 → 53 taken 52 times.
✓ Branch 52 → 54 taken 775 times.
827 if (has || account.email.isEmpty()) continue;
175 775 Identity identity;
176
1/1
✓ Branch 54 → 55 taken 775 times.
775 identity.id = newId();
177 775 identity.accountId = account.id;
178 775 identity.accountAddress = account.email;
179 775 identity.accountServer = account.imap.host;
180 775 identity.name = account.name;
181 775 identity.accountName = account.name;
182 775 identity.email = account.email;
183 775 identity.isDefault = true;
184
1/1
✓ Branch 63 → 64 taken 775 times.
775 m_identities.append(identity);
185 775 changed = true;
186 775 }
187 1603 const qsizetype before = m_identities.size();
188
1/1
✓ Branch 71 → 72 taken 1603 times.
2448 m_identities.removeIf([&known](const Identity &identity) { return !known.contains(identity.accountId); });
189
4/4
✓ Branch 72 → 73 taken 828 times.
✓ Branch 72 → 75 taken 775 times.
✓ Branch 74 → 75 taken 17 times.
✓ Branch 74 → 76 taken 811 times.
3206 return changed || before != m_identities.size();
190 1603 }
191
192 44 QList<Identity> IdentityStore::identitiesFor(const QString &accountId) const {
193 44 QList<Identity> out;
194
2/2
✓ Branch 10 → 4 taken 66 times.
✓ Branch 10 → 11 taken 44 times.
110 for (const Identity &identity : m_identities)
195
3/3
✓ Branch 6 → 7 taken 41 times.
✓ Branch 6 → 8 taken 25 times.
✓ Branch 7 → 8 taken 41 times.
66 if (identity.accountId == accountId) out.append(identity);
196 44 return out;
197 ✗ }
198
199 82 std::optional<Identity> IdentityStore::find(const QString &identityId) const {
200
2/2
✓ Branch 10 → 4 taken 86 times.
✓ Branch 10 → 11 taken 1 time.
87 for (const Identity &identity : m_identities)
201
2/2
✓ Branch 6 → 7 taken 81 times.
✓ Branch 6 → 8 taken 5 times.
86 if (identity.id == identityId) return identity;
202 1 return std::nullopt;
203 }
204
205 19 std::optional<Identity> IdentityStore::defaultFor(const QString &accountId) const {
206 19 std::optional<Identity> first;
207
1/2
✓ Branch 15 → 4 taken 20 times.
✗ Branch 15 → 16 not taken.
20 for (const Identity &identity : m_identities) {
208
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 19 times.
20 if (identity.accountId != accountId) continue;
209
1/2
✓ Branch 8 → 9 taken 19 times.
✗ Branch 8 → 10 not taken.
19 if (identity.isDefault) return identity;
210 ✗ if (!first) first = identity;
211 }
212 ✗ return first;
213 19 }
214
215 15 bool IdentityStore::upsert(const Identity &identity) {
216
4/7
✓ Branch 3 → 4 taken 15 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 15 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 15 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 15 times.
15 if (identity.id.isEmpty() || !identity.email.contains(QLatin1Char('@'))) return false;
217
2/2
✓ Branch 12 → 13 taken 8 times.
✓ Branch 12 → 29 taken 7 times.
15 if (identity.isDefault)
218
4/4
✓ Branch 13 → 14 taken 8 times.
✓ Branch 14 → 15 taken 8 times.
✓ Branch 27 → 16 taken 6 times.
✓ Branch 27 → 28 taken 8 times.
14 for (Identity &other : m_identities)
219
3/6
✓ Branch 18 → 19 taken 6 times.
✗ Branch 18 → 22 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 6 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 6 times.
6 if (other.accountId == identity.accountId && other.id != identity.id) other.isDefault = false;
220
4/4
✓ Branch 29 → 30 taken 15 times.
✓ Branch 30 → 31 taken 15 times.
✓ Branch 40 → 32 taken 14 times.
✓ Branch 40 → 41 taken 9 times.
23 for (Identity &existing : m_identities) {
221
2/2
✓ Branch 34 → 35 taken 8 times.
✓ Branch 34 → 36 taken 6 times.
14 if (existing.id != identity.id) continue;
222 6 existing = identity;
223 6 return true;
224 }
225
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 9 times.
9 if (m_identities.size() >= MaxIdentities) return false;
226 9 m_identities.append(identity);
227 9 return true;
228 }
229
230 2 bool IdentityStore::remove(const QString &identityId) {
231
1/1
✓ Branch 2 → 3 taken 2 times.
2 const auto found = find(identityId);
232
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (!found) return false;
233
1/1
✓ Branch 6 → 7 taken 2 times.
7 m_identities.removeIf([&identityId](const Identity &identity) { return identity.id == identityId; });
234 // The account keeps a default identity.
235
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 21 taken 1 time.
2 if (found->isDefault)
236
3/4
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 19 → 12 taken 2 times.
✗ Branch 19 → 20 not taken.
2 for (Identity &other : m_identities)
237
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 1 time.
2 if (other.accountId == found->accountId) {
238 1 other.isDefault = true;
239 1 break;
240 }
241 2 return true;
242 2 }
243