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: 94.1% 127 / 0 / 135
Functions: 100.0% 13 / 0 / 13
Branches: 81.5% 203 / 0 / 249

data/RelationSuggestions.cpp
Line Branch Exec Source
1 #include "RelationSuggestions.h"
2
3 #include "ContactIndex.h"
4 #include "ContactQueries.h"
5 #include "MailAddressList.h"
6 #include "RelationIndex.h"
7
8 #include <QDateTime>
9 #include <QLoggingCategory>
10 #include <QSqlError>
11 #include <QSqlQuery>
12 #include <QVariant>
13 #include <algorithm>
14
15 Q_DECLARE_LOGGING_CATEGORY(lcContactPeople)
16
17 namespace RelationSuggestions {
18
19 namespace {
20
21
2/2
✓ Branch 2 → 3 taken 51 times.
✓ Branch 2 → 4 taken 49 times.
100 Pair pairOf(qint64 a, qint64 b) { return a < b ? Pair{a, b} : Pair{b, a}; }
22
23 // Every pair of the cards in one mail or appointment counts once.
24 77 void countTogether(const QSet<qint64> &cards, bool mail, QHash<Pair, Counts> *counts) {
25
5/6
✓ Branch 3 → 4 taken 69 times.
✓ Branch 3 → 6 taken 8 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 69 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 69 times.
77 if (cards.size() < 2 || cards.size() > 30) // a mailing list or a large meeting says little
26 8 return;
27
1/1
✓ Branch 12 → 13 taken 69 times.
69 QList<qint64> list(cards.cbegin(), cards.cend());
28
3/3
✓ Branch 13 → 14 taken 69 times.
✓ Branch 14 → 15 taken 69 times.
✓ Branch 15 → 16 taken 69 times.
69 std::sort(list.begin(), list.end());
29
2/2
✓ Branch 30 → 17 taken 138 times.
✓ Branch 30 → 31 taken 69 times.
207 for (int i = 0; i < list.size(); ++i)
30
2/2
✓ Branch 27 → 18 taken 69 times.
✓ Branch 27 → 28 taken 138 times.
207 for (int j = i + 1; j < list.size(); ++j) {
31
1/1
✓ Branch 21 → 22 taken 69 times.
69 Counts &c = (*counts)[Pair{list.at(i), list.at(j)}];
32
2/2
✓ Branch 22 → 23 taken 59 times.
✓ Branch 22 → 24 taken 10 times.
69 (mail ? c.mails : c.events) += 1;
33 }
34 69 }
35
36 183 void addAddresses(const QString &header, const Cards &cards, QSet<qint64> *found) {
37
5/5
✓ Branch 2 → 3 taken 183 times.
✓ Branch 3 → 4 taken 183 times.
✓ Branch 4 → 5 taken 183 times.
✓ Branch 15 → 6 taken 152 times.
✓ Branch 15 → 16 taken 183 times.
335 for (const MailAddressList::Mailbox &mailbox : MailAddressList::parse(header))
38
3/3
✓ Branch 7 → 8 taken 152 times.
✓ Branch 10 → 11 taken 120 times.
✓ Branch 10 → 13 taken 32 times.
152 if (const qint64 card = cards.byAddress.value(ContactIndex::addressKey(mailbox.address)); card > 0)
39
1/1
✓ Branch 11 → 12 taken 120 times.
303 found->insert(card);
40 183 }
41
42 // S106-173: UIDs compare without "urn:uuid:" and case, as every other UID
43 // resolution of the people cluster does.
44 378 QString uidKey(const QString &uid) {
45
1/1
✓ Branch 2 → 3 taken 378 times.
378 QString key = uid.trimmed();
46
3/3
✓ Branch 4 → 5 taken 378 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 10 taken 377 times.
378 if (key.startsWith(QLatin1String("urn:uuid:"), Qt::CaseInsensitive))
47
1/1
✓ Branch 6 → 7 taken 1 time.
1 key = key.mid(9);
48
1/1
✓ Branch 10 → 11 taken 378 times.
756 return key.toLower();
49 378 }
50
51 } // namespace
52
53 6 Cards readCards(const QSqlDatabase &contacts, const QString &meKey) {
54 6 Cards cards;
55
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery q(contacts);
56
1/1
✓ Branch 3 → 4 taken 6 times.
6 q.setForwardOnly(true);
57
2/3
✓ Branch 7 → 8 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
6 if (!q.exec(QStringLiteral("SELECT c.id, c.book_id, c.uid FROM cards c JOIN address_books b ON b.id = c.book_id"
58 " WHERE b.show_in_all = 1 AND c.kind <> 'group'")))
59 ✗ return cards;
60
3/3
✓ Branch 53 → 54 taken 188 times.
✓ Branch 54 → 13 taken 182 times.
✓ Branch 54 → 55 taken 6 times.
188 while (q.next()) {
61
5/5
✓ Branch 13 → 14 taken 182 times.
✓ Branch 14 → 15 taken 182 times.
✓ Branch 15 → 16 taken 182 times.
✓ Branch 16 → 17 taken 182 times.
✓ Branch 17 → 18 taken 182 times.
182 const QString key = ContactQueries::cardKey(q.value(1).toLongLong(), q.value(2).toString());
62
2/2
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 179 times.
182 if (key == meKey)
63 3 continue;
64
3/3
✓ Branch 24 → 25 taken 179 times.
✓ Branch 25 → 26 taken 179 times.
✓ Branch 26 → 27 taken 179 times.
179 cards.keyOf.insert(q.value(0).toLongLong(), key);
65
4/5
✓ Branch 28 → 29 taken 179 times.
✓ Branch 29 → 30 taken 179 times.
✓ Branch 30 → 31 taken 179 times.
✓ Branch 35 → 36 taken 179 times.
✗ Branch 35 → 47 not taken.
179 if (!cards.byUid.contains(uidKey(q.value(2).toString())))
66
6/6
✓ Branch 36 → 37 taken 179 times.
✓ Branch 37 → 38 taken 179 times.
✓ Branch 38 → 39 taken 179 times.
✓ Branch 39 → 40 taken 179 times.
✓ Branch 40 → 41 taken 179 times.
✓ Branch 41 → 42 taken 179 times.
179 cards.byUid.insert(uidKey(q.value(2).toString()), q.value(0).toLongLong());
67 182 }
68
1/1
✓ Branch 55 → 56 taken 6 times.
6 QSqlQuery addresses(contacts);
69
1/1
✓ Branch 56 → 57 taken 6 times.
6 addresses.setForwardOnly(true);
70
2/3
✓ Branch 60 → 61 taken 6 times.
✓ Branch 63 → 64 taken 6 times.
✗ Branch 63 → 93 not taken.
6 if (addresses.exec(QStringLiteral("SELECT card_id, address_key FROM card_addresses")))
71
3/3
✓ Branch 91 → 92 taken 174 times.
✓ Branch 92 → 65 taken 168 times.
✓ Branch 92 → 93 taken 6 times.
174 while (addresses.next())
72
14/22
✓ Branch 65 → 66 taken 168 times.
✓ Branch 66 → 67 taken 168 times.
✓ Branch 68 → 69 taken 165 times.
✓ Branch 68 → 74 taken 3 times.
✓ Branch 69 → 70 taken 165 times.
✓ Branch 70 → 71 taken 165 times.
✓ Branch 72 → 73 taken 165 times.
✗ Branch 72 → 74 not taken.
✓ Branch 75 → 76 taken 165 times.
✓ Branch 75 → 77 taken 3 times.
✓ Branch 77 → 78 taken 165 times.
✓ Branch 77 → 79 taken 3 times.
✓ Branch 79 → 80 taken 168 times.
✗ Branch 79 → 81 not taken.
✓ Branch 81 → 82 taken 165 times.
✓ Branch 81 → 91 taken 3 times.
✗ Branch 145 → 146 not taken.
✗ Branch 145 → 147 not taken.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
168 if (cards.keyOf.contains(addresses.value(0).toLongLong()) && !cards.byAddress.contains(addresses.value(1).toString()))
73
5/5
✓ Branch 82 → 83 taken 165 times.
✓ Branch 83 → 84 taken 165 times.
✓ Branch 84 → 85 taken 165 times.
✓ Branch 85 → 86 taken 165 times.
✓ Branch 86 → 87 taken 165 times.
165 cards.byAddress.insert(addresses.value(1).toString(), addresses.value(0).toLongLong());
74 6 return cards;
75 6 }
76
77 5 void countMails(const QSqlDatabase &mailCache, const Cards &cards, qint64 sinceSecs, QHash<Pair, Counts> *counts) {
78
1/1
✓ Branch 2 → 3 taken 5 times.
5 QSqlQuery q(mailCache);
79
1/1
✓ Branch 3 → 4 taken 5 times.
5 q.setForwardOnly(true);
80
1/1
✓ Branch 7 → 8 taken 5 times.
5 q.prepare(QStringLiteral("SELECT message_id, from_addr, to_addr, cc_addr FROM headers WHERE date >= ?"
81 " ORDER BY date DESC LIMIT ?"));
82
1/1
✓ Branch 12 → 13 taken 5 times.
5 q.addBindValue(sinceSecs);
83
1/1
✓ Branch 16 → 17 taken 5 times.
5 q.addBindValue(MaxMails);
84
2/3
✓ Branch 18 → 19 taken 5 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 36 taken 5 times.
5 if (!q.exec()) {
85 ✗ qCWarning(lcContactPeople) << "Suggestions: mails could not be read:" << q.lastError().text();
86 ✗ return;
87 }
88 5 QSet<QString> seen; // a mail in Inbox and Sent counts once
89 5 int mails = 0;
90
3/3
✓ Branch 67 → 68 taken 69 times.
✓ Branch 68 → 38 taken 64 times.
✓ Branch 68 → 69 taken 5 times.
69 while (q.next()) {
91
2/2
✓ Branch 38 → 39 taken 64 times.
✓ Branch 39 → 40 taken 64 times.
64 const QString id = q.value(0).toString();
92
5/6
✓ Branch 42 → 43 taken 64 times.
✗ Branch 42 → 46 not taken.
✓ Branch 44 → 45 taken 3 times.
✓ Branch 44 → 46 taken 61 times.
✓ Branch 47 → 48 taken 3 times.
✓ Branch 47 → 49 taken 61 times.
64 if (!id.isEmpty() && seen.contains(id))
93 3 continue;
94
1/1
✓ Branch 49 → 50 taken 61 times.
61 seen.insert(id);
95 61 QSet<qint64> found;
96
2/2
✓ Branch 58 → 52 taken 183 times.
✓ Branch 58 → 59 taken 61 times.
244 for (int column = 1; column <= 3; ++column)
97
3/3
✓ Branch 52 → 53 taken 183 times.
✓ Branch 53 → 54 taken 183 times.
✓ Branch 54 → 55 taken 183 times.
183 addAddresses(q.value(column).toString(), cards, &found);
98
1/1
✓ Branch 59 → 60 taken 61 times.
61 countTogether(found, true, counts);
99 61 ++mails;
100 64 }
101
6/6
✓ Branch 69 → 70 taken 5 times.
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
✓ Branch 75 → 76 taken 5 times.
✓ Branch 79 → 71 taken 5 times.
✓ Branch 79 → 80 taken 5 times.
10 qCDebug(lcContactPeople) << "Suggestions: mails looked at:" << mails;
102 5 }
103
104 6 void countEvents(const QSqlDatabase &calendar, const Cards &cards, qint64 sinceSecs, QHash<Pair, Counts> *counts) {
105
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery q(calendar);
106
1/1
✓ Branch 3 → 4 taken 6 times.
6 q.setForwardOnly(true);
107
1/1
✓ Branch 7 → 8 taken 6 times.
6 q.prepare(QStringLiteral("SELECT p.event_id, p.card_uid, p.address FROM event_people p JOIN events e"
108 " ON e.id = p.event_id WHERE e.dt_start >= ? ORDER BY p.event_id"));
109
1/1
✓ Branch 12 → 13 taken 6 times.
6 q.addBindValue(sinceSecs);
110
2/3
✓ Branch 14 → 15 taken 6 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 32 taken 6 times.
6 if (!q.exec()) {
111 ✗ qCWarning(lcContactPeople) << "Suggestions: appointments could not be read:" << q.lastError().text();
112 ✗ return;
113 }
114 6 qint64 current = -1;
115 6 QSet<qint64> found;
116
3/3
✓ Branch 72 → 73 taken 26 times.
✓ Branch 73 → 34 taken 20 times.
✓ Branch 73 → 74 taken 6 times.
26 while (q.next()) {
117
2/2
✓ Branch 34 → 35 taken 20 times.
✓ Branch 35 → 36 taken 20 times.
20 const qint64 event = q.value(0).toLongLong();
118
2/2
✓ Branch 37 → 38 taken 10 times.
✓ Branch 37 → 41 taken 10 times.
20 if (event != current) {
119
1/1
✓ Branch 38 → 39 taken 10 times.
10 countTogether(found, false, counts);
120 10 found.clear();
121 10 current = event;
122 }
123
3/3
✓ Branch 41 → 42 taken 20 times.
✓ Branch 42 → 43 taken 20 times.
✓ Branch 43 → 44 taken 20 times.
20 qint64 card = cards.byUid.value(uidKey(q.value(1).toString()), 0);
124
11/16
✓ Branch 48 → 49 taken 3 times.
✓ Branch 48 → 54 taken 17 times.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 50 → 51 taken 3 times.
✓ Branch 52 → 53 taken 3 times.
✗ Branch 52 → 54 not taken.
✓ Branch 55 → 56 taken 3 times.
✓ Branch 55 → 57 taken 17 times.
✓ Branch 57 → 58 taken 3 times.
✓ Branch 57 → 59 taken 17 times.
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 68 taken 17 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
20 if (card == 0 && !q.value(2).toString().isEmpty())
125
3/3
✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
3 card = cards.byAddress.value(ContactIndex::addressKey(q.value(2).toString()), 0);
126
1/2
✓ Branch 68 → 69 taken 20 times.
✗ Branch 68 → 71 not taken.
20 if (card > 0)
127
1/1
✓ Branch 69 → 70 taken 20 times.
20 found.insert(card);
128 }
129
1/1
✓ Branch 74 → 75 taken 6 times.
6 countTogether(found, false, counts);
130 6 }
131
132 5 QSet<Pair> relatedPairs(const QSqlDatabase &contacts) {
133 5 QSet<Pair> related;
134
5/5
✓ Branch 3 → 4 taken 5 times.
✓ Branch 4 → 5 taken 5 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 15 → 7 taken 101 times.
✓ Branch 15 → 16 taken 5 times.
106 for (const RelationIndex::Edge &edge : RelationIndex::allEdges(contacts))
135
3/4
✓ Branch 8 → 9 taken 99 times.
✓ Branch 8 → 13 taken 2 times.
✓ Branch 9 → 10 taken 99 times.
✗ Branch 9 → 13 not taken.
101 if (edge.toCardId > 0 && edge.fromCardId != edge.toCardId)
136
1/1
✓ Branch 11 → 12 taken 99 times.
104 related.insert(pairOf(edge.fromCardId, edge.toCardId));
137 5 return related;
138 ✗ }
139
140 5 QSet<Pair> dismissedPairs(const QSqlDatabase &contacts, const Cards &cards) {
141 5 QHash<QString, qint64> idOf;
142
2/2
✓ Branch 9 → 3 taken 177 times.
✓ Branch 9 → 10 taken 5 times.
182 for (auto it = cards.keyOf.cbegin(); it != cards.keyOf.cend(); ++it)
143
1/1
✓ Branch 5 → 6 taken 177 times.
177 idOf.insert(it.value(), it.key());
144 5 QSet<Pair> dismissed;
145
1/1
✓ Branch 11 → 12 taken 5 times.
5 QSqlQuery q(contacts);
146
2/3
✓ Branch 15 → 16 taken 5 times.
✓ Branch 18 → 19 taken 5 times.
✗ Branch 18 → 37 not taken.
5 if (q.exec(QStringLiteral("SELECT first_key, second_key FROM graph_dismissed")))
147
3/3
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 20 taken 1 time.
✓ Branch 36 → 37 taken 5 times.
6 while (q.next())
148
6/8
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 35 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 35 not taken.
1 if (const qint64 a = idOf.value(q.value(0).toString()), b = idOf.value(q.value(1).toString()); a > 0 && b > 0)
149
1/1
✓ Branch 33 → 34 taken 1 time.
1 dismissed.insert(pairOf(a, b));
150 5 return dismissed;
151 5 }
152
153 5 QList<Suggestion> select(const QHash<Pair, Counts> &counts, const QSet<Pair> &related, const QSet<Pair> &dismissed) {
154 5 QList<Suggestion> out;
155
2/2
✓ Branch 26 → 3 taken 14 times.
✓ Branch 26 → 27 taken 5 times.
19 for (auto it = counts.cbegin(); it != counts.cend(); ++it) {
156
10/10
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 7 taken 6 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 13 taken 5 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 13 taken 3 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 5 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 17 taken 5 times.
14 if ((it->mails < MinMails && it->events < MinEvents) || related.contains(it.key()) || dismissed.contains(it.key()))
157 9 continue;
158
1/1
✓ Branch 21 → 22 taken 5 times.
5 out.append({it.key().first, it.key().second, it->mails, it->events});
159 }
160
3/3
✓ Branch 27 → 28 taken 5 times.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
5 std::sort(out.begin(), out.end(), [](const Suggestion &a, const Suggestion &b) {
161 4 const int wa = a.mails + 3 * a.events, wb = b.mails + 3 * b.events;
162
1/4
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
4 return wa != wb ? wa > wb : (a.first != b.first ? a.first < b.first : a.second < b.second);
163 });
164
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 5 times.
5 if (out.size() > MaxSuggestions)
165 ✗ out.resize(MaxSuggestions);
166 5 return out;
167 ✗ }
168
169 1 bool dismiss(QSqlDatabase &contacts, const QString &firstKey, const QString &secondKey) {
170
1/1
✓ Branch 2 → 3 taken 1 time.
1 QSqlQuery q(contacts);
171
1/1
✓ Branch 6 → 7 taken 1 time.
1 q.prepare(QStringLiteral("INSERT OR REPLACE INTO graph_dismissed (first_key, second_key, dismissed_at) VALUES (?, ?, ?)"));
172
1/1
✓ Branch 12 → 13 taken 1 time.
1 q.addBindValue(std::min(firstKey, secondKey));
173
1/1
✓ Branch 17 → 18 taken 1 time.
1 q.addBindValue(std::max(firstKey, secondKey));
174
3/3
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
1 q.addBindValue(QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
175
1/1
✓ Branch 27 → 28 taken 1 time.
2 return q.exec();
176 1 }
177
178 1 bool forgetDismissed(QSqlDatabase &contacts) {
179
1/1
✓ Branch 2 → 3 taken 1 time.
1 QSqlQuery q(contacts);
180
1/1
✓ Branch 6 → 7 taken 1 time.
2 return q.exec(QStringLiteral("DELETE FROM graph_dismissed"));
181 1 }
182
183 } // namespace RelationSuggestions
184