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: 98.2% 112 / 0 / 114
Functions: 100.0% 8 / 0 / 8
Branches: 84.8% 151 / 0 / 178

data/RelationInference.cpp
Line Branch Exec Source
1 #include "RelationInference.h"
2
3 #include "RelationVocabulary.h"
4
5 #include <QHash>
6 #include <QLoggingCategory>
7 #include <QMap>
8 #include <QPair>
9 #include <QSet>
10 #include <algorithm>
11
12 // The category of ContactPeopleStore, defined here too: mailjd_vcard stands alone.
13
3/7
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
✓ 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.
1 Q_LOGGING_CATEGORY(lcRelationInference, "mailjd.contacts.relations")
14
15 namespace RelationInference {
16
17 namespace {
18
19 struct Kin {
20 const char *type;
21 int up;
22 int down;
23 };
24
25 // Blood relationships as kinship vectors (see the header).
26 const Kin kKin[] = {
27 {"parent", 1, 0}, {"grandparent", 2, 0}, {"great-grandparent", 3, 0},
28 {"child", 0, 1}, {"grandchild", 0, 2}, {"great-grandchild", 0, 3},
29 {"sibling", 1, 1}, {"aunt-uncle", 2, 1}, {"great-aunt-uncle", 3, 1},
30 {"niece-nephew", 1, 2}, {"grand-niece-nephew", 1, 3}, {"cousin", 2, 2},
31 };
32
33 struct Rule {
34 const char *first; // what B is to A
35 const char *second; // what C is to B
36 const char *result; // what C is to A
37 };
38
39 // Partners and in-laws, each with its mirror. "Aunt" and "uncle" include
40 // the partner of one, as in everyday speech; step relations (a parent's new
41 // partner, a partner's child) are left to the user — an explicit line wins.
42 const Rule kInLaw[] = {
43 {"spouse", "parent", "parent-in-law"}, // my partner's parent
44 {"child", "spouse", "child-in-law"}, // my child's partner
45 {"spouse", "sibling", "sibling-in-law"}, // my partner's sibling
46 {"sibling", "spouse", "sibling-in-law"}, // my sibling's partner
47 {"aunt-uncle", "spouse", "aunt-uncle"}, // my aunt's husband is my uncle
48 {"spouse", "niece-nephew", "niece-nephew"}, // my partner's niece is my niece
49 {"spouse", "aunt-uncle", "aunt-uncle"}, // my partner's uncle
50 {"niece-nephew", "spouse", "niece-nephew"}, // my nephew's wife
51 };
52
53 using Pair = QPair<qint64, qint64>;
54
55 // Blood composition: B is (u1, d1) from A, C is (u2, d2) from B. False when
56 // the result is not certain.
57 17858 bool combine(int u1, int d1, int u2, int d2, int *up, int *down) {
58
2/2
✓ Branch 2 → 3 taken 2446 times.
✓ Branch 2 → 9 taken 15412 times.
17858 if (d1 == 0) { // B is an ancestor of A
59
2/2
✓ Branch 3 → 4 taken 2031 times.
✓ Branch 3 → 5 taken 415 times.
2446 if (u2 >= 1) { // further up, then down a branch that is not A's line
60 2031 *up = u1 + u2;
61 2031 *down = d2;
62 2031 return true;
63 }
64
4/4
✓ Branch 5 → 6 taken 394 times.
✓ Branch 5 → 8 taken 21 times.
✓ Branch 6 → 7 taken 393 times.
✓ Branch 6 → 8 taken 1 time.
415 if (u1 == 1 && d2 == 1) { // my parent's child (not me): my sibling
65 393 *up = 1;
66 393 *down = 1;
67 393 return true;
68 }
69 22 return false; // my grandparent's child: my parent or my aunt
70 }
71
2/2
✓ Branch 9 → 10 taken 2027 times.
✓ Branch 9 → 11 taken 13385 times.
15412 if (u2 == 0) { // further down B's own line
72 2027 *up = u1;
73 2027 *down = d1 + d2;
74 2027 return true;
75 }
76 // Down to B, then up again: certain only through full siblings, who share
77 // every ancestor.
78
8/8
✓ Branch 11 → 12 taken 5327 times.
✓ Branch 11 → 16 taken 8058 times.
✓ Branch 12 → 13 taken 2479 times.
✓ Branch 12 → 16 taken 2848 times.
✓ Branch 13 → 14 taken 1907 times.
✓ Branch 13 → 15 taken 572 times.
✓ Branch 14 → 15 taken 1718 times.
✓ Branch 14 → 16 taken 189 times.
13385 if (u1 == 1 && d1 == 1 && (u2 >= 2 || d2 <= 1)) { // my sibling's parent, aunt, cousin, sibling
79 2290 *up = u2;
80 2290 *down = d2;
81 2290 return true;
82 }
83
8/8
✓ Branch 16 → 17 taken 3720 times.
✓ Branch 16 → 21 taken 7375 times.
✓ Branch 17 → 18 taken 2100 times.
✓ Branch 17 → 21 taken 1620 times.
✓ Branch 18 → 19 taken 1529 times.
✓ Branch 18 → 20 taken 571 times.
✓ Branch 19 → 20 taken 1340 times.
✓ Branch 19 → 21 taken 189 times.
11095 if (u2 == 1 && d2 == 1 && (d1 >= 2 || u1 <= 1)) { // the sibling of my child, niece, cousin
84 1911 *up = u1;
85 1911 *down = d1;
86 1911 return true;
87 }
88 9184 return false; // my child's parent, my niece's parent, my aunt's sibling: ambiguous
89 }
90
91 } // namespace
92
93 188914 bool kinship(const QString &type, int *up, int *down) {
94
1/1
✓ Branch 2 → 3 taken 188914 times.
188914 const QString known = RelationVocabulary::canonical(type);
95
2/2
✓ Branch 9 → 4 taken 1770766 times.
✓ Branch 9 → 10 taken 114802 times.
1885568 for (const Kin &kin : kKin)
96
2/2
✓ Branch 6 → 7 taken 74112 times.
✓ Branch 6 → 8 taken 1696654 times.
1770766 if (known == QLatin1String(kin.type)) {
97 74112 *up = kin.up;
98 74112 *down = kin.down;
99 74112 return true;
100 }
101 114802 return false;
102 188914 }
103
104 8652 QString typeOfKinship(int up, int down) {
105
2/2
✓ Branch 9 → 3 taken 43370 times.
✓ Branch 9 → 10 taken 485 times.
43855 for (const Kin &kin : kKin)
106
4/4
✓ Branch 3 → 4 taken 14370 times.
✓ Branch 3 → 8 taken 29000 times.
✓ Branch 4 → 5 taken 8167 times.
✓ Branch 4 → 8 taken 6203 times.
43370 if (kin.up == up && kin.down == down)
107
1/1
✓ Branch 6 → 7 taken 8167 times.
8167 return QString::fromLatin1(kin.type);
108 485 return {};
109 }
110
111 128885 QString compose(const QString &t1, const QString &t2) {
112 128885 int u1 = 0, d1 = 0, u2 = 0, d2 = 0, up = 0, down = 0;
113
8/8
✓ Branch 2 → 3 taken 128885 times.
✓ Branch 3 → 4 taken 51495 times.
✓ Branch 3 → 7 taken 77390 times.
✓ Branch 4 → 5 taken 51495 times.
✓ Branch 5 → 6 taken 17858 times.
✓ Branch 5 → 7 taken 33637 times.
✓ Branch 8 → 9 taken 17858 times.
✓ Branch 8 → 14 taken 111027 times.
128885 if (kinship(t1, &u1, &d1) && kinship(t2, &u2, &d2))
114
3/3
✓ Branch 10 → 11 taken 8652 times.
✓ Branch 10 → 12 taken 9206 times.
✓ Branch 11 → 13 taken 8652 times.
17858 return combine(u1, d1, u2, d2, &up, &down) ? typeOfKinship(up, down) : QString();
115 // S106-171: the in-law table is matched on canonical ids like the blood
116 // branch (aliased spellings composed on one path only).
117
1/1
✓ Branch 14 → 15 taken 111027 times.
111027 const QString c1 = RelationVocabulary::canonical(t1);
118
1/1
✓ Branch 15 → 16 taken 111027 times.
111027 const QString c2 = RelationVocabulary::canonical(t2);
119
2/2
✓ Branch 30 → 17 taken 865818 times.
✓ Branch 30 → 31 taken 106878 times.
972696 for (const Rule &rule : kInLaw)
120
6/6
✓ Branch 19 → 20 taken 43382 times.
✓ Branch 19 → 24 taken 822436 times.
✓ Branch 22 → 23 taken 4149 times.
✓ Branch 22 → 24 taken 39233 times.
✓ Branch 25 → 26 taken 4149 times.
✓ Branch 25 → 29 taken 861669 times.
865818 if (c1 == QLatin1String(rule.first) && c2 == QLatin1String(rule.second))
121
1/1
✓ Branch 27 → 28 taken 4149 times.
4149 return QString::fromLatin1(rule.result);
122 106878 return {};
123 111027 }
124
125 304 QList<Inferred> infer(const QList<Fact> &stored) {
126 // Known facts by holder; every related pair (either way) is closed for inference.
127 304 QHash<qint64, QList<Fact>> byHolder;
128 304 QSet<Pair> related;
129 304 QSet<Pair> ambiguous; // pairs whose derivations disagree (first < second)
130 304 int count = 0;
131 26886 const auto add = [&](const Fact &fact) {
132 26886 byHolder[fact.holder].append(fact);
133
1/1
✓ Branch 5 → 6 taken 26886 times.
26886 related.insert({fact.holder, fact.other});
134 26886 ++count;
135 26886 };
136
2/2
✓ Branch 37 → 6 taken 8361 times.
✓ Branch 37 → 38 taken 304 times.
8665 for (const Fact &fact : stored) {
137
3/6
✓ Branch 7 → 8 taken 8361 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 8361 times.
✗ Branch 8 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8361 times.
8361 if (fact.holder <= 0 || fact.other <= 0 || fact.holder == fact.other)
138 ✗ continue;
139
1/1
✓ Branch 11 → 12 taken 8361 times.
8361 const QString type = RelationVocabulary::canonical(fact.type);
140
1/1
✓ Branch 12 → 13 taken 8361 times.
8361 const QString mirror = RelationVocabulary::inverseOf(type);
141
2/2
✓ Branch 15 → 16 taken 8172 times.
✓ Branch 15 → 20 taken 189 times.
8361 if (!related.contains({fact.holder, fact.other}))
142
1/1
✓ Branch 17 → 18 taken 8172 times.
8172 add({fact.holder, fact.other, type});
143
6/6
✓ Branch 21 → 22 taken 8173 times.
✓ Branch 21 → 26 taken 188 times.
✓ Branch 24 → 25 taken 7984 times.
✓ Branch 24 → 26 taken 189 times.
✓ Branch 27 → 28 taken 7984 times.
✓ Branch 27 → 32 taken 377 times.
8361 if (!mirror.isEmpty() && !related.contains({fact.other, fact.holder}))
144
1/1
✓ Branch 29 → 30 taken 7984 times.
7984 add({fact.other, fact.holder, mirror});
145 8361 }
146 304 QList<Inferred> out;
147
2/4
✓ Branch 170 → 171 taken 719 times.
✗ Branch 170 → 172 not taken.
✓ Branch 171 → 39 taken 719 times.
✗ Branch 171 → 172 not taken.
719 for (int round = 0; round < MaxRounds && count < MaxFacts; ++round) {
148 // The derivations of this round by pair, seen from the smaller card id
149 // so that both directions of one pair compare.
150 719 QMap<Pair, Inferred> found;
151
1/1
✓ Branch 39 → 40 taken 719 times.
719 QList<qint64> holders = byHolder.keys();
152
3/3
✓ Branch 40 → 41 taken 719 times.
✓ Branch 41 → 42 taken 719 times.
✓ Branch 42 → 43 taken 719 times.
719 std::sort(holders.begin(), holders.end());
153
2/2
✓ Branch 129 → 46 taken 18097 times.
✓ Branch 129 → 130 taken 719 times.
18816 for (qint64 a : std::as_const(holders))
154
4/4
✓ Branch 48 → 49 taken 18097 times.
✓ Branch 49 → 50 taken 18097 times.
✓ Branch 125 → 51 taken 65490 times.
✓ Branch 125 → 126 taken 18097 times.
83587 for (const Fact &ab : byHolder.value(a))
155
4/4
✓ Branch 53 → 54 taken 65490 times.
✓ Branch 54 → 55 taken 65490 times.
✓ Branch 121 → 56 taken 357194 times.
✓ Branch 121 → 122 taken 65490 times.
422684 for (const Fact &bc : byHolder.value(ab.other)) {
156 357194 const qint64 c = bc.other;
157
8/8
✓ Branch 57 → 58 taken 292268 times.
✓ Branch 57 → 64 taken 64926 times.
✓ Branch 60 → 61 taken 129980 times.
✓ Branch 60 → 64 taken 162288 times.
✓ Branch 63 → 64 taken 1128 times.
✓ Branch 63 → 65 taken 128852 times.
✓ Branch 66 → 67 taken 228342 times.
✓ Branch 66 → 68 taken 128852 times.
357194 if (c == a || related.contains({a, c}) || related.contains({c, a}))
158 344906 continue;
159
1/1
✓ Branch 68 → 69 taken 128852 times.
128852 const QString type = compose(ab.type, bc.type);
160 128852 const Pair key{std::min(a, c), std::max(a, c)};
161
6/6
✓ Branch 73 → 74 taken 12294 times.
✓ Branch 73 → 76 taken 116558 times.
✓ Branch 75 → 76 taken 6 times.
✓ Branch 75 → 77 taken 12288 times.
✓ Branch 78 → 79 taken 116564 times.
✓ Branch 78 → 80 taken 12288 times.
128852 if (type.isEmpty() || ambiguous.contains(key))
162 116564 continue;
163 const Inferred derived =
164
0/8
✗ Branch 200 → 201 not taken.
✗ Branch 200 → 203 not taken.
✗ Branch 201 → 202 not taken.
✗ Branch 201 → 203 not taken.
✗ Branch 204 → 205 not taken.
✗ Branch 204 → 207 not taken.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
6145 a < c ? Inferred{a, c, type, ab.other, ab.type, bc.type}
165
1/9
✓ Branch 85 → 86 taken 6143 times.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 195 not taken.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 199 not taken.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
12286 : Inferred{c, a, RelationVocabulary::inverseOf(type), ab.other,
166
4/4
✓ Branch 80 → 81 taken 6145 times.
✓ Branch 80 → 85 taken 6143 times.
✓ Branch 86 → 87 taken 6143 times.
✓ Branch 87 → 88 taken 6143 times.
18431 RelationVocabulary::inverseOf(bc.type), RelationVocabulary::inverseOf(ab.type)};
167
1/1
✓ Branch 89 → 90 taken 12288 times.
12288 const auto it = found.constFind(key);
168
3/3
✓ Branch 90 → 91 taken 12288 times.
✓ Branch 92 → 93 taken 5366 times.
✓ Branch 92 → 94 taken 6922 times.
12288 if (it == found.cend()) {
169
1/1
✓ Branch 93 → 112 taken 5366 times.
5366 found.insert(key, derived);
170
2/2
✓ Branch 96 → 97 taken 1 time.
✓ Branch 96 → 112 taken 6921 times.
6922 } else if (it->type != derived.type) {
171
6/6
✓ Branch 97 → 98 taken 1 time.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
✓ Branch 103 → 104 taken 1 time.
✓ Branch 108 → 99 taken 1 time.
✓ Branch 108 → 109 taken 1 time.
2 qCDebug(lcRelationInference) << "Inference: contradicting derivations for cards" << key.first
172
1/1
✓ Branch 104 → 105 taken 1 time.
1 << key.second;
173
1/1
✓ Branch 109 → 110 taken 1 time.
1 found.remove(key);
174
1/1
✓ Branch 110 → 111 taken 1 time.
1 ambiguous.insert(key);
175 }
176 212439 }
177
3/3
✓ Branch 130 → 131 taken 719 times.
✓ Branch 131 → 132 taken 304 times.
✓ Branch 131 → 133 taken 415 times.
719 if (found.isEmpty())
178 304 break;
179
4/4
✓ Branch 134 → 135 taken 415 times.
✓ Branch 135 → 136 taken 415 times.
✓ Branch 159 → 137 taken 5365 times.
✓ Branch 159 → 160 taken 415 times.
5780 for (const Inferred &f : std::as_const(found)) {
180
1/1
✓ Branch 139 → 140 taken 5365 times.
5365 add({f.holder, f.other, f.type});
181
1/1
✓ Branch 141 → 142 taken 5365 times.
5365 out.append(f);
182 // The mirror, so both cards show it.
183
2/3
✓ Branch 142 → 143 taken 5365 times.
✓ Branch 144 → 145 taken 5365 times.
✗ Branch 144 → 154 not taken.
5365 if (const QString mirror = RelationVocabulary::inverseOf(f.type); !mirror.isEmpty()) {
184
1/1
✓ Branch 146 → 147 taken 5365 times.
5365 add({f.other, f.holder, mirror});
185 5365 out.append({f.other, f.holder, mirror, f.via, RelationVocabulary::inverseOf(f.secondType),
186 5365 RelationVocabulary::inverseOf(f.firstType)});
187 5365 }
188
1/2
✗ Branch 155 → 156 not taken.
✓ Branch 155 → 157 taken 5365 times.
5365 if (count >= MaxFacts)
189 ✗ break;
190 }
191 1023 }
192
3/3
✓ Branch 172 → 173 taken 304 times.
✓ Branch 173 → 174 taken 304 times.
✓ Branch 174 → 175 taken 304 times.
304 std::sort(out.begin(), out.end(), [](const Inferred &x, const Inferred &y) {
193
2/2
✓ Branch 2 → 3 taken 63467 times.
✓ Branch 2 → 4 taken 14609 times.
78076 return x.holder != y.holder ? x.holder < y.holder : x.other < y.other;
194 });
195 304 return out;
196 32555 }
197
198 } // namespace RelationInference
199