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.8% 151 / 0 / 178
Functions: 71.4% 15 / 0 / 21
Branches: 54.6% 189 / 0 / 346

data/ContactPeopleMerge.cpp
Line Branch Exec Source
1 #include "ContactPeopleMerge.h"
2
3 #include "RelationVocabulary.h"
4
5 #include <algorithm>
6
7 namespace ContactPeopleMerge {
8
9 namespace {
10
11 const QString kUrnUuid = QStringLiteral("urn:uuid:");
12
13
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 3 → 4 taken 10 times.
10 QString folded(const QString &text) { return text.trimmed().toCaseFolded(); }
14
15 // The UID a reference names: its uid, else the UID inside an urn:uuid URI.
16 38 QString referencedUid(const QString &uid, const QString &uri) {
17
3/3
✓ Branch 2 → 3 taken 38 times.
✓ Branch 5 → 6 taken 33 times.
✓ Branch 5 → 11 taken 5 times.
38 if (!uid.trimmed().isEmpty())
18
2/2
✓ Branch 6 → 7 taken 33 times.
✓ Branch 7 → 8 taken 33 times.
33 return uid.trimmed().toLower();
19
1/1
✓ Branch 11 → 12 taken 5 times.
5 const QString value = uri.trimmed();
20
3/3
✓ Branch 12 → 13 taken 5 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 20 taken 2 times.
5 if (value.startsWith(kUrnUuid, Qt::CaseInsensitive))
21
2/2
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
3 return value.mid(kUrnUuid.size()).toLower();
22 2 return {};
23 5 }
24
25 // Two relationships say the same: same type (and custom label) and the same person.
26 16 bool sameRelation(const ContactRelation &a, const ContactRelation &b) {
27
7/12
✓ Branch 2 → 3 taken 16 times.
✓ Branch 3 → 4 taken 16 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 10 taken 11 times.
✓ Branch 18 → 19 taken 16 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 22 taken 5 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
37 if (RelationVocabulary::canonical(a.type) != RelationVocabulary::canonical(b.type) ||
28
8/14
✓ Branch 6 → 7 taken 5 times.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 5 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 11 times.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 16 taken 11 times.
✓ Branch 16 → 17 taken 16 times.
✗ Branch 16 → 18 not taken.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
21 folded(a.label) != folded(b.label))
29 11 return false;
30
2/2
✓ Branch 22 → 23 taken 5 times.
✓ Branch 23 → 24 taken 5 times.
5 const QString ua = referencedUid(a.targetUid, a.targetUri), ub = referencedUid(b.targetUid, b.targetUri);
31
2/6
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 28 taken 5 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
✓ Branch 30 → 31 taken 5 times.
✗ Branch 30 → 32 not taken.
5 if (!ua.isEmpty() || !ub.isEmpty())
32 5 return ua == ub;
33 ✗ if (!a.targetUri.trimmed().isEmpty() || !b.targetUri.trimmed().isEmpty())
34 ✗ return a.targetUri.trimmed() == b.targetUri.trimmed();
35 ✗ return folded(a.targetName) == folded(b.targetName);
36 5 }
37
38 ✗ bool samePerson(const ContactPersonRef &a, const ContactPersonRef &b) {
39 ✗ const QString ua = referencedUid(a.uid, a.uri), ub = referencedUid(b.uid, b.uri);
40 ✗ if (!ua.isEmpty() || !ub.isEmpty())
41 ✗ return ua == ub;
42 ✗ return folded(a.uri) == folded(b.uri) && folded(a.name) == folded(b.name);
43 ✗ }
44
45 24 void uniteText(QStringList *into, const QStringList &extra) {
46
2/2
✓ Branch 24 → 4 taken 2 times.
✓ Branch 24 → 25 taken 24 times.
26 for (const QString &value : extra)
47
9/16
✓ Branch 5 → 6 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 12 not taken.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 2 times.
✗ Branch 15 → 17 not taken.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 22 taken 1 time.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
2 if (!value.trimmed().isEmpty() && !into->contains(value.trimmed(), Qt::CaseInsensitive))
48
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
1 into->append(value.trimmed());
49 24 }
50
51 24 void fill(QString *into, const QString &value) {
52
9/16
✓ Branch 2 → 3 taken 24 times.
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 24 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 23 times.
✓ Branch 10 → 11 taken 24 times.
✗ Branch 10 → 12 not taken.
✓ Branch 12 → 13 taken 24 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 19 taken 23 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
24 if (into->trimmed().isEmpty() && !value.trimmed().isEmpty())
53
1/1
✓ Branch 15 → 16 taken 1 time.
1 *into = value.trimmed();
54 24 }
55
56 // The relationships of the merged card: the keeper's own first, then the
57 // others' new ones; none names one of the merged cards.
58 16 QList<ContactRelation> unitedRelations(const ContactCard &keeper, const QList<ContactCard> &others,
59 const QSet<QString> &mergedUids) {
60 16 QList<ContactRelation> united;
61
2/2
✓ Branch 10 → 4 taken 4 times.
✓ Branch 10 → 11 taken 16 times.
20 for (const ContactRelation &relation : keeper.relations)
62
3/3
✓ Branch 5 → 6 taken 4 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 2 times.
4 if (!namesAny(relation.targetUid, relation.targetUri, mergedUids))
63
1/1
✓ Branch 7 → 8 taken 2 times.
2 united.append(relation);
64
2/2
✓ Branch 37 → 13 taken 25 times.
✓ Branch 37 → 38 taken 16 times.
41 for (const ContactCard &other : others)
65
2/2
✓ Branch 34 → 16 taken 14 times.
✓ Branch 34 → 35 taken 25 times.
39 for (ContactRelation relation : other.relations) {
66
3/3
✓ Branch 18 → 19 taken 14 times.
✓ Branch 19 → 20 taken 5 times.
✓ Branch 19 → 21 taken 9 times.
14 if (namesAny(relation.targetUid, relation.targetUri, mergedUids))
67 5 continue;
68
3/3
✓ Branch 23 → 24 taken 9 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 26 taken 7 times.
9 if (std::any_of(united.cbegin(), united.cend(),
69 8 [&](const ContactRelation &known) { return sameRelation(known, relation); }))
70 2 continue;
71 7 relation.sourceIndex = -1;
72 7 relation.model.clear(); // written in the kept card's own dialect; a legacy line becomes writable
73
1/1
✓ Branch 27 → 28 taken 7 times.
7 united.append(relation);
74 14 }
75 16 return united;
76 ✗ }
77
78 } // namespace
79
80 14 QSet<QString> uidsOf(const QList<ContactCard> &cards) {
81 14 QSet<QString> uids;
82
2/2
✓ Branch 18 → 5 taken 37 times.
✓ Branch 18 → 19 taken 14 times.
51 for (const ContactCard &card : cards)
83
2/3
✓ Branch 6 → 7 taken 37 times.
✓ Branch 9 → 10 taken 37 times.
✗ Branch 9 → 16 not taken.
37 if (!card.uid.trimmed().isEmpty())
84
3/3
✓ Branch 10 → 11 taken 37 times.
✓ Branch 11 → 12 taken 37 times.
✓ Branch 12 → 13 taken 37 times.
37 uids.insert(card.uid.trimmed().toLower());
85 14 return uids;
86 ✗ }
87
88 28 bool namesAny(const QString &uid, const QString &uri, const QSet<QString> &uids) {
89
1/1
✓ Branch 2 → 3 taken 28 times.
28 const QString named = referencedUid(uid, uri);
90
4/4
✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 8 taken 2 times.
✓ Branch 6 → 7 taken 12 times.
✓ Branch 6 → 8 taken 14 times.
56 return !named.isEmpty() && uids.contains(named);
91 28 }
92
93 8 void mergeInto(ContactCard *keeper, const QList<ContactCard> &others, const QSet<QString> &mergedUids) {
94
1/1
✓ Branch 2 → 3 taken 8 times.
8 keeper->relations = unitedRelations(*keeper, others, mergedUids);
95 // How we met: the keeper's meeting stays; an empty one is taken from the first other card that has one.
96 8 QList<ContactPersonRef> via;
97
1/2
✗ Branch 14 → 8 not taken.
✓ Branch 14 → 15 taken 8 times.
8 for (const ContactPersonRef &ref : std::as_const(keeper->met.via))
98 ✗ if (!namesAny(ref.uid, ref.uri, mergedUids))
99 ✗ via.append(ref);
100
2/2
✓ Branch 56 → 17 taken 12 times.
✓ Branch 56 → 57 taken 8 times.
20 for (const ContactCard &other : others) {
101
7/8
✓ Branch 18 → 19 taken 12 times.
✓ Branch 19 → 20 taken 12 times.
✗ Branch 19 → 23 not taken.
✓ Branch 20 → 21 taken 12 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 11 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 32 taken 11 times.
12 if (keeper->met.isEmpty() && !other.met.isEmpty()) {
102 1 const ContactMet met = other.met;
103 1 keeper->met.date = met.date;
104 1 keeper->met.place = met.place;
105 1 keeper->met.placeGeo = met.placeGeo;
106 1 keeper->met.eventUid = met.eventUid;
107 1 keeper->met.note = met.note;
108 1 keeper->met.channel = met.channel;
109 1 }
110
2/2
✓ Branch 53 → 34 taken 1 time.
✓ Branch 53 → 54 taken 12 times.
13 for (ContactPersonRef ref : other.met.via) {
111
3/5
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✗ Branch 37 → 42 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 1 time.
2 if (namesAny(ref.uid, ref.uri, mergedUids) ||
112
2/3
✓ Branch 40 → 41 taken 1 time.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 1 time.
1 std::any_of(via.cbegin(), via.cend(), [&](const ContactPersonRef &known) { return samePerson(known, ref); }))
113 ✗ continue;
114 1 ref.sourceIndex = -1;
115
1/1
✓ Branch 46 → 47 taken 1 time.
1 via.append(ref);
116 1 }
117 }
118 8 keeper->met.via = via;
119
2/2
✓ Branch 148 → 60 taken 12 times.
✓ Branch 148 → 149 taken 8 times.
20 for (const ContactCard &other : others) {
120
2/2
✓ Branch 86 → 63 taken 1 time.
✓ Branch 86 → 87 taken 12 times.
13 for (ContactSocial social : other.social) {
121
1/1
✓ Branch 67 → 68 taken 1 time.
1 const bool known = std::any_of(keeper->social.cbegin(), keeper->social.cend(), [&](const ContactSocial &s) {
122 ✗ return s.kind == social.kind && folded(s.value) == folded(social.value);
123 });
124
5/11
✓ Branch 68 → 69 taken 1 time.
✗ Branch 68 → 72 not taken.
✓ Branch 69 → 70 taken 1 time.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✗ Branch 74 → 76 not taken.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 1 time.
✗ Branch 157 → 158 not taken.
✗ Branch 157 → 159 not taken.
1 if (known || social.value.trimmed().isEmpty())
125 ✗ continue;
126 1 social.sourceIndex = -1;
127 1 social.property.clear();
128
1/1
✓ Branch 79 → 80 taken 1 time.
1 keeper->social.append(social);
129 1 }
130
1/1
✓ Branch 87 → 88 taken 12 times.
12 uniteText(&keeper->languages, other.languages);
131
1/1
✓ Branch 88 → 89 taken 12 times.
12 uniteText(&keeper->interests, other.interests);
132
1/2
✗ Branch 110 → 91 not taken.
✓ Branch 110 → 111 taken 12 times.
12 for (ContactDate date : other.otherDates) {
133 ✗ const bool known = std::any_of(keeper->otherDates.cbegin(), keeper->otherDates.cend(), [&](const ContactDate &d) {
134 ✗ return d.date == date.date && folded(d.label) == folded(date.label);
135 });
136 ✗ if (known || !date.date.isValid())
137 ✗ continue;
138 ✗ date.sourceIndex = -1;
139 ✗ keeper->otherDates.append(date);
140 ✗ }
141
2/2
✓ Branch 132 → 113 taken 1 time.
✓ Branch 132 → 133 taken 12 times.
13 for (ContactMailLink link : other.mailLinks) {
142
1/1
✓ Branch 117 → 118 taken 1 time.
1 const bool known = std::any_of(keeper->mailLinks.cbegin(), keeper->mailLinks.cend(),
143 ✗ [&](const ContactMailLink &l) { return l.messageId == link.messageId; });
144
3/6
✓ Branch 118 → 119 taken 1 time.
✗ Branch 118 → 121 not taken.
✗ Branch 120 → 121 not taken.
✓ Branch 120 → 122 taken 1 time.
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 125 taken 1 time.
1 if (known || link.messageId.isEmpty())
145 ✗ continue;
146 1 link.sourceIndex = -1;
147
1/1
✓ Branch 125 → 126 taken 1 time.
1 keeper->mailLinks.append(link);
148 1 }
149
1/1
✓ Branch 133 → 134 taken 12 times.
12 fill(&keeper->birthplace, other.birthplace);
150
1/1
✓ Branch 134 → 135 taken 12 times.
12 fill(&keeper->gender, other.gender);
151
5/6
✓ Branch 136 → 137 taken 12 times.
✗ Branch 136 → 140 not taken.
✓ Branch 138 → 139 taken 1 time.
✓ Branch 138 → 140 taken 11 times.
✓ Branch 141 → 142 taken 1 time.
✓ Branch 141 → 143 taken 11 times.
12 if (!keeper->anniversary.isValid() && other.anniversary.isValid())
152 1 keeper->anniversary = other.anniversary;
153
3/4
✓ Branch 143 → 144 taken 12 times.
✗ Branch 143 → 146 not taken.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 146 taken 11 times.
12 if (keeper->photo.kind == ContactPhoto::Kind::None && other.photo.kind != ContactPhoto::Kind::None)
154 1 keeper->photo = other.photo;
155 }
156 8 }
157
158 8 int mergedRelationCount(const ContactCard &keeper, const QList<ContactCard> &others, const QSet<QString> &mergedUids) {
159
1/1
✓ Branch 2 → 3 taken 8 times.
8 return int(unitedRelations(keeper, others, mergedUids).size());
160 }
161
162 5 bool retarget(ContactCard *card, const QSet<QString> &goneUids, const QString &keeperUid, const QString &keeperName) {
163
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 3 → 4 taken 5 times.
5 const QString ownUid = card->uid.trimmed().toLower();
164
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 6 → 7 taken 5 times.
5 const QString keeper = keeperUid.trimmed().toLower();
165 5 bool changed = false;
166 5 QList<ContactRelation> relations;
167
2/2
✓ Branch 38 → 11 taken 9 times.
✓ Branch 38 → 39 taken 5 times.
14 for (ContactRelation relation : std::as_const(card->relations)) {
168 // A legacy line is never written (VCardPeoplePatch); it stays as it is.
169
7/7
✓ Branch 14 → 15 taken 7 times.
✓ Branch 14 → 18 taken 2 times.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 16 → 18 taken 3 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 31 taken 5 times.
9 if (!relation.readOnly() && namesAny(relation.targetUid, relation.targetUri, goneUids)) {
170 4 changed = true;
171
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 4 times.
4 if (keeper == ownUid)
172 ✗ continue; // the kept card would be related to itself
173 4 relation.targetUid = keeperUid;
174 4 relation.targetUri.clear();
175 4 relation.targetName = keeperName;
176
3/3
✓ Branch 28 → 29 taken 4 times.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 31 taken 3 times.
4 if (std::any_of(relations.cbegin(), relations.cend(),
177 2 [&](const ContactRelation &known) { return sameRelation(known, relation); }))
178 1 continue;
179 }
180
1/1
✓ Branch 31 → 32 taken 8 times.
8 relations.append(relation);
181 9 }
182 // A line rewritten above may also repeat one that comes later in the card.
183 5 QList<ContactRelation> unique;
184
2/2
✓ Branch 50 → 42 taken 8 times.
✓ Branch 50 → 51 taken 5 times.
13 for (const ContactRelation &relation : std::as_const(relations))
185
2/3
✓ Branch 45 → 46 taken 8 times.
✓ Branch 46 → 47 taken 8 times.
✗ Branch 46 → 48 not taken.
8 if (!std::any_of(unique.cbegin(), unique.cend(),
186 6 [&](const ContactRelation &known) { return sameRelation(known, relation); }))
187
1/1
✓ Branch 47 → 48 taken 8 times.
8 unique.append(relation);
188
3/4
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 55 taken 3 times.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 2 times.
5 changed = changed || unique.size() != relations.size();
189 5 card->relations = unique;
190 5 QList<ContactPersonRef> via;
191
2/2
✓ Branch 82 → 61 taken 2 times.
✓ Branch 82 → 83 taken 5 times.
7 for (ContactPersonRef ref : std::as_const(card->met.via)) {
192
3/3
✓ Branch 63 → 64 taken 2 times.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 71 taken 1 time.
2 if (namesAny(ref.uid, ref.uri, goneUids)) {
193 1 changed = true;
194
1/2
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 1 time.
1 if (keeper == ownUid)
195 ✗ continue;
196 1 ref.uid = keeperUid;
197 1 ref.uri.clear();
198 1 ref.name = keeperName;
199 }
200
2/3
✓ Branch 73 → 74 taken 2 times.
✓ Branch 74 → 75 taken 2 times.
✗ Branch 74 → 76 not taken.
2 if (!std::any_of(via.cbegin(), via.cend(), [&](const ContactPersonRef &known) { return samePerson(known, ref); }))
201
1/1
✓ Branch 75 → 76 taken 2 times.
2 via.append(ref);
202 2 }
203 5 card->met.via = via;
204 // Group cards: MEMBER urn:uuid:<uid> (or the bare UID) of a removed card names the kept card.
205 5 QStringList members;
206
2/2
✓ Branch 122 → 87 taken 4 times.
✓ Branch 122 → 123 taken 5 times.
9 for (const QString &member : std::as_const(card->members)) {
207 4 QString value = member;
208
2/2
✓ Branch 89 → 90 taken 4 times.
✓ Branch 90 → 91 taken 4 times.
4 const bool urn = member.trimmed().startsWith(kUrnUuid, Qt::CaseInsensitive);
209
7/19
✓ Branch 92 → 93 taken 4 times.
✗ Branch 92 → 97 not taken.
✓ Branch 93 → 94 taken 4 times.
✓ Branch 95 → 96 taken 4 times.
✓ Branch 96 → 99 taken 4 times.
✗ Branch 97 → 98 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 99 → 100 not taken.
✓ Branch 99 → 101 taken 4 times.
✓ Branch 101 → 102 taken 4 times.
✗ Branch 101 → 103 not taken.
✓ Branch 103 → 104 taken 4 times.
✗ Branch 103 → 105 not taken.
✗ Branch 148 → 149 not taken.
✗ Branch 148 → 150 not taken.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
✗ Branch 156 → 157 not taken.
✗ Branch 156 → 158 not taken.
4 const QString named = urn ? member.trimmed().mid(kUrnUuid.size()).toLower() : member.trimmed().toLower();
210
2/2
✓ Branch 106 → 107 taken 2 times.
✓ Branch 106 → 113 taken 2 times.
4 if (goneUids.contains(named)) {
211 2 changed = true;
212
2/3
✓ Branch 107 → 108 taken 2 times.
✗ Branch 107 → 109 not taken.
✓ Branch 108 → 110 taken 2 times.
2 value = urn ? kUrnUuid + keeperUid : keeperUid;
213 }
214
3/3
✓ Branch 115 → 116 taken 4 times.
✓ Branch 116 → 117 taken 3 times.
✓ Branch 116 → 118 taken 1 time.
4 if (!std::any_of(members.cbegin(), members.cend(),
215 1 [&](const QString &known) { return known.compare(value, Qt::CaseInsensitive) == 0; }))
216
1/1
✓ Branch 117 → 118 taken 3 times.
3 members.append(value);
217 4 }
218 5 card->members = members;
219 5 return changed;
220 5 }
221
222 } // namespace ContactPeopleMerge
223