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: 94.1% 305 / 0 / 324
Functions: 100.0% 15 / 0 / 15
Branches: 78.0% 350 / 0 / 449

app/core/CalendarOutboxResolve.cpp
Line Branch Exec Source
1 // The conflict resolver's write paths of the calendar outbox (Sprint 107
2 // S107.3.1, S107.3.2, S107.3.6, S107.3.7; decisions D-9, D-14, D-21,
3 // D-22). The CalendarOutbox class is split over CalendarOutbox.cpp,
4 // CalendarOutboxEdits.cpp, CalendarOutboxTasks.cpp and this file.
5 #include "CalendarOutbox.h"
6
7 #include "data/CalendarStore.h"
8 #include "data/ICalEventCodec.h"
9 #include "data/ICalMerge.h"
10 #include "data/ICalTodoCodec.h"
11 #include "data/ICalendar.h"
12 #include "service/CalDavClient.h"
13
14 #include <QCryptographicHash>
15 #include <QLoggingCategory>
16 #include <QTimer>
17 #include <QUuid>
18
19 Q_DECLARE_LOGGING_CATEGORY(lcCalendarOutbox)
20
4/7
✓ Branch 2 → 3 taken 12 times.
✓ Branch 2 → 8 taken 341 times.
✓ Branch 4 → 5 taken 12 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 12 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
353 Q_LOGGING_CATEGORY(lcConflict, "mailjd.conflict")
21
22 namespace {
23
24 const QString kMoved = QStringLiteral("moved");
25
26 // Inverse of CalendarOutbox::key(): "<len>:<account><path>\n<uid>".
27 6 bool splitKey(const QString &key, QString *accountId, QString *calendarPath, QString *uid) {
28 6 const int colon = int(key.indexOf(QLatin1Char(':')));
29 6 const int newline = int(key.indexOf(QLatin1Char('\n')));
30
2/4
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 if (colon <= 0 || newline < colon)
31 ✗ return false;
32 6 bool ok = false;
33
2/2
✓ Branch 11 → 12 taken 6 times.
✓ Branch 12 → 13 taken 6 times.
6 const int length = key.left(colon).toInt(&ok);
34
3/6
✓ Branch 14 → 15 taken 6 times.
✗ Branch 14 → 17 not taken.
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 6 times.
6 if (!ok || length < 0 || colon + 1 + length > newline)
35 ✗ return false;
36
1/1
✓ Branch 18 → 19 taken 6 times.
6 *accountId = key.mid(colon + 1, length);
37
1/1
✓ Branch 21 → 22 taken 6 times.
6 *calendarPath = key.mid(colon + 1 + length, newline - colon - 1 - length);
38
1/1
✓ Branch 24 → 25 taken 6 times.
6 *uid = key.mid(newline + 1);
39 6 return true;
40 }
41
42 // A short identity of a text for the logs (D-19: never values).
43 38 QString hashOf(const QString &text) {
44
5/5
✓ Branch 2 → 3 taken 38 times.
✓ Branch 4 → 5 taken 38 times.
✓ Branch 5 → 6 taken 38 times.
✓ Branch 6 → 7 taken 38 times.
✓ Branch 7 → 8 taken 38 times.
38 return QString::fromLatin1(QCryptographicHash::hash(text.toUtf8(), QCryptographicHash::Sha256).toHex().left(8));
45 }
46
47 2 QString withUid(const QString &icalRaw, const QString &uid) {
48
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (icalRaw.isEmpty())
49 ✗ return {};
50
1/1
✓ Branch 8 → 9 taken 2 times.
2 ICal::Component todo = ICal::parseFirst(icalRaw, QStringLiteral("VTODO"));
51
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 2 times.
2 if (todo.name.isEmpty())
52 ✗ return {};
53
1/1
✓ Branch 17 → 18 taken 2 times.
2 todo.setPropertyValue(QStringLiteral("UID"), uid);
54
1/1
✓ Branch 20 → 21 taken 2 times.
2 return ICal::serialize(todo);
55 2 }
56
57 } // namespace
58
59 8 bool CalendarOutbox::isMoveSource(const CalendarOutboxEntry &entry) const {
60
2/4
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 11 not taken.
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 11 not taken.
16 return entry.operation == QLatin1String("delete") && entry.componentType == QLatin1String("VTODO") &&
61
2/2
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 11 taken 2 times.
16 !entry.dependencyKey.isEmpty();
62 }
63
64 108 CalendarOutbox::ConflictInputs CalendarOutbox::conflictInputs(const QString &uid, const QString &accountId,
65 const QString &calendarPath) const {
66 108 ConflictInputs inputs;
67 108 CalendarStore *store = m_deps.store;
68
1/1
✓ Branch 2 → 3 taken 108 times.
108 const auto entry = store->outboxEntry(accountId, calendarPath, uid);
69
6/6
✓ Branch 4 → 5 taken 95 times.
✓ Branch 4 → 9 taken 13 times.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 10 taken 89 times.
✓ Branch 11 → 12 taken 19 times.
✓ Branch 11 → 13 taken 89 times.
108 if (!entry || entry->state != QLatin1String("conflict"))
70 19 return inputs;
71 89 inputs.valid = true;
72 89 inputs.server = entry->serverIcal;
73 89 inputs.serverEtag = entry->serverEtag;
74 89 inputs.componentType = entry->componentType;
75 89 inputs.operation = entry->operation;
76 89 inputs.fields = entry->conflictFields;
77
2/2
✓ Branch 24 → 25 taken 89 times.
✓ Branch 25 → 26 taken 89 times.
89 inputs.local = QString::fromUtf8(localBody(*entry));
78
1/1
✓ Branch 29 → 30 taken 89 times.
89 inputs.base = store->resourceFor(accountId, calendarPath, uid);
79
2/2
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 59 taken 85 times.
89 if (entry->conflictFields.contains(kMoved)) {
80 // D-22: the base is the source's last server version.
81
1/1
✓ Branch 35 → 36 taken 4 times.
4 const QString target = key(accountId, calendarPath, uid);
82
5/5
✓ Branch 36 → 37 taken 4 times.
✓ Branch 37 → 38 taken 4 times.
✓ Branch 38 → 39 taken 4 times.
✓ Branch 55 → 40 taken 8 times.
✓ Branch 55 → 56 taken 4 times.
12 for (const CalendarOutboxEntry &source : store->outboxEntries())
83
5/6
✓ Branch 42 → 43 taken 4 times.
✓ Branch 42 → 46 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
✗ Branch 44 → 46 not taken.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 47 → 53 taken 4 times.
8 if (source.dependencyKey == target && source.uid == uid) {
84 4 inputs.movedFrom = source.calendarPath;
85
1/1
✓ Branch 49 → 50 taken 4 times.
4 inputs.base = store->resourceFor(source.accountId, source.calendarPath, uid);
86 4 }
87 4 }
88 89 return inputs;
89 108 }
90
91 // The rows of the resource are replaced by `ical` (kept server identity:
92 // href and `etag`); the outbox entry is left alone.
93 22 bool CalendarOutbox::replaceRows(const CalendarOutboxEntry &entry, const QString &ical, const QString &etag) {
94 22 CalendarStore *store = m_deps.store;
95
2/2
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 70 taken 13 times.
22 if (entry.componentType == QLatin1String("VTODO")) {
96 9 ICalTodoCodec::Resource resource;
97
3/3
✓ Branch 5 → 6 taken 9 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 8 times.
9 if (!ICalTodoCodec::parseResource(ical, &resource))
98 1 return false;
99
1/1
✓ Branch 8 → 9 taken 8 times.
8 const auto previous = store->taskFor(entry.uid, entry.accountId, entry.calendarPath);
100
1/1
✓ Branch 9 → 10 taken 8 times.
8 store->beginTransaction();
101
1/1
✓ Branch 10 → 11 taken 8 times.
8 store->deleteTask(entry.uid, entry.accountId, entry.calendarPath);
102 8 bool ok = true;
103
4/4
✓ Branch 11 → 12 taken 8 times.
✓ Branch 12 → 13 taken 8 times.
✓ Branch 61 → 14 taken 8 times.
✓ Branch 61 → 62 taken 8 times.
16 for (CalendarTask task : resource.tasks) {
104
1/2
✓ Branch 17 → 18 taken 8 times.
✗ Branch 17 → 27 not taken.
24 if (task.uid != entry.uid ||
105
9/26
✓ Branch 21 → 22 taken 8 times.
✓ Branch 25 → 26 taken 8 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 8 times.
✓ Branch 29 → 30 taken 8 times.
✗ Branch 29 → 31 not taken.
✓ Branch 31 → 32 taken 8 times.
✗ Branch 31 → 33 not taken.
✓ Branch 33 → 34 taken 8 times.
✗ Branch 33 → 35 not taken.
✓ Branch 35 → 36 taken 8 times.
✗ Branch 35 → 37 not taken.
✓ Branch 37 → 38 taken 8 times.
✗ Branch 37 → 39 not taken.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 8 times.
✗ Branch 121 → 122 not taken.
✗ Branch 121 → 123 not taken.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ 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.
24 ICal::parseFirst(task.icalRaw, QStringLiteral("VTODO")).hasProperty(QStringLiteral("RECURRENCE-ID")))
106 ✗ continue;
107 8 task.accountId = entry.accountId;
108 8 task.calendarPath = entry.calendarPath;
109 8 task.etag = etag;
110
1/2
✓ Branch 45 → 46 taken 8 times.
✗ Branch 45 → 48 not taken.
8 task.resourceHref = previous ? previous->resourceHref : entry.href;
111
3/5
✓ Branch 50 → 51 taken 8 times.
✓ Branch 51 → 52 taken 8 times.
✗ Branch 51 → 54 not taken.
✓ Branch 52 → 53 taken 8 times.
✗ Branch 52 → 54 not taken.
8 ok = store->upsertTask(task) && ok;
112 8 }
113
1/2
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 65 taken 8 times.
8 if (!ok) {
114 ✗ store->rollbackTransaction();
115 ✗ return false;
116 }
117
1/1
✓ Branch 65 → 66 taken 8 times.
8 return store->commitTransaction();
118 9 }
119 13 ICalEventCodec::Resource resource;
120
3/3
✓ Branch 70 → 71 taken 13 times.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 12 times.
13 if (!ICalEventCodec::parseResource(ical, &resource))
121 1 return false;
122
1/1
✓ Branch 73 → 74 taken 12 times.
12 const auto master = store->eventFor(entry.uid, entry.accountId, entry.calendarPath);
123
2/4
✓ Branch 75 → 76 taken 12 times.
✗ Branch 75 → 81 not taken.
✓ Branch 78 → 79 taken 12 times.
✗ Branch 78 → 81 not taken.
12 const QString href = master && !master->resourceHref.isEmpty() ? master->resourceHref : entry.href;
124
1/1
✓ Branch 83 → 84 taken 12 times.
12 store->beginTransaction();
125
1/1
✓ Branch 85 → 86 taken 12 times.
12 store->deleteEvent(entry.uid, entry.accountId, entry.calendarPath);
126 12 bool ok = true;
127
4/4
✓ Branch 87 → 88 taken 12 times.
✓ Branch 88 → 89 taken 12 times.
✓ Branch 110 → 90 taken 12 times.
✓ Branch 110 → 111 taken 12 times.
24 for (CalendarEvent event : resource.events) {
128
1/2
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 95 taken 12 times.
12 if (event.uid != entry.uid)
129 ✗ continue;
130 12 event.accountId = entry.accountId;
131 12 event.calendarPath = entry.calendarPath;
132 12 event.etag = etag;
133 12 event.resourceHref = href;
134
3/5
✓ Branch 99 → 100 taken 12 times.
✓ Branch 100 → 101 taken 12 times.
✗ Branch 100 → 103 not taken.
✓ Branch 101 → 102 taken 12 times.
✗ Branch 101 → 103 not taken.
12 ok = store->upsertEvent(event) && ok;
135 12 }
136
1/2
✗ Branch 111 → 112 not taken.
✓ Branch 111 → 114 taken 12 times.
12 if (!ok) {
137 ✗ store->rollbackTransaction();
138 ✗ return false;
139 }
140
1/1
✓ Branch 114 → 115 taken 12 times.
12 return store->commitTransaction();
141 13 }
142
143 52 void CalendarOutbox::recordHistory(const CalendarOutboxEntry &entry, const QString &keptSide,
144 const QString &losingText) {
145
2/2
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 5 taken 45 times.
52 if (losingText.isEmpty())
146 7 return;
147 45 CalendarStore *store = m_deps.store;
148 45 ConflictHistory::Entry history;
149 45 history.account = entry.accountId;
150 45 history.collection = entry.calendarPath;
151 45 history.uid = entry.uid;
152 45 history.component = entry.componentType;
153
2/2
✓ Branch 11 → 12 taken 19 times.
✓ Branch 11 → 19 taken 26 times.
45 if (entry.componentType == QLatin1String("VTODO")) {
154
3/3
✓ Branch 12 → 13 taken 19 times.
✓ Branch 14 → 15 taken 18 times.
✓ Branch 14 → 17 taken 1 time.
19 if (const auto task = store->taskFor(entry.uid, entry.accountId, entry.calendarPath))
155 19 history.title = task->summary;
156
2/3
✓ Branch 19 → 20 taken 26 times.
✓ Branch 21 → 22 taken 26 times.
✗ Branch 21 → 24 not taken.
26 } else if (const auto event = store->eventFor(entry.uid, entry.accountId, entry.calendarPath)) {
157 26 history.title = event->summary;
158 26 }
159 45 history.keptSide = keptSide;
160 45 history.losingText = losingText;
161
2/2
✓ Branch 28 → 29 taken 45 times.
✓ Branch 29 → 30 taken 45 times.
45 store->addConflictHistory(history, m_deps.now());
162 45 }
163
164 23 bool CalendarOutbox::resolveWith(const QString &uid, const QString &accountId, const QString &calendarPath,
165 const QString &resolved, const QString &serverEtag, const QString &keptSide) {
166 23 CalendarStore *store = m_deps.store;
167
1/1
✓ Branch 2 → 3 taken 23 times.
23 auto entry = store->outboxEntry(accountId, calendarPath, uid);
168
5/6
✓ Branch 4 → 5 taken 22 times.
✓ Branch 4 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 22 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 22 times.
23 if (!entry || entry->state != QLatin1String("conflict"))
169 1 return false;
170
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 27 taken 21 times.
22 if (entry->serverEtag != serverEtag) {
171 // D-9: the shown server version is no longer the one in conflict.
172
5/5
✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 25 → 18 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
2 qCInfo(lcConflict) << "calendar resolution refused: the server version changed meanwhile";
173 1 return false;
174 }
175 21 const bool moved = entry->conflictFields.contains(kMoved);
176
2/2
✓ Branch 30 → 31 taken 21 times.
✓ Branch 31 → 32 taken 21 times.
21 const QString before = QString::fromUtf8(localBody(*entry));
177 // A move keeps the target's own server identity (D-22); otherwise the
178 // rows now stand on the server version the user resolved against.
179 21 QString rowEtag = serverEtag;
180
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 45 taken 20 times.
21 if (moved) {
181
1/1
✓ Branch 35 → 36 taken 1 time.
1 const auto task = store->taskFor(uid, accountId, calendarPath);
182
1/2
✓ Branch 37 → 38 taken 1 time.
✗ Branch 37 → 40 not taken.
1 rowEtag = task ? task->etag : QString();
183 1 }
184
3/3
✓ Branch 46 → 47 taken 21 times.
✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 59 taken 19 times.
21 if (!replaceRows(*entry, resolved, rowEtag)) {
185
5/5
✓ Branch 48 → 49 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✓ Branch 57 → 50 taken 2 times.
✓ Branch 57 → 58 taken 2 times.
4 qCWarning(lcConflict) << "calendar resolution refused: the result cannot be read";
186 2 return false;
187 }
188
5/6
✓ Branch 59 → 60 taken 18 times.
✓ Branch 59 → 64 taken 1 time.
✓ Branch 62 → 63 taken 18 times.
✗ Branch 62 → 64 not taken.
✓ Branch 65 → 66 taken 18 times.
✓ Branch 65 → 69 taken 1 time.
19 if (!moved && !entry->serverIcal.isEmpty())
189
1/1
✓ Branch 68 → 69 taken 18 times.
18 store->upsertResource(accountId, calendarPath, uid, entry->href, serverEtag, entry->serverIcal);
190 19 const QString componentType = entry->componentType;
191
3/4
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 74 taken 18 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 18 times.
19 const QString baseEtag = moved ? entry->baseEtag : (entry->serverIcal.isEmpty() ? QString() : serverEtag);
192
1/1
✓ Branch 83 → 84 taken 19 times.
38 store->enqueueOutbox(accountId, calendarPath, uid, QStringLiteral("put"), entry->href, baseEtag);
193
2/3
✓ Branch 86 → 87 taken 19 times.
✓ Branch 88 → 89 taken 19 times.
✗ Branch 88 → 95 not taken.
19 if (auto current = store->outboxEntry(accountId, calendarPath, uid)) {
194 19 current->baseEtag = baseEtag;
195 19 current->componentType = componentType;
196
1/1
✓ Branch 94 → 95 taken 19 times.
19 store->updateOutbox(*current);
197 19 }
198
1/1
✓ Branch 97 → 98 taken 19 times.
19 recordHistory(*entry, keptSide, before);
199
9/9
✓ Branch 98 → 99 taken 19 times.
✓ Branch 102 → 103 taken 19 times.
✓ Branch 103 → 104 taken 19 times.
✓ Branch 104 → 105 taken 19 times.
✓ Branch 105 → 106 taken 19 times.
✓ Branch 106 → 107 taken 19 times.
✓ Branch 107 → 108 taken 19 times.
✓ Branch 123 → 100 taken 19 times.
✓ Branch 123 → 124 taken 19 times.
38 qCInfo(lcConflict) << "calendar conflict resolved" << componentType << "kept" << keptSide << "result"
200
8/8
✓ Branch 108 → 109 taken 19 times.
✓ Branch 109 → 110 taken 19 times.
✓ Branch 110 → 111 taken 19 times.
✓ Branch 112 → 113 taken 19 times.
✓ Branch 113 → 114 taken 19 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 18 times.
✓ Branch 117 → 118 taken 19 times.
19 << hashOf(resolved) << "against server" << hashOf(entry->serverIcal) << (moved ? "(move)" : "");
201
1/1
✓ Branch 124 → 125 taken 19 times.
19 publishChange();
202
2/2
✓ Branch 125 → 126 taken 19 times.
✓ Branch 126 → 127 taken 19 times.
19 emit statusMessage(tr("Your choices will be saved on the server."));
203
1/1
✓ Branch 128 → 129 taken 19 times.
19 requestFlush();
204 19 return true;
205 23 }
206
207 6 void CalendarOutbox::mergeMovedSource(const CalendarOutboxEntry &entry, const QString &theirs, const QString &etag) {
208 6 CalendarStore *store = m_deps.store;
209 6 QString targetAccount, targetPath, uid;
210
1/1
✓ Branch 2 → 3 taken 6 times.
6 const auto target = splitKey(entry.dependencyKey, &targetAccount, &targetPath, &uid)
211
1/2
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 5 not taken.
6 ? store->taskFor(entry.uid, targetAccount, targetPath)
212
1/1
✓ Branch 4 → 6 taken 6 times.
6 : std::nullopt;
213
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 19 taken 6 times.
6 if (!target) {
214 ✗ markConflict(entry, theirs, etag, {QStringLiteral("changed")});
215 ✗ return;
216 }
217
1/1
✓ Branch 19 → 20 taken 6 times.
6 const QString base = store->resourceFor(entry.accountId, entry.calendarPath, entry.uid);
218
2/2
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
6 const QString mine = QString::fromUtf8(ICalTodoCodec::buildResource(*target));
219
1/1
✓ Branch 24 → 25 taken 6 times.
6 const ICalMerge::Result merged = ICalMerge::merge(base, mine, theirs);
220
1/1
✓ Branch 25 → 26 taken 6 times.
6 finishInFlight(entry);
221 // The source DELETE goes out with the version the merge saw.
222
2/3
✓ Branch 26 → 27 taken 6 times.
✓ Branch 28 → 29 taken 6 times.
✗ Branch 28 → 35 not taken.
6 if (auto source = store->outboxEntry(entry.accountId, entry.calendarPath, entry.uid)) {
223 6 source->baseEtag = etag;
224 6 source->attempts = 0;
225 6 source->nextAttempt = 0;
226
1/1
✓ Branch 34 → 35 taken 6 times.
6 store->updateOutbox(*source);
227 6 }
228 6 CalendarOutboxEntry targetEntry = entry;
229 6 targetEntry.accountId = targetAccount;
230 6 targetEntry.calendarPath = targetPath;
231 6 targetEntry.operation = QStringLiteral("put");
232 6 targetEntry.href = target->resourceHref;
233 6 targetEntry.dependencyKey.clear();
234
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 110 taken 5 times.
6 if (merged.ok) {
235
1/1
✓ Branch 49 → 50 taken 1 time.
1 store->upsertResource(entry.accountId, entry.calendarPath, entry.uid, entry.href, etag, theirs);
236
1/2
✓ Branch 51 → 52 taken 1 time.
✗ Branch 51 → 88 not taken.
1 if (merged.merged != mine) {
237
2/3
✓ Branch 53 → 54 taken 1 time.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 66 taken 1 time.
1 if (!replaceRows(targetEntry, merged.merged, target->etag)) {
238 ✗ markConflict(entry, theirs, etag, {QStringLiteral("changed")});
239 ✗ return;
240 }
241
1/1
✓ Branch 71 → 72 taken 1 time.
2 store->enqueueOutbox(targetAccount, targetPath, uid, QStringLiteral("put"), target->resourceHref, target->etag);
242
2/3
✓ Branch 74 → 75 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 86 not taken.
1 if (auto current = store->outboxEntry(targetAccount, targetPath, uid)) {
243 1 current->componentType = QStringLiteral("VTODO");
244
1/1
✓ Branch 85 → 86 taken 1 time.
1 store->updateOutbox(*current);
245 1 }
246 }
247
1/1
✓ Branch 91 → 92 taken 1 time.
1 recordHistory(targetEntry, QStringLiteral("combined"), mine);
248
5/5
✓ Branch 94 → 95 taken 1 time.
✓ Branch 98 → 99 taken 1 time.
✓ Branch 99 → 100 taken 1 time.
✓ Branch 103 → 96 taken 1 time.
✓ Branch 103 → 104 taken 1 time.
2 qCInfo(lcConflict) << "moved task: the source's server change was combined into the moved task";
249
1/1
✓ Branch 104 → 105 taken 1 time.
1 publishChange();
250
2/2
✓ Branch 105 → 106 taken 1 time.
✓ Branch 106 → 107 taken 1 time.
1 emit statusMessage(tr("The task was moved; a change made in its old list on the server was combined into it."));
251
1/1
✓ Branch 108 → 109 taken 1 time.
1 flush(true);
252 1 return;
253 }
254 // Overlap: the question is asked on the moved task (D-22); the source
255 // DELETE waits for it through its dependency.
256
1/1
✓ Branch 115 → 116 taken 5 times.
10 store->enqueueOutbox(targetAccount, targetPath, uid, QStringLiteral("put"), target->resourceHref, target->etag);
257
2/3
✓ Branch 118 → 119 taken 5 times.
✓ Branch 120 → 121 taken 5 times.
✗ Branch 120 → 162 not taken.
5 if (auto current = store->outboxEntry(targetAccount, targetPath, uid)) {
258 5 current->componentType = QStringLiteral("VTODO");
259 5 current->baseEtag = target->etag;
260 5 current->state = QStringLiteral("conflict");
261 5 current->serverIcal = theirs;
262 5 current->serverEtag = etag;
263
8/14
✓ Branch 143 → 144 taken 1 time.
✓ Branch 143 → 148 taken 4 times.
✓ Branch 152 → 153 taken 1 time.
✓ Branch 152 → 156 taken 4 times.
✓ Branch 154 → 155 taken 1 time.
✓ Branch 154 → 156 taken 1 time.
✓ Branch 156 → 157 taken 1 time.
✓ Branch 156 → 158 taken 4 times.
✗ Branch 279 → 280 not taken.
✗ Branch 279 → 283 not taken.
✗ Branch 281 → 282 not taken.
✗ Branch 281 → 283 not taken.
✗ Branch 285 → 286 not taken.
✗ Branch 285 → 287 not taken.
7 current->conflictFields = merged.conflicts.isEmpty() ? QStringList{QStringLiteral("unreadable")} : merged.conflicts;
264
1/1
✓ Branch 159 → 160 taken 5 times.
5 current->conflictFields.append(kMoved);
265
1/1
✓ Branch 161 → 162 taken 5 times.
5 store->updateOutbox(*current);
266 5 }
267
5/5
✓ Branch 163 → 164 taken 5 times.
✓ Branch 167 → 168 taken 5 times.
✓ Branch 168 → 169 taken 5 times.
✓ Branch 172 → 165 taken 5 times.
✓ Branch 172 → 173 taken 5 times.
10 qCWarning(lcConflict) << "moved task: overlapping changes in the old list; asking on the moved task";
268
1/1
✓ Branch 173 → 174 taken 5 times.
5 publishChange();
269
2/2
✓ Branch 174 → 175 taken 5 times.
✓ Branch 175 → 176 taken 5 times.
5 emit statusMessage(tr("The task was moved, and the same details were changed in its old list on the server. Open it to choose."));
270 15 }
271
272 2 bool CalendarOutbox::keepBothTask(const CalendarOutboxEntry &entry) {
273 2 CalendarStore *store = m_deps.store;
274
1/1
✓ Branch 2 → 3 taken 2 times.
2 const auto task = store->taskFor(entry.uid, entry.accountId, entry.calendarPath);
275
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (!task)
276 ✗ return false;
277 // D-14: the local version as a new task in the same list, without its
278 // subtasks (they keep pointing at the original).
279 2 CalendarTask copy = *task;
280 2 copy.id = 0;
281
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
2 copy.uid = QUuid::createUuid().toString(QUuid::WithoutBraces);
282
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 14 → 15 taken 2 times.
2 copy.summary = task->summary + tr(" (copy)");
283 2 copy.etag.clear();
284 2 copy.resourceHref.clear();
285 2 copy.resourceIcal.clear();
286
1/1
✓ Branch 22 → 23 taken 2 times.
2 copy.icalRaw = withUid(task->icalRaw, copy.uid);
287 2 copy.syncState.clear();
288
1/1
✓ Branch 26 → 27 taken 2 times.
2 return saveTask(copy);
289 2 }
290
291 68 void CalendarOutbox::refreshConflict(const QString &uid, const QString &accountId, const QString &calendarPath) {
292
1/1
✓ Branch 2 → 3 taken 68 times.
68 const auto entry = m_deps.store->outboxEntry(accountId, calendarPath, uid);
293
5/6
✓ Branch 4 → 5 taken 67 times.
✓ Branch 4 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 67 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 67 times.
68 if (!entry || entry->state != QLatin1String("conflict"))
294 1 return;
295
3/3
✓ Branch 13 → 14 taken 67 times.
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 21 taken 64 times.
67 if (m_inFlight.contains(key(accountId, calendarPath, uid))) {
296
2/2
✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
3 m_refreshAgain.insert(key(accountId, calendarPath, uid)); // checked again when this one answers
297 3 return;
298 }
299
6/7
✓ Branch 22 → 23 taken 64 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 64 times.
✓ Branch 24 → 25 taken 39 times.
✓ Branch 24 → 26 taken 25 times.
✓ Branch 27 → 28 taken 39 times.
✓ Branch 27 → 29 taken 25 times.
64 if (m_deps.networkAllowed && !m_deps.networkAllowed())
300 39 return;
301
1/1
✓ Branch 29 → 30 taken 25 times.
25 const auto endpoint = m_deps.endpoint(accountId);
302
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 25 times.
25 if (!endpoint)
303 ✗ return;
304 25 CalendarOutboxEntry shown = *entry;
305 // A moved task's server side is its source's resource.
306 25 QString path = calendarPath;
307 25 QString href = entry->href;
308
2/2
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 63 taken 23 times.
25 if (entry->conflictFields.contains(kMoved)) {
309
1/1
✓ Branch 41 → 42 taken 2 times.
2 const QString target = key(accountId, calendarPath, uid);
310
5/5
✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 59 → 46 taken 4 times.
✓ Branch 59 → 60 taken 2 times.
6 for (const CalendarOutboxEntry &source : m_deps.store->outboxEntries())
311
5/6
✓ Branch 48 → 49 taken 2 times.
✓ Branch 48 → 52 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
✗ Branch 50 → 52 not taken.
✓ Branch 53 → 54 taken 2 times.
✓ Branch 53 → 57 taken 2 times.
4 if (source.dependencyKey == target && source.uid == uid) {
312 2 path = source.calendarPath;
313 2 href = source.href;
314 2 shown = source;
315 2 }
316 2 }
317
1/1
✓ Branch 64 → 65 taken 25 times.
25 CalDavClient *client = clientFor(*endpoint);
318
1/1
✓ Branch 65 → 66 taken 25 times.
25 const QString url = client->eventResourceUrl(href, path, uid);
319
2/2
✓ Branch 66 → 67 taken 25 times.
✓ Branch 67 → 68 taken 25 times.
25 m_inFlight.insert(key(shown.accountId, shown.calendarPath, uid));
320
1/1
✓ Branch 69 → 70 taken 25 times.
25 const quint64 request = client->getResource(url);
321
5/5
✓ Branch 70 → 71 taken 25 times.
✓ Branch 74 → 75 taken 25 times.
✓ Branch 75 → 76 taken 25 times.
✓ Branch 79 → 72 taken 25 times.
✓ Branch 79 → 80 taken 25 times.
50 qCInfo(lcConflict) << "checking the server version of a calendar conflict again";
322 25 connect(client, &CalDavClient::resourceResponse, this,
323
1/3
✓ Branch 83 → 84 taken 25 times.
✗ Branch 123 → 124 not taken.
✗ Branch 123 → 125 not taken.
50 [this, request, shown, entry = *entry](const DavResponse &response) {
324
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 19 times.
19 if (response.request != request)
325 17 return;
326
1/1
✓ Branch 4 → 5 taken 19 times.
19 finishInFlight(shown);
327
1/1
✓ Branch 5 → 6 taken 19 times.
19 const QString again = key(entry.accountId, entry.calendarPath, entry.uid);
328
3/3
✓ Branch 6 → 7 taken 19 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 12 taken 17 times.
19 if (m_refreshAgain.remove(again))
329
1/1
✓ Branch 9 → 10 taken 2 times.
4 QTimer::singleShot(0, this, [this, entry] { refreshConflict(entry.uid, entry.accountId, entry.calendarPath); });
330 // Resolved (or edited) while the check ran: the answer belongs to a
331 // conflict that no longer exists, and the push the resolution queued
332 // was held back by this check — send it now.
333
1/1
✓ Branch 12 → 13 taken 19 times.
19 const auto now = m_deps.store->outboxEntry(entry.accountId, entry.calendarPath, entry.uid);
334
6/8
✓ Branch 14 → 15 taken 19 times.
✗ Branch 14 → 21 not taken.
✓ Branch 18 → 19 taken 8 times.
✓ Branch 18 → 21 taken 11 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 8 times.
✓ Branch 23 → 24 taken 11 times.
✓ Branch 23 → 26 taken 8 times.
19 if (!now || now->state != QLatin1String("conflict") || now->revision != entry.revision) {
335
1/1
✓ Branch 24 → 25 taken 11 times.
11 requestFlush();
336 11 return;
337 }
338
6/6
✓ Branch 26 → 27 taken 7 times.
✓ Branch 26 → 30 taken 1 time.
✓ Branch 28 → 29 taken 3 times.
✓ Branch 28 → 30 taken 4 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 31 → 33 taken 5 times.
8 if (response.status == 200 && response.etag == entry.serverEtag)
339 3 return; // unchanged
340
5/6
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 39 taken 3 times.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 47 taken 3 times.
5 if (shown.operation == QLatin1String("delete") && isMoveSource(shown)) {
341
1/2
✓ Branch 41 → 42 taken 2 times.
✗ Branch 41 → 46 not taken.
2 if (response.status == 200)
342
2/2
✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
2 mergeMovedSource(shown, QString::fromUtf8(response.body), response.etag);
343 2 return;
344 }
345
5/6
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 51 taken 2 times.
✓ Branch 49 → 50 taken 1 time.
✗ Branch 49 → 51 not taken.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 2 times.
3 if (response.status == 404 && entry.serverIcal.isEmpty())
346 1 return; // still deleted
347
2/2
✓ Branch 54 → 55 taken 2 times.
✓ Branch 55 → 56 taken 2 times.
2 onServerVersion(entry, localBody(entry), response);
348
4/4
✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 61 taken 17 times.
✓ Branch 63 → 64 taken 2 times.
✓ Branch 63 → 66 taken 17 times.
36 });
349 68 }
350
351 4 bool CalendarOutbox::restoreFromHistory(qint64 historyId) {
352 4 CalendarStore *store = m_deps.store;
353
1/1
✓ Branch 2 → 3 taken 4 times.
4 const auto history = store->conflictHistoryEntry(historyId);
354
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 3 times.
4 if (!history)
355 1 return false;
356 3 bool ok = false;
357
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 49 taken 2 times.
3 if (history->component == QLatin1String("VTODO")) {
358 1 ICalTodoCodec::Resource resource;
359
1/1
✓ Branch 13 → 14 taken 1 time.
1 const auto current = store->taskFor(history->uid, history->account, history->collection);
360
2/3
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 46 not taken.
1 if (ICalTodoCodec::parseResource(history->losingText, &resource))
361
3/4
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 44 → 20 taken 1 time.
✗ Branch 44 → 45 not taken.
1 for (CalendarTask task : resource.tasks) {
362
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
1 if (task.uid != history->uid)
363 ✗ continue;
364 1 task.accountId = history->account;
365 1 task.calendarPath = history->collection;
366
1/2
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 38 not taken.
1 if (current) {
367 1 task.etag = current->etag;
368 1 task.resourceHref = current->resourceHref;
369 1 task.resourceIcal = current->resourceIcal;
370 }
371
1/1
✓ Branch 38 → 39 taken 1 time.
1 ok = saveTask(task);
372 1 break;
373 1 }
374 1 } else {
375 2 ICalEventCodec::Resource resource;
376
2/3
✓ Branch 50 → 51 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✗ Branch 51 → 82 not taken.
2 if (ICalEventCodec::parseResource(history->losingText, &resource)) {
377 2 QList<CalendarEvent> rows;
378
4/4
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✓ Branch 70 → 55 taken 2 times.
✓ Branch 70 → 71 taken 2 times.
4 for (CalendarEvent event : resource.events)
379
1/2
✓ Branch 59 → 60 taken 2 times.
✗ Branch 59 → 67 not taken.
2 if (event.uid == history->uid) {
380 2 event.accountId = history->account;
381 2 event.calendarPath = history->collection;
382 2 event.etag.clear(); // restoreSeries keeps the stored server identity
383 2 event.resourceHref.clear();
384
1/1
✓ Branch 66 → 67 taken 2 times.
2 rows.append(event);
385 2 }
386
3/5
✓ Branch 72 → 73 taken 2 times.
✗ Branch 72 → 79 not taken.
✓ Branch 76 → 77 taken 2 times.
✓ Branch 77 → 78 taken 2 times.
✗ Branch 77 → 79 not taken.
2 ok = !rows.isEmpty() && restoreSeries(history->uid, history->account, history->collection, rows);
387 2 }
388 2 }
389
1/2
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 96 taken 3 times.
3 if (!ok) {
390 ✗ qCWarning(lcConflict) << "a resolved version could not be restored";
391 ✗ return false;
392 }
393
1/1
✓ Branch 96 → 97 taken 3 times.
3 store->removeConflictHistory(historyId);
394
6/6
✓ Branch 97 → 98 taken 3 times.
✓ Branch 101 → 102 taken 3 times.
✓ Branch 102 → 103 taken 3 times.
✓ Branch 104 → 105 taken 3 times.
✓ Branch 108 → 99 taken 3 times.
✓ Branch 108 → 109 taken 3 times.
6 qCInfo(lcConflict) << "restored the version a resolution replaced" << history->component;
395
2/2
✓ Branch 109 → 110 taken 3 times.
✓ Branch 110 → 111 taken 3 times.
3 emit statusMessage(tr("The earlier version was restored."));
396 3 return true;
397 4 }
398