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: 83.6% 112 / 0 / 134
Functions: 100.0% 3 / 0 / 3
Branches: 71.0% 137 / 0 / 193

app/core/CalendarOutboxEdits.cpp
Line Branch Exec Source
1 // Occurrence edits from invitations (F-52) and conflict resolution of the
2 // calendar outbox; the CalendarOutbox class is split over CalendarOutbox.cpp,
3 // CalendarOutboxTasks.cpp and this file.
4 #include "CalendarOutbox.h"
5 #include "data/ICalEventCodec.h"
6 #include "data/ICalTodoCodec.h"
7
8 #include "data/CalendarStore.h"
9
10 #include <QLoggingCategory>
11 #include <QTimeZone>
12 #include <QUuid>
13
14 Q_DECLARE_LOGGING_CATEGORY(lcCalendarOutbox)
15
16 // F-52 (SPRINT-104): merge occurrence-only invitation rows into the stored
17 // series. The master row and every other exception survive; each patched
18 // exception replaces its stored counterpart first, so a moved occurrence
19 // does not leave a stale row behind. The queued PUT serializes the whole
20 // series (master + exceptions) exactly as restoreSeries does.
21 1 bool CalendarOutbox::applyOccurrenceUpdate(const QString &uid, const QString &accountId,
22 const QString &calendarPath,
23 const QList<CalendarEvent> &exceptionRows) {
24 1 CalendarStore *store = m_deps.store;
25
1/1
✓ Branch 3 → 4 taken 1 time.
1 const auto master = store->occurrenceEvent(uid, accountId, calendarPath, {});
26
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
1 if (!master)
27 ✗ return false;
28
1/1
✓ Branch 8 → 9 taken 1 time.
1 const auto existing = store->outboxEntry(accountId, calendarPath, uid);
29 1 QList<CalendarEvent> stored = exceptionRows;
30
4/4
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 28 → 13 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
2 for (CalendarEvent &row : stored) {
31
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 18 not taken.
1 if (row.etag.isEmpty()) row.etag = master->etag;
32
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 22 not taken.
1 if (row.resourceHref.isEmpty()) row.resourceHref = master->resourceHref;
33
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 26 not taken.
1 if (row.resourceIcal.isEmpty()) row.resourceIcal = master->resourceIcal;
34 }
35
1/1
✓ Branch 29 → 30 taken 1 time.
1 store->beginTransaction();
36 1 bool ok = true;
37
4/4
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 46 → 33 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
2 for (const CalendarEvent &row : stored) {
38
2/3
✓ Branch 34 → 35 taken 1 time.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 1 time.
1 if (!row.recurrenceId.isValid()) continue; // a master row never rides along
39
1/1
✓ Branch 37 → 38 taken 1 time.
1 store->deleteEvent(uid, accountId, calendarPath, row.recurrenceId);
40
3/5
✓ 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 ok = store->upsertEvent(row) && ok;
41 }
42
1/1
✓ Branch 47 → 48 taken 1 time.
1 store->commitTransaction();
43
1/2
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 1 time.
1 if (!ok)
44 ✗ return false;
45
3/5
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 54 taken 1 time.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 60 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
5 store->enqueueOutbox(accountId, calendarPath, uid, QStringLiteral("put"),
46 2 existing ? existing->href : master->resourceHref,
47 2 existing ? existing->baseEtag : master->etag);
48
1/1
✓ Branch 68 → 69 taken 1 time.
1 publishChange();
49
1/1
✓ Branch 69 → 70 taken 1 time.
1 requestFlush();
50 1 return true;
51 1 }
52
53 // F-52 (SPRINT-104): a cancelled occurrence leaves the series through the
54 // RFC 5545 mechanism — its exception row is deleted and its date joins the
55 // master's EXDATE list. The stored master row (and its other exceptions)
56 // survive; the codec's struct-vs-raw diff writes the EXDATE into the next
57 // resource PUT.
58 1 bool CalendarOutbox::applyOccurrenceCancel(const QString &uid, const QString &accountId,
59 const QString &calendarPath,
60 const QDateTime &recurrenceId) {
61 1 CalendarStore *store = m_deps.store;
62
1/1
✓ Branch 3 → 4 taken 1 time.
1 const auto master = store->occurrenceEvent(uid, accountId, calendarPath, {});
63
4/7
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
1 if (!master || !recurrenceId.isValid())
64 ✗ return false;
65
1/1
✓ Branch 13 → 14 taken 1 time.
1 const auto existing = store->outboxEntry(accountId, calendarPath, uid);
66 1 CalendarEvent updated = *master;
67 1 QDateTime anchor = recurrenceId;
68
4/7
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 21 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 29 not taken.
1 if (!updated.allDay && updated.dtStart.timeSpec() == Qt::TimeZone)
69
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
1 anchor = anchor.toTimeZone(updated.dtStart.timeZone());
70
1/2
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 35 not taken.
1 if (!updated.exdates.contains(anchor)) {
71
1/1
✓ Branch 31 → 32 taken 1 time.
1 updated.exdates.append(anchor);
72
3/3
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
1 std::sort(updated.exdates.begin(), updated.exdates.end());
73 }
74
1/1
✓ Branch 35 → 36 taken 1 time.
1 store->beginTransaction();
75
1/1
✓ Branch 36 → 37 taken 1 time.
1 store->deleteEvent(uid, accountId, calendarPath, recurrenceId);
76
1/1
✓ Branch 37 → 38 taken 1 time.
1 const bool ok = store->upsertEvent(updated);
77
1/1
✓ Branch 38 → 39 taken 1 time.
1 store->commitTransaction();
78
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 1 time.
1 if (!ok)
79 ✗ return false;
80
3/5
✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 45 not taken.
✓ Branch 48 → 49 taken 1 time.
✗ Branch 48 → 51 not taken.
✓ Branch 56 → 57 taken 1 time.
5 store->enqueueOutbox(accountId, calendarPath, uid, QStringLiteral("put"),
81 2 existing ? existing->href : master->resourceHref,
82 2 existing ? existing->baseEtag : master->etag);
83
1/1
✓ Branch 59 → 60 taken 1 time.
1 publishChange();
84
1/1
✓ Branch 60 → 61 taken 1 time.
1 requestFlush();
85 1 return true;
86 1 }
87
88 37 bool CalendarOutbox::resolveConflict(const QString &uid, const QString &accountId,
89 const QString &calendarPath, const QString &choice) {
90 37 CalendarStore *store = m_deps.store;
91
1/1
✓ Branch 2 → 3 taken 37 times.
37 auto entry = store->outboxEntry(accountId, calendarPath, uid);
92
3/6
✓ Branch 4 → 5 taken 37 times.
✗ Branch 4 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 37 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 37 times.
37 if (!entry || entry->state != QLatin1String("conflict"))
93 ✗ return false;
94
8/8
✓ Branch 13 → 14 taken 37 times.
✓ Branch 17 → 18 taken 37 times.
✓ Branch 18 → 19 taken 37 times.
✓ Branch 19 → 20 taken 37 times.
✓ Branch 20 → 21 taken 37 times.
✓ Branch 21 → 22 taken 37 times.
✓ Branch 25 → 15 taken 37 times.
✓ Branch 25 → 26 taken 37 times.
74 qCInfo(lcCalendarOutbox) << "Resolving conflict on" << uid << "with" << choice;
95 // D-22: a moved task's question is about its old list's server version;
96 // "both" has no meaning there, the whole sides go through resolveWith().
97
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 59 taken 37 times.
74 if (entry->conflictFields.contains(QStringLiteral("moved"))) {
98 ✗ if (choice == QLatin1String("server"))
99 ✗ return resolveWith(uid, accountId, calendarPath, entry->serverIcal, entry->serverEtag, choice);
100 ✗ if (choice == QLatin1String("mine"))
101 ✗ return resolveWith(uid, accountId, calendarPath, QString::fromUtf8(localBody(*entry)), entry->serverEtag,
102 ✗ QStringLiteral("local"));
103 ✗ return false;
104 }
105 // D-21: the losing side is kept (local body for "server", server version
106 // for "mine"); the written bytes are unchanged (G2).
107
2/2
✓ Branch 60 → 61 taken 37 times.
✓ Branch 61 → 62 taken 37 times.
37 const QString localVersion = QString::fromUtf8(localBody(*entry));
108
2/2
✓ Branch 65 → 66 taken 17 times.
✓ Branch 65 → 83 taken 20 times.
37 if (choice == QLatin1String("server")) {
109
1/1
✓ Branch 70 → 71 taken 17 times.
17 recordHistory(*entry, QStringLiteral("server"), localVersion);
110
2/3
✓ Branch 76 → 77 taken 17 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 79 taken 17 times.
17 if (!adoptServerVersion(*entry, entry->serverIcal, entry->serverEtag))
111 ✗ return false;
112
2/2
✓ Branch 79 → 80 taken 17 times.
✓ Branch 80 → 81 taken 17 times.
17 emit statusMessage(tr("The version from the server was kept."));
113 17 return true;
114 }
115
6/6
✓ Branch 85 → 86 taken 6 times.
✓ Branch 85 → 91 taken 14 times.
✓ Branch 89 → 90 taken 2 times.
✓ Branch 89 → 91 taken 4 times.
✓ Branch 92 → 93 taken 2 times.
✓ Branch 92 → 135 taken 18 times.
20 if (choice == QLatin1String("both") && entry->componentType == QLatin1String("VTODO")) {
116 // D-14 (Sprint 107): the local task as a copy in the same list.
117
1/2
✓ Branch 95 → 96 taken 2 times.
✗ Branch 95 → 118 not taken.
2 if (!entry->serverIcal.isEmpty()) {
118 2 ICalTodoCodec::Resource tasks;
119
2/3
✓ Branch 97 → 98 taken 2 times.
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 112 taken 2 times.
2 if (!ICalTodoCodec::parseResource(entry->serverIcal, &tasks)) {
120 ✗ qCWarning(lcCalendarOutbox) << "Server version of" << uid << "cannot be read; both not kept";
121 ✗ return false;
122 }
123 2 }
124
5/8
✓ Branch 119 → 120 taken 2 times.
✓ Branch 120 → 121 taken 2 times.
✗ Branch 120 → 126 not taken.
✓ Branch 124 → 125 taken 2 times.
✗ Branch 125 → 126 not taken.
✓ Branch 125 → 127 taken 2 times.
✗ Branch 128 → 129 not taken.
✓ Branch 128 → 130 taken 2 times.
2 if (!keepBothTask(*entry) || !adoptServerVersion(*entry, entry->serverIcal, entry->serverEtag))
125 ✗ return false;
126
2/2
✓ Branch 130 → 131 taken 2 times.
✓ Branch 131 → 132 taken 2 times.
2 emit statusMessage(tr("Both versions were kept."));
127
1/1
✓ Branch 133 → 134 taken 2 times.
2 requestFlush();
128 2 return true;
129 }
130
2/2
✓ Branch 137 → 138 taken 4 times.
✓ Branch 137 → 226 taken 14 times.
18 if (choice == QLatin1String("both")) {
131 // S106-179: the server version must be readable before the copy is
132 // made — adopting it can only fail afterwards, and every retry used to
133 // leave another copy (locally and on the server).
134
1/2
✓ Branch 140 → 141 taken 4 times.
✗ Branch 140 → 175 not taken.
4 if (!entry->serverIcal.isEmpty()) {
135 4 ICalTodoCodec::Resource tasks;
136 4 ICalEventCodec::Resource events;
137 4 const bool readable = entry->componentType == QLatin1String("VTODO")
138
1/3
✗ Branch 144 → 145 not taken.
✓ Branch 144 → 148 taken 4 times.
✗ Branch 146 → 147 not taken.
4 ? ICalTodoCodec::parseResource(entry->serverIcal, &tasks)
139
1/1
✓ Branch 149 → 150 taken 4 times.
4 : ICalEventCodec::parseResource(entry->serverIcal, &events);
140
2/2
✓ Branch 151 → 152 taken 2 times.
✓ Branch 151 → 165 taken 2 times.
4 if (!readable) {
141
7/7
✓ Branch 152 → 153 taken 2 times.
✓ Branch 156 → 157 taken 2 times.
✓ Branch 157 → 158 taken 2 times.
✓ Branch 158 → 159 taken 2 times.
✓ Branch 159 → 160 taken 2 times.
✓ Branch 163 → 154 taken 2 times.
✓ Branch 163 → 164 taken 2 times.
4 qCWarning(lcCalendarOutbox) << "Server version of" << uid << "cannot be read; both not kept";
142 2 return false;
143 }
144 6 }
145
2/2
✓ Branch 175 → 176 taken 2 times.
✓ Branch 176 → 177 taken 2 times.
2 const QString copyUid = QUuid::createUuid().toString(QUuid::WithoutBraces);
146
5/5
✓ Branch 177 → 178 taken 2 times.
✓ Branch 178 → 179 taken 2 times.
✓ Branch 179 → 180 taken 2 times.
✓ Branch 197 → 181 taken 2 times.
✓ Branch 197 → 198 taken 2 times.
4 for (CalendarEvent row : store->eventSeries(uid, accountId, calendarPath)) {
147 2 row.uid = copyUid;
148 2 row.etag.clear();
149 2 row.resourceHref.clear();
150 2 row.resourceIcal.clear();
151
2/3
✓ Branch 187 → 188 taken 2 times.
✗ Branch 188 → 189 not taken.
✓ Branch 188 → 190 taken 2 times.
2 if (!store->upsertEvent(row))
152 ✗ return false;
153 4 }
154
1/1
✓ Branch 207 → 209 taken 2 times.
4 store->enqueueOutbox(accountId, calendarPath, copyUid, QStringLiteral("put"), {}, {});
155
2/3
✓ Branch 216 → 217 taken 2 times.
✗ Branch 217 → 218 not taken.
✓ Branch 217 → 219 taken 2 times.
2 if (!adoptServerVersion(*entry, entry->serverIcal, entry->serverEtag))
156 ✗ return false;
157
2/2
✓ Branch 219 → 220 taken 2 times.
✓ Branch 220 → 221 taken 2 times.
2 emit statusMessage(tr("Both versions were kept."));
158
1/1
✓ Branch 222 → 223 taken 2 times.
2 requestFlush();
159 2 return true;
160 2 }
161
1/2
✗ Branch 228 → 229 not taken.
✓ Branch 228 → 230 taken 14 times.
14 if (choice != QLatin1String("mine"))
162 ✗ return false;
163 14 const bool deletedOnServer = entry->serverIcal.isEmpty();
164
4/6
✓ Branch 235 → 236 taken 1 time.
✓ Branch 235 → 238 taken 13 times.
✗ Branch 236 → 237 not taken.
✓ Branch 236 → 238 taken 1 time.
✗ Branch 239 → 240 not taken.
✓ Branch 239 → 244 taken 14 times.
14 if (entry->operation == QLatin1String("delete") && deletedOnServer) {
165 ✗ store->removeOutbox(accountId, calendarPath, uid);
166 ✗ store->removeResource(accountId, calendarPath, uid);
167 ✗ publishChange();
168 ✗ return true;
169 }
170
1/1
✓ Branch 249 → 250 taken 14 times.
28 recordHistory(*entry, QStringLiteral("local"), entry->serverIcal);
171 14 entry->state = QStringLiteral("pending");
172
2/2
✓ Branch 259 → 260 taken 6 times.
✓ Branch 259 → 261 taken 8 times.
14 entry->baseEtag = deletedOnServer ? QString() : entry->serverEtag;
173 14 entry->attempts = 0;
174 14 entry->nextAttempt = 0;
175
1/1
✓ Branch 269 → 270 taken 14 times.
14 entry->conflictFields.clear();
176 14 entry->serverIcal.clear();
177 14 entry->serverEtag.clear();
178
1/1
✓ Branch 275 → 276 taken 14 times.
14 store->updateOutbox(*entry);
179
1/1
✓ Branch 276 → 277 taken 14 times.
14 publishChange();
180
2/2
✓ Branch 277 → 278 taken 14 times.
✓ Branch 278 → 279 taken 14 times.
14 emit statusMessage(tr("Your version will be saved on the server."));
181
1/1
✓ Branch 280 → 281 taken 14 times.
14 requestFlush(); // attempts and backoff were reset above
182 14 return true;
183 37 }
184