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.2% 124 / 0 / 133
Functions: 100.0% 14 / 0 / 14
Branches: 73.1% 141 / 0 / 193

presentation/QuickContactGraphSuggestions.cpp
Line Branch Exec Source
1 // Relationship suggestions of QuickContactGraph (Sprint 103 S103.5.8, I-8):
2 // opt-in, computed on a worker from contacts.db, the mail cache and the
3 // calendar (RelationSuggestions); only dismissals are stored.
4
5 #include "QuickContactGraph.h"
6
7 #include "data/ContactStore.h"
8 #include "data/RelationSuggestions.h"
9
10 #include <QDateTime>
11 #include <QElapsedTimer>
12 #include <QLoggingCategory>
13 #include <QPointer>
14 #include <QSettings>
15 #include <QSqlDatabase>
16 #include <QUuid>
17 #include <algorithm>
18
19 Q_DECLARE_LOGGING_CATEGORY(lcGraph)
20
21 namespace {
22
23 // A read-only connection of its own on the worker thread; "" when missing.
24 10 QSqlDatabase openReadOnly(const QString &path, const QString &name) {
25
1/1
✓ Branch 5 → 6 taken 10 times.
10 QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), name);
26
1/1
✓ Branch 8 → 9 taken 10 times.
10 db.setDatabaseName(path);
27
1/1
✓ Branch 12 → 13 taken 10 times.
10 db.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY"));
28
4/7
✓ Branch 16 → 17 taken 10 times.
✗ Branch 16 → 19 not taken.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 10 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 10 times.
10 if (path.isEmpty() || !db.open())
29 ✗ return {};
30
1/1
✓ Branch 24 → 25 taken 10 times.
10 return db;
31 10 }
32
33 } // namespace
34
35 1603 QuickContactGraph::~QuickContactGraph() { m_pool.waitForDone(); }
36
37 109 RelationGraph::Layers QuickContactGraph::layers() const {
38 109 RelationGraph::Layers layers;
39 109 layers.suggestions = m_suggestions;
40 109 return layers;
41 }
42
43 796 void QuickContactGraph::setSuggestionSources(std::function<QString()> mailCachePath, const QString &calendarPath) {
44 796 m_mailCachePath = std::move(mailCachePath);
45 796 m_calendarPath = calendarPath;
46 796 }
47
48 6 void QuickContactGraph::setSuggestions(bool on) {
49
2/4
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
6 if (on == m_suggestions || !m_suggestionsAllowed)
50 ✗ return;
51 6 m_suggestions = on;
52
2/2
✓ Branch 5 → 6 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
12 QSettings().setValue(QStringLiteral("contacts/graphSuggestions"), on);
53
8/8
✓ Branch 16 → 17 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 3 times.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 29 → 18 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
12 qCInfo(lcGraph) << "Relationship suggestions" << (on ? "on" : "off");
54
2/2
✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 32 taken 3 times.
6 if (on) {
55 3 computeSuggestions();
56 } else {
57 3 m_suggestionCounts.clear();
58 3 m_input.suggestions.clear();
59 3 ++m_suggestionRun; // a running worker's answer is dropped
60 3 m_suggestionDone = m_suggestionRun;
61 }
62 6 emit suggestionsChanged();
63
2/6
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 6 times.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 6 times.
✗ Branch 41 → 43 not taken.
6 if (!m_centerKey.isEmpty() || m_overview)
64 6 rebuild();
65 }
66
67 6 bool QuickContactGraph::suggestionsExplained() const {
68
3/3
✓ Branch 2 → 3 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
12 return QSettings().value(QStringLiteral("contacts/graphSuggestionsExplained"), false).toBool();
69 }
70
71 1 void QuickContactGraph::setSuggestionsExplained(bool shown) {
72
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
2 QSettings().setValue(QStringLiteral("contacts/graphSuggestionsExplained"), shown);
73 1 emit suggestionsChanged();
74 1 }
75
76 5 void QuickContactGraph::computeSuggestions() {
77
3/6
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 5 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
5 if (m_databasePath.isEmpty() || !m_suggestionsAllowed)
78 ✗ return;
79 5 const int run = ++m_suggestionRun;
80 5 const QString contactsPath = m_databasePath;
81
2/3
✓ Branch 11 → 12 taken 5 times.
✗ Branch 11 → 13 not taken.
✓ Branch 12 → 14 taken 5 times.
5 const QString mailPath = m_mailCachePath ? m_mailCachePath() : QString();
82 5 const QString calendarPath = m_calendarPath;
83
1/1
✓ Branch 15 → 16 taken 5 times.
5 const QString me = meKey();
84
1/1
✓ Branch 16 → 17 taken 5 times.
5 QPointer<QuickContactGraph> self(this);
85
1/9
✓ Branch 22 → 23 taken 5 times.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
5 m_pool.start([self, run, contactsPath, mailPath, calendarPath, me] {
86 5 QElapsedTimer timer;
87 5 timer.start();
88 5 QList<RelationSuggestions::Suggestion> found;
89
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 4 → 5 taken 5 times.
5 const QString suffix = QUuid::createUuid().toString(QUuid::WithoutBraces);
90 {
91
1/1
✓ Branch 5 → 6 taken 5 times.
5 ContactStore store;
92
2/3
✓ Branch 6 → 7 taken 5 times.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 7 → 46 not taken.
5 if (store.open(contactsPath)) {
93
1/1
✓ Branch 8 → 9 taken 5 times.
5 const QSqlDatabase contacts = store.database();
94
1/1
✓ Branch 9 → 10 taken 5 times.
5 const RelationSuggestions::Cards cards = RelationSuggestions::readCards(contacts, me);
95 5 const qint64 since = QDateTime::currentSecsSinceEpoch() - 2LL * 366 * 24 * 3600; // two years
96 5 QHash<RelationSuggestions::Pair, RelationSuggestions::Counts> counts;
97 {
98
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
5 QSqlDatabase mail = openReadOnly(mailPath, QStringLiteral("suggest-mail-") + suffix);
99
2/3
✓ Branch 19 → 20 taken 5 times.
✓ Branch 20 → 21 taken 5 times.
✗ Branch 20 → 22 not taken.
5 if (mail.isOpen())
100
1/1
✓ Branch 21 → 22 taken 5 times.
5 RelationSuggestions::countMails(mail, cards, since, &counts);
101
2/2
✓ Branch 25 → 26 taken 5 times.
✓ Branch 26 → 27 taken 5 times.
5 QSqlDatabase calendar = openReadOnly(calendarPath, QStringLiteral("suggest-cal-") + suffix);
102
2/3
✓ Branch 30 → 31 taken 5 times.
✓ Branch 31 → 32 taken 5 times.
✗ Branch 31 → 33 not taken.
5 if (calendar.isOpen())
103
1/1
✓ Branch 32 → 33 taken 5 times.
5 RelationSuggestions::countEvents(calendar, cards, since, &counts);
104 5 }
105
2/2
✓ Branch 36 → 37 taken 5 times.
✓ Branch 37 → 38 taken 5 times.
10 found = RelationSuggestions::select(counts, RelationSuggestions::relatedPairs(contacts),
106
1/1
✓ Branch 35 → 36 taken 5 times.
15 RelationSuggestions::dismissedPairs(contacts, cards));
107 5 }
108 5 }
109
2/2
✓ Branch 50 → 51 taken 5 times.
✓ Branch 51 → 52 taken 5 times.
5 QSqlDatabase::removeDatabase(QStringLiteral("suggest-mail-") + suffix);
110
2/2
✓ Branch 58 → 59 taken 5 times.
✓ Branch 59 → 60 taken 5 times.
5 QSqlDatabase::removeDatabase(QStringLiteral("suggest-cal-") + suffix);
111
9/9
✓ Branch 63 → 64 taken 5 times.
✓ Branch 67 → 68 taken 5 times.
✓ Branch 68 → 69 taken 5 times.
✓ Branch 70 → 71 taken 5 times.
✓ Branch 71 → 72 taken 5 times.
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
✓ Branch 78 → 65 taken 5 times.
✓ Branch 78 → 79 taken 5 times.
10 qCInfo(lcGraph) << "Relationship suggestions:" << found.size() << "in" << timer.elapsed() << "ms";
112
1/1
✓ Branch 82 → 83 taken 5 times.
5 QMetaObject::invokeMethod(
113 5 self.data(),
114
0/2
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
10 [self, run, found] {
115
4/8
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 5 times.
5 if (!self || run != self->m_suggestionRun || !self->m_suggestions)
116 ✗ return;
117 5 self->m_suggestionDone = run;
118 5 self->m_suggestionCounts.clear();
119
2/2
✓ Branch 24 → 17 taken 5 times.
✓ Branch 24 → 25 taken 5 times.
10 for (const RelationSuggestions::Suggestion &s : found)
120
1/1
✓ Branch 21 → 22 taken 5 times.
5 self->m_suggestionCounts.insert({s.first, s.second}, {s.mails, s.events});
121 5 self->m_loaded = false; // the input carries them
122 // S106-276: suggestionCount reads m_input, so the input is refreshed
123 // before the NOTIFY, not after it.
124
2/6
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 5 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✓ Branch 33 → 34 taken 5 times.
✗ Branch 33 → 36 not taken.
5 if (!self->m_centerKey.isEmpty() || self->m_overview)
125 5 self->rebuild();
126 else
127 ✗ self->ensureInput();
128 5 emit self->suggestionsChanged();
129 },
130 Qt::QueuedConnection);
131 5 });
132 5 }
133
134 7 QString QuickContactGraph::suggestionSentence(const RelationGraph::Node &a, const RelationGraph::Node &b) const {
135 7 const QPair<qint64, qint64> pair{std::min(a.cardId, b.cardId), std::max(a.cardId, b.cardId)};
136 7 const QPair<int, int> counts = m_suggestionCounts.value(pair);
137 7 QStringList reasons;
138
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 13 taken 5 times.
7 if (counts.first > 0)
139
3/5
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
2 reasons << (counts.first == 1 ? tr("1 mail") : tr("%n mails", "", counts.first));
140
2/2
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 20 taken 2 times.
7 if (counts.second > 0)
141
3/5
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 5 times.
✗ Branch 15 → 17 not taken.
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
5 reasons << (counts.second == 1 ? tr("1 appointment") : tr("%n appointments", "", counts.second));
142 //: Graph: a suggested relationship — "Ana Silva and Rui Costa: together in 7 mails and 3 appointments".
143
4/4
✓ Branch 20 → 21 taken 7 times.
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
14 return tr("%1 and %2: together in %3 — related?").arg(a.name, b.name, reasons.join(tr(" and ")));
144 7 }
145
146 2 bool QuickContactGraph::dismissSuggestion(int from, int to) {
147
5/10
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 8 not taken.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
2 if (from < 0 || to < 0 || from >= m_graph.nodes.size() || to >= m_graph.nodes.size())
148 ✗ return false;
149 // S106-277: a rebuild while the edge menu was open re-indexes the nodes;
150 // only a pair that is a suggestion of the current graph is dismissed.
151
1/1
✓ Branch 14 → 15 taken 2 times.
2 const bool suggested = std::any_of(m_graph.edges.cbegin(), m_graph.edges.cend(), [from, to](const RelationGraph::Edge &edge) {
152
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 10 taken 2 times.
10 return edge.kind == QLatin1String("suggestion") &&
153
4/8
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 3 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
10 ((edge.from == from && edge.to == to) || (edge.from == to && edge.to == from));
154 });
155
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 29 taken 1 time.
2 if (!suggested) {
156
7/7
✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 18 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
2 qCInfo(lcGraph) << "Suggestion dismiss refused: no suggestion between" << from << to;
157 1 return false;
158 }
159 1 const RelationGraph::Node &a = m_graph.nodes.at(from);
160 1 const RelationGraph::Node &b = m_graph.nodes.at(to);
161
1/1
✓ Branch 31 → 32 taken 1 time.
1 ContactStore store;
162
2/3
✓ Branch 32 → 33 taken 1 time.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 1 time.
1 if (!store.open(m_databasePath))
163 ✗ return false;
164
1/1
✓ Branch 35 → 36 taken 1 time.
1 QSqlDatabase db = store.database();
165
2/3
✓ Branch 36 → 37 taken 1 time.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 1 time.
1 if (!RelationSuggestions::dismiss(db, a.key, b.key))
166 ✗ return false;
167
1/1
✓ Branch 42 → 43 taken 1 time.
1 m_suggestionCounts.remove({std::min(a.cardId, b.cardId), std::max(a.cardId, b.cardId)});
168 1 m_loaded = false;
169
5/5
✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 52 → 45 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
2 qCInfo(lcGraph) << "Suggestion dismissed";
170
1/1
✓ Branch 53 → 54 taken 1 time.
1 rebuild();
171
1/1
✓ Branch 54 → 55 taken 1 time.
1 emit suggestionsChanged();
172 1 return true;
173 1 }
174
175 1 void QuickContactGraph::forgetDismissedSuggestions() {
176
1/1
✓ Branch 2 → 3 taken 1 time.
1 ContactStore store;
177
2/3
✓ Branch 3 → 4 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 9 not taken.
1 if (store.open(m_databasePath)) {
178
1/1
✓ Branch 5 → 6 taken 1 time.
1 QSqlDatabase db = store.database();
179
1/1
✓ Branch 6 → 7 taken 1 time.
1 RelationSuggestions::forgetDismissed(db);
180 1 }
181
5/5
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 11 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
2 qCInfo(lcGraph) << "Dismissed suggestions forgotten";
182
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
1 if (m_suggestions)
183
1/1
✓ Branch 20 → 21 taken 1 time.
1 computeSuggestions();
184 1 }
185