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: 93.9% 77 / 0 / 82
Functions: 100.0% 5 / 0 / 5
Branches: 79.6% 125 / 0 / 157

data/ContactGroupQueries.cpp
Line Branch Exec Source
1 // Contact groups — CATEGORIES names and group cards (kind = 'group') with
2 // their members. Part of ContactQueries (see ContactQueries.cpp).
3 #include "ContactQueries.h"
4
5 #include "ContactIndex.h"
6 #include "ContactSqlValue.h"
7 #include "MailAddressList.h"
8
9 #include <QSet>
10 #include <QSqlQuery>
11 #include <QUrl>
12 #include <algorithm>
13
14 namespace ContactQueries {
15
16 857 QList<Group> groups(QSqlDatabase db, qint64 bookId) {
17 857 QList<Group> result;
18 857 QHash<QString, int> index;
19
7/11
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 856 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 856 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 856 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
858 const QString bookFilter = bookId > 0 ? QStringLiteral(" AND c.book_id = %1").arg(bookId) : QString();
20
1/1
✓ Branch 13 → 14 taken 857 times.
857 QSqlQuery categories(db);
21
1/1
✓ Branch 19 → 20 taken 857 times.
2571 categories.exec(QStringLiteral("SELECT c.book_id, g.name, COUNT(DISTINCT c.id) FROM card_groups g "
22 "JOIN cards c ON c.id = g.card_id WHERE c.kind <> 'group'%1 "
23
1/1
✓ Branch 18 → 19 taken 857 times.
1714 "GROUP BY c.book_id, g.name").arg(bookFilter));
24
3/3
✓ Branch 46 → 47 taken 1022 times.
✓ Branch 47 → 24 taken 165 times.
✓ Branch 47 → 48 taken 857 times.
1022 while (categories.next()) {
25
6/8
✓ Branch 24 → 25 taken 165 times.
✓ Branch 25 → 26 taken 165 times.
✓ Branch 26 → 27 taken 165 times.
✓ Branch 27 → 28 taken 165 times.
✓ Branch 28 → 29 taken 165 times.
✓ Branch 29 → 30 taken 165 times.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
165 Group group{categories.value(0).toLongLong(), categories.value(1).toString(), 0, categories.value(2).toInt()};
26
4/4
✓ Branch 36 → 37 taken 165 times.
✓ Branch 37 → 38 taken 165 times.
✓ Branch 38 → 39 taken 165 times.
✓ Branch 39 → 40 taken 165 times.
165 index.insert(QString::number(group.bookId) + QLatin1Char('\n') + group.name, int(result.size()));
27
1/1
✓ Branch 43 → 44 taken 165 times.
165 result.append(group);
28 165 }
29
1/1
✓ Branch 48 → 49 taken 857 times.
857 QSqlQuery cards(db);
30
1/1
✓ Branch 54 → 55 taken 857 times.
2571 cards.exec(QStringLiteral("SELECT c.book_id, c.display_name, c.id, "
31 "(SELECT COUNT(*) FROM card_members m WHERE m.card_id = c.id) "
32
1/1
✓ Branch 53 → 54 taken 857 times.
1714 "FROM cards c WHERE c.kind = 'group'%1").arg(bookFilter));
33
3/3
✓ Branch 103 → 104 taken 910 times.
✓ Branch 104 → 59 taken 53 times.
✓ Branch 104 → 105 taken 857 times.
910 while (cards.next()) {
34
6/6
✓ Branch 59 → 60 taken 53 times.
✓ Branch 60 → 61 taken 53 times.
✓ Branch 63 → 64 taken 53 times.
✓ Branch 64 → 65 taken 53 times.
✓ Branch 65 → 66 taken 53 times.
✓ Branch 66 → 67 taken 53 times.
53 const QString key = cards.value(0).toString() + QLatin1Char('\n') + cards.value(1).toString();
35
1/2
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 83 taken 53 times.
53 if (index.contains(key)) {
36 ✗ Group &existing = result[index.value(key)];
37 ✗ existing.cardId = cards.value(2).toLongLong();
38 ✗ existing.memberCount += cards.value(3).toInt();
39 ✗ continue;
40 ✗ }
41
3/3
✓ Branch 83 → 84 taken 53 times.
✓ Branch 84 → 85 taken 53 times.
✓ Branch 88 → 89 taken 53 times.
212 result.append({cards.value(0).toLongLong(), cards.value(1).toString(), cards.value(2).toLongLong(),
42
1/1
✓ Branch 90 → 91 taken 53 times.
106 cards.value(3).toInt()});
43 53 }
44
3/3
✓ Branch 105 → 106 taken 857 times.
✓ Branch 106 → 107 taken 857 times.
✓ Branch 107 → 108 taken 857 times.
857 std::sort(result.begin(), result.end(), [](const Group &a, const Group &b) {
45
6/10
✓ Branch 2 → 3 taken 153 times.
✓ Branch 2 → 8 taken 104 times.
✓ Branch 3 → 4 taken 153 times.
✓ Branch 4 → 5 taken 153 times.
✓ Branch 11 → 12 taken 153 times.
✓ Branch 11 → 13 taken 104 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
514 return a.bookId == b.bookId ? ContactIndex::foldForSort(a.name) < ContactIndex::foldForSort(b.name)
46
2/2
✓ Branch 9 → 10 taken 153 times.
✓ Branch 9 → 11 taken 104 times.
514 : a.bookId < b.bookId;
47 });
48 857 return result;
49 1016 }
50
51 25 QStringList groupMemberKeys(QSqlDatabase db, qint64 bookId, const QString &group) {
52
1/1
✓ Branch 2 → 3 taken 25 times.
25 QSqlQuery q(db);
53
1/1
✓ Branch 6 → 7 taken 25 times.
25 q.prepare(QStringLiteral(
54 "SELECT c.uid FROM cards c WHERE c.book_id = :book AND c.kind <> 'group' AND ("
55 " EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id AND g.name = :group) OR"
56 " EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id"
57 " AND gc.kind = 'group' AND gc.display_name = :group"
58 " AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid)))"));
59
1/1
✓ Branch 14 → 15 taken 25 times.
50 q.bindValue(QStringLiteral(":book"), bookId);
60
2/2
✓ Branch 20 → 21 taken 25 times.
✓ Branch 24 → 25 taken 25 times.
50 q.bindValue(QStringLiteral(":group"), sqlValue(group));
61 25 QStringList keys;
62
2/3
✓ Branch 29 → 30 taken 25 times.
✓ Branch 30 → 31 taken 25 times.
✗ Branch 30 → 42 not taken.
25 if (q.exec())
63
3/3
✓ Branch 40 → 41 taken 99 times.
✓ Branch 41 → 32 taken 74 times.
✓ Branch 41 → 42 taken 25 times.
99 while (q.next())
64
4/4
✓ Branch 32 → 33 taken 74 times.
✓ Branch 33 → 34 taken 74 times.
✓ Branch 34 → 35 taken 74 times.
✓ Branch 35 → 36 taken 74 times.
74 keys.append(cardKey(bookId, q.value(0).toString()));
65 25 return keys;
66 25 }
67
68 2 QString groupCardKey(QSqlDatabase db, qint64 bookId, const QString &group) {
69
1/1
✓ Branch 2 → 3 taken 2 times.
2 QSqlQuery q(db);
70
1/1
✓ Branch 6 → 7 taken 2 times.
2 q.prepare(QStringLiteral("SELECT uid FROM cards WHERE book_id = ? AND kind = 'group' AND display_name = ? LIMIT 1"));
71
1/1
✓ Branch 11 → 12 taken 2 times.
2 q.addBindValue(bookId);
72
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
2 q.addBindValue(sqlValue(group));
73
6/17
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 27 not taken.
✓ Branch 21 → 22 taken 2 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 2 times.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 2 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 2 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
6 return q.exec() && q.next() ? cardKey(bookId, q.value(0).toString()) : QString();
74 2 }
75
76 13 QStringList groupMembers(QSqlDatabase db, qint64 bookId, const QString &group) {
77
1/1
✓ Branch 2 → 3 taken 13 times.
13 QSqlQuery q(db);
78
1/1
✓ Branch 6 → 7 taken 13 times.
13 q.prepare(QStringLiteral(
79 "SELECT c.display_name, c.primary_address FROM cards c WHERE c.book_id = :book AND c.kind <> 'group' "
80 "AND c.primary_address <> '' AND ("
81 " EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id AND g.name = :group) OR"
82 " EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id"
83 " AND gc.kind = 'group' AND gc.display_name = :group"
84 " AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid))) "
85 "ORDER BY c.sort_display"));
86
1/1
✓ Branch 14 → 15 taken 13 times.
26 q.bindValue(QStringLiteral(":book"), bookId);
87
2/2
✓ Branch 20 → 21 taken 13 times.
✓ Branch 24 → 25 taken 13 times.
26 q.bindValue(QStringLiteral(":group"), sqlValue(group));
88 13 QStringList members;
89 13 QSet<QString> seen;
90
2/3
✓ Branch 30 → 31 taken 13 times.
✓ Branch 31 → 32 taken 13 times.
✗ Branch 31 → 57 not taken.
13 if (q.exec())
91
3/3
✓ Branch 55 → 56 taken 39 times.
✓ Branch 56 → 33 taken 26 times.
✓ Branch 56 → 57 taken 13 times.
39 while (q.next()) {
92
2/2
✓ Branch 33 → 34 taken 26 times.
✓ Branch 34 → 35 taken 26 times.
26 const QString name = q.value(0).toString();
93
2/2
✓ Branch 36 → 37 taken 26 times.
✓ Branch 37 → 38 taken 26 times.
26 const QString address = q.value(1).toString();
94 26 const QString formatted = MailAddressList::format({name, address});
95
2/3
✓ Branch 43 → 44 taken 26 times.
✓ Branch 46 → 47 taken 26 times.
✗ Branch 46 → 51 not taken.
26 if (!seen.contains(address.toLower())) {
96
2/2
✓ Branch 47 → 48 taken 26 times.
✓ Branch 48 → 49 taken 26 times.
26 seen.insert(address.toLower());
97
1/1
✓ Branch 50 → 51 taken 26 times.
26 members.append(formatted);
98 }
99 26 }
100 // F-62 (SPRINT-104): a direct mailto member of the group (MEMBER:mailto:
101 // external@example.test, RFC 6350 §6.6.5) is a valid recipient even
102 // when no local card carries it. The raw MEMBER values stay untouched;
103 // unsupported schemes (tel, xmpp, …) are not email recipients and are
104 // skipped.
105
1/1
✓ Branch 57 → 58 taken 13 times.
13 QSqlQuery mailto(db);
106
1/1
✓ Branch 61 → 62 taken 13 times.
13 mailto.prepare(QStringLiteral(
107 "SELECT m.member FROM card_members m JOIN cards gc ON gc.id = m.card_id "
108 "WHERE gc.book_id = :book AND gc.kind = 'group' AND gc.display_name = :group"));
109
1/1
✓ Branch 69 → 70 taken 13 times.
26 mailto.bindValue(QStringLiteral(":book"), bookId);
110
2/2
✓ Branch 75 → 76 taken 13 times.
✓ Branch 79 → 80 taken 13 times.
26 mailto.bindValue(QStringLiteral(":group"), sqlValue(group));
111
2/3
✓ Branch 84 → 85 taken 13 times.
✓ Branch 85 → 86 taken 13 times.
✗ Branch 85 → 136 not taken.
13 if (mailto.exec()) {
112
3/3
✓ Branch 134 → 135 taken 19 times.
✓ Branch 135 → 87 taken 6 times.
✓ Branch 135 → 136 taken 13 times.
19 while (mailto.next()) {
113
3/3
✓ Branch 87 → 88 taken 6 times.
✓ Branch 88 → 89 taken 6 times.
✓ Branch 89 → 90 taken 6 times.
6 const QString value = mailto.value(0).toString().trimmed();
114
3/3
✓ Branch 95 → 96 taken 6 times.
✓ Branch 98 → 99 taken 2 times.
✓ Branch 98 → 100 taken 4 times.
6 if (!value.startsWith(QStringLiteral("mailto:"), Qt::CaseInsensitive))
115 2 continue;
116 // S106-168: RFC 6068 header fields (?subject=…) are no part of the address.
117 const QString address =
118
6/6
✓ Branch 100 → 101 taken 4 times.
✓ Branch 104 → 105 taken 4 times.
✓ Branch 105 → 106 taken 4 times.
✓ Branch 106 → 107 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✓ Branch 108 → 109 taken 4 times.
8 QUrl::fromPercentEncoding(value.mid(7).section(QLatin1Char('?'), 0, 0).toUtf8()).trimmed().toLower();
119 // A mailto that also resolves to a member card above adds nothing.
120
5/6
✓ Branch 115 → 116 taken 4 times.
✗ Branch 115 → 118 not taken.
✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 119 taken 3 times.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 122 taken 3 times.
4 if (address.isEmpty() || seen.contains(address))
121 1 continue;
122
1/1
✓ Branch 122 → 123 taken 3 times.
3 seen.insert(address);
123
1/1
✓ Branch 123 → 124 taken 3 times.
3 members.append(address);
124 7 }
125 }
126 13 return members;
127 39 }
128
129 } // namespace ContactQueries
130