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: 99.1% 106 / 0 / 107
Functions: 100.0% 10 / 0 / 10
Branches: 89.0% 178 / 0 / 200

presentation/RuleSuggestions.cpp
Line Branch Exec Source
1 #include "RuleSuggestions.h"
2
3 #include <QDir>
4 #include <QFile>
5 #include <QFileInfo>
6 #include <QJsonArray>
7 #include <QJsonDocument>
8 #include <QJsonObject>
9 #include <QLoggingCategory>
10 #include <QSaveFile>
11
12 #include <algorithm>
13
14
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 3 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.
4 Q_LOGGING_CATEGORY(lcRuleSuggestions, "mailjd.sieve.suggest")
15
16 1082 QString RuleSuggestions::key(const QString &sender, const QString &folder) {
17
3/3
✓ Branch 4 → 5 taken 1082 times.
✓ Branch 5 → 6 taken 1082 times.
✓ Branch 6 → 7 taken 1082 times.
1082 return sender.toLower() + QLatin1Char('\n') + folder;
18 }
19
20 20 void RuleSuggestions::setFile(const QString &path) {
21
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 10 times.
20 if (path == m_file)
22 10 return;
23 10 m_file = path;
24 10 m_pairs.clear();
25 10 m_silenced.clear();
26 10 load();
27 }
28
29 10 void RuleSuggestions::load() {
30
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 10 times.
10 if (m_file.isEmpty())
31 7 return;
32
1/1
✓ Branch 5 → 6 taken 10 times.
10 QFile file(m_file);
33
3/3
✓ Branch 7 → 8 taken 10 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 10 taken 3 times.
10 if (!file.open(QIODevice::ReadOnly))
34 7 return;
35
3/3
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
3 const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
36
7/7
✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 27 → 28 taken 5 times.
✓ Branch 124 → 26 taken 5 times.
✓ Branch 124 → 125 taken 3 times.
8 for (const QJsonValue &value : root.value(QStringLiteral("pairs")).toArray()) {
37
1/1
✓ Branch 28 → 29 taken 5 times.
5 const QJsonObject o = value.toObject();
38 5 Pair pair;
39
2/2
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
5 pair.count = o.value(QStringLiteral("count")).toInt();
40
3/3
✓ Branch 41 → 42 taken 5 times.
✓ Branch 42 → 43 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
5 pair.first = QDateTime::fromString(o.value(QStringLiteral("first")).toString(), Qt::ISODate);
41
3/3
✓ Branch 53 → 54 taken 5 times.
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 5 times.
5 pair.last = QDateTime::fromString(o.value(QStringLiteral("last")).toString(), Qt::ISODate);
42
2/2
✓ Branch 65 → 66 taken 5 times.
✓ Branch 66 → 67 taken 5 times.
5 pair.offered = o.value(QStringLiteral("offered")).toBool();
43
2/2
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
5 const QString sender = o.value(QStringLiteral("sender")).toString();
44
2/2
✓ Branch 81 → 82 taken 5 times.
✓ Branch 82 → 83 taken 5 times.
5 const QString folder = o.value(QStringLiteral("folder")).toString();
45
9/11
✓ Branch 87 → 88 taken 4 times.
✓ Branch 87 → 93 taken 1 time.
✓ Branch 89 → 90 taken 4 times.
✗ Branch 89 → 93 not taken.
✓ Branch 90 → 91 taken 3 times.
✓ Branch 90 → 93 taken 1 time.
✓ Branch 91 → 92 taken 3 times.
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 94 taken 3 times.
✓ Branch 95 → 96 taken 2 times.
✓ Branch 95 → 97 taken 3 times.
5 if (sender.isEmpty() || folder.isEmpty() || pair.count <= 0 || !pair.last.isValid())
46 2 continue;
47
2/2
✓ Branch 97 → 98 taken 3 times.
✓ Branch 98 → 99 taken 3 times.
3 m_pairs.insert(key(sender, folder), pair);
48 16 }
49
7/7
✓ Branch 129 → 130 taken 3 times.
✓ Branch 130 → 131 taken 3 times.
✓ Branch 134 → 135 taken 3 times.
✓ Branch 135 → 136 taken 3 times.
✓ Branch 138 → 139 taken 1 time.
✓ Branch 147 → 137 taken 1 time.
✓ Branch 147 → 148 taken 3 times.
4 for (const QJsonValue &value : root.value(QStringLiteral("silenced")).toArray())
50
3/3
✓ Branch 139 → 140 taken 1 time.
✓ Branch 140 → 141 taken 1 time.
✓ Branch 141 → 142 taken 1 time.
4 m_silenced.insert(value.toString().toLower());
51
9/9
✓ Branch 149 → 150 taken 3 times.
✓ Branch 153 → 154 taken 3 times.
✓ Branch 154 → 155 taken 3 times.
✓ Branch 156 → 157 taken 3 times.
✓ Branch 157 → 158 taken 3 times.
✓ Branch 159 → 160 taken 3 times.
✓ Branch 160 → 161 taken 3 times.
✓ Branch 164 → 151 taken 3 times.
✓ Branch 164 → 165 taken 3 times.
6 qCDebug(lcRuleSuggestions) << "Loaded" << m_pairs.size() << "move pairs," << m_silenced.size() << "silenced senders";
52 10 }
53
54 539 void RuleSuggestions::save() const {
55
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 536 times.
539 if (m_file.isEmpty())
56 4 return;
57
1/1
✓ Branch 5 → 6 taken 536 times.
536 QJsonArray pairs;
58
2/2
✓ Branch 72 → 7 taken 127792 times.
✓ Branch 72 → 73 taken 536 times.
128328 for (auto it = m_pairs.cbegin(); it != m_pairs.cend(); ++it) {
59 127792 const int split = it.key().indexOf(QLatin1Char('\n'));
60
2/2
✓ Branch 15 → 16 taken 127792 times.
✓ Branch 47 → 48 taken 127792 times.
383376 pairs.append(QJsonObject{{QStringLiteral("sender"), it.key().left(split)},
61
1/1
✓ Branch 21 → 22 taken 127792 times.
255584 {QStringLiteral("folder"), it.key().mid(split + 1)},
62 255584 {QStringLiteral("count"), it->count},
63
1/1
✓ Branch 32 → 33 taken 127792 times.
255584 {QStringLiteral("first"), it->first.toString(Qt::ISODate)},
64
1/1
✓ Branch 38 → 39 taken 127792 times.
255584 {QStringLiteral("last"), it->last.toString(Qt::ISODate)},
65
2/4
✓ Branch 51 → 52 taken 766752 times.
✓ Branch 51 → 53 taken 127792 times.
✗ Branch 166 → 167 not taken.
✗ Branch 166 → 168 not taken.
1150128 {QStringLiteral("offered"), it->offered}});
66 }
67
1/1
✓ Branch 75 → 76 taken 536 times.
536 QStringList silenced(m_silenced.cbegin(), m_silenced.cend());
68
1/1
✓ Branch 76 → 77 taken 536 times.
536 silenced.sort();
69
4/4
✓ Branch 78 → 79 taken 536 times.
✓ Branch 79 → 80 taken 536 times.
✓ Branch 80 → 81 taken 536 times.
✓ Branch 81 → 82 taken 536 times.
536 QDir().mkpath(QFileInfo(m_file).absolutePath());
70
1/1
✓ Branch 86 → 87 taken 536 times.
536 QSaveFile file(m_file);
71
3/3
✓ Branch 88 → 89 taken 536 times.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 104 taken 535 times.
536 if (!file.open(QIODevice::WriteOnly)) {
72
7/7
✓ Branch 90 → 91 taken 1 time.
✓ Branch 94 → 95 taken 1 time.
✓ Branch 95 → 96 taken 1 time.
✓ Branch 96 → 97 taken 1 time.
✓ Branch 97 → 98 taken 1 time.
✓ Branch 102 → 92 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
2 qCWarning(lcRuleSuggestions) << "Cannot write rule suggestions:" << file.errorString();
73 1 return;
74 }
75
1/1
✓ Branch 105 → 106 taken 535 times.
535 file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
76
1/1
✓ Branch 118 → 119 taken 535 times.
2140 file.write(QJsonDocument(QJsonObject{{QStringLiteral("pairs"), pairs},
77
4/6
✓ Branch 113 → 114 taken 535 times.
✓ Branch 116 → 117 taken 535 times.
✓ Branch 123 → 124 taken 1070 times.
✓ Branch 123 → 125 taken 535 times.
✗ Branch 256 → 257 not taken.
✗ Branch 256 → 258 not taken.
2675 {QStringLiteral("silenced"), QJsonArray::fromStringList(silenced)}})
78
1/1
✓ Branch 117 → 118 taken 535 times.
1070 .toJson(QJsonDocument::Compact));
79
2/3
✓ Branch 130 → 131 taken 535 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 146 taken 535 times.
535 if (!file.commit())
80 ✗ qCWarning(lcRuleSuggestions) << "Cannot write rule suggestions:" << file.errorString();
81 896687 }
82
83 532 void RuleSuggestions::prune(const QDateTime &now) {
84
3/3
✓ Branch 2 → 3 taken 532 times.
✓ Branch 18 → 4 taken 127792 times.
✓ Branch 18 → 19 taken 532 times.
128324 for (auto it = m_pairs.begin(); it != m_pairs.end();) {
85
6/7
✓ Branch 5 → 6 taken 127792 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 10 taken 127791 times.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 127791 times.
127792 if (it->last.daysTo(now) > kWindowDays && !it->offered)
86
1/1
✓ Branch 13 → 14 taken 1 time.
1 it = m_pairs.erase(it);
87 else
88 127791 ++it;
89 }
90
2/2
✓ Branch 20 → 21 taken 527 times.
✓ Branch 20 → 22 taken 5 times.
532 if (m_pairs.size() <= kMaxPairs)
91 527 return;
92 5 QList<QPair<QDateTime, QString>> byAge;
93
2/2
✓ Branch 31 → 23 taken 2505 times.
✓ Branch 31 → 32 taken 5 times.
2510 for (auto it = m_pairs.cbegin(); it != m_pairs.cend(); ++it)
94
1/1
✓ Branch 26 → 27 taken 2505 times.
2505 byAge.append({it->last, it.key()});
95
3/3
✓ Branch 32 → 33 taken 5 times.
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
5 std::sort(byAge.begin(), byAge.end());
96
2/2
✓ Branch 40 → 36 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
10 for (int i = 0; i < byAge.size() - kMaxPairs; ++i)
97
1/1
✓ Branch 37 → 38 taken 5 times.
5 m_pairs.remove(byAge.at(i).second);
98 5 }
99
100 534 std::optional<RuleSuggestions::Suggestion> RuleSuggestions::record(const QString &sender, const QString &folder,
101 const QDateTime &now) {
102
2/2
✓ Branch 2 → 3 taken 534 times.
✓ Branch 3 → 4 taken 534 times.
534 const QString address = sender.trimmed().toLower();
103
4/5
✓ Branch 6 → 7 taken 534 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 11 taken 531 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
534 const bool list = address.startsWith(QLatin1String("list:")) && address.size() > 5;
104
10/11
✓ Branch 13 → 14 taken 534 times.
✗ Branch 13 → 21 not taken.
✓ Branch 14 → 15 taken 531 times.
✓ Branch 14 → 19 taken 3 times.
✓ Branch 17 → 18 taken 531 times.
✓ Branch 18 → 19 taken 530 times.
✓ Branch 18 → 21 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 532 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 532 times.
534 if (address.isEmpty() || (!list && !address.contains(QLatin1Char('@'))) || folder.isEmpty())
105 2 return std::nullopt;
106
1/1
✓ Branch 25 → 26 taken 532 times.
532 Pair pair = m_pairs.value(key(address, folder));
107 // Moves older than the window start a new count.
108
8/9
✓ Branch 28 → 29 taken 17 times.
✓ Branch 28 → 33 taken 515 times.
✓ Branch 29 → 30 taken 17 times.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 33 taken 16 times.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 39 taken 531 times.
532 if (pair.count > 0 && pair.first.daysTo(now) > kWindowDays && !pair.offered) {
109 1 pair.count = 0;
110 1 pair.first = {};
111 }
112 532 ++pair.count;
113
3/3
✓ Branch 39 → 40 taken 532 times.
✓ Branch 40 → 41 taken 516 times.
✓ Branch 40 → 42 taken 16 times.
532 if (!pair.first.isValid())
114 516 pair.first = now;
115 532 pair.last = now;
116
2/2
✓ Branch 43 → 44 taken 532 times.
✓ Branch 44 → 45 taken 532 times.
532 m_pairs.insert(key(address, folder), pair);
117
1/1
✓ Branch 46 → 47 taken 532 times.
532 prune(now);
118 532 std::optional<Suggestion> result;
119
9/9
✓ Branch 47 → 48 taken 9 times.
✓ Branch 47 → 52 taken 523 times.
✓ Branch 48 → 49 taken 6 times.
✓ Branch 48 → 52 taken 3 times.
✓ Branch 49 → 50 taken 6 times.
✓ Branch 50 → 51 taken 5 times.
✓ Branch 50 → 52 taken 1 time.
✓ Branch 53 → 54 taken 5 times.
✓ Branch 53 → 59 taken 527 times.
532 if (pair.count >= kThreshold && !pair.offered && !silenced(address))
120 5 result = Suggestion{address, folder, pair.count};
121
1/1
✓ Branch 59 → 60 taken 532 times.
532 save();
122 532 return result;
123 534 }
124
125 6 void RuleSuggestions::markOffered(const QString &sender, const QString &folder) {
126
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
6 auto it = m_pairs.find(key(sender, folder));
127
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 5 times.
6 if (it == m_pairs.end())
128 1 return;
129 5 it->offered = true;
130
1/1
✓ Branch 10 → 11 taken 5 times.
5 save();
131 }
132
133 2 void RuleSuggestions::silenceSender(const QString &sender) {
134
3/3
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
2 m_silenced.insert(sender.trimmed().toLower());
135 2 save();
136 2 }
137
138 9 int RuleSuggestions::count(const QString &sender, const QString &folder) const {
139
1/1
✓ Branch 2 → 3 taken 9 times.
9 return m_pairs.value(key(sender, folder)).count;
140 }
141