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: 88.8% 87 / 0 / 98
Functions: 100.0% 14 / 0 / 14
Branches: 60.3% 161 / 0 / 267

data/FolderPreferences.cpp
Line Branch Exec Source
1 #include "FolderPreferences.h"
2 #include <QDir>
3 #include <QCryptographicHash>
4 #include <QFile>
5 #include <QJsonArray>
6 #include <QJsonDocument>
7 #include <QJsonObject>
8 #include <QLoggingCategory>
9 #include <QSaveFile>
10 #include <optional>
11
12
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 7 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
9 Q_LOGGING_CATEGORY(lcFolderPreferences, "mailjd.subscription")
13
14 namespace {
15 2547 QString filePath(const QString &directory) {
16
2/2
✓ Branch 2 → 3 taken 2547 times.
✓ Branch 6 → 7 taken 2547 times.
5094 return QDir(directory).filePath(QStringLiteral("subscriptions.json"));
17 }
18
19 1712 std::optional<QJsonObject> loadObject(const QString &path) {
20
1/1
✓ Branch 2 → 3 taken 1712 times.
1712 QFile file(path);
21
4/4
✓ Branch 3 → 4 taken 1712 times.
✓ Branch 4 → 5 taken 1674 times.
✓ Branch 4 → 9 taken 38 times.
✓ Branch 5 → 6 taken 1674 times.
1712 if (!file.exists()) return QJsonObject{};
22
2/3
✓ Branch 10 → 11 taken 38 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 27 taken 38 times.
38 if (!file.open(QIODevice::ReadOnly)) {
23 ✗ qCWarning(lcFolderPreferences) << "Cannot read folder preferences" << path << file.errorString();
24 ✗ return std::nullopt;
25 }
26
2/2
✓ Branch 27 → 28 taken 38 times.
✓ Branch 28 → 29 taken 38 times.
38 const auto document = QJsonDocument::fromJson(file.readAll());
27
3/3
✓ Branch 30 → 31 taken 38 times.
✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 44 taken 30 times.
38 if (!document.isObject()) {
28
6/6
✓ Branch 32 → 33 taken 8 times.
✓ Branch 36 → 37 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 42 → 34 taken 8 times.
✓ Branch 42 → 43 taken 8 times.
16 qCWarning(lcFolderPreferences) << "Invalid folder preferences JSON; preserving file" << path;
29 8 return std::nullopt;
30 }
31
1/1
✓ Branch 44 → 45 taken 30 times.
30 return document.object();
32 1712 }
33
34 1686 QStringList loadPaths(const QString &directory, const QString &key) {
35
2/2
✓ Branch 2 → 3 taken 1686 times.
✓ Branch 3 → 4 taken 1686 times.
1686 const auto object = loadObject(filePath(directory));
36
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 8 taken 1683 times.
1686 if (!object) return {};
37 1683 QStringList result;
38
6/6
✓ Branch 9 → 10 taken 1683 times.
✓ Branch 10 → 11 taken 1683 times.
✓ Branch 12 → 13 taken 1683 times.
✓ Branch 13 → 14 taken 1683 times.
✓ Branch 37 → 15 taken 32 times.
✓ Branch 37 → 38 taken 1683 times.
1715 for (const auto &value : object->value(key).toArray()) {
39
8/17
✓ Branch 17 → 18 taken 32 times.
✗ Branch 17 → 23 not taken.
✓ Branch 19 → 20 taken 32 times.
✓ Branch 21 → 22 taken 32 times.
✗ Branch 21 → 23 not taken.
✓ Branch 24 → 25 taken 32 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 32 times.
✗ Branch 26 → 28 not taken.
✓ Branch 28 → 29 taken 32 times.
✗ Branch 28 → 35 not taken.
✓ Branch 30 → 31 taken 32 times.
✓ Branch 31 → 32 taken 32 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
32 if (value.isString() && !value.toString().isEmpty()) result.append(value.toString());
40 1683 }
41 1683 return result;
42 1686 }
43
44 26 bool savePaths(const QString &directory, const QString &key, const QStringList &folders) {
45
1/1
✓ Branch 2 → 3 taken 26 times.
26 const auto path = filePath(directory);
46 // Updating one preference must retain the other key and unknown future keys.
47 // An unreadable/corrupt existing file is not an empty preference document.
48
1/1
✓ Branch 3 → 4 taken 26 times.
26 auto object = loadObject(path);
49
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 21 times.
26 if (!object) return false;
50
1/1
✓ Branch 7 → 8 taken 21 times.
21 QJsonArray paths;
51
4/4
✓ Branch 11 → 12 taken 26 times.
✓ Branch 12 → 13 taken 26 times.
✓ Branch 16 → 10 taken 26 times.
✓ Branch 16 → 17 taken 21 times.
47 for (const auto &folder : folders) paths.append(folder);
52
11/13
✓ Branch 18 → 19 taken 21 times.
✓ Branch 19 → 20 taken 5 times.
✓ Branch 19 → 25 taken 16 times.
✓ Branch 21 → 22 taken 5 times.
✓ Branch 22 → 23 taken 5 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 4 times.
✓ Branch 26 → 27 taken 5 times.
✓ Branch 26 → 28 taken 16 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 20 times.
✗ Branch 123 → 124 not taken.
✗ Branch 123 → 125 not taken.
21 if (object->contains(key) && object->value(key) == paths) return true;
53
2/2
✓ Branch 31 → 32 taken 20 times.
✓ Branch 32 → 33 taken 20 times.
20 object->insert(key, paths);
54
4/4
✓ Branch 35 → 36 taken 20 times.
✓ Branch 36 → 37 taken 20 times.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 52 taken 19 times.
20 if (!QDir().mkpath(directory)) {
55
6/6
✓ Branch 40 → 41 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 50 → 42 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
2 qCWarning(lcFolderPreferences) << "Cannot create folder preferences directory" << directory;
56 1 return false;
57 }
58
1/1
✓ Branch 52 → 53 taken 19 times.
19 QSaveFile file(path);
59
2/3
✓ Branch 54 → 55 taken 19 times.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 71 taken 19 times.
19 if (!file.open(QIODevice::WriteOnly)) {
60 ✗ qCWarning(lcFolderPreferences) << "Cannot write folder preferences" << path << file.errorString();
61 ✗ return false;
62 }
63
2/2
✓ Branch 72 → 73 taken 19 times.
✓ Branch 73 → 74 taken 19 times.
19 const auto data = QJsonDocument(*object).toJson(QJsonDocument::Indented);
64
2/3
✓ Branch 75 → 76 taken 19 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 94 taken 19 times.
19 if (file.write(data) != data.size()) {
65 ✗ qCWarning(lcFolderPreferences) << "Incomplete folder preferences write" << path << file.errorString();
66 ✗ file.cancelWriting();
67 ✗ return false;
68 }
69
2/3
✓ Branch 94 → 95 taken 19 times.
✗ Branch 95 → 96 not taken.
✓ Branch 95 → 111 taken 19 times.
19 if (!file.commit()) {
70 ✗ qCWarning(lcFolderPreferences) << "Cannot commit folder preferences" << path << file.errorString();
71 ✗ return false;
72 }
73 19 return true;
74 26 }
75 }
76
77 843 QStringList FolderPreferences::loadSubscriptions(const QString &directory) {
78
1/1
✓ Branch 5 → 6 taken 843 times.
843 return loadPaths(directory, QStringLiteral("subscribed"));
79 }
80 843 QStringList FolderPreferences::loadHidden(const QString &directory) {
81
1/1
✓ Branch 5 → 6 taken 843 times.
843 return loadPaths(directory, QStringLiteral("hidden"));
82 }
83 15 bool FolderPreferences::saveSubscriptions(const QString &directory, const QStringList &folders) {
84
1/1
✓ Branch 5 → 6 taken 15 times.
15 return savePaths(directory, QStringLiteral("subscribed"), folders);
85 }
86 11 bool FolderPreferences::saveHidden(const QString &directory, const QStringList &folders) {
87
1/1
✓ Branch 5 → 6 taken 11 times.
11 return savePaths(directory, QStringLiteral("hidden"), folders);
88 }
89
90 namespace {
91 841 QString accountDirectory(const QString &directory, const QString &account) {
92
3/3
✓ Branch 2 → 3 taken 841 times.
✓ Branch 4 → 5 taken 841 times.
✓ Branch 5 → 6 taken 841 times.
841 const auto key = QCryptographicHash::hash(account.toUtf8(), QCryptographicHash::Sha256).toHex();
93
4/4
✓ Branch 8 → 9 taken 841 times.
✓ Branch 9 → 10 taken 841 times.
✓ Branch 13 → 14 taken 841 times.
✓ Branch 14 → 15 taken 841 times.
2523 return QDir(directory).filePath(QStringLiteral("folder-preferences/") + QString::fromLatin1(key));
94 841 }
95 }
96 834 QStringList FolderPreferences::accountSubscriptions(const QString &directory, const QString &account,
97 const QStringList &available) {
98
1/1
✓ Branch 2 → 3 taken 834 times.
834 const auto scoped = accountDirectory(directory, account);
99
2/2
✓ Branch 3 → 4 taken 834 times.
✓ Branch 4 → 5 taken 834 times.
834 const bool explicitAccount = QFile::exists(filePath(scoped));
100
3/3
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 828 times.
✓ Branch 9 → 10 taken 834 times.
834 const auto stored = loadSubscriptions(explicitAccount ? scoped : directory);
101 // Legacy empty meant "all folders"; a new account document can explicitly
102 // choose no monitored folders without silently re-enabling them on restart.
103
6/6
✓ Branch 10 → 11 taken 828 times.
✓ Branch 10 → 14 taken 6 times.
✓ Branch 12 → 13 taken 825 times.
✓ Branch 12 → 14 taken 3 times.
✓ Branch 15 → 16 taken 825 times.
✓ Branch 15 → 17 taken 9 times.
834 if (!explicitAccount && stored.isEmpty()) return available;
104 9 QStringList result;
105
8/9
✓ Branch 21 → 22 taken 9 times.
✓ Branch 21 → 25 taken 1 time.
✓ Branch 23 → 24 taken 9 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 9 times.
✓ Branch 26 → 28 taken 1 time.
✓ Branch 27 → 28 taken 9 times.
✓ Branch 30 → 19 taken 10 times.
✓ Branch 30 → 31 taken 9 times.
19 for (const auto &path : stored) if (available.contains(path) && !result.contains(path)) result.append(path);
106 9 return result;
107 834 }
108 6 bool FolderPreferences::saveAccountSubscriptions(const QString &directory, const QString &account,
109 const QStringList &folders) {
110
4/8
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
6 if (account.isEmpty() || account.size() > 256 || folders.size() > 4096) return false;
111
5/8
✓ Branch 16 → 17 taken 6 times.
✗ Branch 16 → 19 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 6 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 6 times.
✓ Branch 25 → 14 taken 6 times.
✓ Branch 25 → 26 taken 6 times.
12 for (const auto &path : folders) if (path.isEmpty() || path.size() > 1024) return false;
112
2/2
✓ Branch 26 → 28 taken 6 times.
✓ Branch 28 → 29 taken 6 times.
6 return saveSubscriptions(accountDirectory(directory, account), folders);
113 }
114
115 1 bool FolderPreferences::renameFolder(const QString &directory, const QString &account,
116 const QString &oldPath, const QString &newPath,
117 const QString &delimiter) {
118
4/8
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
1 if (oldPath.isEmpty() || newPath.isEmpty() || oldPath == newPath) return false;
119 2 const auto rewrite = [oldPath, newPath, delimiter](const QStringList &folders) {
120 2 QStringList result = folders;
121
1/1
✓ Branch 3 → 4 taken 2 times.
2 const QString prefix = oldPath + delimiter;
122
4/4
✓ Branch 4 → 5 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 22 → 7 taken 3 times.
✓ Branch 22 → 23 taken 2 times.
5 for (QString &folder : result) {
123
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 2 times.
3 if (folder == oldPath) folder = newPath;
124
5/5
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 20 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
2 else if (folder.startsWith(prefix)) folder = newPath + folder.mid(oldPath.size());
125 }
126 2 return result;
127 3 };
128 1 bool ok = true;
129
1/2
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 49 not taken.
1 if (!account.isEmpty()) {
130
1/1
✓ Branch 17 → 18 taken 1 time.
1 const QString scoped = accountDirectory(directory, account);
131
3/4
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 34 taken 1 time.
1 if (QFile::exists(filePath(scoped))) {
132 ✗ ok = saveSubscriptions(scoped, rewrite(loadSubscriptions(scoped))) && ok;
133 } else {
134 // S106-121: the legacy list (before per-account documents) is shared
135 // by every account of the profile. Renaming account A's "X" there
136 // would rename account B's "X" as well; instead this account gets its
137 // own document with the renamed list and the legacy list stays.
138
1/1
✓ Branch 34 → 35 taken 1 time.
1 const QStringList legacy = loadSubscriptions(directory);
139
1/1
✓ Branch 35 → 36 taken 1 time.
1 const QStringList renamed = rewrite(legacy);
140
5/8
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✗ Branch 37 → 44 not taken.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✗ Branch 39 → 42 not taken.
✓ Branch 40 → 41 taken 1 time.
✗ Branch 40 → 42 not taken.
1 if (renamed != legacy) ok = saveSubscriptions(scoped, renamed) && ok;
141 1 }
142 1 } else {
143 ✗ ok = saveSubscriptions(directory, rewrite(loadSubscriptions(directory))) && ok;
144 }
145 // Hidden folders are one profile-wide list by design (no account scope).
146
7/15
✓ Branch 61 → 62 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
✗ Branch 64 → 67 not taken.
✓ Branch 65 → 66 taken 1 time.
✗ Branch 65 → 67 not taken.
✓ Branch 68 → 69 taken 1 time.
✗ Branch 68 → 70 not taken.
✓ Branch 70 → 71 taken 1 time.
✗ Branch 70 → 72 not taken.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
✗ Branch 106 → 107 not taken.
✗ Branch 106 → 108 not taken.
1 ok = saveHidden(directory, rewrite(loadHidden(directory))) && ok;
147 1 return ok;
148 1 }
149