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: 68.3% 114 / 0 / 167
Functions: 100.0% 11 / 0 / 11
Branches: 51.6% 95 / 0 / 184

data/RelationGraphForce.cpp
Line Branch Exec Source
1 #include "RelationGraphForce.h"
2
3 #include <QCryptographicHash>
4 #include <algorithm>
5 #include <cmath>
6 #include <numeric>
7 #include <vector>
8
9 namespace RelationGraphForce {
10
11 2 QPointF seeded(const QString &key, double spread) {
12
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
2 const QByteArray hash = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha1);
13 2 const quint32 a = quint32(quint8(hash[0])) << 8 | quint8(hash[1]);
14 2 const quint32 b = quint32(quint8(hash[2])) << 8 | quint8(hash[3]);
15 2 const double angle = 2 * M_PI * double(a) / 65535.0, radius = spread * std::sqrt(double(b) / 65535.0);
16 4 return {radius * std::cos(angle), radius * std::sin(angle)};
17 2 }
18
19 1 void layout(QList<QPointF> *positions, const QList<QPair<int, int>> &edges, const QList<bool> &pinned,
20 double idealDistance, int iterations, double temperature, const QList<double> &radius) {
21 1 QList<QPointF> &pos = *positions;
22 1 const int n = int(pos.size());
23
2/4
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (n <= 0 || iterations <= 0)
24 ✗ return;
25 1 const double k = idealDistance;
26 1 const double cooling = temperature / (iterations + 1);
27 1 const bool grid = n > 300;
28 // A cell holds a typical disc (the median) and the reach of its forces.
29
1/1
✓ Branch 10 → 11 taken 1 time.
1 std::vector<double> sorted(radius.cbegin(), radius.cend());
30
1/1
✓ Branch 14 → 15 taken 1 time.
1 std::sort(sorted.begin(), sorted.end());
31
1/2
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
1 const double typical = sorted.empty() ? 0 : sorted[sorted.size() / 2];
32 1 const double cell = 2.5 * k + 2 * typical;
33 // The gap between two rims, never below a small positive distance.
34 400 const auto gap = [&radius](int i, int j, double centres) {
35
2/2
✓ Branch 2 → 3 taken 400 times.
✓ Branch 3 → 4 taken 400 times.
400 return std::max(centres - radius.value(i) - radius.value(j), 1.0);
36 1 };
37 1 std::vector<int> large; // discs too big for the grid's neighbourhood
38
2/2
✓ Branch 26 → 22 taken 2 times.
✓ Branch 26 → 27 taken 1 time.
3 for (int i = 0; i < n; ++i)
39
2/3
✓ Branch 22 → 23 taken 2 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 2 times.
2 if (radius.value(i) > cell / 2)
40 ✗ large.push_back(i);
41
1/1
✓ Branch 27 → 28 taken 1 time.
1 QList<QPointF> displacement(n);
42
2/2
✓ Branch 194 → 29 taken 200 times.
✓ Branch 194 → 195 taken 1 time.
201 for (int iteration = 0; iteration < iterations; ++iteration) {
43
3/3
✓ Branch 30 → 31 taken 200 times.
✓ Branch 31 → 32 taken 200 times.
✓ Branch 32 → 33 taken 200 times.
200 std::fill(displacement.begin(), displacement.end(), QPointF());
44 // Repulsion: all pairs, or the neighbouring cells of a flat uniform grid
45 // above 300 points (rebuilt per iteration over the current bounds).
46 200 double minX = 0, minY = 0;
47 200 int columns = 1, rows = 1;
48 200 std::vector<int> cellStart, cellItems, cellOf;
49
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 91 taken 200 times.
200 if (grid) {
50 ✗ double maxX = pos.at(0).x(), maxY = pos.at(0).y();
51 ✗ minX = maxX;
52 ✗ minY = maxY;
53 ✗ for (const QPointF &p : std::as_const(pos)) {
54 ✗ minX = std::min(minX, p.x());
55 ✗ minY = std::min(minY, p.y());
56 ✗ maxX = std::max(maxX, p.x());
57 ✗ maxY = std::max(maxY, p.y());
58 }
59 ✗ columns = std::clamp(int((maxX - minX) / cell) + 1, 1, 4096);
60 ✗ rows = std::clamp(int((maxY - minY) / cell) + 1, 1, 4096);
61 ✗ cellOf.resize(size_t(n));
62 ✗ cellStart.assign(size_t(columns) * rows + 1, 0);
63 ✗ for (int i = 0; i < n; ++i) {
64 ✗ const int cx = std::min(int((pos.at(i).x() - minX) / cell), columns - 1);
65 ✗ const int cy = std::min(int((pos.at(i).y() - minY) / cell), rows - 1);
66 ✗ cellOf[size_t(i)] = cy * columns + cx;
67 ✗ ++cellStart[size_t(cellOf[size_t(i)]) + 1];
68 }
69 ✗ for (size_t c = 1; c < cellStart.size(); ++c)
70 ✗ cellStart[c] += cellStart[c - 1];
71 ✗ cellItems.resize(size_t(n));
72 ✗ std::vector<int> fill(cellStart.begin(), cellStart.end() - 1);
73 ✗ for (int i = 0; i < n; ++i)
74 ✗ cellItems[size_t(fill[size_t(cellOf[size_t(i)])]++)] = i;
75 ✗ }
76
2/2
✓ Branch 133 → 92 taken 400 times.
✓ Branch 133 → 134 taken 200 times.
600 for (int i = 0; i < n; ++i) {
77 200 const auto push = [&](int j) {
78 200 double dx = pos.at(i).x() - pos.at(j).x();
79 200 double dy = pos.at(i).y() - pos.at(j).y();
80 200 double centres = std::hypot(dx, dy);
81
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 200 times.
200 if (centres < 1e-2) {
82 ✗ dx = 0.01 * (i - j);
83 ✗ dy = 0.01;
84 ✗ centres = std::hypot(dx, dy);
85 }
86 // |force| = k² / gap along the unit vector.
87 200 const double scale = k * k / gap(i, j, centres) / centres;
88
1/1
✓ Branch 14 → 15 taken 200 times.
200 displacement[i] += QPointF(dx * scale, dy * scale);
89
1/1
✓ Branch 17 → 18 taken 200 times.
200 displacement[j] -= QPointF(dx * scale, dy * scale);
90 200 };
91 200 const auto repel = [&](int j) {
92 // Large discs meet everyone below; a pair of them once.
93
4/12
✓ Branch 2 → 3 taken 200 times.
✗ Branch 2 → 10 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 200 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 10 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 200 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 200 times.
✗ Branch 11 → 13 not taken.
200 if (j > i && !(grid && radius.value(i) > cell / 2) && !(grid && radius.value(j) > cell / 2))
94 200 push(j);
95 600 };
96
1/2
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 108 taken 400 times.
400 if (grid)
97 ✗ for (int j : large)
98 ✗ if (j != i && (radius.value(i) <= cell / 2 || j > i))
99 ✗ push(j);
100
1/2
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 128 taken 400 times.
400 if (grid) {
101 ✗ const int cx = cellOf[size_t(i)] % columns;
102 ✗ const int cy = cellOf[size_t(i)] / columns;
103 ✗ for (int y = std::max(cy - 1, 0); y <= std::min(cy + 1, rows - 1); ++y)
104 ✗ for (int x = std::max(cx - 1, 0); x <= std::min(cx + 1, columns - 1); ++x) {
105 ✗ const size_t c = size_t(y) * columns + x;
106 ✗ for (int item = cellStart[c]; item < cellStart[c + 1]; ++item)
107 ✗ repel(cellItems[size_t(item)]);
108 }
109 } else {
110
2/2
✓ Branch 131 → 129 taken 200 times.
✓ Branch 131 → 132 taken 400 times.
600 for (int j = i + 1; j < n; ++j)
111
1/1
✓ Branch 129 → 130 taken 200 times.
200 repel(j);
112 }
113 }
114 // Attraction along edges.
115
2/2
✓ Branch 161 → 136 taken 200 times.
✓ Branch 161 → 162 taken 200 times.
400 for (const auto &[from, to] : edges) {
116
5/10
✓ Branch 139 → 140 taken 200 times.
✗ Branch 139 → 144 not taken.
✓ Branch 140 → 141 taken 200 times.
✗ Branch 140 → 144 not taken.
✓ Branch 141 → 142 taken 200 times.
✗ Branch 141 → 144 not taken.
✓ Branch 142 → 143 taken 200 times.
✗ Branch 142 → 144 not taken.
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 145 taken 200 times.
200 if (from < 0 || to < 0 || from >= n || to >= n || from == to)
117 ✗ continue;
118 200 const QPointF d = pos.at(from) - pos.at(to);
119 200 const double dist = std::max(std::hypot(d.x(), d.y()), 0.01);
120
1/1
✓ Branch 151 → 152 taken 200 times.
200 const double rim = gap(from, to, dist);
121 200 const QPointF force = d / dist * (rim * rim / k);
122
1/1
✓ Branch 154 → 155 taken 200 times.
200 displacement[from] -= force;
123
1/1
✓ Branch 156 → 157 taken 200 times.
200 displacement[to] += force;
124 }
125 // Gravity keeps separate components on screen.
126
2/2
✓ Branch 168 → 163 taken 400 times.
✓ Branch 168 → 169 taken 200 times.
600 for (int i = 0; i < n; ++i)
127
1/1
✓ Branch 165 → 166 taken 400 times.
400 displacement[i] -= pos.at(i) * 0.02;
128
2/2
✓ Branch 188 → 170 taken 400 times.
✓ Branch 188 → 189 taken 200 times.
600 for (int i = 0; i < n; ++i) {
129 400 const double length = std::hypot(displacement.at(i).x(), displacement.at(i).y());
130
4/7
✓ Branch 174 → 175 taken 400 times.
✗ Branch 174 → 178 not taken.
✓ Branch 175 → 176 taken 400 times.
✓ Branch 176 → 177 taken 400 times.
✗ Branch 176 → 178 not taken.
✓ Branch 179 → 180 taken 400 times.
✗ Branch 179 → 187 not taken.
400 if (length > 0 && !pinned.value(i))
131
1/1
✓ Branch 184 → 185 taken 400 times.
400 pos[i] += displacement.at(i) / length * std::min(length, temperature);
132 }
133 200 temperature = std::max(temperature - cooling, 0.5);
134 200 }
135 1 }
136
137 7 void separate(QList<QPointF> *positions, const QList<QSizeF> &sizes, const QList<bool> &pinned, int passes) {
138 7 QList<QPointF> &pos = *positions;
139 7 const int n = int(pos.size());
140
1/1
✓ Branch 5 → 6 taken 7 times.
7 std::vector<int> order(static_cast<size_t>(n));
141
1/2
✓ Branch 88 → 8 taken 8 times.
✗ Branch 88 → 89 not taken.
8 for (int pass = 0; pass < passes; ++pass) {
142 // Sweep by left edge: only pairs that overlap horizontally are compared.
143 8 std::iota(order.begin(), order.end(), 0);
144
1/1
✓ Branch 13 → 14 taken 8 times.
8 std::sort(order.begin(), order.end(), [&](int a, int b) {
145 136 const double la = pos.at(a).x() - sizes.at(a).width() / 2, lb = pos.at(b).x() - sizes.at(b).width() / 2;
146
1/2
✓ Branch 10 → 11 taken 136 times.
✗ Branch 10 → 12 not taken.
136 return la != lb ? la < lb : a < b;
147 });
148 8 bool moved = false;
149
2/2
✓ Branch 84 → 15 taken 37 times.
✓ Branch 84 → 85 taken 8 times.
45 for (size_t oi = 0; oi < order.size(); ++oi) {
150 37 const int i = order[oi];
151
2/2
✓ Branch 81 → 17 taken 107 times.
✓ Branch 81 → 82 taken 15 times.
122 for (size_t oj = oi + 1; oj < order.size(); ++oj) {
152 107 const int j = order[oj];
153 107 const double dx = pos.at(j).x() - pos.at(i).x(), dy = pos.at(j).y() - pos.at(i).y();
154 107 const double ox = (sizes.at(i).width() + sizes.at(j).width()) / 2 - std::abs(dx);
155
2/2
✓ Branch 39 → 40 taken 22 times.
✓ Branch 39 → 41 taken 85 times.
107 if (pos.at(j).x() - sizes.at(j).width() / 2 >= pos.at(i).x() + sizes.at(i).width() / 2)
156 22 break; // sorted by left edge: nobody further right overlaps i
157 85 const double oy = (sizes.at(i).height() + sizes.at(j).height()) / 2 - std::abs(dy);
158
4/4
✓ Branch 46 → 47 taken 84 times.
✓ Branch 46 → 48 taken 1 time.
✓ Branch 47 → 48 taken 83 times.
✓ Branch 47 → 49 taken 1 time.
85 if (ox <= 0 || oy <= 0)
159 84 continue;
160
2/2
✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
1 const bool pi = pinned.value(i), pj = pinned.value(j);
161
1/4
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 54 taken 1 time.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
1 if (pi && pj)
162 ✗ continue;
163 // Along the smaller overlap; ties and coincident centres go right/down.
164
2/6
✓ Branch 54 → 55 taken 1 time.
✗ Branch 54 → 59 not taken.
✓ Branch 55 → 56 taken 1 time.
✗ Branch 55 → 57 not taken.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
1 QPointF step = ox < oy ? QPointF(dx >= 0 ? ox : -ox, 0) : QPointF(0, dy >= 0 ? oy : -oy);
165
2/4
✓ Branch 63 → 64 taken 1 time.
✗ Branch 63 → 72 not taken.
✓ Branch 64 → 65 taken 1 time.
✗ Branch 64 → 72 not taken.
1 if (!pi && !pj) {
166
1/1
✓ Branch 66 → 67 taken 1 time.
1 pos[i] -= step / 2;
167
1/1
✓ Branch 69 → 70 taken 1 time.
1 pos[j] += step / 2;
168 ✗ } else if (pi) {
169 ✗ pos[j] += step;
170 } else {
171 ✗ pos[i] -= step;
172 }
173 1 moved = true;
174 }
175 }
176
2/2
✓ Branch 85 → 86 taken 7 times.
✓ Branch 85 → 87 taken 1 time.
8 if (!moved)
177 7 break;
178 }
179 // What still overlaps (crowds where pushes cancel out) moves to the nearest
180 // free place on a spiral around it: pinned blocks first, then from the
181 // middle outwards, each against those already placed.
182 50 const auto overlap = [&](QPointF at, int i, int j) {
183
2/2
✓ Branch 10 → 11 taken 11 times.
✓ Branch 10 → 21 taken 39 times.
61 return std::abs(at.x() - pos.at(j).x()) < (sizes.at(i).width() + sizes.at(j).width()) / 2 - 0.01 &&
184
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 11 times.
61 std::abs(at.y() - pos.at(j).y()) < (sizes.at(i).height() + sizes.at(j).height()) / 2 - 0.01;
185 7 };
186 7 std::iota(order.begin(), order.end(), 0);
187
1/1
✓ Branch 94 → 95 taken 7 times.
7 std::sort(order.begin(), order.end(), [&](int a, int b) {
188
2/2
✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 7 taken 137 times.
163 if (pinned.value(a) != pinned.value(b))
189 26 return pinned.value(a);
190 137 const double da = std::hypot(pos.at(a).x(), pos.at(a).y()), db = std::hypot(pos.at(b).x(), pos.at(b).y());
191
1/2
✓ Branch 15 → 16 taken 137 times.
✗ Branch 15 → 17 not taken.
137 return da != db ? da < db : a < b;
192 });
193 7 std::vector<int> placed;
194
2/2
✓ Branch 130 → 97 taken 35 times.
✓ Branch 130 → 131 taken 7 times.
42 for (const int i : order) {
195 4 const auto isFree = [&](QPointF at) {
196 54 return std::none_of(placed.cbegin(), placed.cend(), [&](int j) { return overlap(at, i, j); });
197 35 };
198
6/8
✓ Branch 98 → 99 taken 35 times.
✓ Branch 99 → 100 taken 4 times.
✓ Branch 99 → 104 taken 31 times.
✓ Branch 101 → 102 taken 4 times.
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 104 taken 4 times.
✗ Branch 105 → 106 not taken.
✓ Branch 105 → 127 taken 35 times.
35 if (!pinned.value(i) && !isFree(pos.at(i))) {
199 ✗ const double step = std::min(sizes.at(i).width(), sizes.at(i).height()) / 4 + 10;
200 ✗ bool found = false;
201 ✗ for (int ring = 1; ring <= 400 && !found; ++ring)
202 ✗ for (int k = 0; k < 8 * ring && !found; ++k) {
203 ✗ const double angle = 2 * M_PI * k / (8 * ring);
204 ✗ const QPointF at = pos.at(i) + QPointF(std::cos(angle), std::sin(angle)) * (ring * step);
205 ✗ if (isFree(at)) {
206 ✗ pos[i] = at;
207 ✗ found = true;
208 }
209 }
210 }
211
1/1
✓ Branch 127 → 128 taken 35 times.
35 placed.push_back(i);
212 }
213 7 }
214
215 } // namespace RelationGraphForce
216