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: 93.6% 249 / 0 / 266
Functions: 100.0% 11 / 0 / 11
Branches: 84.6% 710 / 0 / 839

presentation/UiStateStore.cpp
Line Branch Exec Source
1 #include "UiStateStore.h"
2
3 #include <algorithm>
4 #include "DesignTokens.h"
5 #include "WorkspaceState.h"
6 #include <QDir>
7 #include <QFile>
8 #include <QFileInfo>
9 #include <QJsonDocument>
10 #include <QJsonArray>
11 #include <QLockFile>
12 #include <QSaveFile>
13 #include <QSettings>
14 #include <cmath>
15
16
1/1
✓ Branch 5 → 6 taken 1613 times.
1613 UiStateStore::UiStateStore(QString directory) : m_directory(std::move(directory)) {}
17
18 3963 QVariantMap UiStateStore::defaults() {
19 ✗ return {{"ui/schemaVersion", 1}, {"ui/layout", "classic"},
20 ✗ {"ui/motion", "system"}, {"ui/fontScale", 1.0},
21 {"appearance/theme", "system"}, {"appearance/accentColor", ""}, {"view/defaultMode", "text"},
22 {"view/threaded", false}, {"view/suggestions", false}, {"ui/composeWindowed", false},
23 {"ui/closeToTray", true},
24 // Sprint 93 S93.3.9 (ux.md §6): the calendar's remembered state.
25 ✗ {"calendar/viewMode", "month"}, {"calendar/workWeek", false}, {"calendar/hourZoom", 48},
26 ✗ {"calendar/hiddenCalendars", QStringList{}}, {"calendar/lastCalendar", ""},
27 ✗ {"calendar/agendaPanel", false}, {"calendar/agendaWidth", 320}, {"calendar/agendaTasksHeight", 0}, {"calendar/agendaTasks", "today"},
28 ✗ {"calendar/agendaTasksShown", true}, {"calendar/agendaTaskFilter", ""}, {"calendar/agendaTaskSort", "due"}, {"calendar/workdayStart", 8}, {"calendar/workdayEnd", 18},
29 ✗ {"calendar/firstDayOfWeek", 0}, {"calendar/tasks", QVariantMap{}}, {"calendar/lastTaskList", ""},
30 // Sprint 108 D-22: the working days of *Suggest times* (Qt::DayOfWeek, Mon–Fri).
31
2/4
✓ Branch 55 → 56 taken 19815 times.
✓ Branch 55 → 57 taken 3963 times.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
23778 {"calendar/workdays", QVariantList{1, 2, 3, 4, 5}},
32 // Sprint 100 SB-13: the default reminder of new tasks (0 none, 1 due day 09:00).
33 ✗ {"calendar/taskDefaultReminder", 0},
34 // Sprint 98 S98.1.6 (D-9, D-20): command-bar history and recent folders.
35 3963 {"commandbar/history", QStringList{}}, {"commandbar/recentFolders", QVariantMap{}},
36 // Sprint 101 (D-26, D-30): the search timeline's band and its one-time hint.
37
2/4
✓ Branch 49 → 50 taken 138705 times.
✓ Branch 49 → 51 taken 3963 times.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
146631 {"search/timelineCollapsed", false}, {"search/timelineHintSeen", false}, {"search/grouped", false}};
38 11889 }
39
40 726 QVariantMap UiStateStore::normalized(const QVariantMap &values) {
41
1/1
✓ Branch 2 → 3 taken 726 times.
726 auto result = defaults();
42 const QMap<QString, QStringList> choices = {
43 {"ui/layout", {"classic", "wide"}},
44 {"ui/motion", {"system", "reduced", "full"}},
45 {"appearance/theme", {"light", "dark", "system"}},
46 {"view/defaultMode", {"text", "light", "html"}},
47
12/24
✓ Branch 36 → 37 taken 3630 times.
✓ Branch 36 → 38 taken 726 times.
✓ Branch 40 → 41 taken 3630 times.
✓ Branch 40 → 42 taken 726 times.
✓ Branch 45 → 46 taken 2178 times.
✓ Branch 45 → 47 taken 726 times.
✓ Branch 50 → 51 taken 2178 times.
✓ Branch 50 → 52 taken 726 times.
✓ Branch 55 → 56 taken 2178 times.
✓ Branch 55 → 57 taken 726 times.
✓ Branch 60 → 61 taken 1452 times.
✓ Branch 60 → 62 taken 726 times.
✗ Branch 930 → 931 not taken.
✗ Branch 930 → 932 not taken.
✗ Branch 936 → 937 not taken.
✗ Branch 936 → 938 not taken.
✗ Branch 952 → 953 not taken.
✗ Branch 952 → 954 not taken.
✗ Branch 968 → 969 not taken.
✗ Branch 968 → 970 not taken.
✗ Branch 984 → 985 not taken.
✗ Branch 984 → 986 not taken.
✗ Branch 1000 → 1001 not taken.
✗ Branch 1000 → 1002 not taken.
15972 {"calendar/viewMode", {"day", "week", "month", "year", "agenda"}}};
48
4/4
✓ Branch 63 → 64 taken 726 times.
✓ Branch 82 → 83 taken 4356 times.
✓ Branch 84 → 65 taken 3630 times.
✓ Branch 84 → 85 taken 726 times.
4356 for (auto it = choices.cbegin(); it != choices.cend(); ++it) {
49
2/2
✓ Branch 67 → 68 taken 3630 times.
✓ Branch 68 → 69 taken 3630 times.
3630 const QString value = values.value(it.key()).toString();
50
3/3
✓ Branch 73 → 74 taken 3620 times.
✓ Branch 73 → 80 taken 10 times.
✓ Branch 76 → 77 taken 3620 times.
3630 if (it.value().contains(value)) result[it.key()] = value;
51 3630 }
52 726 bool valid = false;
53
3/3
✓ Branch 86 → 87 taken 726 times.
✓ Branch 87 → 88 taken 726 times.
✓ Branch 88 → 89 taken 726 times.
726 const auto accent = values.value("appearance/accentColor").toString();
54
7/7
✓ Branch 92 → 93 taken 726 times.
✓ Branch 93 → 94 taken 722 times.
✓ Branch 93 → 105 taken 4 times.
✓ Branch 94 → 95 taken 722 times.
✓ Branch 95 → 96 taken 722 times.
✓ Branch 97 → 98 taken 722 times.
✓ Branch 98 → 99 taken 722 times.
726 if (DesignTokens::validAccentColor(accent)) result["appearance/accentColor"] = accent.trimmed().toLower();
55
2/2
✓ Branch 106 → 107 taken 726 times.
✓ Branch 107 → 108 taken 726 times.
726 const QVariant threaded = values.value("view/threaded");
56
7/7
✓ Branch 110 → 111 taken 726 times.
✓ Branch 111 → 112 taken 726 times.
✓ Branch 112 → 113 taken 722 times.
✓ Branch 112 → 121 taken 4 times.
✓ Branch 113 → 114 taken 722 times.
✓ Branch 115 → 116 taken 722 times.
✓ Branch 116 → 117 taken 722 times.
726 if (threaded.metaType().id() == QMetaType::Bool) result["view/threaded"] = threaded.toBool();
57
2/2
✓ Branch 143 → 123 taken 2178 times.
✓ Branch 143 → 144 taken 726 times.
2904 for (const char *key : {"search/timelineCollapsed", "search/timelineHintSeen", "search/grouped"}) {
58
2/2
✓ Branch 125 → 126 taken 2178 times.
✓ Branch 126 → 127 taken 2178 times.
2178 const QVariant value = values.value(QString::fromLatin1(key));
59
7/7
✓ Branch 129 → 130 taken 2178 times.
✓ Branch 130 → 131 taken 2178 times.
✓ Branch 131 → 132 taken 2172 times.
✓ Branch 131 → 141 taken 6 times.
✓ Branch 132 → 133 taken 2172 times.
✓ Branch 135 → 136 taken 2172 times.
✓ Branch 136 → 137 taken 2172 times.
2178 if (value.metaType().id() == QMetaType::Bool) result[QString::fromLatin1(key)] = value.toBool();
60 2178 }
61
2/2
✓ Branch 145 → 146 taken 726 times.
✓ Branch 146 → 147 taken 726 times.
726 const QVariant suggestions = values.value("view/suggestions");
62
7/7
✓ Branch 149 → 150 taken 726 times.
✓ Branch 150 → 151 taken 726 times.
✓ Branch 151 → 152 taken 722 times.
✓ Branch 151 → 160 taken 4 times.
✓ Branch 152 → 153 taken 722 times.
✓ Branch 154 → 155 taken 722 times.
✓ Branch 155 → 156 taken 722 times.
726 if (suggestions.metaType().id() == QMetaType::Bool) result["view/suggestions"] = suggestions.toBool();
63
2/2
✓ Branch 161 → 162 taken 726 times.
✓ Branch 162 → 163 taken 726 times.
726 const QVariant composeWindowed = values.value("ui/composeWindowed");
64
4/4
✓ Branch 165 → 166 taken 726 times.
✓ Branch 166 → 167 taken 726 times.
✓ Branch 167 → 168 taken 724 times.
✓ Branch 167 → 176 taken 2 times.
726 if (composeWindowed.metaType().id() == QMetaType::Bool)
65
3/3
✓ Branch 168 → 169 taken 724 times.
✓ Branch 170 → 171 taken 724 times.
✓ Branch 171 → 172 taken 724 times.
724 result["ui/composeWindowed"] = composeWindowed.toBool();
66
2/2
✓ Branch 177 → 178 taken 726 times.
✓ Branch 178 → 179 taken 726 times.
726 const QVariant closeToTray = values.value("ui/closeToTray");
67
4/4
✓ Branch 181 → 182 taken 726 times.
✓ Branch 182 → 183 taken 726 times.
✓ Branch 183 → 184 taken 724 times.
✓ Branch 183 → 192 taken 2 times.
726 if (closeToTray.metaType().id() == QMetaType::Bool)
68
3/3
✓ Branch 184 → 185 taken 724 times.
✓ Branch 186 → 187 taken 724 times.
✓ Branch 187 → 188 taken 724 times.
724 result["ui/closeToTray"] = closeToTray.toBool();
69 // F93-20: working hours (start < end) and the first day of the week.
70
3/3
✓ Branch 193 → 194 taken 726 times.
✓ Branch 194 → 195 taken 726 times.
✓ Branch 195 → 196 taken 726 times.
726 const int workdayStart = values.value("calendar/workdayStart").toInt(&valid);
71
5/5
✓ Branch 199 → 200 taken 724 times.
✓ Branch 199 → 208 taken 2 times.
✓ Branch 200 → 201 taken 724 times.
✓ Branch 202 → 203 taken 724 times.
✓ Branch 203 → 204 taken 724 times.
726 if (valid) result["calendar/workdayStart"] = qBound(0, workdayStart, 22);
72 // The end is bounded even when only the start is stored (Sprint 102 fuzz
73 // target ui_state: start 22 with the default end 18).
74
3/3
✓ Branch 209 → 210 taken 726 times.
✓ Branch 210 → 211 taken 726 times.
✓ Branch 211 → 212 taken 726 times.
726 const int workdayEnd = values.value("calendar/workdayEnd").toInt(&valid);
75
5/5
✓ Branch 222 → 223 taken 726 times.
✓ Branch 223 → 224 taken 726 times.
✓ Branch 224 → 225 taken 726 times.
✓ Branch 226 → 227 taken 726 times.
✓ Branch 227 → 228 taken 726 times.
2904 result["calendar/workdayEnd"] = qBound(result["calendar/workdayStart"].toInt() + 1,
76
8/10
✓ Branch 215 → 216 taken 721 times.
✓ Branch 215 → 217 taken 5 times.
✓ Branch 217 → 218 taken 5 times.
✓ Branch 218 → 219 taken 5 times.
✓ Branch 219 → 220 taken 5 times.
✓ Branch 221 → 222 taken 726 times.
✓ Branch 232 → 233 taken 5 times.
✓ Branch 232 → 234 taken 721 times.
✗ Branch 1160 → 1161 not taken.
✗ Branch 1160 → 1162 not taken.
3630 valid ? workdayEnd : result["calendar/workdayEnd"].toInt(), 24);
77
3/3
✓ Branch 235 → 236 taken 726 times.
✓ Branch 236 → 237 taken 726 times.
✓ Branch 237 → 238 taken 726 times.
726 const int taskReminder = values.value("calendar/taskDefaultReminder").toInt(&valid);
78
7/8
✓ Branch 241 → 242 taken 724 times.
✓ Branch 241 → 251 taken 2 times.
✓ Branch 242 → 243 taken 8 times.
✓ Branch 242 → 244 taken 716 times.
✓ Branch 243 → 244 taken 8 times.
✗ Branch 243 → 251 not taken.
✓ Branch 245 → 246 taken 724 times.
✓ Branch 246 → 247 taken 724 times.
726 if (valid && (taskReminder == 0 || taskReminder == 1)) result["calendar/taskDefaultReminder"] = taskReminder;
79 // Sprint 108 D-22: one to seven distinct days 1–7, sorted.
80
6/6
✓ Branch 252 → 253 taken 726 times.
✓ Branch 253 → 254 taken 726 times.
✓ Branch 254 → 255 taken 726 times.
✓ Branch 255 → 256 taken 726 times.
✓ Branch 259 → 260 taken 724 times.
✓ Branch 259 → 310 taken 2 times.
726 if (values.value("calendar/workdays").metaType().id() == QMetaType::QVariantList) {
81 724 QList<int> days;
82
7/7
✓ Branch 261 → 262 taken 724 times.
✓ Branch 262 → 263 taken 724 times.
✓ Branch 263 → 264 taken 724 times.
✓ Branch 267 → 268 taken 724 times.
✓ Branch 268 → 269 taken 724 times.
✓ Branch 283 → 270 taken 3662 times.
✓ Branch 283 → 284 taken 724 times.
4386 for (const QVariant &day : values.value("calendar/workdays").toList()) {
83
1/1
✓ Branch 271 → 272 taken 3662 times.
3662 const int value = day.toInt(&valid);
84
6/11
✓ Branch 272 → 273 taken 3662 times.
✗ Branch 272 → 278 not taken.
✓ Branch 273 → 274 taken 3662 times.
✗ Branch 273 → 278 not taken.
✓ Branch 274 → 275 taken 3662 times.
✗ Branch 274 → 278 not taken.
✓ Branch 276 → 277 taken 3662 times.
✗ Branch 276 → 278 not taken.
✓ Branch 279 → 280 taken 3662 times.
✗ Branch 279 → 281 not taken.
✓ Branch 280 → 281 taken 3662 times.
3662 if (valid && value >= 1 && value <= 7 && !days.contains(value)) days.append(value);
85 724 }
86
3/3
✓ Branch 285 → 286 taken 724 times.
✓ Branch 286 → 287 taken 724 times.
✓ Branch 287 → 288 taken 724 times.
724 std::sort(days.begin(), days.end());
87
1/2
✓ Branch 289 → 290 taken 724 times.
✗ Branch 289 → 308 not taken.
724 if (!days.isEmpty()) {
88 724 QVariantList list;
89
5/5
✓ Branch 290 → 291 taken 724 times.
✓ Branch 291 → 292 taken 724 times.
✓ Branch 295 → 296 taken 3662 times.
✓ Branch 299 → 293 taken 3662 times.
✓ Branch 299 → 300 taken 724 times.
4386 for (const int day : days) list.append(day);
90
2/2
✓ Branch 301 → 302 taken 724 times.
✓ Branch 302 → 303 taken 724 times.
724 result["calendar/workdays"] = list;
91 724 }
92 724 }
93
3/3
✓ Branch 311 → 312 taken 726 times.
✓ Branch 312 → 313 taken 726 times.
✓ Branch 313 → 314 taken 726 times.
726 const int firstDay = values.value("calendar/firstDayOfWeek").toInt(&valid);
94
7/8
✓ Branch 317 → 318 taken 724 times.
✓ Branch 317 → 327 taken 2 times.
✓ Branch 318 → 319 taken 724 times.
✗ Branch 318 → 327 not taken.
✓ Branch 319 → 320 taken 720 times.
✓ Branch 319 → 327 taken 4 times.
✓ Branch 321 → 322 taken 720 times.
✓ Branch 322 → 323 taken 720 times.
726 if (valid && firstDay >= 0 && firstDay <= 7) result["calendar/firstDayOfWeek"] = firstDay;
95 // F94-59: a smart list, "list:<key>" or "tag:<name>".
96
3/3
✓ Branch 328 → 329 taken 726 times.
✓ Branch 329 → 330 taken 726 times.
✓ Branch 330 → 331 taken 726 times.
726 const QString agendaTasks = values.value("calendar/agendaTasks").toString();
97
7/14
✓ Branch 340 → 341 taken 7 times.
✓ Branch 340 → 349 taken 719 times.
✓ Branch 357 → 358 taken 726 times.
✗ Branch 357 → 361 not taken.
✓ Branch 359 → 360 taken 2904 times.
✓ Branch 359 → 361 taken 726 times.
✓ Branch 361 → 362 taken 719 times.
✓ Branch 361 → 369 taken 7 times.
✗ Branch 1253 → 1254 not taken.
✗ Branch 1253 → 1255 not taken.
✗ Branch 1257 → 1258 not taken.
✗ Branch 1257 → 1261 not taken.
✗ Branch 1259 → 1260 not taken.
✗ Branch 1259 → 1261 not taken.
4363 if (QStringList{"today", "week", "planned", "mine"}.contains(agendaTasks) ||
98
11/20
✓ Branch 341 → 342 taken 7 times.
✓ Branch 342 → 343 taken 7 times.
✓ Branch 343 → 344 taken 7 times.
✗ Branch 343 → 347 not taken.
✓ Branch 344 → 345 taken 7 times.
✓ Branch 345 → 346 taken 7 times.
✗ Branch 346 → 347 not taken.
✓ Branch 346 → 350 taken 7 times.
✗ Branch 348 → 349 not taken.
✗ Branch 348 → 350 not taken.
✓ Branch 351 → 352 taken 7 times.
✓ Branch 351 → 353 taken 719 times.
✓ Branch 353 → 354 taken 7 times.
✓ Branch 353 → 355 taken 719 times.
✓ Branch 355 → 356 taken 726 times.
✗ Branch 355 → 357 not taken.
✗ Branch 1245 → 1246 not taken.
✗ Branch 1245 → 1247 not taken.
✗ Branch 1249 → 1250 not taken.
✗ Branch 1249 → 1251 not taken.
733 ((agendaTasks.startsWith("list:") || agendaTasks.startsWith("tag:")) && agendaTasks.size() <= 2100))
99
2/2
✓ Branch 363 → 364 taken 719 times.
✓ Branch 364 → 365 taken 719 times.
719 result["calendar/agendaTasks"] = agendaTasks;
100 // F94-67: optional task section, its filter tokens and order.
101
2/2
✓ Branch 370 → 371 taken 726 times.
✓ Branch 371 → 372 taken 726 times.
726 const QVariant agendaTasksShown = values.value("calendar/agendaTasksShown");
102
7/7
✓ Branch 374 → 375 taken 726 times.
✓ Branch 375 → 376 taken 726 times.
✓ Branch 376 → 377 taken 724 times.
✓ Branch 376 → 385 taken 2 times.
✓ Branch 377 → 378 taken 724 times.
✓ Branch 379 → 380 taken 724 times.
✓ Branch 380 → 381 taken 724 times.
726 if (agendaTasksShown.metaType().id() == QMetaType::Bool) result["calendar/agendaTasksShown"] = agendaTasksShown.toBool();
103
3/3
✓ Branch 386 → 387 taken 726 times.
✓ Branch 387 → 388 taken 726 times.
✓ Branch 388 → 389 taken 726 times.
726 const QString agendaTaskFilter = values.value("calendar/agendaTaskFilter").toString();
104
3/4
✓ Branch 393 → 394 taken 726 times.
✗ Branch 393 → 401 not taken.
✓ Branch 395 → 396 taken 726 times.
✓ Branch 396 → 397 taken 726 times.
726 if (agendaTaskFilter.size() <= 256) result["calendar/agendaTaskFilter"] = agendaTaskFilter;
105
3/3
✓ Branch 402 → 403 taken 726 times.
✓ Branch 403 → 404 taken 726 times.
✓ Branch 404 → 405 taken 726 times.
726 const QString agendaTaskSort = values.value("calendar/agendaTaskSort").toString();
106
6/8
✓ Branch 416 → 417 taken 2904 times.
✓ Branch 416 → 418 taken 726 times.
✓ Branch 418 → 419 taken 719 times.
✓ Branch 418 → 426 taken 7 times.
✓ Branch 420 → 421 taken 719 times.
✓ Branch 421 → 422 taken 719 times.
✗ Branch 1313 → 1314 not taken.
✗ Branch 1313 → 1315 not taken.
3630 if (QStringList{"due", "priority", "title", "created"}.contains(agendaTaskSort)) result["calendar/agendaTaskSort"] = agendaTaskSort;
107
3/3
✓ Branch 427 → 428 taken 726 times.
✓ Branch 428 → 429 taken 726 times.
✓ Branch 429 → 430 taken 726 times.
726 const int agendaWidth = values.value("calendar/agendaWidth").toInt(&valid);
108
5/5
✓ Branch 433 → 434 taken 724 times.
✓ Branch 433 → 442 taken 2 times.
✓ Branch 434 → 435 taken 724 times.
✓ Branch 436 → 437 taken 724 times.
✓ Branch 437 → 438 taken 724 times.
726 if (valid) result["calendar/agendaWidth"] = qBound(240, agendaWidth, 640);
109
3/3
✓ Branch 443 → 444 taken 726 times.
✓ Branch 444 → 445 taken 726 times.
✓ Branch 445 → 446 taken 726 times.
726 const int agendaTasksHeight = values.value("calendar/agendaTasksHeight").toInt(&valid);
110
5/7
✓ Branch 449 → 450 taken 724 times.
✓ Branch 449 → 461 taken 2 times.
✗ Branch 450 → 451 not taken.
✓ Branch 450 → 453 taken 724 times.
✗ Branch 451 → 452 not taken.
✓ Branch 455 → 456 taken 724 times.
✓ Branch 456 → 457 taken 724 times.
726 if (valid) result["calendar/agendaTasksHeight"] = agendaTasksHeight <= 0 ? 0 : qBound(60, agendaTasksHeight, 2000);
111
2/2
✓ Branch 462 → 463 taken 726 times.
✓ Branch 463 → 464 taken 726 times.
726 const QVariant agendaPanel = values.value("calendar/agendaPanel");
112
7/7
✓ Branch 466 → 467 taken 726 times.
✓ Branch 467 → 468 taken 726 times.
✓ Branch 468 → 469 taken 724 times.
✓ Branch 468 → 477 taken 2 times.
✓ Branch 469 → 470 taken 724 times.
✓ Branch 471 → 472 taken 724 times.
✓ Branch 472 → 473 taken 724 times.
726 if (agendaPanel.metaType().id() == QMetaType::Bool) result["calendar/agendaPanel"] = agendaPanel.toBool();
113
2/2
✓ Branch 478 → 479 taken 726 times.
✓ Branch 479 → 480 taken 726 times.
726 const QVariant workWeek = values.value("calendar/workWeek");
114
7/7
✓ Branch 482 → 483 taken 726 times.
✓ Branch 483 → 484 taken 726 times.
✓ Branch 484 → 485 taken 722 times.
✓ Branch 484 → 493 taken 4 times.
✓ Branch 485 → 486 taken 722 times.
✓ Branch 487 → 488 taken 722 times.
✓ Branch 488 → 489 taken 722 times.
726 if (workWeek.metaType().id() == QMetaType::Bool) result["calendar/workWeek"] = workWeek.toBool();
115
3/3
✓ Branch 494 → 495 taken 726 times.
✓ Branch 495 → 496 taken 726 times.
✓ Branch 496 → 497 taken 726 times.
726 const int zoom = values.value("calendar/hourZoom").toInt(&valid);
116
5/5
✓ Branch 500 → 501 taken 724 times.
✓ Branch 500 → 509 taken 2 times.
✓ Branch 501 → 502 taken 724 times.
✓ Branch 503 → 504 taken 724 times.
✓ Branch 504 → 505 taken 724 times.
726 if (valid) result["calendar/hourZoom"] = qBound(32, zoom, 96);
117 // Calendar keys are "accountId\npath" (bounded, no control characters
118 // besides the separator).
119 1476 const auto boundedKey = [](const QString &key) {
120
5/6
✓ Branch 3 → 4 taken 35 times.
✓ Branch 3 → 6 taken 1441 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 35 times.
✓ Branch 8 → 9 taken 1441 times.
✓ Branch 8 → 10 taken 35 times.
1476 if (key.isEmpty() || key.size() > 2048) return false;
121
2/2
✓ Branch 23 → 12 taken 440 times.
✓ Branch 23 → 24 taken 30 times.
470 for (const QChar ch : key)
122
6/6
✓ Branch 13 → 14 taken 19 times.
✓ Branch 13 → 19 taken 421 times.
✓ Branch 17 → 18 taken 5 times.
✓ Branch 17 → 19 taken 14 times.
✓ Branch 20 → 21 taken 5 times.
✓ Branch 20 → 22 taken 435 times.
440 if (ch.category() == QChar::Other_Control && ch != QLatin1Char('\n')) return false;
123 30 return true;
124 };
125 726 QStringList hidden;
126
7/7
✓ Branch 510 → 511 taken 726 times.
✓ Branch 511 → 512 taken 726 times.
✓ Branch 512 → 513 taken 726 times.
✓ Branch 516 → 517 taken 726 times.
✓ Branch 517 → 518 taken 726 times.
✓ Branch 532 → 519 taken 24 times.
✓ Branch 532 → 533 taken 726 times.
750 for (const auto &value : values.value("calendar/hiddenCalendars").toStringList())
127
8/9
✓ Branch 521 → 522 taken 14 times.
✓ Branch 521 → 527 taken 10 times.
✓ Branch 523 → 524 taken 14 times.
✗ Branch 523 → 527 not taken.
✓ Branch 525 → 526 taken 9 times.
✓ Branch 525 → 527 taken 5 times.
✓ Branch 528 → 529 taken 9 times.
✓ Branch 528 → 530 taken 15 times.
✓ Branch 529 → 530 taken 9 times.
750 if (boundedKey(value) && hidden.size() < 256 && !hidden.contains(value)) hidden.append(value);
128
2/2
✓ Branch 535 → 536 taken 726 times.
✓ Branch 536 → 537 taken 726 times.
726 result["calendar/hiddenCalendars"] = hidden;
129
3/3
✓ Branch 541 → 542 taken 726 times.
✓ Branch 542 → 543 taken 726 times.
✓ Branch 543 → 544 taken 726 times.
726 const QString lastCalendar = values.value("calendar/lastCalendar").toString();
130
4/4
✓ Branch 548 → 549 taken 14 times.
✓ Branch 548 → 556 taken 712 times.
✓ Branch 550 → 551 taken 14 times.
✓ Branch 551 → 552 taken 14 times.
726 if (boundedKey(lastCalendar)) result["calendar/lastCalendar"] = lastCalendar;
131 // D-9: at most 50 lines of at most 1024 characters; recent folders per
132 // account (at most 20 paths, 64 accounts). CommandHistory re-checks
133 // every line against the catalog on restore.
134
7/7
✓ Branch 3 → 4 taken 2961 times.
✓ Branch 3 → 10 taken 2321 times.
✓ Branch 5 → 6 taken 2960 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 7 → 8 taken 2960 times.
✓ Branch 8 → 9 taken 2958 times.
✓ Branch 8 → 10 taken 2 times.
5282 const auto boundedText = [](const QString &text) { return !text.isEmpty() && text.size() <= 1024 && !text.contains('\n'); };
135 726 QStringList history;
136
7/7
✓ Branch 557 → 558 taken 726 times.
✓ Branch 558 → 559 taken 726 times.
✓ Branch 559 → 560 taken 726 times.
✓ Branch 563 → 564 taken 726 times.
✓ Branch 564 → 565 taken 726 times.
✓ Branch 579 → 566 taken 151 times.
✓ Branch 579 → 580 taken 726 times.
877 for (const auto &line : values.value("commandbar/history").toStringList())
137
9/10
✓ Branch 567 → 568 taken 151 times.
✓ Branch 568 → 569 taken 148 times.
✓ Branch 568 → 574 taken 3 times.
✓ Branch 570 → 571 taken 127 times.
✓ Branch 570 → 574 taken 21 times.
✓ Branch 572 → 573 taken 127 times.
✗ Branch 572 → 574 not taken.
✓ Branch 575 → 576 taken 127 times.
✓ Branch 575 → 577 taken 24 times.
✓ Branch 576 → 577 taken 127 times.
877 if (boundedText(line) && history.size() < 50 && !history.contains(line)) history.append(line);
138
2/2
✓ Branch 582 → 583 taken 726 times.
✓ Branch 583 → 584 taken 726 times.
726 result["commandbar/history"] = history;
139 726 QVariantMap recentFolders;
140
3/3
✓ Branch 588 → 589 taken 726 times.
✓ Branch 589 → 590 taken 726 times.
✓ Branch 590 → 591 taken 726 times.
726 const QVariantMap storedFolders = values.value("commandbar/recentFolders").toMap();
141
8/9
✓ Branch 594 → 595 taken 726 times.
✓ Branch 631 → 632 taken 841 times.
✓ Branch 633 → 634 taken 115 times.
✓ Branch 633 → 637 taken 726 times.
✓ Branch 634 → 635 taken 115 times.
✓ Branch 635 → 636 taken 115 times.
✗ Branch 635 → 637 not taken.
✓ Branch 638 → 596 taken 115 times.
✓ Branch 638 → 639 taken 726 times.
841 for (auto it = storedFolders.cbegin(); it != storedFolders.cend() && recentFolders.size() < 64; ++it) {
142
3/3
✓ Branch 597 → 598 taken 115 times.
✓ Branch 598 → 599 taken 1 time.
✓ Branch 598 → 600 taken 114 times.
115 if (!boundedText(it.key())) continue;
143 114 QStringList paths;
144
5/5
✓ Branch 601 → 602 taken 114 times.
✓ Branch 602 → 603 taken 114 times.
✓ Branch 603 → 604 taken 114 times.
✓ Branch 618 → 605 taken 220 times.
✓ Branch 618 → 619 taken 114 times.
334 for (const auto &path : it.value().toStringList())
145
8/10
✓ Branch 606 → 607 taken 220 times.
✓ Branch 607 → 608 taken 220 times.
✗ Branch 607 → 613 not taken.
✓ Branch 609 → 610 taken 210 times.
✓ Branch 609 → 613 taken 10 times.
✓ Branch 611 → 612 taken 210 times.
✗ Branch 611 → 613 not taken.
✓ Branch 614 → 615 taken 210 times.
✓ Branch 614 → 616 taken 10 times.
✓ Branch 615 → 616 taken 210 times.
334 if (boundedText(path) && paths.size() < 20 && !paths.contains(path)) paths.append(path);
146
2/3
✓ Branch 621 → 622 taken 114 times.
✗ Branch 621 → 627 not taken.
✓ Branch 624 → 625 taken 114 times.
114 if (!paths.isEmpty()) recentFolders.insert(it.key(), paths);
147 114 }
148
2/2
✓ Branch 640 → 641 taken 726 times.
✓ Branch 641 → 642 taken 726 times.
726 result["commandbar/recentFolders"] = recentFolders;
149
3/3
✓ Branch 646 → 647 taken 726 times.
✓ Branch 647 → 648 taken 726 times.
✓ Branch 648 → 649 taken 726 times.
726 const double scale = values.value("ui/fontScale", 1.0).toDouble(&valid);
150
9/9
✓ Branch 652 → 653 taken 725 times.
✓ Branch 652 → 656 taken 1 time.
✓ Branch 654 → 655 taken 724 times.
✓ Branch 654 → 656 taken 1 time.
✓ Branch 657 → 658 taken 724 times.
✓ Branch 657 → 666 taken 2 times.
✓ Branch 658 → 659 taken 724 times.
✓ Branch 660 → 661 taken 724 times.
✓ Branch 661 → 662 taken 724 times.
726 if (valid && std::isfinite(scale)) result["ui/fontScale"] = qBound(1.0, scale, 2.0);
151
2/2
✓ Branch 754 → 676 taken 1452 times.
✓ Branch 754 → 755 taken 726 times.
5082 for (const auto &layout : {QStringLiteral("classic"), QStringLiteral("wide")}) {
152
1/1
✓ Branch 680 → 681 taken 1452 times.
2904 const QString key = QStringLiteral("ui/layouts/%1/paneRatios").arg(layout);
153
2/2
✓ Branch 684 → 685 taken 1452 times.
✓ Branch 685 → 686 taken 1452 times.
1452 const auto ratios = values.value(key).toList();
154
2/2
✓ Branch 689 → 690 taken 726 times.
✓ Branch 689 → 691 taken 726 times.
1452 const int expected = layout == "classic" ? 2 : 3;
155
2/2
✓ Branch 693 → 694 taken 1445 times.
✓ Branch 693 → 695 taken 7 times.
1452 if (ratios.size() != expected) continue;
156 7 double sum = 0;
157 7 QVariantList safe;
158
2/2
✓ Branch 715 → 697 taken 17 times.
✓ Branch 715 → 716 taken 5 times.
22 for (const auto &ratio : ratios) {
159
1/1
✓ Branch 698 → 699 taken 17 times.
17 const double value = ratio.toDouble(&valid);
160
8/9
✓ Branch 699 → 700 taken 16 times.
✓ Branch 699 → 703 taken 1 time.
✓ Branch 701 → 702 taken 16 times.
✗ Branch 701 → 703 not taken.
✓ Branch 702 → 703 taken 1 time.
✓ Branch 702 → 704 taken 15 times.
✓ Branch 705 → 706 taken 2 times.
✓ Branch 705 → 708 taken 15 times.
✓ Branch 706 → 707 taken 2 times.
17 if (!valid || !std::isfinite(value) || value <= 0) { safe.clear(); break; }
161
1/1
✓ Branch 709 → 710 taken 15 times.
15 safe.append(value);
162
2/2
✓ Branch 711 → 712 taken 15 times.
✓ Branch 712 → 713 taken 15 times.
15 sum += safe.last().toDouble();
163 }
164
5/6
✓ Branch 717 → 718 taken 5 times.
✓ Branch 717 → 720 taken 2 times.
✗ Branch 719 → 720 not taken.
✓ Branch 719 → 721 taken 5 times.
✓ Branch 722 → 723 taken 2 times.
✓ Branch 722 → 724 taken 5 times.
7 if (safe.size() != expected || !std::isfinite(sum)) continue;
165
5/5
✓ Branch 724 → 725 taken 5 times.
✓ Branch 725 → 726 taken 5 times.
✓ Branch 728 → 729 taken 13 times.
✓ Branch 734 → 727 taken 13 times.
✓ Branch 734 → 735 taken 5 times.
18 for (auto &value : safe) value = value.toDouble() / sum;
166
1/1
✓ Branch 736 → 737 taken 5 times.
5 result[key] = safe;
167 5079 }
168
3/3
✓ Branch 762 → 763 taken 726 times.
✓ Branch 763 → 764 taken 726 times.
✓ Branch 764 → 765 taken 726 times.
726 const auto lastTaskList = values.value("calendar/lastTaskList").toString();
169
4/4
✓ Branch 769 → 770 taken 2 times.
✓ Branch 769 → 777 taken 724 times.
✓ Branch 771 → 772 taken 2 times.
✓ Branch 772 → 773 taken 2 times.
726 if (boundedKey(lastTaskList)) result["calendar/lastTaskList"] = lastTaskList;
170 // S106-335: the task list state is allowlisted like every other key —
171 // known keys only, texts and lists bounded (QuickTaskModel::restoreState
172 // takes them as they are).
173
3/3
✓ Branch 778 → 779 taken 726 times.
✓ Branch 779 → 780 taken 726 times.
✓ Branch 780 → 781 taken 726 times.
726 const auto tasks = values.value("calendar/tasks").toMap();
174 726 QVariantMap safeTasks;
175
2/2
✓ Branch 805 → 786 taken 2904 times.
✓ Branch 805 → 806 taken 726 times.
3630 for (const auto *key : {"view", "grouping", "sort", "filter"}) {
176
3/3
✓ Branch 788 → 789 taken 2904 times.
✓ Branch 789 → 790 taken 2904 times.
✓ Branch 790 → 791 taken 2904 times.
2904 const QString text = tasks.value(QLatin1String(key)).toString();
177
5/5
✓ Branch 794 → 795 taken 2904 times.
✓ Branch 795 → 796 taken 586 times.
✓ Branch 795 → 803 taken 2318 times.
✓ Branch 798 → 799 taken 586 times.
✓ Branch 799 → 800 taken 586 times.
2904 if (boundedText(text)) safeTasks.insert(QLatin1String(key), text);
178 2904 }
179
2/2
✓ Branch 829 → 808 taken 2178 times.
✓ Branch 829 → 830 taken 726 times.
2904 for (const auto *key : {"showCompleted", "hideFutureStarts", "tagsExpanded"})
180
9/9
✓ Branch 809 → 810 taken 2178 times.
✓ Branch 810 → 811 taken 2178 times.
✓ Branch 812 → 813 taken 585 times.
✓ Branch 812 → 828 taken 1593 times.
✓ Branch 815 → 816 taken 585 times.
✓ Branch 816 → 817 taken 585 times.
✓ Branch 817 → 818 taken 585 times.
✓ Branch 820 → 821 taken 585 times.
✓ Branch 821 → 822 taken 585 times.
2178 if (tasks.contains(QLatin1String(key))) safeTasks.insert(QLatin1String(key), tasks.value(QLatin1String(key)).toBool());
181
8/9
✓ Branch 831 → 832 taken 726 times.
✓ Branch 832 → 833 taken 726 times.
✓ Branch 833 → 834 taken 726 times.
✓ Branch 837 → 838 taken 196 times.
✓ Branch 837 → 841 taken 530 times.
✓ Branch 839 → 840 taken 196 times.
✗ Branch 839 → 841 not taken.
✓ Branch 842 → 843 taken 196 times.
✓ Branch 842 → 853 taken 530 times.
726 if (const double width = tasks.value("editorWidth").toDouble(&valid); valid && std::isfinite(width))
182
2/2
✓ Branch 843 → 844 taken 196 times.
✓ Branch 848 → 849 taken 196 times.
392 safeTasks.insert(QStringLiteral("editorWidth"), qBound(200.0, width, 2000.0));
183
2/2
✓ Branch 897 → 855 taken 1452 times.
✓ Branch 897 → 898 taken 726 times.
2178 for (const auto *key : {"collapsedGroups", "collapsedParents"}) {
184
4/4
✓ Branch 856 → 857 taken 1452 times.
✓ Branch 857 → 858 taken 1452 times.
✓ Branch 859 → 860 taken 1060 times.
✓ Branch 859 → 861 taken 392 times.
1452 if (!tasks.contains(QLatin1String(key))) continue;
185 392 QStringList entries;
186
7/7
✓ Branch 863 → 864 taken 392 times.
✓ Branch 864 → 865 taken 392 times.
✓ Branch 865 → 866 taken 392 times.
✓ Branch 869 → 870 taken 392 times.
✓ Branch 870 → 871 taken 392 times.
✓ Branch 885 → 872 taken 1892 times.
✓ Branch 885 → 886 taken 392 times.
2284 for (const auto &entry : tasks.value(QLatin1String(key)).toStringList())
187
9/10
✓ Branch 873 → 874 taken 1892 times.
✓ Branch 874 → 875 taken 1890 times.
✓ Branch 874 → 880 taken 2 times.
✓ Branch 876 → 877 taken 1390 times.
✓ Branch 876 → 880 taken 500 times.
✓ Branch 878 → 879 taken 1390 times.
✗ Branch 878 → 880 not taken.
✓ Branch 881 → 882 taken 1390 times.
✓ Branch 881 → 883 taken 502 times.
✓ Branch 882 → 883 taken 1390 times.
2284 if (boundedText(entry) && entries.size() < 1000 && !entries.contains(entry)) entries.append(entry);
188
2/2
✓ Branch 889 → 890 taken 392 times.
✓ Branch 890 → 891 taken 392 times.
392 safeTasks.insert(QLatin1String(key), entries);
189 392 }
190
5/5
✓ Branch 898 → 899 taken 726 times.
✓ Branch 899 → 900 taken 196 times.
✓ Branch 899 → 907 taken 530 times.
✓ Branch 901 → 902 taken 196 times.
✓ Branch 902 → 903 taken 196 times.
726 if (!safeTasks.isEmpty()) result["calendar/tasks"] = safeTasks;
191 726 return result;
192 2904 }
193
194 4915 bool UiStateStore::readBytes(const QString &name, QByteArray *bytes) {
195
2/2
✓ Branch 2 → 3 taken 4915 times.
✓ Branch 3 → 4 taken 4915 times.
4915 const QString path = QDir(m_directory).filePath(name);
196
1/1
✓ Branch 5 → 6 taken 4915 times.
4915 const QFileInfo info(path);
197
11/11
✓ Branch 6 → 7 taken 4915 times.
✓ Branch 7 → 8 taken 4913 times.
✓ Branch 7 → 12 taken 2 times.
✓ Branch 8 → 9 taken 4913 times.
✓ Branch 9 → 10 taken 1058 times.
✓ Branch 9 → 13 taken 3855 times.
✓ Branch 10 → 11 taken 1058 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1057 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 25 taken 4912 times.
4915 if (info.isSymLink() || (info.exists() && !info.isFile())) {
198
1/1
✓ Branch 19 → 20 taken 3 times.
6 m_error = QStringLiteral("UI state is not a regular file: %1").arg(name);
199 3 return false;
200 }
201
4/4
✓ Branch 25 → 26 taken 4912 times.
✓ Branch 26 → 27 taken 3855 times.
✓ Branch 26 → 29 taken 1057 times.
✓ Branch 27 → 28 taken 3855 times.
4912 if (!info.exists()) { bytes->clear(); return true; }
202
1/1
✓ Branch 29 → 30 taken 1057 times.
1057 QFile file(path);
203
7/8
✓ Branch 30 → 31 taken 1057 times.
✓ Branch 31 → 32 taken 1053 times.
✓ Branch 31 → 35 taken 4 times.
✓ Branch 33 → 34 taken 1053 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 1053 times.
✓ Branch 37 → 38 taken 4 times.
✓ Branch 37 → 48 taken 1053 times.
1057 if (info.size() > MaximumFileBytes || !file.open(QIODevice::ReadOnly)) {
204
1/1
✓ Branch 42 → 43 taken 4 times.
8 m_error = QStringLiteral("Cannot read bounded UI state: %1").arg(name);
205 4 return false;
206 }
207
1/1
✓ Branch 48 → 49 taken 1053 times.
1053 *bytes = file.read(MaximumFileBytes + 1);
208
4/7
✓ Branch 52 → 53 taken 1053 times.
✗ Branch 52 → 55 not taken.
✓ Branch 53 → 54 taken 1053 times.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 1053 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 68 taken 1053 times.
1053 if (bytes->size() > MaximumFileBytes || file.error() != QFileDevice::NoError) {
209 ✗ m_error = QStringLiteral("UI state read failed: %1").arg(name);
210 ✗ return false;
211 }
212 1053 return true;
213 4915 }
214
215 1249 bool UiStateStore::writeJson(const QString &name, const QJsonObject &object) {
216
3/3
✓ Branch 2 → 3 taken 1249 times.
✓ Branch 3 → 4 taken 1249 times.
✓ Branch 4 → 5 taken 1249 times.
1249 const QByteArray bytes = QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n';
217
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 16 taken 1249 times.
1249 if (bytes.size() > MaximumFileBytes) { m_error = QStringLiteral("UI state exceeds byte limit"); return false; }
218 1249 QByteArray previous;
219
2/3
✓ Branch 16 → 17 taken 1249 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1249 times.
1249 if (!readBytes(name, &previous)) return false;
220
2/2
✓ Branch 20 → 21 taken 189 times.
✓ Branch 20 → 23 taken 1060 times.
1249 if (bytes == previous) { m_error.clear(); return true; }
221
3/3
✓ Branch 23 → 24 taken 1060 times.
✓ Branch 24 → 25 taken 1060 times.
✓ Branch 25 → 26 taken 1060 times.
1060 QSaveFile file(QDir(m_directory).filePath(name));
222
1/1
✓ Branch 28 → 29 taken 1060 times.
1060 file.setDirectWriteFallback(false);
223
1/1
✓ Branch 30 → 31 taken 1060 times.
1060 if (!file.open(QIODevice::WriteOnly) ||
224
3/5
✓ Branch 33 → 34 taken 1060 times.
✓ Branch 34 → 35 taken 1060 times.
✗ Branch 34 → 40 not taken.
✓ Branch 37 → 38 taken 1060 times.
✗ Branch 37 → 40 not taken.
2120 !file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner) ||
225
5/8
✓ Branch 31 → 32 taken 1060 times.
✗ Branch 31 → 40 not taken.
✓ Branch 35 → 36 taken 1060 times.
✓ Branch 38 → 39 taken 1060 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 1060 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 53 taken 1060 times.
3180 file.write(bytes) != bytes.size() || !file.commit()) {
226 ✗ m_error = QStringLiteral("Cannot atomically save UI state: %1").arg(name);
227 ✗ return false;
228 }
229 1060 m_error.clear();
230 1060 return true;
231 1249 }
232
233 1620 bool UiStateStore::load(const QString &legacyIniPath) {
234 1620 m_writable = false;
235
1/1
✓ Branch 2 → 3 taken 1620 times.
1620 m_preferences = defaults();
236 1620 m_error.clear();
237
11/24
✓ Branch 7 → 8 taken 1620 times.
✗ Branch 7 → 15 not taken.
✓ Branch 8 → 9 taken 1620 times.
✓ Branch 9 → 10 taken 1620 times.
✓ Branch 10 → 11 taken 1620 times.
✗ Branch 10 → 15 not taken.
✓ Branch 12 → 13 taken 1620 times.
✓ Branch 13 → 14 taken 1620 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1620 times.
✓ Branch 17 → 18 taken 1620 times.
✗ Branch 17 → 19 not taken.
✓ Branch 19 → 20 taken 1620 times.
✗ Branch 19 → 21 not taken.
✓ Branch 21 → 22 taken 1620 times.
✗ Branch 21 → 23 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 31 taken 1620 times.
✗ Branch 288 → 289 not taken.
✗ Branch 288 → 290 not taken.
✗ Branch 292 → 293 not taken.
✗ Branch 292 → 294 not taken.
✗ Branch 296 → 297 not taken.
✗ Branch 296 → 298 not taken.
1620 if (m_directory.isEmpty() || QFileInfo(m_directory).isSymLink() || !QDir().mkpath(m_directory)) {
238 ✗ m_error = QStringLiteral("Cannot open local UI state directory"); return false;
239 }
240
3/3
✓ Branch 31 → 32 taken 1620 times.
✓ Branch 35 → 36 taken 1620 times.
✓ Branch 36 → 37 taken 1620 times.
3240 QLockFile lock(QDir(m_directory).filePath(QStringLiteral("ui-state.lock")));
241 // P90.17: bounded wait — a zero-tolerance read failed spuriously while
242 // the coalescing background writer held the lock during its atomic
243 // commit (reproduced under CPU starvation). One second covers any
244 // legitimate write window; stale locks still fail through QLockFile's
245 // own heuristics after the wait.
246
2/3
✓ Branch 41 → 42 taken 1620 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 50 taken 1620 times.
1620 if (!lock.tryLock(1000)) { m_error = QStringLiteral("UI state is in use"); return false; }
247 1620 QByteArray bytes;
248
3/3
✓ Branch 53 → 54 taken 1620 times.
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 1618 times.
1620 if (!readBytes(QStringLiteral("ui-preferences.json"), &bytes)) return false;
249
2/2
✓ Branch 59 → 60 taken 84 times.
✓ Branch 59 → 101 taken 1534 times.
1618 if (!bytes.isEmpty()) {
250
1/1
✓ Branch 60 → 61 taken 84 times.
84 const auto document = QJsonDocument::fromJson(bytes);
251
3/3
✓ Branch 61 → 62 taken 84 times.
✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 70 taken 83 times.
85 if (!document.isObject()) { m_error = QStringLiteral("Invalid UI preferences JSON; original retained"); return false; }
252
1/1
✓ Branch 70 → 71 taken 83 times.
83 const auto object = document.object();
253
6/6
✓ Branch 71 → 72 taken 83 times.
✓ Branch 72 → 73 taken 83 times.
✓ Branch 73 → 74 taken 83 times.
✓ Branch 74 → 75 taken 83 times.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 86 taken 82 times.
83 if (object.value("ui/schemaVersion") != QJsonValue(1)) {
254 1 m_error = QStringLiteral("Unsupported UI preference version; original retained"); return false;
255 }
256
2/2
✓ Branch 86 → 87 taken 82 times.
✓ Branch 87 → 88 taken 82 times.
82 m_preferences = normalized(object.toVariantMap());
257
9/9
✓ Branch 93 → 94 taken 82 times.
✓ Branch 93 → 95 taken 1 time.
✓ Branch 97 → 98 taken 82 times.
✓ Branch 97 → 100 taken 2 times.
✓ Branch 101 → 102 taken 1534 times.
✓ Branch 105 → 106 taken 1534 times.
✓ Branch 106 → 107 taken 1534 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 119 taken 1533 times.
3153 } else if (QFileInfo::exists(QDir(m_directory).filePath(QStringLiteral("ui-preferences.json")))) {
258 1 m_error = QStringLiteral("Truncated UI preferences; original retained"); return false;
259
6/7
✓ Branch 120 → 121 taken 2 times.
✓ Branch 120 → 124 taken 1531 times.
✓ Branch 121 → 122 taken 2 times.
✓ Branch 122 → 123 taken 2 times.
✗ Branch 122 → 124 not taken.
✓ Branch 125 → 126 taken 2 times.
✓ Branch 125 → 282 taken 1531 times.
1533 } else if (!legacyIniPath.isEmpty() && QFileInfo::exists(legacyIniPath)) {
260
1/1
✓ Branch 126 → 127 taken 2 times.
2 QFile legacy(legacyIniPath);
261
9/16
✓ Branch 127 → 128 taken 2 times.
✓ Branch 128 → 129 taken 2 times.
✓ Branch 129 → 130 taken 2 times.
✗ Branch 129 → 135 not taken.
✓ Branch 131 → 132 taken 2 times.
✓ Branch 132 → 133 taken 2 times.
✗ Branch 132 → 135 not taken.
✓ Branch 133 → 134 taken 2 times.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 136 taken 2 times.
✓ Branch 137 → 138 taken 2 times.
✗ Branch 137 → 139 not taken.
✗ Branch 139 → 140 not taken.
✓ Branch 139 → 147 taken 2 times.
✗ Branch 349 → 350 not taken.
✗ Branch 349 → 351 not taken.
2 if (QFileInfo(legacyIniPath).isSymLink() || !legacy.open(QIODevice::ReadOnly) || legacy.size() > MaximumFileBytes) {
262 ✗ m_error = QStringLiteral("Cannot snapshot legacy settings"); return false;
263 }
264
1/1
✓ Branch 147 → 148 taken 2 times.
2 const auto original = legacy.read(MaximumFileBytes + 1);
265
4/7
✓ Branch 149 → 150 taken 2 times.
✗ Branch 149 → 152 not taken.
✓ Branch 150 → 151 taken 2 times.
✗ Branch 151 → 152 not taken.
✓ Branch 151 → 153 taken 2 times.
✗ Branch 154 → 155 not taken.
✓ Branch 154 → 162 taken 2 times.
2 if (original.size() > MaximumFileBytes || legacy.error() != QFileDevice::NoError) {
266 ✗ m_error = QStringLiteral("Cannot read legacy settings snapshot"); return false;
267 }
268
2/2
✓ Branch 162 → 163 taken 2 times.
✓ Branch 166 → 167 taken 2 times.
4 const QString snapshotPath = QDir(m_directory).filePath(QStringLiteral("pre-quick-settings.ini"));
269 // Keep the first migration snapshot byte-for-byte, including old widget keys.
270
1/1
✓ Branch 170 → 171 taken 2 times.
2 const QFileInfo snapshotInfo(snapshotPath);
271
10/14
✓ Branch 171 → 172 taken 2 times.
✓ Branch 172 → 173 taken 2 times.
✗ Branch 172 → 179 not taken.
✓ Branch 173 → 174 taken 2 times.
✓ Branch 174 → 175 taken 1 time.
✓ Branch 174 → 180 taken 1 time.
✓ Branch 175 → 176 taken 1 time.
✓ Branch 176 → 177 taken 1 time.
✗ Branch 176 → 179 not taken.
✓ Branch 177 → 178 taken 1 time.
✗ Branch 178 → 179 not taken.
✓ Branch 178 → 180 taken 1 time.
✗ Branch 181 → 182 not taken.
✓ Branch 181 → 189 taken 2 times.
2 if (snapshotInfo.isSymLink() || (snapshotInfo.exists() && (!snapshotInfo.isFile() || snapshotInfo.size() > MaximumFileBytes))) {
272 ✗ m_error = QStringLiteral("Invalid legacy snapshot target"); return false;
273 }
274
3/3
✓ Branch 189 → 190 taken 2 times.
✓ Branch 190 → 191 taken 1 time.
✓ Branch 190 → 220 taken 1 time.
2 if (!QFileInfo::exists(snapshotPath)) {
275
1/1
✓ Branch 191 → 192 taken 1 time.
1 QSaveFile snapshot(snapshotPath);
276
1/1
✓ Branch 192 → 193 taken 1 time.
1 snapshot.setDirectWriteFallback(false);
277
1/1
✓ Branch 194 → 195 taken 1 time.
1 if (!snapshot.open(QIODevice::WriteOnly) ||
278
3/5
✓ Branch 197 → 198 taken 1 time.
✓ Branch 198 → 199 taken 1 time.
✗ Branch 198 → 204 not taken.
✓ Branch 201 → 202 taken 1 time.
✗ Branch 201 → 204 not taken.
2 !snapshot.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner) ||
279
5/8
✓ Branch 195 → 196 taken 1 time.
✗ Branch 195 → 204 not taken.
✓ Branch 199 → 200 taken 1 time.
✓ Branch 202 → 203 taken 1 time.
✗ Branch 203 → 204 not taken.
✓ Branch 203 → 205 taken 1 time.
✗ Branch 206 → 207 not taken.
✓ Branch 206 → 214 taken 1 time.
3 snapshot.write(original) != original.size() || !snapshot.commit()) {
280 ✗ m_error = QStringLiteral("Cannot save legacy settings snapshot"); return false;
281 }
282 1 }
283 // Read the retained bytes, not a concurrently changed original settings file.
284
1/1
✓ Branch 220 → 221 taken 2 times.
2 QSettings settings(snapshotPath, QSettings::IniFormat);
285 2 QVariantMap imported;
286
2/2
✓ Branch 237 → 231 taken 4 times.
✓ Branch 237 → 238 taken 2 times.
14 for (const auto &key : {QStringLiteral("appearance/theme"), QStringLiteral("view/defaultMode")})
287
4/6
✓ Branch 232 → 233 taken 4 times.
✓ Branch 233 → 234 taken 4 times.
✓ Branch 239 → 240 taken 4 times.
✓ Branch 239 → 243 taken 2 times.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 375 not taken.
10 imported[key] = settings.value(key);
288
2/3
✓ Branch 244 → 245 taken 2 times.
✗ Branch 245 → 246 not taken.
✓ Branch 245 → 253 taken 2 times.
2 if (settings.status() != QSettings::NoError) { m_error = QStringLiteral("Invalid legacy settings snapshot"); return false; }
289
1/1
✓ Branch 253 → 254 taken 2 times.
2 m_preferences = normalized(imported);
290 2 }
291 // No write on load. A repaired default is persisted only by an explicit save.
292 1615 m_writable = true;
293 1615 return true;
294 1620 }
295
296 649 bool UiStateStore::savePreferences(const QVariantMap &preferences) {
297
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 10 taken 648 times.
650 if (!m_writable) { m_error = QStringLiteral("UI preferences are read-only until a successful load"); return false; }
298
3/3
✓ Branch 10 → 11 taken 648 times.
✓ Branch 14 → 15 taken 648 times.
✓ Branch 15 → 16 taken 648 times.
1296 QLockFile lock(QDir(m_directory).filePath(QStringLiteral("ui-state.lock")));
299
3/3
✓ Branch 20 → 21 taken 648 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 29 taken 647 times.
649 if (!lock.tryLock(1000)) { m_error = QStringLiteral("UI state is in use"); return false; }
300 // Recheck while holding the writer lock: another binary may have upgraded
301 // the file since load(). Never replace a corrupt or unknown-version file.
302 647 QByteArray previous;
303
3/3
✓ Branch 32 → 33 taken 647 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 37 taken 645 times.
647 if (!readBytes(QStringLiteral("ui-preferences.json"), &previous)) return false;
304
5/5
✓ Branch 37 → 38 taken 645 times.
✓ Branch 41 → 42 taken 645 times.
✓ Branch 42 → 43 taken 645 times.
✓ Branch 47 → 48 taken 247 times.
✓ Branch 47 → 81 taken 398 times.
1290 if (QFileInfo::exists(QDir(m_directory).filePath(QStringLiteral("ui-preferences.json")))) {
305
1/1
✓ Branch 48 → 49 taken 247 times.
247 const auto document = QJsonDocument::fromJson(previous);
306
20/28
✓ Branch 49 → 50 taken 247 times.
✓ Branch 50 → 51 taken 245 times.
✓ Branch 50 → 57 taken 2 times.
✓ Branch 51 → 52 taken 245 times.
✓ Branch 52 → 53 taken 245 times.
✓ Branch 53 → 54 taken 245 times.
✓ Branch 54 → 55 taken 245 times.
✓ Branch 55 → 56 taken 245 times.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 58 taken 244 times.
✓ Branch 59 → 60 taken 245 times.
✓ Branch 59 → 61 taken 2 times.
✓ Branch 61 → 62 taken 245 times.
✓ Branch 61 → 63 taken 2 times.
✓ Branch 63 → 64 taken 245 times.
✓ Branch 63 → 65 taken 2 times.
✓ Branch 65 → 66 taken 245 times.
✓ Branch 65 → 67 taken 2 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 67 → 75 taken 244 times.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 137 → 138 not taken.
✗ Branch 137 → 139 not taken.
✗ Branch 141 → 142 not taken.
✗ Branch 141 → 143 not taken.
247 if (!document.isObject() || document.object().value("ui/schemaVersion") != QJsonValue(1)) {
307 3 m_error = QStringLiteral("Changed or invalid UI preferences; original retained"); return false;
308 }
309 247 }
310
1/1
✓ Branch 81 → 82 taken 642 times.
642 const auto safe = normalized(preferences);
311
3/4
✓ Branch 82 → 83 taken 642 times.
✓ Branch 86 → 87 taken 642 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 642 times.
1284 if (!writeJson(QStringLiteral("ui-preferences.json"), QJsonObject::fromVariantMap(safe))) return false;
312 642 m_preferences = safe;
313 642 return true;
314 648 }
315
316 1400 bool UiStateStore::readSession(QJsonObject *session) {
317 1400 QByteArray bytes;
318
11/15
✓ Branch 2 → 3 taken 1399 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 6 → 7 taken 1399 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 1396 times.
✓ Branch 10 → 11 taken 1399 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1399 times.
✓ Branch 12 → 14 taken 1 time.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 1396 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
2799 if (!session || !readBytes(QStringLiteral("workspace-session.json"), &bytes)) return false;
319
17/25
✓ Branch 17 → 18 taken 1147 times.
✓ Branch 17 → 26 taken 249 times.
✓ Branch 18 → 19 taken 1147 times.
✓ Branch 22 → 23 taken 1147 times.
✓ Branch 23 → 24 taken 1147 times.
✓ Branch 24 → 25 taken 1145 times.
✓ Branch 24 → 26 taken 2 times.
✓ Branch 27 → 28 taken 1147 times.
✓ Branch 27 → 29 taken 249 times.
✓ Branch 29 → 30 taken 1147 times.
✓ Branch 29 → 31 taken 249 times.
✓ Branch 31 → 32 taken 1147 times.
✓ Branch 31 → 33 taken 249 times.
✓ Branch 33 → 34 taken 1147 times.
✓ Branch 33 → 35 taken 249 times.
✓ Branch 35 → 36 taken 1145 times.
✓ Branch 35 → 40 taken 251 times.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 86 not taken.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
2543 if (bytes.isEmpty() && !QFileInfo::exists(QDir(m_directory).filePath(QStringLiteral("workspace-session.json")))) {
320
1/1
✓ Branch 36 → 37 taken 1145 times.
1145 *session = {}; return true;
321 }
322
1/1
✓ Branch 40 → 41 taken 251 times.
251 const auto document = QJsonDocument::fromJson(bytes);
323
3/3
✓ Branch 41 → 42 taken 251 times.
✓ Branch 42 → 43 taken 6 times.
✓ Branch 42 → 50 taken 245 times.
257 if (!document.isObject()) { m_error = QStringLiteral("Invalid workspace session; original retained"); return false; }
324
1/1
✓ Branch 50 → 51 taken 245 times.
245 WorkspaceState validator;
325
4/4
✓ Branch 52 → 53 taken 245 times.
✓ Branch 53 → 54 taken 245 times.
✓ Branch 56 → 57 taken 4 times.
✓ Branch 56 → 58 taken 241 times.
245 if (!validator.restoreSession(document.object(), &m_error)) return false;
326
1/1
✓ Branch 58 → 59 taken 241 times.
241 *session = document.object();
327 241 m_error.clear();
328 241 return true;
329 1400 }
330
331 616 bool UiStateStore::saveSession(const QJsonObject &session) {
332
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 10 taken 615 times.
617 if (!m_writable) { m_error = QStringLiteral("UI session is read-only until a successful load"); return false; }
333
3/3
✓ Branch 10 → 11 taken 615 times.
✓ Branch 14 → 15 taken 615 times.
✓ Branch 15 → 16 taken 615 times.
1230 QLockFile lock(QDir(m_directory).filePath(QStringLiteral("ui-state.lock")));
334
3/3
✓ Branch 20 → 21 taken 615 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 29 taken 614 times.
616 if (!lock.tryLock(1000)) { m_error = QStringLiteral("UI state is in use"); return false; }
335 // Refuse to overwrite an unknown/corrupt session from a newer binary.
336
1/1
✓ Branch 29 → 30 taken 614 times.
614 QJsonObject previous;
337
3/3
✓ Branch 30 → 31 taken 614 times.
✓ Branch 31 → 32 taken 6 times.
✓ Branch 31 → 33 taken 608 times.
614 if (!readSession(&previous)) return false;
338
1/1
✓ Branch 33 → 34 taken 608 times.
608 WorkspaceState validator;
339
3/3
✓ Branch 35 → 36 taken 608 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 607 times.
608 if (!validator.restoreSession(session, &m_error)) return false;
340
1/1
✓ Branch 42 → 43 taken 607 times.
607 return writeJson(QStringLiteral("workspace-session.json"), session);
341 615 }
342