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: 97.4% 114 / 0 / 117
Functions: 100.0% 7 / 0 / 7
Branches: 83.2% 163 / 0 / 196

data/SieveBlockDiff.cpp
Line Branch Exec Source
1 #include "SieveBlockDiff.h"
2
3 #include "SieveRuleCodec.h"
4
5 #include <QHash>
6 #include <QSet>
7 #include <QLoggingCategory>
8
9 #include <algorithm>
10 #include <vector>
11
12
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 5 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
9 Q_LOGGING_CATEGORY(lcSieveDiff, "mailjd.sieve.diff")
13
14 namespace Sieve {
15
16 namespace {
17
18 struct Entry {
19 BlockKind kind = BlockKind::Advanced;
20 QString key; // "name:<marker>" or "bytes:<hash>"
21 QString name;
22 bool named = false;
23 QByteArray bytes;
24 bool matched = false;
25 int partner = -1;
26 };
27
28 20004 QString firstLine(const QByteArray &bytes) {
29
5/5
✓ Branch 2 → 3 taken 20004 times.
✓ Branch 3 → 4 taken 20004 times.
✓ Branch 4 → 5 taken 20004 times.
✓ Branch 40 → 6 taken 20004 times.
✓ Branch 40 → 41 taken 2 times.
20006 for (const QString &line : LineDiff::splitLines(bytes)) {
30
1/1
✓ Branch 7 → 8 taken 20004 times.
20004 const QString trimmed = line.trimmed();
31
6/7
✓ Branch 9 → 10 taken 20004 times.
✗ Branch 9 → 15 not taken.
✓ Branch 12 → 13 taken 20004 times.
✓ Branch 13 → 14 taken 20002 times.
✓ Branch 13 → 15 taken 2 times.
✓ Branch 16 → 17 taken 20002 times.
✓ Branch 16 → 33 taken 2 times.
20004 if (!trimmed.isEmpty() && !trimmed.startsWith(QLatin1Char('#')))
32
4/16
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 24 taken 20002 times.
✗ Branch 22 → 23 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 20002 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 20002 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 20002 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
20002 return trimmed.length() > 60 ? trimmed.left(59) + QStringLiteral("…") : trimmed;
33 40008 }
34 2 return QStringLiteral("…");
35 }
36
37 // Blocks that the review shows: everything but require and blank comments.
38 18 QList<Entry> entries(const Document &document) {
39 18 QList<Entry> result;
40
2/2
✓ Branch 56 → 4 taken 20095 times.
✓ Branch 56 → 57 taken 18 times.
20113 for (const Block &block : document.blocks) {
41
2/2
✓ Branch 5 → 6 taken 18 times.
✓ Branch 5 → 7 taken 20077 times.
20095 if (block.kind == BlockKind::Require)
42 18 continue;
43
7/11
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 12 taken 20075 times.
✓ Branch 8 → 9 taken 2 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 20075 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 20077 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
20077 if (block.kind == BlockKind::Comment && block.original.trimmed().isEmpty())
44 ✗ continue;
45 20077 Entry entry;
46 20077 entry.kind = block.kind;
47 20077 entry.bytes = block.original;
48
3/4
✓ Branch 18 → 19 taken 73 times.
✓ Branch 18 → 22 taken 20004 times.
✓ Branch 20 → 21 taken 73 times.
✗ Branch 20 → 22 not taken.
20077 entry.named = block.named && !block.name.isEmpty();
49
3/3
✓ Branch 23 → 24 taken 73 times.
✓ Branch 23 → 25 taken 20004 times.
✓ Branch 25 → 26 taken 20004 times.
20077 entry.name = entry.named ? block.name : firstLine(block.original);
50
15/25
✓ Branch 28 → 29 taken 73 times.
✓ Branch 28 → 33 taken 20004 times.
✓ Branch 32 → 38 taken 73 times.
✓ Branch 33 → 34 taken 20004 times.
✓ Branch 37 → 38 taken 20004 times.
✓ Branch 40 → 41 taken 20004 times.
✓ Branch 40 → 42 taken 73 times.
✓ Branch 42 → 43 taken 20004 times.
✓ Branch 42 → 44 taken 73 times.
✓ Branch 44 → 45 taken 20004 times.
✓ Branch 44 → 46 taken 73 times.
✓ Branch 46 → 47 taken 73 times.
✓ Branch 46 → 48 taken 20004 times.
✓ Branch 48 → 49 taken 73 times.
✓ Branch 48 → 50 taken 20004 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
40154 entry.key = entry.named ? QStringLiteral("name:") + block.name : QStringLiteral("bytes:") + block.hash();
51
1/1
✓ Branch 50 → 51 taken 20077 times.
20077 result.append(entry);
52 20077 }
53 18 return result;
54 ✗ }
55
56 18 QByteArray requireBytes(const Document &document) {
57 18 QByteArray bytes;
58
2/2
✓ Branch 12 → 4 taken 20095 times.
✓ Branch 12 → 13 taken 18 times.
20113 for (const Block &block : document.blocks)
59
2/2
✓ Branch 5 → 6 taken 18 times.
✓ Branch 5 → 10 taken 20077 times.
20095 if (block.kind == BlockKind::Require)
60
2/2
✓ Branch 6 → 7 taken 18 times.
✓ Branch 7 → 8 taken 18 times.
18 bytes += block.original.trimmed();
61 18 return bytes;
62 ✗ }
63
64 // Longest strictly increasing subsequence of `values` (distinct numbers);
65 // returns its positions. O(n log n) — the moved-block detection compares a
66 // permutation with its sorted order, where the LCS equals this (S106-097:
67 // the quadratic table cost ~400 MB for two 10 000-block scripts).
68 9 QList<int> increasingIndices(const QList<int> &values) {
69 9 QList<int> tails; // position of the smallest tail of each length
70
1/1
✓ Branch 3 → 4 taken 9 times.
9 QList<int> previous(values.size(), -1);
71
2/2
✓ Branch 25 → 5 taken 10035 times.
✓ Branch 25 → 26 taken 9 times.
10044 for (int i = 0; i < values.size(); ++i) {
72 10035 int low = 0;
73 10035 int high = int(tails.size());
74
2/2
✓ Branch 13 → 7 taken 113663 times.
✓ Branch 13 → 14 taken 10035 times.
123698 while (low < high) {
75 113663 const int mid = (low + high) / 2;
76
2/2
✓ Branch 10 → 11 taken 113647 times.
✓ Branch 10 → 12 taken 16 times.
113663 if (values.at(tails.at(mid)) < values.at(i))
77 113647 low = mid + 1;
78 else
79 16 high = mid;
80 }
81
2/2
✓ Branch 14 → 15 taken 10023 times.
✓ Branch 14 → 18 taken 12 times.
10035 if (low > 0)
82
1/1
✓ Branch 16 → 17 taken 10023 times.
10023 previous[i] = tails.at(low - 1);
83
2/2
✓ Branch 19 → 20 taken 10032 times.
✓ Branch 19 → 21 taken 3 times.
10035 if (low == tails.size())
84
1/1
✓ Branch 20 → 23 taken 10032 times.
10032 tails.append(i);
85 else
86
1/1
✓ Branch 21 → 22 taken 3 times.
3 tails[low] = i;
87 }
88 9 QList<int> kept;
89
4/5
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 9 times.
✓ Branch 29 → 30 taken 9 times.
✓ Branch 35 → 32 taken 10032 times.
✓ Branch 35 → 36 taken 9 times.
10041 for (int i = tails.isEmpty() ? -1 : tails.last(); i >= 0; i = previous.at(i))
90
1/1
✓ Branch 32 → 33 taken 10032 times.
10032 kept.prepend(i);
91 9 return kept;
92 9 }
93
94 } // namespace
95
96 18 QList<DiffLine> lineDiff(const QByteArray &before, const QByteArray &after) {
97
3/3
✓ Branch 2 → 3 taken 18 times.
✓ Branch 3 → 4 taken 18 times.
✓ Branch 4 → 5 taken 18 times.
18 return LineDiff::diff(LineDiff::splitLines(before), LineDiff::splitLines(after));
98 }
99
100 12 ScriptDiff diffScripts(const QByteArray &before, const QByteArray &after) {
101 12 ScriptDiff diff;
102
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 11 times.
12 if (before == after)
103 1 return diff;
104
1/1
✓ Branch 5 → 6 taken 11 times.
11 const Document oldDocument = decode(before);
105
1/1
✓ Branch 6 → 7 taken 11 times.
11 const Document newDocument = decode(after);
106
3/4
✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 9 not taken.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 13 taken 9 times.
11 if (oldDocument.readOnly || newDocument.readOnly) {
107 2 diff.wholeScript = true;
108 2 diff.changes.append({BlockChange::Kind::Changed, QString(), false, lineDiff(before, after)});
109 2 return diff;
110 }
111
2/2
✓ Branch 13 → 14 taken 9 times.
✓ Branch 14 → 15 taken 9 times.
9 diff.requireChanged = requireBytes(oldDocument) != requireBytes(newDocument);
112
1/1
✓ Branch 18 → 19 taken 9 times.
9 QList<Entry> olds = entries(oldDocument);
113
1/1
✓ Branch 19 → 20 taken 9 times.
9 QList<Entry> news = entries(newDocument);
114 // 1. Same key (marker name or identical bytes).
115 9 QHash<QString, QList<int>> byKey;
116
2/2
✓ Branch 26 → 21 taken 10038 times.
✓ Branch 26 → 27 taken 9 times.
10047 for (int i = 0; i < news.size(); ++i)
117
2/2
✓ Branch 22 → 23 taken 10038 times.
✓ Branch 23 → 24 taken 10038 times.
10038 byKey[news.at(i).key].append(i);
118
2/2
✓ Branch 41 → 28 taken 10039 times.
✓ Branch 41 → 42 taken 9 times.
10048 for (int i = 0; i < olds.size(); ++i) {
119
1/1
✓ Branch 29 → 30 taken 10039 times.
10039 QList<int> &candidates = byKey[olds.at(i).key];
120
2/2
✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 33 taken 10032 times.
10039 if (candidates.isEmpty())
121 7 continue;
122
1/1
✓ Branch 33 → 34 taken 10032 times.
10032 const int j = candidates.takeFirst();
123
2/2
✓ Branch 34 → 35 taken 10032 times.
✓ Branch 35 → 36 taken 10032 times.
10032 olds[i].matched = news[j].matched = true;
124
1/1
✓ Branch 36 → 37 taken 10032 times.
10032 olds[i].partner = j;
125
1/1
✓ Branch 37 → 38 taken 10032 times.
10032 news[j].partner = i;
126 }
127 // 2. Unnamed leftovers of the same kind pair up by position (an edited
128 // unnamed rule is a change, not a removal plus an addition).
129 // One queue per kind keeps this linear (S106-097).
130 9 QHash<int, QList<int>> freeNew;
131
2/2
✓ Branch 56 → 43 taken 10038 times.
✓ Branch 56 → 57 taken 9 times.
10047 for (int j = 0; j < news.size(); ++j)
132
6/6
✓ Branch 44 → 45 taken 6 times.
✓ Branch 44 → 48 taken 10032 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 48 taken 2 times.
✓ Branch 49 → 50 taken 4 times.
✓ Branch 49 → 54 taken 10034 times.
10038 if (!news.at(j).matched && !news.at(j).named)
133
2/2
✓ Branch 51 → 52 taken 4 times.
✓ Branch 52 → 53 taken 4 times.
4 freeNew[int(news.at(j).kind)].append(j);
134
2/2
✓ Branch 79 → 58 taken 10039 times.
✓ Branch 79 → 80 taken 9 times.
10048 for (int i = 0; i < olds.size(); ++i) {
135
6/6
✓ Branch 59 → 60 taken 7 times.
✓ Branch 59 → 62 taken 10032 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 61 → 63 taken 4 times.
✓ Branch 64 → 65 taken 10035 times.
✓ Branch 64 → 66 taken 4 times.
10039 if (olds.at(i).matched || olds.at(i).named)
136 10035 continue;
137
1/1
✓ Branch 67 → 68 taken 4 times.
4 QList<int> &queue = freeNew[int(olds.at(i).kind)];
138
2/2
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 71 taken 3 times.
4 if (queue.isEmpty())
139 1 continue;
140
1/1
✓ Branch 71 → 72 taken 3 times.
3 const int j = queue.takeFirst();
141
2/2
✓ Branch 72 → 73 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
3 olds[i].matched = news[j].matched = true;
142
1/1
✓ Branch 74 → 75 taken 3 times.
3 olds[i].partner = j;
143
1/1
✓ Branch 75 → 76 taken 3 times.
3 news[j].partner = i;
144 }
145 // Order of the matched blocks: those outside the longest run kept in
146 // order moved.
147 9 QList<int> newOrder; // old index of each matched new block, in new order
148
2/2
✓ Branch 88 → 83 taken 10038 times.
✓ Branch 88 → 89 taken 9 times.
10047 for (const Entry &entry : std::as_const(news))
149
2/2
✓ Branch 84 → 85 taken 10035 times.
✓ Branch 84 → 86 taken 3 times.
10038 if (entry.matched)
150
1/1
✓ Branch 85 → 86 taken 10035 times.
10035 newOrder.append(entry.partner);
151 9 QSet<int> stayedOld;
152
5/5
✓ Branch 90 → 91 taken 9 times.
✓ Branch 91 → 92 taken 9 times.
✓ Branch 92 → 93 taken 9 times.
✓ Branch 99 → 94 taken 10032 times.
✓ Branch 99 → 100 taken 9 times.
10041 for (const int position : increasingIndices(newOrder))
153
1/1
✓ Branch 96 → 97 taken 10032 times.
10041 stayedOld.insert(newOrder.at(position));
154
155
2/2
✓ Branch 127 → 102 taken 10038 times.
✓ Branch 127 → 128 taken 9 times.
10047 for (int j = 0; j < news.size(); ++j) {
156 10038 const Entry &entry = news.at(j);
157
2/2
✓ Branch 103 → 104 taken 3 times.
✓ Branch 103 → 111 taken 10035 times.
10038 if (!entry.matched) {
158 3 diff.changes.append({BlockChange::Kind::Added, entry.name, entry.named, lineDiff(QByteArray(), entry.bytes)});
159 3 continue;
160 }
161 10035 const Entry &old = olds.at(entry.partner);
162
2/2
✓ Branch 113 → 114 taken 7 times.
✓ Branch 113 → 119 taken 10028 times.
10035 if (old.bytes != entry.bytes)
163 7 diff.changes.append({BlockChange::Kind::Changed, entry.name, entry.named, lineDiff(old.bytes, entry.bytes)});
164
2/2
✓ Branch 120 → 121 taken 3 times.
✓ Branch 120 → 125 taken 10025 times.
10028 else if (!stayedOld.contains(entry.partner))
165 3 diff.changes.append({BlockChange::Kind::Moved, entry.name, entry.named, {}});
166 }
167
2/2
✓ Branch 142 → 131 taken 10039 times.
✓ Branch 142 → 143 taken 9 times.
10048 for (const Entry &old : std::as_const(olds))
168
2/2
✓ Branch 132 → 133 taken 4 times.
✓ Branch 132 → 140 taken 10035 times.
10039 if (!old.matched)
169 4 diff.changes.append({BlockChange::Kind::Removed, old.name, old.named, lineDiff(old.bytes, QByteArray())});
170
7/7
✓ Branch 143 → 144 taken 9 times.
✓ Branch 147 → 148 taken 9 times.
✓ Branch 148 → 149 taken 9 times.
✓ Branch 150 → 151 taken 9 times.
✓ Branch 151 → 152 taken 9 times.
✓ Branch 156 → 145 taken 9 times.
✓ Branch 156 → 157 taken 9 times.
18 qCDebug(lcSieveDiff) << "Script diff:" << diff.changes.size() << "block changes, require changed"
171
1/1
✓ Branch 152 → 153 taken 9 times.
9 << diff.requireChanged;
172 9 return diff;
173 30 }
174
175 } // namespace Sieve
176