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.3% 149 / 0 / 158
Functions: 100.0% 19 / 0 / 19
Branches: 75.9% 208 / 0 / 274

presentation/PlaceLookupSource.cpp
Line Branch Exec Source
1 #include "PlaceLookupSource.h"
2
3 #include <QCoreApplication>
4 #include <QHostAddress>
5 #include <QJsonArray>
6 #include <QJsonDocument>
7 #include <QJsonObject>
8 #include <QLocale>
9 #include <QLoggingCategory>
10 #include <QNetworkAccessManager>
11 #include <QNetworkReply>
12 #include <QNetworkRequest>
13 #include <QTimer>
14 #include <QUrlQuery>
15 #include <algorithm>
16
17
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 6 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.
7 Q_LOGGING_CATEGORY(lcPlaceLookup, "mailjd.contacts.lookup")
18
19 namespace {
20
21 14 bool loopback(const QString &host) {
22
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 14 times.
14 if (host.compare(QLatin1String("localhost"), Qt::CaseInsensitive) == 0)
23 ✗ return true;
24
1/1
✓ Branch 6 → 7 taken 14 times.
14 const QHostAddress address(host);
25
5/6
✓ Branch 7 → 8 taken 14 times.
✓ Branch 8 → 9 taken 13 times.
✓ Branch 8 → 12 taken 1 time.
✓ Branch 9 → 10 taken 13 times.
✓ Branch 10 → 11 taken 13 times.
✗ Branch 10 → 12 not taken.
14 return !address.isNull() && address.isLoopback();
26 14 }
27
28 // Nominatim's place_rank: 26+ street and house, 16–25 town to suburb,
29 // 5–15 state and county, below that the country.
30 4 QString precisionOf(int rank) {
31
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 8 taken 1 time.
4 if (rank >= 26)
32 3 return QStringLiteral("exact");
33
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 14 not taken.
1 if (rank >= 16)
34 1 return QStringLiteral("city");
35 ✗ if (rank >= 5)
36 ✗ return QStringLiteral("region");
37 ✗ return QStringLiteral("country");
38 }
39
40 } // namespace
41
42 812 PlaceLookupSource::PlaceLookupSource(QObject *parent)
43
5/11
✓ Branch 4 → 5 taken 812 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 812 times.
✓ Branch 7 → 8 taken 812 times.
✓ Branch 8 → 9 taken 812 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 812 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
812 : QObject(parent), m_network(new QNetworkAccessManager(this)), m_pace(new QTimer(this)),
44
2/2
✓ Branch 3 → 4 taken 812 times.
✓ Branch 11 → 12 taken 812 times.
1624 m_server(defaultServer()) {
45
1/1
✓ Branch 15 → 16 taken 812 times.
812 m_network->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
46
1/1
✓ Branch 16 → 17 taken 812 times.
812 m_pace->setSingleShot(true);
47 // A coarse timer may fire up to 5 % early: the usage policy is at most one
48 // request per second, so the pace is precise.
49
1/1
✓ Branch 17 → 18 taken 812 times.
812 m_pace->setTimerType(Qt::PreciseTimer);
50
1/1
✓ Branch 18 → 19 taken 812 times.
812 m_pace->setInterval(IntervalMs);
51
1/1
✓ Branch 19 → 20 taken 812 times.
812 connect(m_pace, &QTimer::timeout, this, &PlaceLookupSource::pump);
52 812 }
53
54 1624 PlaceLookupSource::~PlaceLookupSource() { cancelAll(); }
55
56 3256 QString PlaceLookupSource::defaultServer() { return QStringLiteral("https://nominatim.openstreetmap.org"); }
57
58 826 QString PlaceLookupSource::validate(const QString &server) {
59
2/2
✓ Branch 2 → 3 taken 826 times.
✓ Branch 3 → 4 taken 826 times.
826 const QUrl url(server.trimmed());
60
6/12
✓ Branch 5 → 6 taken 826 times.
✓ Branch 6 → 7 taken 826 times.
✗ Branch 6 → 11 not taken.
✓ Branch 8 → 9 taken 826 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 826 times.
✓ Branch 13 → 14 taken 826 times.
✗ Branch 13 → 15 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 24 taken 826 times.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
1652 if (!url.isValid() || url.host().isEmpty())
61 ✗ return tr("Enter a web address such as %1.").arg(defaultServer());
62
16/24
✓ Branch 25 → 26 taken 826 times.
✓ Branch 27 → 28 taken 14 times.
✓ Branch 27 → 37 taken 812 times.
✓ Branch 29 → 30 taken 14 times.
✓ Branch 31 → 32 taken 14 times.
✗ Branch 31 → 36 not taken.
✓ Branch 33 → 34 taken 14 times.
✓ Branch 34 → 35 taken 14 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 13 times.
✓ Branch 38 → 39 taken 14 times.
✓ Branch 38 → 40 taken 812 times.
✓ Branch 40 → 41 taken 14 times.
✓ Branch 40 → 42 taken 812 times.
✓ Branch 42 → 43 taken 826 times.
✗ Branch 42 → 44 not taken.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 47 taken 825 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
840 if (url.scheme() != QLatin1String("https") && !(url.scheme() == QLatin1String("http") && loopback(url.host())))
63
1/1
✓ Branch 45 → 46 taken 1 time.
1 return tr("Only secure (https) addresses are allowed.");
64
2/3
✓ Branch 48 → 49 taken 825 times.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 54 taken 825 times.
825 if (!url.userInfo().isEmpty())
65 ✗ return tr("The address must not contain a user name or password.");
66
7/8
✓ Branch 54 → 55 taken 825 times.
✓ Branch 55 → 56 taken 824 times.
✓ Branch 55 → 58 taken 1 time.
✓ Branch 56 → 57 taken 824 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 59 taken 824 times.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 63 taken 824 times.
825 if (url.hasQuery() || url.hasFragment())
67
1/1
✓ Branch 61 → 62 taken 1 time.
1 return tr("Enter the server without a search part.");
68 824 return {};
69 826 }
70
71 31 QUrl PlaceLookupSource::requestUrl(const QString &server, const Query &query) {
72
2/2
✓ Branch 2 → 3 taken 31 times.
✓ Branch 3 → 4 taken 31 times.
31 QUrl url(server.trimmed());
73
1/1
✓ Branch 6 → 7 taken 31 times.
31 QString path = url.path();
74
3/3
✓ Branch 11 → 12 taken 33 times.
✓ Branch 12 → 8 taken 2 times.
✓ Branch 12 → 13 taken 31 times.
33 while (path.endsWith(QLatin1Char('/')))
75
1/1
✓ Branch 8 → 9 taken 2 times.
2 path.chop(1);
76
2/2
✓ Branch 16 → 17 taken 31 times.
✓ Branch 17 → 18 taken 31 times.
31 url.setPath(path + QStringLiteral("/search"));
77
1/1
✓ Branch 21 → 22 taken 31 times.
31 QUrlQuery parameters;
78
1/1
✓ Branch 28 → 29 taken 31 times.
62 parameters.addQueryItem(QStringLiteral("format"), QStringLiteral("jsonv2"));
79
1/1
✓ Branch 39 → 40 taken 31 times.
62 parameters.addQueryItem(QStringLiteral("limit"), QStringLiteral("1"));
80 155 const auto add = [&parameters](const char *name, const QString &value) {
81
3/3
✓ Branch 2 → 3 taken 155 times.
✓ Branch 5 → 6 taken 124 times.
✓ Branch 5 → 13 taken 31 times.
155 if (!value.trimmed().isEmpty())
82
3/3
✓ Branch 6 → 7 taken 124 times.
✓ Branch 8 → 9 taken 124 times.
✓ Branch 9 → 10 taken 124 times.
124 parameters.addQueryItem(QString::fromLatin1(name), value.trimmed());
83 155 };
84
2/8
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 51 taken 31 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 51 not taken.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 51 not taken.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 31 times.
31 if (query.street.isEmpty() && query.postalCode.isEmpty() && query.locality.isEmpty()) {
85 ✗ add("q", query.text);
86 } else {
87
1/1
✓ Branch 54 → 55 taken 31 times.
31 add("street", query.street);
88
1/1
✓ Branch 55 → 56 taken 31 times.
31 add("postalcode", query.postalCode);
89
1/1
✓ Branch 56 → 57 taken 31 times.
31 add("city", query.locality);
90
1/1
✓ Branch 57 → 58 taken 31 times.
31 add("state", query.region);
91
1/1
✓ Branch 58 → 59 taken 31 times.
31 add("country", query.country);
92 }
93
1/1
✓ Branch 59 → 60 taken 31 times.
31 url.setQuery(parameters);
94 31 return url;
95 31 }
96
97 11 PlaceLookupSource::Result PlaceLookupSource::parse(const QByteArray &json) {
98 11 Result result;
99 11 QJsonParseError error;
100
1/1
✓ Branch 2 → 3 taken 11 times.
11 const QJsonDocument document = QJsonDocument::fromJson(json, &error);
101
6/7
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 4 → 5 taken 10 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 10 times.
11 if (error.error != QJsonParseError::NoError || !document.isArray())
102 1 return result;
103 10 result.answered = true;
104 // S106-249: "[]" is the normal not-found answer; first() of an empty
105 // array is undefined behaviour (an assertion in debug builds).
106
1/1
✓ Branch 10 → 11 taken 10 times.
10 const QJsonArray places = document.array();
107
3/3
✓ Branch 11 → 12 taken 10 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 5 times.
10 if (places.isEmpty())
108 5 return result;
109
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
5 const QJsonObject first = places.first().toObject();
110 5 bool latOk = false, lonOk = false;
111
3/3
✓ Branch 20 → 21 taken 5 times.
✓ Branch 21 → 22 taken 5 times.
✓ Branch 22 → 23 taken 5 times.
5 const double latitude = first.value(QStringLiteral("lat")).toString().toDouble(&latOk);
112
3/3
✓ Branch 30 → 31 taken 5 times.
✓ Branch 31 → 32 taken 5 times.
✓ Branch 32 → 33 taken 5 times.
5 const double longitude = first.value(QStringLiteral("lon")).toString().toDouble(&lonOk);
113
7/12
✓ Branch 37 → 38 taken 5 times.
✗ Branch 37 → 43 not taken.
✓ Branch 38 → 39 taken 5 times.
✗ Branch 38 → 43 not taken.
✓ Branch 39 → 40 taken 5 times.
✗ Branch 39 → 43 not taken.
✓ Branch 40 → 41 taken 4 times.
✓ Branch 40 → 43 taken 1 time.
✓ Branch 41 → 42 taken 4 times.
✗ Branch 41 → 43 not taken.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 4 times.
5 if (!latOk || !lonOk || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180)
114 1 return result; // an empty array: not found
115 4 result.found = true;
116 4 result.latitude = latitude;
117 4 result.longitude = longitude;
118
3/3
✓ Branch 47 → 48 taken 4 times.
✓ Branch 48 → 49 taken 4 times.
✓ Branch 49 → 50 taken 4 times.
4 result.precision = precisionOf(first.value(QStringLiteral("place_rank")).toInt());
119
3/3
✓ Branch 58 → 59 taken 4 times.
✓ Branch 59 → 60 taken 4 times.
✓ Branch 60 → 61 taken 4 times.
4 result.placeName = first.value(QStringLiteral("name")).toString().left(200);
120 4 return result;
121 11 }
122
123 817 void PlaceLookupSource::setServer(const QString &server) {
124
5/5
✓ Branch 2 → 3 taken 817 times.
✓ Branch 4 → 5 taken 810 times.
✓ Branch 4 → 6 taken 7 times.
✓ Branch 5 → 7 taken 810 times.
✓ Branch 6 → 7 taken 7 times.
817 const QString text = server.trimmed().isEmpty() ? defaultServer() : server.trimmed();
125
7/11
✓ Branch 8 → 9 taken 817 times.
✓ Branch 10 → 11 taken 817 times.
✗ Branch 10 → 13 not taken.
✓ Branch 12 → 13 taken 811 times.
✓ Branch 12 → 14 taken 6 times.
✓ Branch 15 → 16 taken 817 times.
✗ Branch 15 → 17 not taken.
✓ Branch 17 → 18 taken 811 times.
✓ Branch 17 → 19 taken 6 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
817 if (!validate(text).isEmpty() || text == m_server)
126 811 return;
127 6 m_server = text;
128 817 }
129
130 12 void PlaceLookupSource::lookup(const QString &id, const Query &query) {
131
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12 times.
13 if (std::any_of(m_queue.cbegin(), m_queue.cend(), [&id](const Pending &p) { return p.id == id; }))
132 ✗ return;
133 12 m_queue.append({id, query});
134
5/6
✓ Branch 12 → 13 taken 7 times.
✓ Branch 12 → 16 taken 5 times.
✓ Branch 14 → 15 taken 7 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 7 times.
✓ Branch 17 → 19 taken 5 times.
12 if (!m_active && !m_pace->isActive())
135 7 pump();
136 12 }
137
138 816 void PlaceLookupSource::cancelAll() {
139 816 m_queue.clear();
140 816 m_pace->stop();
141
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 11 taken 814 times.
816 if (m_active) {
142 2 QNetworkReply *reply = m_active;
143 2 m_active = nullptr;
144 2 reply->disconnect(this);
145 2 reply->abort();
146 2 reply->deleteLater();
147 }
148 816 }
149
150 26 void PlaceLookupSource::cancelMatching(const QString &prefix) {
151 28 m_queue.removeIf([&prefix](const Pending &pending) { return pending.id.startsWith(prefix); });
152
5/6
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 8 taken 24 times.
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 15 taken 24 times.
26 if (m_active && m_activeId.startsWith(prefix)) {
153 2 QNetworkReply *reply = m_active;
154 2 m_active = nullptr;
155 2 reply->disconnect(this);
156 2 reply->abort();
157 2 reply->deleteLater();
158 }
159 // A running pace starts the next lookup itself (usage policy).
160
2/2
✓ Branch 16 → 17 taken 24 times.
✓ Branch 16 → 18 taken 2 times.
26 if (!m_pace->isActive())
161 24 pump();
162 26 }
163
164 // One request at a time; the next starts IntervalMs after the previous one
165 // started (usage policy: at most one per second).
166 38 void PlaceLookupSource::pump() {
167
7/9
✓ Branch 3 → 4 taken 38 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 11 times.
✓ Branch 5 → 8 taken 27 times.
✓ Branch 6 → 7 taken 11 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 11 times.
✓ Branch 10 → 11 taken 27 times.
✓ Branch 10 → 12 taken 11 times.
38 if (m_active || m_queue.isEmpty() || m_pace->isActive())
168 27 return;
169
1/1
✓ Branch 12 → 13 taken 11 times.
11 const Pending next = m_queue.takeFirst();
170
2/2
✓ Branch 13 → 14 taken 11 times.
✓ Branch 14 → 15 taken 11 times.
11 QNetworkRequest request(requestUrl(m_server, next.query));
171
1/1
✓ Branch 29 → 30 taken 11 times.
11 request.setHeader(QNetworkRequest::UserAgentHeader,
172 22 QStringLiteral("MailJD/%1 (+https://git.jdsoft.de/jens/qmailjd)")
173
4/9
✓ Branch 20 → 21 taken 11 times.
✓ Branch 22 → 23 taken 11 times.
✗ Branch 22 → 26 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 27 → 28 taken 11 times.
✓ Branch 33 → 34 taken 11 times.
✗ Branch 33 → 35 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
33 .arg(QCoreApplication::applicationVersion().isEmpty() ? QStringLiteral("dev")
174 : QCoreApplication::applicationVersion()));
175
6/6
✓ Branch 38 → 39 taken 11 times.
✓ Branch 39 → 40 taken 11 times.
✓ Branch 44 → 45 taken 11 times.
✓ Branch 45 → 46 taken 11 times.
✓ Branch 46 → 47 taken 11 times.
✓ Branch 47 → 48 taken 11 times.
11 request.setRawHeader("Accept-Language", QLocale().name().replace(QLatin1Char('_'), QLatin1Char('-')).toLatin1());
176
1/1
✓ Branch 53 → 54 taken 11 times.
11 request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual);
177
1/1
✓ Branch 56 → 57 taken 11 times.
11 request.setAttribute(QNetworkRequest::CookieLoadControlAttribute, QNetworkRequest::Manual);
178
1/1
✓ Branch 59 → 60 taken 11 times.
11 request.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
179
1/1
✓ Branch 61 → 62 taken 11 times.
11 request.setTransferTimeout(15000);
180
1/1
✓ Branch 62 → 63 taken 11 times.
11 QNetworkReply *reply = m_network->get(request);
181
1/1
✓ Branch 63 → 64 taken 11 times.
11 m_active = reply;
182 11 m_activeId = next.id;
183 11 ++m_requestsStarted;
184
1/1
✓ Branch 65 → 66 taken 11 times.
11 m_pace->start();
185 11 const QString id = next.id;
186
1/1
✓ Branch 67 → 68 taken 11 times.
11 emit requestStarted(id);
187
1/1
✓ Branch 69 → 70 taken 11 times.
18 connect(reply, &QNetworkReply::finished, this, [this, reply, id] { done(reply, id); });
188 11 }
189
190 7 void PlaceLookupSource::done(QNetworkReply *reply, const QString &id) {
191
1/1
✓ Branch 2 → 3 taken 7 times.
7 reply->deleteLater();
192
1/2
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 6 not taken.
7 if (m_active == reply)
193
1/1
✓ Branch 5 → 6 taken 7 times.
7 m_active = nullptr;
194
2/2
✓ Branch 6 → 7 taken 7 times.
✓ Branch 7 → 8 taken 7 times.
7 const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
195 7 Result result;
196
6/7
✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 13 taken 2 times.
✓ Branch 11 → 12 taken 5 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 24 taken 2 times.
7 if (reply->error() == QNetworkReply::NoError && status == 200) {
197
1/1
✓ Branch 15 → 16 taken 5 times.
5 const QByteArray bytes = reply->read(MaxReplyBytes + 1);
198
1/2
✓ Branch 17 → 18 taken 5 times.
✗ Branch 17 → 22 not taken.
5 if (bytes.size() <= MaxReplyBytes)
199
1/1
✓ Branch 18 → 19 taken 5 times.
5 result = parse(bytes);
200 5 }
201 // Counts only, never the address (D-16).
202
10/10
✓ Branch 24 → 25 taken 7 times.
✓ Branch 28 → 29 taken 7 times.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 5 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 2 times.
✓ Branch 36 → 37 taken 7 times.
✓ Branch 46 → 26 taken 7 times.
✓ Branch 46 → 47 taken 7 times.
14 qCInfo(lcPlaceLookup) << "Address lookup:" << (result.found ? "found" : result.answered ? "not found" : "failed")
203
5/5
✓ Branch 37 → 38 taken 7 times.
✓ Branch 38 → 39 taken 7 times.
✓ Branch 39 → 40 taken 7 times.
✓ Branch 40 → 41 taken 7 times.
✓ Branch 42 → 43 taken 7 times.
7 << int(reply->error()) << status << "queued:" << m_queue.size();
204
1/1
✓ Branch 47 → 48 taken 7 times.
7 emit finished(id, result);
205
2/3
✓ Branch 48 → 49 taken 7 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 7 times.
7 if (!m_pace->isActive())
206 ✗ pump();
207 7 }
208