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: 96.6% 84 / 0 / 87
Functions: 90.0% 9 / 0 / 10
Branches: 78.5% 175 / 0 / 223

app/core/ProfileActivation.cpp
Line Branch Exec Source
1 #include "ProfileActivation.h"
2 #include "app/MailtoUrl.h"
3 #include <QCryptographicHash>
4 #include <QDeadlineTimer>
5 #include <algorithm>
6 #include <QDir>
7 #include <QFileInfo>
8 #include <QJsonDocument>
9 #include <QJsonObject>
10 #include <QLocalSocket>
11 #include <QStandardPaths>
12 #include <QThread>
13 #include <QTimer>
14
15 namespace {
16 constexpr qsizetype MaximumPacket = 256 * 1024;
17 constexpr qsizetype MaximumUrl = 65536;
18 }
19
20
1/1
✓ Branch 3 → 4 taken 5 times.
5 ProfileActivation::ProfileActivation(QObject *parent) : QObject(parent) {
21
1/1
✓ Branch 7 → 8 taken 5 times.
5 m_server.setSocketOptions(QLocalServer::UserAccessOption);
22
1/1
✓ Branch 8 → 9 taken 5 times.
5 m_server.setMaxPendingConnections(8);
23
1/1
✓ Branch 9 → 10 taken 5 times.
5 connect(&m_server, &QLocalServer::newConnection, this, [this] {
24
2/2
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 16 taken 11 times.
22 while (auto *client = m_server.nextPendingConnection()) {
25
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 11 times.
11 if (m_clients >= 8) { client->abort(); client->deleteLater(); continue; }
26 11 ++m_clients;
27 11 client->setReadBufferSize(MaximumPacket + 1);
28
1/1
✓ Branch 9 → 10 taken 11 times.
11 connect(client, &QLocalSocket::disconnected, this, [this, client] {
29 11 --m_clients;
30 11 client->deleteLater();
31 11 });
32
1/1
✓ Branch 11 → 12 taken 11 times.
11 QTimer::singleShot(5000, client, [client] { client->abort(); });
33
1/1
✓ Branch 12 → 13 taken 11 times.
11 connect(client, &QLocalSocket::readyRead, this, [this, client] {
34
3/4
✓ Branch 2 → 3 taken 12 times.
✓ Branch 3 → 4 taken 12 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12 times.
13 if (client->property("answered").toBool()) return;
35
8/8
✓ Branch 7 → 8 taken 12 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 12 taken 10 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 11 times.
12 if (!client->canReadLine() && client->bytesAvailable() <= MaximumPacket) return;
36
1/1
✓ Branch 16 → 17 taken 11 times.
11 client->setProperty("answered", true);
37
1/1
✓ Branch 18 → 19 taken 11 times.
11 const auto bytes = client->readAll();
38
1/1
✓ Branch 19 → 20 taken 11 times.
11 const auto document = QJsonDocument::fromJson(bytes);
39
1/1
✓ Branch 20 → 21 taken 11 times.
11 const auto request = document.object();
40
3/3
✓ Branch 21 → 22 taken 11 times.
✓ Branch 22 → 23 taken 11 times.
✓ Branch 23 → 24 taken 11 times.
11 const auto url = request.value("mailto").toString();
41 // Protocol v2 carries the forwarder's xdg-activation token; v1
42 // requests (three keys, no token) stay admissible so an old
43 // binary can still activate a new owner.
44
3/3
✓ Branch 26 → 27 taken 11 times.
✓ Branch 27 → 28 taken 11 times.
✓ Branch 28 → 29 taken 11 times.
11 const int version = request.value("version").toInt();
45
3/3
✓ Branch 31 → 32 taken 11 times.
✓ Branch 32 → 33 taken 11 times.
✓ Branch 33 → 34 taken 11 times.
11 const QString token = request.value("activationToken").toString();
46
4/4
✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 44 taken 8 times.
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 45 taken 1 time.
16 const bool tokenValid = token.isEmpty() ||
47
3/3
✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 1 time.
5 (token.size() <= 4096 && std::none_of(token.cbegin(), token.cend(),
48 13 [](const QChar c) {
49
3/4
✓ Branch 3 → 4 taken 13 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 12 times.
13 return c.isNull() || c.category() == QChar::Other_Control;
50 11 }));
51
4/5
✓ Branch 48 → 49 taken 10 times.
✓ Branch 49 → 50 taken 10 times.
✗ Branch 49 → 74 not taken.
✓ Branch 54 → 55 taken 8 times.
✓ Branch 54 → 74 taken 2 times.
20 const bool valid = bytes.size() <= MaximumPacket && document.isObject() &&
52
5/5
✓ Branch 50 → 51 taken 10 times.
✓ Branch 51 → 52 taken 6 times.
✓ Branch 51 → 53 taken 4 times.
✓ Branch 55 → 56 taken 5 times.
✓ Branch 55 → 57 taken 3 times.
18 request.size() == (version == 2 ? 4 : 3) &&
53
1/2
✓ Branch 56 → 57 taken 5 times.
✗ Branch 56 → 74 not taken.
5 (version == 1 || version == 2) &&
54
7/11
✓ Branch 57 → 58 taken 8 times.
✓ Branch 58 → 59 taken 8 times.
✓ Branch 59 → 60 taken 8 times.
✓ Branch 60 → 61 taken 7 times.
✓ Branch 60 → 74 taken 1 time.
✓ Branch 83 → 84 taken 8 times.
✓ Branch 83 → 85 taken 3 times.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 153 not taken.
✗ Branch 155 → 156 not taken.
✗ Branch 155 → 157 not taken.
19 request.value("compose").isBool() &&
55
11/17
✓ Branch 61 → 62 taken 7 times.
✓ Branch 62 → 63 taken 7 times.
✓ Branch 63 → 64 taken 7 times.
✓ Branch 64 → 65 taken 7 times.
✗ Branch 64 → 74 not taken.
✓ Branch 66 → 67 taken 7 times.
✗ Branch 66 → 74 not taken.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 68 → 72 taken 4 times.
✓ Branch 79 → 80 taken 7 times.
✓ Branch 79 → 81 taken 4 times.
✓ Branch 81 → 82 taken 8 times.
✓ Branch 81 → 83 taken 3 times.
✗ Branch 143 → 144 not taken.
✗ Branch 143 → 145 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
25 request.value("mailto").isString() && url.size() <= MaximumUrl &&
56
11/13
✓ Branch 47 → 48 taken 10 times.
✓ Branch 47 → 74 taken 1 time.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 74 taken 1 time.
✓ Branch 72 → 73 taken 4 times.
✓ Branch 72 → 74 taken 2 times.
✓ Branch 75 → 76 taken 3 times.
✓ Branch 75 → 77 taken 8 times.
✓ Branch 77 → 78 taken 7 times.
✓ Branch 77 → 79 taken 4 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
28 (url.isEmpty() || MailtoRequest::parse(url)) && tokenValid;
57
3/4
✓ Branch 85 → 86 taken 4 times.
✓ Branch 85 → 94 taken 7 times.
✓ Branch 87 → 88 taken 4 times.
✗ Branch 87 → 94 not taken.
15 const bool admitted = valid && m_handler &&
58
10/14
✓ Branch 88 → 89 taken 4 times.
✓ Branch 89 → 90 taken 4 times.
✓ Branch 90 → 91 taken 4 times.
✓ Branch 91 → 92 taken 4 times.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 94 taken 1 time.
✓ Branch 95 → 96 taken 4 times.
✓ Branch 95 → 97 taken 7 times.
✓ Branch 97 → 98 taken 4 times.
✓ Branch 97 → 99 taken 7 times.
✗ Branch 159 → 160 not taken.
✗ Branch 159 → 161 not taken.
✗ Branch 163 → 164 not taken.
✗ Branch 163 → 165 not taken.
15 m_handler(url, request.value("compose").toBool(), token);
59
5/5
✓ Branch 99 → 100 taken 3 times.
✓ Branch 99 → 101 taken 8 times.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 101 → 103 taken 7 times.
✓ Branch 105 → 106 taken 11 times.
11 client->write(admitted ? "ok\n" : valid ? "busy\n" : "invalid\n");
60
1/1
✓ Branch 106 → 107 taken 11 times.
11 client->disconnectFromServer();
61 11 });
62 11 }
63 11 });
64 5 }
65
66 22 QString ProfileActivation::endpoint(const QString &profile) {
67
2/2
✓ Branch 2 → 3 taken 22 times.
✓ Branch 3 → 4 taken 22 times.
22 const auto canonical = QFileInfo(profile).canonicalFilePath();
68
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 22 times.
22 if (canonical.isEmpty()) return {};
69
1/1
✓ Branch 8 → 9 taken 22 times.
22 const auto root = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
70
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 22 times.
22 if (root.isEmpty()) return {};
71
4/4
✓ Branch 12 → 13 taken 22 times.
✓ Branch 14 → 15 taken 22 times.
✓ Branch 15 → 16 taken 22 times.
✓ Branch 16 → 17 taken 22 times.
22 const auto hash = QCryptographicHash::hash(canonical.toUtf8(), QCryptographicHash::Sha256).toHex().left(32);
72
4/4
✓ Branch 20 → 21 taken 22 times.
✓ Branch 21 → 22 taken 22 times.
✓ Branch 25 → 26 taken 22 times.
✓ Branch 26 → 27 taken 22 times.
44 return QDir(root).filePath(QStringLiteral("mailjd-") + QString::fromLatin1(hash));
73 22 }
74
75 5 bool ProfileActivation::listen(const ProfileLease &lease, Handler handler) {
76
7/11
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 4 times.
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 11 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 4 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
5 if (lease.directory().isEmpty() || m_server.isListening()) return false;
77
1/1
✓ Branch 14 → 15 taken 4 times.
4 const auto name = endpoint(lease.directory());
78
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 25 taken 4 times.
4 if (name.isEmpty()) { m_error = QStringLiteral("No private activation endpoint is available."); return false; }
79 // Only the current lock owner can remove a socket left by a crashed owner.
80
1/1
✓ Branch 25 → 26 taken 4 times.
4 QLocalServer::removeServer(name);
81 4 m_handler = std::move(handler);
82
2/4
✓ Branch 28 → 29 taken 4 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 34 taken 4 times.
✗ Branch 30 → 31 not taken.
4 if (!m_server.listen(name)) { m_error = m_server.errorString(); return false; }
83 4 m_error.clear();
84 4 return true;
85 4 }
86
87 6 bool ProfileActivation::forward(const QString &profile, const QString &mailto, bool compose,
88 QString *error, int timeoutMs,
89 const QString &activationToken) {
90
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
3 const auto fail = [error](const QString &message) { if (error) *error = message; return false; };
91
1/1
✓ Branch 2 → 3 taken 6 times.
6 const auto name = endpoint(profile);
92
9/15
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 13 taken 2 times.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 2 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 24 taken 6 times.
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
6 if (name.isEmpty() || mailto.size() > MaximumUrl || (!mailto.isEmpty() && !MailtoRequest::parse(mailto)))
93 ✗ return fail(QStringLiteral("Invalid profile or desktop request."));
94
1/1
✓ Branch 24 → 25 taken 6 times.
6 QLocalSocket client;
95
1/1
✓ Branch 25 → 26 taken 6 times.
6 client.setReadBufferSize(32);
96
1/1
✓ Branch 26 → 27 taken 6 times.
6 QDeadlineTimer deadline(qBound(1, timeoutMs, 10000));
97 // Another process may own the lease while still constructing its scene.
98 // Retry only connection establishment, never a submitted request.
99 do {
100
1/1
✓ Branch 30 → 31 taken 276 times.
276 client.connectToServer(name);
101
3/3
✓ Branch 33 → 34 taken 276 times.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 36 taken 272 times.
276 if (client.waitForConnected(qMin<qint64>(250, deadline.remainingTime()))) break;
102
1/1
✓ Branch 36 → 37 taken 272 times.
272 client.abort();
103
2/3
✓ Branch 38 → 39 taken 272 times.
✗ Branch 38 → 40 not taken.
✓ Branch 39 → 40 taken 272 times.
272 if (!deadline.hasExpired()) QThread::msleep(20);
104
2/2
✓ Branch 41 → 29 taken 270 times.
✓ Branch 41 → 42 taken 2 times.
272 } while (!deadline.hasExpired());
105
3/3
✓ Branch 42 → 43 taken 6 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 51 taken 4 times.
6 if (client.state() != QLocalSocket::ConnectedState)
106 2 return fail(QStringLiteral("The running profile owner has no available activation channel."));
107 4 const auto bytes = QJsonDocument(QJsonObject{{"version", 2},
108
3/5
✓ Branch 56 → 57 taken 4 times.
✓ Branch 63 → 64 taken 16 times.
✓ Branch 63 → 65 taken 4 times.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
24 {"compose", compose}, {"mailto", mailto}, {"activationToken", activationToken}})
109
2/2
✓ Branch 57 → 58 taken 4 times.
✓ Branch 58 → 59 taken 4 times.
4 .toJson(QJsonDocument::Compact) + '\n';
110
4/7
✓ Branch 66 → 67 taken 4 times.
✗ Branch 66 → 70 not taken.
✓ Branch 67 → 68 taken 4 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 4 times.
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 80 taken 4 times.
4 if (bytes.size() > MaximumPacket || client.write(bytes) != bytes.size())
111 ✗ return fail(QStringLiteral("The desktop request could not be submitted."));
112
6/7
✓ Branch 85 → 86 taken 8 times.
✓ Branch 86 → 87 taken 4 times.
✓ Branch 86 → 90 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
✗ Branch 88 → 90 not taken.
✓ Branch 91 → 81 taken 4 times.
✓ Branch 91 → 92 taken 4 times.
8 while (!client.canReadLine() && !deadline.hasExpired()) {
113
2/3
✓ Branch 82 → 83 taken 4 times.
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 85 taken 4 times.
4 if (!client.waitForReadyRead(deadline.remainingTime())) break;
114 }
115
1/1
✓ Branch 92 → 93 taken 4 times.
4 const auto reply = client.readLine(32);
116
3/4
✓ Branch 94 → 95 taken 3 times.
✓ Branch 94 → 98 taken 1 time.
✓ Branch 95 → 96 taken 3 times.
✗ Branch 95 → 97 not taken.
4 if (reply == "ok\n") { if (error) error->clear(); return true; }
117
1/2
✓ Branch 99 → 100 taken 1 time.
✗ Branch 99 → 107 not taken.
2 if (reply == "busy\n") return fail(QStringLiteral("The running composer is busy. Finish its current action first."));
118 ✗ return fail(QStringLiteral("The running instance did not confirm the desktop request. It was not retried."));
119 10 }
120