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: 97.9% 95 / 0 / 97
Functions: 100.0% 6 / 0 / 6
Branches: 70.1% 185 / 0 / 264

presentation/QuickContactActionsMirror.cpp
Line Branch Exec Source
1 // Sprint 103 F103-22: relationships mirror each other. "Anna is Ben's parent"
2 // saved on Ben's card is also "Ben is Anna's child" on Anna's card, and
3 // Anna's editor shows it as an ordinary row. Saving Anna's card then:
4 // - keeps an unchanged mirrored row where it is (nothing is written);
5 // - moves a changed row to Anna's card (and removes the line from Ben's);
6 // - removes a deleted row from Ben's card.
7 // The me card is the exception (D-6): its changed rows stay on the other
8 // card with the inverse type ("Ben is my child" → Ben's card: "parent").
9 // The editor sends the mirrored rows as relation entries with `mirrorOf`
10 // {storedOnKey, sourceIndex, shownType} and the loaded ones as `mirrors`.
11
12 #include "QuickContactActions.h"
13
14 #include "data/ContactQueries.h"
15 #include "data/RelationVocabulary.h"
16 #include "data/VCardCodec.h"
17
18 #include <QLoggingCategory>
19 #include <QMap>
20 #include <QSet>
21 #include <algorithm>
22
23
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 4 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.
5 Q_LOGGING_CATEGORY(lcContactMirror, "mailjd.contacts.mirror")
24
25 namespace {
26
27 16 QString mirrorId(const QVariantMap &mirror) {
28
3/3
✓ Branch 15 → 16 taken 16 times.
✓ Branch 16 → 17 taken 16 times.
✓ Branch 17 → 18 taken 16 times.
48 return mirror.value(QStringLiteral("storedOnKey")).toString() + QLatin1Char('\n') +
29
4/4
✓ Branch 6 → 7 taken 16 times.
✓ Branch 7 → 8 taken 16 times.
✓ Branch 8 → 9 taken 16 times.
✓ Branch 18 → 19 taken 16 times.
64 QString::number(mirror.value(QStringLiteral("sourceIndex"), -1).toInt());
30 }
31
32 } // namespace
33
34 57 QVariantMap QuickContactActions::splitMirrors(const QString &key, const QVariantMap &fields,
35 QList<MirrorEdit> *edits) const {
36
14/24
✓ Branch 5 → 6 taken 57 times.
✓ Branch 6 → 7 taken 25 times.
✓ Branch 6 → 12 taken 32 times.
✓ Branch 10 → 11 taken 25 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 11 → 13 taken 20 times.
✓ Branch 14 → 15 taken 25 times.
✓ Branch 14 → 16 taken 32 times.
✓ Branch 16 → 17 taken 25 times.
✓ Branch 16 → 18 taken 32 times.
✓ Branch 18 → 19 taken 57 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 57 times.
✗ Branch 20 → 22 not taken.
✓ Branch 22 → 23 taken 37 times.
✓ Branch 22 → 24 taken 20 times.
✗ Branch 330 → 331 not taken.
✗ Branch 330 → 332 not taken.
✗ Branch 334 → 335 not taken.
✗ Branch 334 → 336 not taken.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
✗ Branch 342 → 343 not taken.
✗ Branch 342 → 344 not taken.
139 if (!fields.contains(QStringLiteral("relations")) || !fields.contains(QStringLiteral("mirrors")))
37 37 return fields;
38
5/9
✓ Branch 25 → 26 taken 20 times.
✗ Branch 25 → 30 not taken.
✓ Branch 26 → 27 taken 20 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 18 times.
✓ Branch 31 → 32 taken 20 times.
✗ Branch 31 → 33 not taken.
✗ Branch 346 → 347 not taken.
✗ Branch 346 → 348 not taken.
20 const bool onMe = !key.isEmpty() && key == meKey();
39 20 QVariantList own;
40 20 QSet<QString> listed;
41
6/6
✓ Branch 38 → 39 taken 20 times.
✓ Branch 39 → 40 taken 20 times.
✓ Branch 44 → 45 taken 20 times.
✓ Branch 45 → 46 taken 20 times.
✓ Branch 253 → 47 taken 17 times.
✓ Branch 253 → 254 taken 20 times.
57 for (const QVariant &item : fields.value(QStringLiteral("relations")).toList()) {
42
1/1
✓ Branch 48 → 49 taken 17 times.
17 QVariantMap entry = item.toMap();
43
2/2
✓ Branch 53 → 54 taken 17 times.
✓ Branch 54 → 55 taken 17 times.
34 const QVariantMap mirror = entry.value(QStringLiteral("mirrorOf")).toMap();
44
3/3
✓ Branch 59 → 60 taken 17 times.
✓ Branch 60 → 61 taken 10 times.
✓ Branch 60 → 65 taken 7 times.
17 if (mirror.isEmpty()) {
45
1/1
✓ Branch 62 → 63 taken 10 times.
10 own.append(entry);
46 10 continue;
47 }
48
2/2
✓ Branch 65 → 66 taken 7 times.
✓ Branch 66 → 67 taken 7 times.
7 listed.insert(mirrorId(mirror));
49 7 MirrorEdit edit;
50
2/2
✓ Branch 72 → 73 taken 7 times.
✓ Branch 73 → 74 taken 7 times.
14 edit.storedOnKey = mirror.value(QStringLiteral("storedOnKey")).toString();
51
2/2
✓ Branch 84 → 85 taken 7 times.
✓ Branch 85 → 86 taken 7 times.
14 edit.sourceIndex = mirror.value(QStringLiteral("sourceIndex"), -1).toInt();
52
2/2
✓ Branch 94 → 95 taken 7 times.
✓ Branch 95 → 96 taken 7 times.
14 edit.otherKey = entry.value(QStringLiteral("otherKey")).toString();
53
3/3
✓ Branch 106 → 107 taken 7 times.
✓ Branch 107 → 108 taken 7 times.
✓ Branch 108 → 109 taken 7 times.
14 edit.type = RelationVocabulary::canonical(entry.value(QStringLiteral("type")).toString());
54
6/25
✗ Branch 118 → 119 not taken.
✓ Branch 118 → 126 taken 7 times.
✗ Branch 123 → 124 not taken.
✗ Branch 124 → 125 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 129 → 130 not taken.
✓ Branch 129 → 131 taken 7 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 133 taken 7 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 135 taken 7 times.
✗ Branch 135 → 136 not taken.
✓ Branch 135 → 137 taken 7 times.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 139 taken 7 times.
✗ Branch 435 → 436 not taken.
✗ Branch 435 → 437 not taken.
✗ Branch 439 → 440 not taken.
✗ Branch 439 → 441 not taken.
✗ Branch 443 → 444 not taken.
✗ Branch 443 → 445 not taken.
✗ Branch 447 → 448 not taken.
✗ Branch 447 → 449 not taken.
✗ Branch 451 → 452 not taken.
✗ Branch 451 → 453 not taken.
7 edit.label = edit.type == QLatin1String("custom") ? entry.value(QStringLiteral("label")).toString().trimmed() : QString();
55
13/28
✓ Branch 143 → 144 taken 7 times.
✓ Branch 144 → 145 taken 7 times.
✓ Branch 146 → 147 taken 5 times.
✓ Branch 146 → 150 taken 2 times.
✓ Branch 148 → 149 taken 4 times.
✓ Branch 148 → 150 taken 1 time.
✓ Branch 151 → 152 taken 7 times.
✗ Branch 151 → 153 not taken.
✓ Branch 153 → 154 taken 7 times.
✗ Branch 153 → 155 not taken.
✓ Branch 155 → 156 taken 7 times.
✗ Branch 155 → 157 not taken.
✓ Branch 157 → 158 taken 7 times.
✗ Branch 157 → 159 not taken.
✓ Branch 159 → 160 taken 7 times.
✗ Branch 159 → 161 not taken.
✓ Branch 161 → 162 taken 4 times.
✓ Branch 161 → 163 taken 3 times.
✗ Branch 457 → 458 not taken.
✗ Branch 457 → 459 not taken.
✗ Branch 461 → 462 not taken.
✗ Branch 461 → 463 not taken.
✗ Branch 465 → 466 not taken.
✗ Branch 465 → 467 not taken.
✗ Branch 469 → 470 not taken.
✗ Branch 469 → 471 not taken.
✗ Branch 473 → 474 not taken.
✗ Branch 473 → 475 not taken.
14 if (edit.type == mirror.value(QStringLiteral("shownType")).toString() && edit.otherKey == edit.storedOnKey)
56 4 continue; // unchanged: stays on the other card
57
1/1
✓ Branch 163 → 164 taken 3 times.
3 edits->append(edit);
58
9/13
✓ Branch 164 → 165 taken 2 times.
✓ Branch 164 → 171 taken 1 time.
✓ Branch 166 → 167 taken 2 times.
✗ Branch 166 → 171 not taken.
✓ Branch 167 → 168 taken 2 times.
✓ Branch 169 → 170 taken 2 times.
✗ Branch 169 → 171 not taken.
✓ Branch 172 → 173 taken 2 times.
✓ Branch 172 → 174 taken 1 time.
✓ Branch 174 → 175 taken 2 times.
✓ Branch 174 → 176 taken 1 time.
✗ Branch 477 → 478 not taken.
✗ Branch 477 → 479 not taken.
3 if (onMe && !edit.otherKey.isEmpty() && !RelationVocabulary::inverseOf(edit.type).isEmpty())
59 2 continue; // D-6: written on the other card with the inverse type
60
1/1
✓ Branch 179 → 180 taken 1 time.
1 entry.remove(QStringLiteral("mirrorOf"));
61
1/1
✓ Branch 186 → 187 taken 1 time.
2 entry.insert(QStringLiteral("sourceIndex"), -1);
62 1 qint64 book = 0;
63 1 QString uid;
64
4/7
✓ Branch 191 → 192 taken 1 time.
✗ Branch 191 → 195 not taken.
✓ Branch 192 → 193 taken 1 time.
✓ Branch 193 → 194 taken 1 time.
✗ Branch 193 → 195 not taken.
✓ Branch 196 → 197 taken 1 time.
✗ Branch 196 → 233 not taken.
1 if (edit.otherKey == edit.storedOnKey && ContactQueries::splitKey(edit.storedOnKey, &book, &uid)) {
65 // The row showed "Me" or the linked name; the line names the card.
66
1/1
✓ Branch 201 → 202 taken 1 time.
2 entry.insert(QStringLiteral("targetUid"), uid);
67
1/1
✓ Branch 210 → 211 taken 1 time.
2 entry.insert(QStringLiteral("targetUri"), QString());
68
3/3
✓ Branch 215 → 216 taken 1 time.
✓ Branch 220 → 221 taken 1 time.
✓ Branch 224 → 225 taken 1 time.
3 entry.insert(QStringLiteral("targetName"), personEntry(edit.storedOnKey).value(QStringLiteral("name")));
69 }
70
1/1
✓ Branch 234 → 235 taken 1 time.
1 own.append(entry);
71 59 }
72
6/6
✓ Branch 259 → 260 taken 20 times.
✓ Branch 260 → 261 taken 20 times.
✓ Branch 265 → 266 taken 20 times.
✓ Branch 266 → 267 taken 20 times.
✓ Branch 307 → 268 taken 9 times.
✓ Branch 307 → 308 taken 20 times.
49 for (const QVariant &item : fields.value(QStringLiteral("mirrors")).toList()) {
73
1/1
✓ Branch 269 → 270 taken 9 times.
9 const QVariantMap mirror = item.toMap();
74
3/3
✓ Branch 270 → 271 taken 9 times.
✓ Branch 273 → 274 taken 7 times.
✓ Branch 273 → 275 taken 2 times.
9 if (listed.contains(mirrorId(mirror)))
75 7 continue;
76 2 MirrorEdit removed;
77
2/2
✓ Branch 279 → 280 taken 2 times.
✓ Branch 280 → 281 taken 2 times.
4 removed.storedOnKey = mirror.value(QStringLiteral("storedOnKey")).toString();
78
2/2
✓ Branch 291 → 292 taken 2 times.
✓ Branch 292 → 293 taken 2 times.
4 removed.sourceIndex = mirror.value(QStringLiteral("sourceIndex"), -1).toInt();
79
1/1
✓ Branch 297 → 298 taken 2 times.
2 edits->append(removed);
80 29 }
81 20 QVariantMap out = fields;
82
1/1
✓ Branch 314 → 315 taken 20 times.
40 out.insert(QStringLiteral("relations"), own);
83
1/1
✓ Branch 321 → 322 taken 20 times.
20 out.remove(QStringLiteral("mirrors"));
84 20 return out;
85 20 }
86
87 // One write per other card: its line numbers (sourceIndex) are only valid
88 // for the card as it was loaded.
89 52 int QuickContactActions::applyMirrorEdits(const QString &key, const QList<MirrorEdit> &edits) {
90 52 int failed = 0;
91
5/9
✓ Branch 3 → 4 taken 52 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 52 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 50 times.
✓ Branch 9 → 10 taken 52 times.
✗ Branch 9 → 11 not taken.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
52 const bool onMe = !key.isEmpty() && key == meKey();
92 52 QMap<QString, QList<MirrorEdit>> byCard;
93
2/2
✓ Branch 18 → 13 taken 5 times.
✓ Branch 18 → 19 taken 52 times.
57 for (const MirrorEdit &edit : edits)
94
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
5 byCard[edit.storedOnKey].append(edit);
95 52 QList<MirrorEdit> elsewhere;
96
4/4
✓ Branch 19 → 20 taken 52 times.
✓ Branch 116 → 117 taken 57 times.
✓ Branch 118 → 21 taken 5 times.
✓ Branch 118 → 119 taken 52 times.
57 for (auto it = byCard.cbegin(); it != byCard.cend(); ++it) {
97 5 const QString holder = it.key();
98 5 const QList<MirrorEdit> list = it.value();
99 5 const QVariantMap result = editPeople(
100 holder,
101
1/1
✓ Branch 26 → 27 taken 5 times.
5 [&](ContactCard *card) -> QString {
102 5 QList<ContactRelation> kept;
103
4/4
✓ Branch 2 → 3 taken 5 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 40 → 5 taken 6 times.
✓ Branch 40 → 41 taken 5 times.
11 for (const ContactRelation &relation : card->relations) {
104
1/1
✓ Branch 8 → 9 taken 6 times.
6 const auto edit = std::find_if(list.cbegin(), list.cend(), [&](const MirrorEdit &candidate) {
105 6 return candidate.sourceIndex == relation.sourceIndex;
106 });
107
5/6
✓ Branch 11 → 12 taken 5 times.
✓ Branch 11 → 14 taken 1 time.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 5 times.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 5 times.
6 if (edit == list.cend() || relation.readOnly()) {
108
1/1
✓ Branch 17 → 18 taken 1 time.
1 kept.append(relation);
109 1 continue;
110 }
111
1/1
✓ Branch 20 → 21 taken 5 times.
5 const QString inverse = RelationVocabulary::inverseOf(edit->type);
112
7/8
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 28 taken 3 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 28 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 28 not taken.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 36 taken 4 times.
5 if (onMe && edit->otherKey == holder && !inverse.isEmpty()) {
113 1 ContactRelation changed = relation;
114 1 changed.type = inverse;
115 1 changed.label.clear();
116
1/1
✓ Branch 33 → 34 taken 1 time.
1 kept.append(changed);
117 1 } // otherwise the line is gone here (saved on the edited card, or removed)
118 5 }
119 5 card->relations = kept;
120 10 return {};
121 5 },
122
2/2
✓ Branch 25 → 26 taken 5 times.
✓ Branch 27 → 28 taken 5 times.
15 tr("Relationship changed"));
123
8/8
✓ Branch 30 → 31 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
✓ Branch 35 → 36 taken 5 times.
✓ Branch 36 → 37 taken 5 times.
✓ Branch 37 → 38 taken 5 times.
✓ Branch 39 → 40 taken 5 times.
✓ Branch 57 → 32 taken 5 times.
✓ Branch 57 → 58 taken 5 times.
10 qCInfo(lcContactMirror) << "Mirrored relations on" << holder << "changed:" << list.size()
124
4/5
✓ Branch 44 → 45 taken 5 times.
✓ Branch 45 → 46 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
✗ Branch 46 → 48 not taken.
✓ Branch 49 → 50 taken 5 times.
10 << (result.value(QStringLiteral("ok")).toBool() ? "done" : "refused");
125
3/4
✓ Branch 62 → 63 taken 5 times.
✓ Branch 63 → 64 taken 5 times.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 92 taken 5 times.
10 if (!result.value(QStringLiteral("ok")).toBool()) {
126 ✗ ++failed;
127 ✗ qCWarning(lcContactMirror) << "Mirror write refused:" << result.value(QStringLiteral("error")).toString();
128 }
129
2/2
✓ Branch 111 → 94 taken 5 times.
✓ Branch 111 → 112 taken 5 times.
10 for (const MirrorEdit &edit : list)
130
5/6
✓ Branch 95 → 96 taken 2 times.
✓ Branch 95 → 104 taken 3 times.
✓ Branch 97 → 98 taken 2 times.
✗ Branch 97 → 104 not taken.
✓ Branch 99 → 100 taken 1 time.
✓ Branch 99 → 104 taken 1 time.
6 if (onMe && !edit.otherKey.isEmpty() && edit.otherKey != holder &&
131
6/9
✓ Branch 100 → 101 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
✗ Branch 102 → 104 not taken.
✓ Branch 105 → 106 taken 1 time.
✓ Branch 105 → 107 taken 4 times.
✓ Branch 107 → 108 taken 1 time.
✓ Branch 107 → 109 taken 4 times.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
6 !RelationVocabulary::inverseOf(edit.type).isEmpty())
132
1/1
✓ Branch 108 → 109 taken 1 time.
1 elsewhere.append(edit);
133 5 }
134 // The me card's row now names another person: saved on that person's card (D-6).
135
4/4
✓ Branch 119 → 120 taken 52 times.
✓ Branch 120 → 121 taken 52 times.
✓ Branch 139 → 122 taken 1 time.
✓ Branch 139 → 140 taken 52 times.
53 for (const MirrorEdit &edit : elsewhere)
136
4/5
✓ Branch 123 → 124 taken 1 time.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 129 → 130 taken 1 time.
✗ Branch 135 → 136 not taken.
✓ Branch 135 → 137 taken 1 time.
2 if (!addRelation(key, edit.otherKey, edit.type, edit.label).value(QStringLiteral("ok")).toBool()) ++failed;
137 52 return failed;
138 52 }
139