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: 96.5% 469 / 0 / 486
Functions: 100.0% 26 / 0 / 26
Branches: 84.4% 661 / 0 / 783

data/ICalMergePlan.cpp
Line Branch Exec Source
1 #include "ICalMergePlan.h"
2
3 #include "ICalendar.h"
4
5 #include <QHash>
6 #include <QMap>
7 #include <QSet>
8 #include <algorithm>
9
10 namespace ICalMergePlan {
11
12 namespace {
13
14 const QSet<QString> kBookkeeping{QStringLiteral("DTSTAMP"), QStringLiteral("LAST-MODIFIED"),
15 QStringLiteral("SEQUENCE")};
16
17 // Group key -> sorted content lines (properties) or serialized components.
18 using Groups = QMap<QString, QStringList>;
19
20 struct Calendar {
21 ICal::Component root;
22 QMap<QString, ICal::Component> events; // component key -> component
23 };
24
25 struct Inputs {
26 Calendar base;
27 Calendar local;
28 Calendar server;
29 QString error;
30 };
31
32 32511 Groups groupsOf(const ICal::Component &event) {
33 32511 Groups groups;
34
2/2
✓ Branch 15 → 4 taken 197311 times.
✓ Branch 15 → 16 taken 32511 times.
229822 for (const ICal::Property &property : event.properties) {
35
2/2
✓ Branch 6 → 7 taken 35094 times.
✓ Branch 6 → 8 taken 162217 times.
197311 if (kBookkeeping.contains(property.name))
36 35094 continue;
37
3/3
✓ Branch 8 → 9 taken 162217 times.
✓ Branch 9 → 10 taken 162217 times.
✓ Branch 10 → 11 taken 162217 times.
162217 groups[property.name].append(property.contentLine());
38 }
39
2/2
✓ Branch 32 → 18 taken 110 times.
✓ Branch 32 → 33 taken 32511 times.
32621 for (const ICal::Component &child : event.components)
40
4/4
✓ Branch 22 → 23 taken 110 times.
✓ Branch 23 → 24 taken 110 times.
✓ Branch 24 → 25 taken 110 times.
✓ Branch 25 → 26 taken 110 times.
220 groups[QStringLiteral("BEGIN:") + child.name].append(ICal::serialize(child));
41
4/4
✓ Branch 33 → 34 taken 32511 times.
✓ Branch 38 → 39 taken 194741 times.
✓ Branch 40 → 35 taken 162230 times.
✓ Branch 40 → 41 taken 32511 times.
194741 for (auto it = groups.begin(); it != groups.end(); ++it)
42
1/1
✓ Branch 36 → 37 taken 162230 times.
162230 it.value().sort();
43 32511 return groups;
44 ✗ }
45
46 32590 QString eventKey(const ICal::Component &event) {
47
1/1
✓ Branch 5 → 6 taken 32590 times.
32590 const ICal::Property *recurrence = event.property(QStringLiteral("RECURRENCE-ID"));
48
4/4
✓ Branch 16 → 17 taken 32590 times.
✓ Branch 19 → 20 taken 32590 times.
✓ Branch 20 → 21 taken 32590 times.
✓ Branch 21 → 22 taken 32590 times.
97770 return event.name + QLatin1Char(':') + event.value(QStringLiteral("UID")) + QLatin1Char(':') +
49
4/4
✓ Branch 8 → 9 taken 30145 times.
✓ Branch 8 → 10 taken 2445 times.
✓ Branch 9 → 11 taken 30145 times.
✓ Branch 22 → 23 taken 32590 times.
97770 (recurrence ? recurrence->value.trimmed() : QString());
50 }
51
52 // S106-137: two components with the same key cannot be matched;
53 // `*duplicate` names the key.
54 2430 QMap<QString, ICal::Component> eventsByKey(const ICal::Component &calendar, QString *duplicate) {
55 2430 QMap<QString, ICal::Component> events;
56
2/2
✓ Branch 29 → 4 taken 34372 times.
✓ Branch 29 → 30 taken 2430 times.
36802 for (const ICal::Component &component : calendar.components) {
57
6/6
✓ Branch 7 → 8 taken 2325 times.
✓ Branch 7 → 12 taken 32047 times.
✓ Branch 10 → 11 taken 1782 times.
✓ Branch 10 → 12 taken 543 times.
✓ Branch 13 → 14 taken 1782 times.
✓ Branch 13 → 15 taken 32590 times.
34372 if (component.name != QLatin1String("VEVENT") && component.name != QLatin1String("VTODO"))
58 1782 continue;
59
1/1
✓ Branch 15 → 16 taken 32590 times.
32590 const QString key = eventKey(component);
60
6/7
✓ Branch 16 → 17 taken 32590 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 17 → 21 taken 32581 times.
✓ Branch 19 → 20 taken 9 times.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 9 times.
✓ Branch 22 → 24 taken 32581 times.
32590 if (events.contains(key) && duplicate->isEmpty())
61 9 *duplicate = key;
62
1/1
✓ Branch 24 → 25 taken 32590 times.
32590 events.insert(key, component);
63 32590 }
64 2430 return events;
65 ✗ }
66
67 // The VCALENDAR of `text`; `*lossy` is set when the parse dropped lines.
68 2469 ICal::Component parseCalendar(const QString &text, QString *error, bool *lossy) {
69 2469 QList<ICal::Component> components;
70 2469 int skipped = 0;
71
2/3
✓ Branch 2 → 3 taken 2469 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2469 times.
2469 if (!ICal::parse(text, &components, error, &skipped))
72 ✗ return {};
73
2/2
✓ Branch 5 → 6 taken 12 times.
✓ Branch 5 → 7 taken 2457 times.
2469 if (skipped > 0)
74 12 *lossy = true;
75
4/4
✓ Branch 7 → 8 taken 2469 times.
✓ Branch 8 → 9 taken 2469 times.
✓ Branch 18 → 10 taken 2467 times.
✓ Branch 18 → 19 taken 2 times.
2469 for (const ICal::Component &component : components)
76
1/2
✓ Branch 13 → 14 taken 2467 times.
✗ Branch 13 → 16 not taken.
2467 if (component.name == QLatin1String("VCALENDAR"))
77 2467 return component;
78 2 return {};
79 2469 }
80
81 823 Inputs parseInputs(const QString &baseText, const QString &localText, const QString &serverText) {
82 823 Inputs inputs;
83 823 QString error;
84 823 bool lossy = false;
85
1/1
✓ Branch 2 → 3 taken 823 times.
823 inputs.base.root = parseCalendar(baseText, &error, &lossy);
86
1/1
✓ Branch 5 → 6 taken 823 times.
823 inputs.local.root = parseCalendar(localText, &error, &lossy);
87
1/1
✓ Branch 8 → 9 taken 823 times.
823 inputs.server.root = parseCalendar(serverText, &error, &lossy);
88
6/8
✓ Branch 12 → 13 taken 823 times.
✗ Branch 12 → 17 not taken.
✓ Branch 14 → 15 taken 823 times.
✗ Branch 14 → 17 not taken.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 821 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 31 taken 821 times.
823 if (inputs.base.root.name.isEmpty() || inputs.local.root.name.isEmpty() || inputs.server.root.name.isEmpty()) {
89
2/4
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 25 not taken.
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 30 not taken.
4 inputs.error = error.isEmpty() ? QStringLiteral("Unparseable resource") : error;
90 2 return inputs;
91 }
92 // S106-129: lines the parser skipped would look deleted in every group.
93
2/2
✓ Branch 31 → 32 taken 11 times.
✓ Branch 31 → 39 taken 810 times.
821 if (lossy) {
94 11 inputs.error = QStringLiteral("A version has lines MailJD cannot read; it is not merged");
95 11 return inputs;
96 }
97 810 QString duplicate;
98
1/1
✓ Branch 39 → 40 taken 810 times.
810 inputs.base.events = eventsByKey(inputs.base.root, &duplicate);
99
1/1
✓ Branch 42 → 43 taken 810 times.
810 inputs.local.events = eventsByKey(inputs.local.root, &duplicate);
100
1/1
✓ Branch 45 → 46 taken 810 times.
810 inputs.server.events = eventsByKey(inputs.server.root, &duplicate);
101
2/2
✓ Branch 49 → 50 taken 9 times.
✓ Branch 49 → 60 taken 801 times.
810 if (!duplicate.isEmpty())
102
1/1
✓ Branch 54 → 55 taken 9 times.
18 inputs.error = QStringLiteral("A version holds the component %1 twice; it is not merged").arg(duplicate);
103 810 return inputs;
104 823 }
105
106 27495 QString groupOfName(const QString &name, bool master) {
107 13 static const QSet<QString> time{QStringLiteral("DTSTART"), QStringLiteral("DTEND"), QStringLiteral("DUE"),
108
5/10
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 28 taken 27482 times.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 28 not taken.
✓ Branch 21 → 22 taken 52 times.
✓ Branch 21 → 23 taken 13 times.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 121 → 122 not taken.
✗ Branch 121 → 123 not taken.
27573 QStringLiteral("DURATION")};
109 13 static const QSet<QString> completion{QStringLiteral("STATUS"), QStringLiteral("PERCENT-COMPLETE"),
110
5/10
✓ Branch 28 → 29 taken 13 times.
✓ Branch 28 → 50 taken 27482 times.
✓ Branch 30 → 31 taken 13 times.
✗ Branch 30 → 50 not taken.
✓ Branch 44 → 45 taken 39 times.
✓ Branch 44 → 46 taken 13 times.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 143 → 144 not taken.
✗ Branch 143 → 145 not taken.
27560 QStringLiteral("COMPLETED")};
111
5/10
✓ Branch 50 → 51 taken 13 times.
✓ Branch 50 → 72 taken 27482 times.
✓ Branch 52 → 53 taken 13 times.
✗ Branch 52 → 72 not taken.
✓ Branch 66 → 67 taken 39 times.
✓ Branch 66 → 68 taken 13 times.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 165 → 166 not taken.
✗ Branch 165 → 167 not taken.
27547 static const QSet<QString> recurrence{QStringLiteral("RRULE"), QStringLiteral("RDATE"), QStringLiteral("EXDATE")};
112
2/2
✓ Branch 73 → 74 taken 10736 times.
✓ Branch 73 → 79 taken 16759 times.
27495 if (time.contains(name))
113 10736 return QStringLiteral("time");
114
2/2
✓ Branch 80 → 81 taken 113 times.
✓ Branch 80 → 86 taken 16646 times.
16759 if (completion.contains(name))
115 113 return QStringLiteral("completion");
116
6/6
✓ Branch 86 → 87 taken 1619 times.
✓ Branch 86 → 90 taken 15027 times.
✓ Branch 88 → 89 taken 117 times.
✓ Branch 88 → 90 taken 1502 times.
✓ Branch 91 → 92 taken 117 times.
✓ Branch 91 → 97 taken 16529 times.
16646 if (master && recurrence.contains(name))
117 117 return QStringLiteral("recurrence");
118 16529 return {};
119 169 }
120
121 27239 bool isTextName(const QString &name) {
122
4/4
✓ Branch 4 → 5 taken 27135 times.
✓ Branch 4 → 8 taken 104 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 27134 times.
27239 return name == QLatin1String("DESCRIPTION") || name == QLatin1String("COMMENT");
123 }
124
125 // D-8 (Sprint 107 S107.2.4): list properties merge as sets like vCard's —
126 // one item per attendee (by calendar address), tag, excluded/added date,
127 // parent link (by RELTYPE + UID), contact, attachment and reminder (by its
128 // UID, else ACTION + TRIGGER). Two people adding different values is no
129 // conflict; the same value changed differently on both sides still is.
130 27386 bool isLineSetName(const QString &name) {
131
4/4
✓ Branch 7 → 8 taken 27336 times.
✓ Branch 7 → 14 taken 13 times.
✓ Branch 10 → 11 taken 27327 times.
✓ Branch 10 → 14 taken 9 times.
82071 return name == QLatin1String("ATTENDEE") || name == QLatin1String("RELATED-TO") ||
132
4/4
✓ Branch 4 → 5 taken 27349 times.
✓ Branch 4 → 14 taken 37 times.
✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 15 taken 27318 times.
82071 name == QLatin1String("CONTACT") || name == QLatin1String("ATTACH");
133 }
134
135 27812 bool isValueSetName(const QString &name) {
136
6/6
✓ Branch 4 → 5 taken 27644 times.
✓ Branch 4 → 11 taken 168 times.
✓ Branch 7 → 8 taken 27565 times.
✓ Branch 7 → 11 taken 79 times.
✓ Branch 10 → 11 taken 23 times.
✓ Branch 10 → 12 taken 27542 times.
27812 return name == QLatin1String("CATEGORIES") || name == QLatin1String("EXDATE") || name == QLatin1String("RDATE");
137 }
138
139 27386 bool isSetName(const QString &name) {
140
6/6
✓ Branch 3 → 4 taken 27318 times.
✓ Branch 3 → 9 taken 68 times.
✓ Branch 5 → 6 taken 27263 times.
✓ Branch 5 → 9 taken 55 times.
✓ Branch 8 → 9 taken 24 times.
✓ Branch 8 → 10 taken 27239 times.
27386 return isLineSetName(name) || isValueSetName(name) || name == QLatin1String("BEGIN:VALARM");
141 }
142
143 struct SetUnit {
144 QString key;
145 QString text; // the content line (one value) or the serialized VALARM
146 };
147
148 614 QList<SetUnit> setUnits(const ICal::Component *component, const QString &name) {
149 614 QList<SetUnit> units;
150
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 614 times.
614 if (!component)
151 ✗ return units;
152
2/2
✓ Branch 6 → 7 taken 97 times.
✓ Branch 6 → 86 taken 517 times.
614 if (name == QLatin1String("BEGIN:VALARM")) {
153
2/2
✓ Branch 84 → 9 taken 87 times.
✓ Branch 84 → 85 taken 97 times.
184 for (const ICal::Component &alarm : component->components) {
154
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 87 times.
87 if (alarm.name != QLatin1String("VALARM"))
155 ✗ continue;
156
1/1
✓ Branch 17 → 18 taken 87 times.
87 const ICal::Property *trigger = alarm.property(QStringLiteral("TRIGGER"));
157
2/2
✓ Branch 23 → 24 taken 87 times.
✓ Branch 24 → 25 taken 87 times.
87 const QString uid = alarm.value(QStringLiteral("UID")).trimmed();
158
2/6
✓ Branch 73 → 74 taken 36 times.
✓ Branch 73 → 75 taken 51 times.
✗ Branch 235 → 236 not taken.
✗ Branch 235 → 237 not taken.
✗ Branch 239 → 240 not taken.
✗ Branch 239 → 241 not taken.
210 const QString key = !uid.isEmpty() ? QStringLiteral("uid:") + uid
159
22/40
✓ Branch 42 → 43 taken 51 times.
✓ Branch 43 → 44 taken 51 times.
✓ Branch 44 → 45 taken 51 times.
✓ Branch 48 → 49 taken 51 times.
✓ Branch 51 → 52 taken 51 times.
✓ Branch 51 → 53 taken 36 times.
✓ Branch 53 → 54 taken 51 times.
✓ Branch 53 → 55 taken 36 times.
✓ Branch 55 → 56 taken 51 times.
✓ Branch 55 → 57 taken 36 times.
✓ Branch 57 → 58 taken 51 times.
✓ Branch 57 → 59 taken 36 times.
✓ Branch 59 → 60 taken 51 times.
✓ Branch 59 → 61 taken 36 times.
✓ Branch 61 → 62 taken 51 times.
✓ Branch 61 → 63 taken 36 times.
✓ Branch 63 → 64 taken 51 times.
✓ Branch 63 → 65 taken 36 times.
✓ Branch 65 → 66 taken 51 times.
✓ Branch 65 → 67 taken 36 times.
✓ Branch 67 → 68 taken 51 times.
✓ Branch 67 → 69 taken 36 times.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
✗ Branch 201 → 202 not taken.
✗ Branch 201 → 203 not taken.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
✗ Branch 209 → 210 not taken.
✗ Branch 209 → 211 not taken.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 215 not taken.
✗ Branch 217 → 218 not taken.
✗ Branch 217 → 219 not taken.
✗ Branch 221 → 222 not taken.
✗ Branch 221 → 223 not taken.
✗ Branch 225 → 226 not taken.
✗ Branch 225 → 227 not taken.
189 : QStringLiteral("a:") + alarm.value(QStringLiteral("ACTION")).trimmed().toUpper() +
160
11/14
✓ Branch 29 → 30 taken 36 times.
✓ Branch 29 → 34 taken 51 times.
✓ Branch 33 → 51 taken 36 times.
✓ Branch 34 → 35 taken 51 times.
✗ Branch 34 → 36 not taken.
✓ Branch 35 → 37 taken 51 times.
✓ Branch 49 → 50 taken 51 times.
✓ Branch 50 → 51 taken 51 times.
✓ Branch 69 → 70 taken 51 times.
✓ Branch 69 → 71 taken 36 times.
✓ Branch 71 → 72 taken 36 times.
✓ Branch 71 → 73 taken 51 times.
✗ Branch 231 → 232 not taken.
✗ Branch 231 → 233 not taken.
261 QLatin1Char('|') + (trigger ? trigger->contentLine() : QString());
161 87 units.append({key, ICal::serialize(alarm)});
162 87 }
163 97 return units;
164 }
165
5/5
✓ Branch 86 → 87 taken 517 times.
✓ Branch 87 → 88 taken 517 times.
✓ Branch 88 → 89 taken 517 times.
✓ Branch 174 → 90 taken 469 times.
✓ Branch 174 → 175 taken 517 times.
986 for (const ICal::Property &property : component->propertiesNamed(name)) {
166
2/2
✓ Branch 92 → 93 taken 204 times.
✓ Branch 92 → 122 taken 265 times.
469 if (isValueSetName(name)) {
167
5/5
✓ Branch 93 → 94 taken 204 times.
✓ Branch 94 → 95 taken 204 times.
✓ Branch 95 → 96 taken 204 times.
✓ Branch 119 → 97 taken 261 times.
✓ Branch 119 → 120 taken 204 times.
465 for (const QString &value : ICal::splitValues(property.value)) {
168 261 ICal::Property single = property;
169
1/1
✓ Branch 99 → 100 taken 261 times.
261 single.setValue(value);
170
1/1
✓ Branch 100 → 101 taken 261 times.
261 const QString text = single.contentLine();
171
6/10
✓ Branch 104 → 105 taken 191 times.
✓ Branch 105 → 106 taken 191 times.
✓ Branch 111 → 112 taken 191 times.
✓ Branch 111 → 113 taken 70 times.
✓ Branch 113 → 114 taken 191 times.
✓ Branch 113 → 115 taken 70 times.
✗ Branch 260 → 261 not taken.
✗ Branch 260 → 262 not taken.
✗ Branch 264 → 265 not taken.
✗ Branch 264 → 266 not taken.
713 units.append({name == QLatin1String("CATEGORIES") ? ICal::unescapeText(value).trimmed().toLower() : text, text});
172 465 }
173 204 continue;
174 204 }
175
1/1
✓ Branch 122 → 123 taken 265 times.
265 QString key = property.value.trimmed();
176
2/2
✓ Branch 125 → 126 taken 190 times.
✓ Branch 125 → 130 taken 75 times.
265 if (name == QLatin1String("ATTENDEE"))
177
1/1
✓ Branch 126 → 127 taken 190 times.
190 key = key.toLower();
178
2/2
✓ Branch 132 → 133 taken 42 times.
✓ Branch 132 → 165 taken 33 times.
75 else if (name == QLatin1String("RELATED-TO"))
179
9/15
✓ Branch 138 → 139 taken 42 times.
✓ Branch 139 → 140 taken 39 times.
✓ Branch 139 → 145 taken 3 times.
✓ Branch 143 → 144 taken 39 times.
✓ Branch 144 → 148 taken 39 times.
✓ Branch 158 → 159 taken 39 times.
✓ Branch 158 → 160 taken 3 times.
✓ Branch 160 → 161 taken 39 times.
✓ Branch 160 → 162 taken 3 times.
✗ Branch 287 → 288 not taken.
✗ Branch 287 → 289 not taken.
✗ Branch 291 → 292 not taken.
✗ Branch 291 → 293 not taken.
✗ Branch 295 → 296 not taken.
✗ Branch 295 → 297 not taken.
168 key = (property.hasParameter(QStringLiteral("RELTYPE")) ? property.parameter(QStringLiteral("RELTYPE")).toUpper()
180
4/6
✓ Branch 154 → 155 taken 3 times.
✓ Branch 154 → 156 taken 39 times.
✓ Branch 156 → 157 taken 39 times.
✓ Branch 156 → 158 taken 3 times.
✗ Branch 283 → 284 not taken.
✗ Branch 283 → 285 not taken.
84 : QStringLiteral("PARENT")) +
181
2/2
✓ Branch 148 → 149 taken 42 times.
✓ Branch 149 → 150 taken 42 times.
126 QLatin1Char('|') + key;
182 265 units.append({key, property.contentLine()});
183 782 }
184 517 return units;
185 804 }
186
187 768 const SetUnit *unitWithKey(const QList<SetUnit> &units, const QString &key) {
188 768 const SetUnit *found = nullptr;
189
2/2
✓ Branch 10 → 4 taken 858 times.
✓ Branch 10 → 11 taken 768 times.
1626 for (const SetUnit &unit : units)
190
2/2
✓ Branch 6 → 7 taken 406 times.
✓ Branch 6 → 8 taken 452 times.
858 if (unit.key == key)
191 406 found = &unit;
192 768 return found;
193 }
194
195 256 QString setItemId(const QString &key, const QString &name, const QString &unitKey) {
196
4/4
✓ Branch 8 → 9 taken 256 times.
✓ Branch 9 → 10 taken 256 times.
✓ Branch 10 → 11 taken 256 times.
✓ Branch 11 → 12 taken 256 times.
512 return key + QStringLiteral("::") + name + QStringLiteral("::") + unitKey;
197 }
198
199 147 void addSetItems(Merge::Plan *plan, const QString &key, const QString &name, const QString &recurrence,
200 const ICal::Component *base, const ICal::Component *local, const ICal::Component *server) {
201
3/3
✓ Branch 2 → 3 taken 147 times.
✓ Branch 3 → 4 taken 147 times.
✓ Branch 4 → 5 taken 147 times.
147 const QList<SetUnit> b = setUnits(base, name), l = setUnits(local, name), s = setUnits(server, name);
202 147 QStringList keys;
203
2/2
✓ Branch 17 → 7 taken 441 times.
✓ Branch 17 → 18 taken 147 times.
588 for (const QList<SetUnit> *units : {&s, &l, &b})
204
2/2
✓ Branch 15 → 9 taken 406 times.
✓ Branch 15 → 16 taken 441 times.
847 for (const SetUnit &unit : *units)
205
2/2
✓ Branch 11 → 12 taken 256 times.
✓ Branch 11 → 13 taken 150 times.
406 if (!keys.contains(unit.key))
206
1/1
✓ Branch 12 → 13 taken 256 times.
256 keys.append(unit.key);
207
2/2
✓ Branch 2 → 3 taken 406 times.
✓ Branch 2 → 4 taken 362 times.
768 const auto value = [](const SetUnit *unit) { return unit ? Merge::Value{true, unit->text} : Merge::Value{}; };
208
4/4
✓ Branch 18 → 19 taken 147 times.
✓ Branch 19 → 20 taken 147 times.
✓ Branch 49 → 21 taken 256 times.
✓ Branch 49 → 50 taken 147 times.
403 for (const QString &unitKey : keys) {
209 256 Merge::Item item;
210
1/1
✓ Branch 22 → 23 taken 256 times.
256 item.id = setItemId(key, name, unitKey);
211 256 item.name = name;
212 256 item.parent = key;
213 256 item.context = recurrence;
214 256 item.kind = Merge::Kind::SetValue;
215
1/1
✓ Branch 29 → 30 taken 256 times.
256 item.group = groupOfName(name, recurrence.isEmpty());
216
1/1
✓ Branch 32 → 33 taken 256 times.
256 item.base = value(unitWithKey(b, unitKey));
217
1/1
✓ Branch 36 → 37 taken 256 times.
256 item.local = value(unitWithKey(l, unitKey));
218
1/1
✓ Branch 40 → 41 taken 256 times.
256 item.server = value(unitWithKey(s, unitKey));
219
1/1
✓ Branch 44 → 45 taken 256 times.
256 item.state = Merge::stateOf(item.base, item.local, item.server);
220
1/1
✓ Branch 45 → 46 taken 256 times.
256 plan->items.append(item);
221 256 }
222 147 }
223
224 // The lines of a set name for the chosen values. Unchanged sets keep the
225 // exact lines of the side they equal; mixed value lists are written as one
226 // line per parameter set (values comma-separated).
227 126 QStringList setLines(const QList<const Merge::Item *> &siblings, const QString &name, const Groups &localGroups,
228 const Groups &serverGroups, const ICal::Component *local, const ICal::Component *server) {
229 126 QStringList texts;
230
2/2
✓ Branch 19 → 4 taken 812 times.
✓ Branch 19 → 20 taken 126 times.
938 for (const Merge::Item *item : siblings) {
231
5/6
✓ Branch 6 → 7 taken 219 times.
✓ Branch 6 → 8 taken 593 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 219 times.
✓ Branch 10 → 11 taken 593 times.
✓ Branch 10 → 12 taken 219 times.
812 if (item->name != name || item->kind != Merge::Kind::SetValue)
232 593 continue;
233 219 bool present = false;
234
1/1
✓ Branch 12 → 13 taken 219 times.
219 const QString text = Merge::resultText(*item, &present);
235
2/2
✓ Branch 13 → 14 taken 164 times.
✓ Branch 13 → 15 taken 55 times.
219 if (present)
236
1/1
✓ Branch 14 → 15 taken 164 times.
164 texts.append(text);
237 219 }
238 173 const auto sameSet = [&texts](const QList<SetUnit> &units) {
239 173 QStringList side;
240
2/2
✓ Branch 8 → 4 taken 207 times.
✓ Branch 8 → 9 taken 173 times.
380 for (const SetUnit &unit : units)
241
1/1
✓ Branch 5 → 6 taken 207 times.
207 side.append(unit.text);
242 173 QStringList mine = texts;
243
1/1
✓ Branch 10 → 11 taken 173 times.
173 side.sort();
244
1/1
✓ Branch 11 → 12 taken 173 times.
173 mine.sort();
245
1/1
✓ Branch 12 → 13 taken 173 times.
346 return side == mine;
246 173 };
247
4/4
✓ Branch 20 → 21 taken 126 times.
✓ Branch 21 → 22 taken 126 times.
✓ Branch 23 → 24 taken 79 times.
✓ Branch 23 → 29 taken 47 times.
126 if (sameSet(setUnits(server, name)))
248
1/1
✓ Branch 25 → 26 taken 79 times.
79 return serverGroups.value(name);
249
4/4
✓ Branch 29 → 30 taken 47 times.
✓ Branch 30 → 31 taken 47 times.
✓ Branch 32 → 33 taken 22 times.
✓ Branch 32 → 38 taken 25 times.
47 if (sameSet(setUnits(local, name)))
250
1/1
✓ Branch 34 → 35 taken 22 times.
22 return localGroups.value(name);
251
2/2
✓ Branch 39 → 40 taken 14 times.
✓ Branch 39 → 42 taken 11 times.
25 if (!isValueSetName(name)) {
252
1/1
✓ Branch 40 → 41 taken 14 times.
14 texts.sort();
253 14 return texts;
254 }
255 // One line per parameter set, values in the chosen order.
256 11 QStringList heads;
257 11 QHash<QString, QStringList> values;
258
4/4
✓ Branch 42 → 43 taken 11 times.
✓ Branch 43 → 44 taken 11 times.
✓ Branch 92 → 45 taken 24 times.
✓ Branch 92 → 93 taken 11 times.
35 for (const QString &text : texts) {
259 const ICal::Component parsed =
260
3/3
✓ Branch 55 → 56 taken 24 times.
✓ Branch 56 → 57 taken 24 times.
✓ Branch 57 → 58 taken 24 times.
72 ICal::parseFirst(QStringLiteral("BEGIN:X\r\n") + text + QStringLiteral("\r\nEND:X\r\n"), QStringLiteral("X"));
261
1/2
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 24 times.
24 if (parsed.properties.isEmpty())
262 ✗ continue;
263 24 ICal::Property head = parsed.properties.first();
264 24 const QString value = head.value;
265
1/1
✓ Branch 73 → 74 taken 24 times.
24 head.setValue(QString());
266
1/1
✓ Branch 75 → 76 taken 24 times.
24 const QString headLine = head.contentLine();
267
2/2
✓ Branch 77 → 78 taken 11 times.
✓ Branch 77 → 79 taken 13 times.
24 if (!heads.contains(headLine))
268
1/1
✓ Branch 78 → 79 taken 11 times.
11 heads.append(headLine);
269
2/2
✓ Branch 79 → 80 taken 24 times.
✓ Branch 80 → 81 taken 24 times.
24 values[headLine].append(value);
270 24 }
271 11 QStringList lines;
272
4/4
✓ Branch 93 → 94 taken 11 times.
✓ Branch 94 → 95 taken 11 times.
✓ Branch 108 → 96 taken 11 times.
✓ Branch 108 → 109 taken 11 times.
22 for (const QString &head : heads)
273
3/3
✓ Branch 100 → 101 taken 11 times.
✓ Branch 101 → 102 taken 11 times.
✓ Branch 102 → 103 taken 11 times.
11 lines.append(head + values.value(head).join(QLatin1Char(',')));
274
1/1
✓ Branch 109 → 110 taken 11 times.
11 lines.sort();
275 11 return lines;
276 126 }
277
278 81717 Merge::Value valueOf(const QStringList &lines) {
279 326868 return {!lines.isEmpty(), lines.join(QLatin1Char('\n'))};
280 245151 }
281
282 315 QString plainOf(const QStringList &lines) {
283
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 309 times.
315 if (lines.isEmpty())
284 6 return {};
285
1/1
✓ Branch 15 → 16 taken 309 times.
927 const ICal::Component parsed = ICal::parseFirst(QStringLiteral("BEGIN:X\r\n") + lines.first() +
286
1/1
✓ Branch 16 → 17 taken 309 times.
927 QStringLiteral("\r\nEND:X\r\n"),
287
1/1
✓ Branch 17 → 18 taken 309 times.
927 QStringLiteral("X"));
288
2/3
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 309 times.
✓ Branch 30 → 31 taken 309 times.
309 return parsed.properties.isEmpty() ? QString() : ICal::unescapeText(parsed.properties.first().value);
289 309 }
290
291 22 Merge::Plan rawPlan(const QString &base, const QString &local, const QString &server, const QString &error) {
292 22 Merge::Plan plan;
293 22 plan.domain = QStringLiteral("ical");
294 22 plan.comparable = false;
295 22 plan.error = error;
296 22 plan.baseText = base;
297 22 plan.localText = local;
298 22 plan.serverText = server;
299 22 Merge::Item raw;
300 22 raw.id = kRawItem;
301 22 raw.name = QStringLiteral("resource");
302 22 raw.kind = Merge::Kind::Text;
303 22 raw.base = {true, base};
304 22 raw.local = {true, local};
305 22 raw.server = {true, server};
306
1/1
✓ Branch 28 → 29 taken 22 times.
22 raw.state = Merge::stateOf(raw.base, raw.local, raw.server);
307
1/2
✓ Branch 29 → 30 taken 22 times.
✗ Branch 29 → 31 not taken.
22 if (raw.state != Merge::State::Same)
308 22 raw.state = Merge::State::Conflict; // a person decides an unreadable resource
309 22 raw.basePlain = base;
310 22 raw.localPlain = local;
311 22 raw.serverPlain = server;
312
1/1
✓ Branch 34 → 35 taken 22 times.
22 raw.hunks = TextMerge::diff3(base, local, server);
313
1/1
✓ Branch 37 → 38 taken 22 times.
22 plan.items.append(raw);
314
1/1
✓ Branch 38 → 39 taken 22 times.
22 Merge::applyDefaults(&plan);
315 22 return plan;
316 88 }
317
318 // Rebuilds a component from merged groups, keeping the server's property
319 // order where possible (server first, then local-only names).
320 5346 ICal::Component rebuild(const ICal::Component &theirs, const ICal::Component &ours, const Groups &merged,
321 int sequence) {
322 5346 ICal::Component event;
323 5346 event.name = theirs.name;
324 5346 QStringList order;
325
2/2
✓ Branch 11 → 5 taken 32417 times.
✓ Branch 11 → 12 taken 5346 times.
37763 for (const ICal::Property &property : theirs.properties)
326
3/3
✓ Branch 7 → 8 taken 32387 times.
✓ Branch 7 → 9 taken 30 times.
✓ Branch 8 → 9 taken 32387 times.
32417 if (!order.contains(property.name)) order.append(property.name);
327
2/2
✓ Branch 20 → 14 taken 32405 times.
✓ Branch 20 → 21 taken 5346 times.
37751 for (const ICal::Property &property : ours.properties)
328
3/3
✓ Branch 16 → 17 taken 45 times.
✓ Branch 16 → 18 taken 32360 times.
✓ Branch 17 → 18 taken 45 times.
32405 if (!order.contains(property.name)) order.append(property.name);
329
4/4
✓ Branch 21 → 22 taken 5346 times.
✓ Branch 22 → 23 taken 5346 times.
✓ Branch 88 → 24 taken 32432 times.
✓ Branch 88 → 89 taken 5346 times.
37778 for (const QString &name : order) {
330
2/2
✓ Branch 26 → 27 taken 5684 times.
✓ Branch 26 → 47 taken 26748 times.
32432 if (kBookkeeping.contains(name)) {
331
3/5
✓ Branch 27 → 28 taken 5684 times.
✓ Branch 28 → 29 taken 5684 times.
✗ Branch 28 → 31 not taken.
✓ Branch 29 → 30 taken 5684 times.
✗ Branch 31 → 32 not taken.
5684 const ICal::Property *source = theirs.property(name) ? theirs.property(name) : ours.property(name);
332
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 5684 times.
5684 if (!source)
333 ✗ continue;
334 5684 ICal::Property property = *source;
335
2/2
✓ Branch 38 → 39 taken 279 times.
✓ Branch 38 → 43 taken 5405 times.
5684 if (name == QLatin1String("SEQUENCE"))
336
2/2
✓ Branch 39 → 40 taken 279 times.
✓ Branch 40 → 41 taken 279 times.
279 property.setValue(QString::number(sequence));
337
1/1
✓ Branch 43 → 44 taken 5684 times.
5684 event.appendProperty(property);
338 5684 continue;
339 5684 }
340
5/5
✓ Branch 48 → 49 taken 26748 times.
✓ Branch 50 → 51 taken 26748 times.
✓ Branch 51 → 52 taken 26748 times.
✓ Branch 83 → 53 taken 26751 times.
✓ Branch 83 → 84 taken 26748 times.
53499 for (const QString &line : merged.value(name)) {
341 26751 QList<ICal::Component> parsed;
342
3/3
✓ Branch 60 → 61 taken 26751 times.
✓ Branch 61 → 62 taken 26751 times.
✓ Branch 62 → 63 taken 26751 times.
53502 ICal::parse(QStringLiteral("BEGIN:X\r\n") + line + QStringLiteral("\r\nEND:X\r\n"), &parsed);
343
4/7
✓ Branch 70 → 71 taken 26751 times.
✗ Branch 70 → 75 not taken.
✓ Branch 71 → 72 taken 26751 times.
✓ Branch 73 → 74 taken 26751 times.
✗ Branch 73 → 75 not taken.
✓ Branch 76 → 77 taken 26751 times.
✗ Branch 76 → 80 not taken.
26751 if (!parsed.isEmpty() && !parsed.first().properties.isEmpty())
344
3/3
✓ Branch 77 → 78 taken 26751 times.
✓ Branch 78 → 79 taken 26751 times.
✓ Branch 79 → 80 taken 26751 times.
26751 event.appendProperty(parsed.first().properties.first());
345 53499 }
346 }
347
4/4
✓ Branch 89 → 90 taken 5346 times.
✓ Branch 115 → 116 taken 32113 times.
✓ Branch 117 → 91 taken 26767 times.
✓ Branch 117 → 118 taken 5346 times.
32113 for (auto it = merged.constBegin(); it != merged.constEnd(); ++it) {
348
3/3
✓ Branch 93 → 94 taken 26767 times.
✓ Branch 94 → 95 taken 26748 times.
✓ Branch 94 → 96 taken 19 times.
26767 if (!it.key().startsWith(QLatin1String("BEGIN:")))
349 26748 continue;
350
2/2
✓ Branch 112 → 99 taken 22 times.
✓ Branch 112 → 113 taken 19 times.
41 for (const QString &text : it.value()) {
351 22 QList<ICal::Component> parsed;
352
4/7
✓ Branch 100 → 101 taken 22 times.
✓ Branch 101 → 102 taken 22 times.
✗ Branch 101 → 105 not taken.
✓ Branch 103 → 104 taken 22 times.
✗ Branch 103 → 105 not taken.
✓ Branch 106 → 107 taken 22 times.
✗ Branch 106 → 109 not taken.
22 if (ICal::parse(text, &parsed) && !parsed.isEmpty())
353
2/2
✓ Branch 107 → 108 taken 22 times.
✓ Branch 108 → 109 taken 22 times.
22 event.components.append(parsed.first());
354 22 }
355 }
356 5346 return event;
357 5346 }
358
359 // The lines of a text item the user edited: the property of the local side
360 // (else the server's) with the new value; empty text removes it.
361 13 QStringList editedLines(const QString &text, const QStringList &local, const QStringList &server) {
362
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 13 times.
13 if (text.isEmpty())
363 ✗ return {};
364
1/4
✓ Branch 6 → 7 taken 13 times.
✗ Branch 6 → 9 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 13 not taken.
13 const QString source = !local.isEmpty() ? local.first() : !server.isEmpty() ? server.first() : QString();
365
2/2
✓ Branch 23 → 24 taken 13 times.
✓ Branch 24 → 25 taken 13 times.
39 ICal::Component parsed = ICal::parseFirst(QStringLiteral("BEGIN:X\r\n") + source + QStringLiteral("\r\nEND:X\r\n"),
366
1/1
✓ Branch 25 → 26 taken 13 times.
39 QStringLiteral("X"));
367
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 13 times.
13 if (parsed.properties.isEmpty())
368 ✗ return {};
369
1/1
✓ Branch 37 → 38 taken 13 times.
13 ICal::Property property = parsed.properties.first();
370
2/2
✓ Branch 39 → 40 taken 13 times.
✓ Branch 40 → 41 taken 13 times.
13 property.setValue(ICal::escapeText(text));
371
2/4
✓ Branch 45 → 46 taken 13 times.
✓ Branch 45 → 47 taken 13 times.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
26 return {property.contentLine()};
372 26 }
373
374 // Sprint 107 J3: an occurrence added or moved on one side while the other
375 // side cancelled it (a new EXDATE with its RECURRENCE-ID) would combine
376 // into an exception the EXDATE hides — the move was lost without a word.
377 // Both items become one question (group "occurrence:<id>"): this side's
378 // component and the other side's EXDATE value are chosen together.
379 465 void coupleMovedAndCancelled(Merge::Plan *plan) {
380 465 QList<Merge::Item *> added; // EXDATE values one side added
381
4/4
✓ Branch 2 → 3 taken 465 times.
✓ Branch 3 → 4 taken 465 times.
✓ Branch 17 → 5 taken 27523 times.
✓ Branch 17 → 18 taken 465 times.
27988 for (Merge::Item &exdate : plan->items)
382
7/8
✓ Branch 6 → 7 taken 256 times.
✓ Branch 6 → 12 taken 27267 times.
✓ Branch 9 → 10 taken 30 times.
✓ Branch 9 → 12 taken 226 times.
✓ Branch 10 → 11 taken 30 times.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 30 times.
✓ Branch 13 → 15 taken 27493 times.
27523 if (exdate.kind == Merge::Kind::SetValue && exdate.name == QLatin1String("EXDATE") && !exdate.base.present)
383
1/1
✓ Branch 14 → 15 taken 30 times.
30 added.append(&exdate);
384
2/2
✓ Branch 19 → 20 taken 444 times.
✓ Branch 19 → 21 taken 21 times.
465 if (added.isEmpty())
385 444 return;
386
4/4
✓ Branch 21 → 22 taken 21 times.
✓ Branch 22 → 23 taken 21 times.
✓ Branch 79 → 24 taken 144 times.
✓ Branch 79 → 80 taken 21 times.
165 for (Merge::Item &occurrence : plan->items) {
387
6/8
✓ Branch 25 → 26 taken 11 times.
✓ Branch 25 → 31 taken 133 times.
✓ Branch 27 → 28 taken 11 times.
✗ Branch 27 → 31 not taken.
✓ Branch 28 → 29 taken 11 times.
✗ Branch 28 → 31 not taken.
✓ Branch 33 → 34 taken 133 times.
✓ Branch 33 → 35 taken 11 times.
155 if (occurrence.kind != Merge::Kind::Component || occurrence.context.isEmpty() || occurrence.base.present ||
388
1/4
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 32 taken 11 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
11 (occurrence.state != Merge::State::LocalOnly && occurrence.state != Merge::State::ServerOnly))
389 133 continue;
390 11 const Merge::State cancelledBy =
391
1/2
✓ Branch 35 → 36 taken 11 times.
✗ Branch 35 → 37 not taken.
11 occurrence.state == Merge::State::LocalOnly ? Merge::State::ServerOnly : Merge::State::LocalOnly;
392
4/4
✓ Branch 38 → 39 taken 11 times.
✓ Branch 39 → 40 taken 11 times.
✓ Branch 75 → 41 taken 11 times.
✓ Branch 75 → 76 taken 11 times.
22 for (Merge::Item *pointer : added) {
393 11 Merge::Item &exdate = *pointer;
394
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 11 times.
11 if (exdate.state != cancelledBy)
395 ✗ continue;
396
1/2
✓ Branch 44 → 45 taken 11 times.
✗ Branch 44 → 46 not taken.
11 const QString line = cancelledBy == Merge::State::ServerOnly ? exdate.server.text : exdate.local.text;
397
3/4
✓ Branch 51 → 52 taken 11 times.
✓ Branch 52 → 53 taken 11 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 11 times.
11 if (line.section(QLatin1Char(':'), -1).trimmed() != occurrence.context)
398 ✗ continue;
399 11 occurrence.state = Merge::State::Conflict;
400 11 exdate.state = Merge::State::Conflict;
401
1/1
✓ Branch 61 → 62 taken 11 times.
22 occurrence.group = exdate.group = QStringLiteral("occurrence:") + occurrence.context;
402 11 }
403 }
404 465 }
405
406 } // namespace
407
408 487 Merge::Plan analyze(const QString &baseText, const QString &localText, const QString &serverText) {
409
1/1
✓ Branch 2 → 3 taken 487 times.
487 const Inputs inputs = parseInputs(baseText, localText, serverText);
410
2/2
✓ Branch 4 → 5 taken 22 times.
✓ Branch 4 → 7 taken 465 times.
487 if (!inputs.error.isEmpty())
411
1/1
✓ Branch 5 → 6 taken 22 times.
22 return rawPlan(baseText, localText, serverText, inputs.error);
412 465 Merge::Plan plan;
413 465 plan.domain = QStringLiteral("ical");
414 465 plan.baseText = baseText;
415 465 plan.localText = localText;
416 465 plan.serverText = serverText;
417
5/5
✓ Branch 16 → 17 taken 465 times.
✓ Branch 17 → 18 taken 465 times.
✓ Branch 18 → 19 taken 465 times.
✓ Branch 19 → 20 taken 465 times.
✓ Branch 20 → 21 taken 465 times.
465 QStringList keys = inputs.base.events.keys() + inputs.local.events.keys() + inputs.server.events.keys();
418
1/1
✓ Branch 25 → 26 taken 465 times.
465 keys.removeDuplicates();
419
3/3
✓ Branch 26 → 27 taken 465 times.
✓ Branch 27 → 28 taken 465 times.
✓ Branch 28 → 29 taken 465 times.
465 std::sort(keys.begin(), keys.end());
420
4/4
✓ Branch 29 → 30 taken 465 times.
✓ Branch 30 → 31 taken 465 times.
✓ Branch 266 → 32 taken 5504 times.
✓ Branch 266 → 267 taken 465 times.
5969 for (const QString &key : keys) {
421
1/1
✓ Branch 33 → 34 taken 5504 times.
5504 const bool inBase = inputs.base.events.contains(key);
422
1/1
✓ Branch 34 → 35 taken 5504 times.
5504 const bool inLocal = inputs.local.events.contains(key);
423
1/1
✓ Branch 35 → 36 taken 5504 times.
5504 const bool inServer = inputs.server.events.contains(key);
424
3/4
✓ Branch 36 → 37 taken 9 times.
✓ Branch 36 → 39 taken 5495 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 9 times.
5504 if (!inLocal && !inServer)
425 28 continue; // removed on both sides
426
4/4
✓ Branch 39 → 40 taken 5495 times.
✓ Branch 39 → 41 taken 9 times.
✓ Branch 40 → 42 taken 5495 times.
✓ Branch 41 → 42 taken 9 times.
5504 const ICal::Component &any = inLocal ? inputs.local.events[key] : inputs.server.events[key];
427
1/1
✓ Branch 45 → 46 taken 5504 times.
5504 const QString recurrence = any.value(QStringLiteral("RECURRENCE-ID"));
428
6/8
✓ Branch 48 → 49 taken 5493 times.
✓ Branch 48 → 51 taken 11 times.
✓ Branch 49 → 50 taken 5493 times.
✓ Branch 50 → 52 taken 5493 times.
✓ Branch 52 → 53 taken 5493 times.
✓ Branch 52 → 54 taken 11 times.
✗ Branch 293 → 294 not taken.
✗ Branch 293 → 295 not taken.
5504 const Groups baseGroups = inBase ? groupsOf(inputs.base.events[key]) : Groups{};
429
6/8
✓ Branch 54 → 55 taken 5495 times.
✓ Branch 54 → 57 taken 9 times.
✓ Branch 55 → 56 taken 5495 times.
✓ Branch 56 → 58 taken 5495 times.
✓ Branch 58 → 59 taken 5495 times.
✓ Branch 58 → 60 taken 9 times.
✗ Branch 297 → 298 not taken.
✗ Branch 297 → 299 not taken.
5504 const Groups localGroups = inLocal ? groupsOf(inputs.local.events[key]) : Groups{};
430
6/8
✓ Branch 60 → 61 taken 5485 times.
✓ Branch 60 → 63 taken 19 times.
✓ Branch 61 → 62 taken 5485 times.
✓ Branch 62 → 64 taken 5485 times.
✓ Branch 64 → 65 taken 5485 times.
✓ Branch 64 → 66 taken 19 times.
✗ Branch 301 → 302 not taken.
✗ Branch 301 → 303 not taken.
5504 const Groups serverGroups = inServer ? groupsOf(inputs.server.events[key]) : Groups{};
431
4/4
✓ Branch 66 → 67 taken 5495 times.
✓ Branch 66 → 68 taken 9 times.
✓ Branch 67 → 68 taken 19 times.
✓ Branch 67 → 121 taken 5476 times.
5504 if (!inLocal || !inServer) {
432 28 Merge::Item component;
433 28 component.id = key;
434 28 component.name = any.name;
435 28 component.context = recurrence;
436 28 component.kind = Merge::Kind::Component;
437
2/2
✓ Branch 71 → 72 taken 17 times.
✓ Branch 71 → 78 taken 11 times.
28 if (inBase)
438 17 component.base = {true, ICal::serialize(inputs.base.events[key])};
439
2/2
✓ Branch 78 → 79 taken 19 times.
✓ Branch 78 → 85 taken 9 times.
28 if (inLocal)
440 19 component.local = {true, ICal::serialize(inputs.local.events[key])};
441
2/2
✓ Branch 85 → 86 taken 9 times.
✓ Branch 85 → 92 taken 19 times.
28 if (inServer)
442 9 component.server = {true, ICal::serialize(inputs.server.events[key])};
443
4/4
✓ Branch 92 → 93 taken 17 times.
✓ Branch 92 → 95 taken 11 times.
✓ Branch 93 → 94 taken 9 times.
✓ Branch 93 → 95 taken 8 times.
28 const bool removedLocally = inBase && !inLocal;
444
4/4
✓ Branch 96 → 97 taken 17 times.
✓ Branch 96 → 99 taken 11 times.
✓ Branch 97 → 98 taken 8 times.
✓ Branch 97 → 99 taken 9 times.
28 const bool removedOnServer = inBase && !inServer;
445
10/12
✓ Branch 100 → 101 taken 9 times.
✓ Branch 100 → 103 taken 19 times.
✓ Branch 101 → 102 taken 9 times.
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 106 taken 9 times.
✓ Branch 103 → 104 taken 8 times.
✓ Branch 103 → 107 taken 11 times.
✓ Branch 104 → 105 taken 8 times.
✗ Branch 105 → 106 not taken.
✓ Branch 105 → 107 taken 8 times.
✓ Branch 108 → 109 taken 9 times.
✓ Branch 108 → 110 taken 19 times.
28 if ((removedLocally && serverGroups != baseGroups) || (removedOnServer && localGroups != baseGroups)) {
446 9 component.kind = Merge::Kind::Deletion;
447 9 component.state = Merge::State::Conflict;
448 } else {
449
1/2
✓ Branch 110 → 111 taken 19 times.
✗ Branch 110 → 112 not taken.
19 component.state = inLocal ? Merge::State::LocalOnly : Merge::State::ServerOnly;
450
1/2
✗ Branch 113 → 114 not taken.
✓ Branch 113 → 115 taken 19 times.
19 if (removedLocally)
451 ✗ component.state = Merge::State::LocalOnly;
452
2/2
✓ Branch 115 → 116 taken 8 times.
✓ Branch 115 → 117 taken 11 times.
19 else if (removedOnServer)
453 8 component.state = Merge::State::ServerOnly;
454 }
455
1/1
✓ Branch 117 → 118 taken 28 times.
28 plan.items.append(component);
456 28 continue;
457 28 }
458
5/5
✓ Branch 121 → 122 taken 5476 times.
✓ Branch 122 → 123 taken 5476 times.
✓ Branch 123 → 124 taken 5476 times.
✓ Branch 124 → 125 taken 5476 times.
✓ Branch 125 → 126 taken 5476 times.
5476 QStringList names = baseGroups.keys() + localGroups.keys() + serverGroups.keys();
459
1/1
✓ Branch 130 → 131 taken 5476 times.
5476 names.removeDuplicates();
460
4/4
✓ Branch 131 → 132 taken 5476 times.
✓ Branch 132 → 133 taken 5476 times.
✓ Branch 240 → 134 taken 27386 times.
✓ Branch 240 → 241 taken 5476 times.
32862 for (const QString &name : names) {
461
2/2
✓ Branch 136 → 137 taken 147 times.
✓ Branch 136 → 155 taken 27239 times.
27386 if (isSetName(name)) {
462
1/1
✓ Branch 138 → 139 taken 147 times.
147 const ICal::Component baseComponent = inputs.base.events.value(key);
463
1/1
✓ Branch 141 → 142 taken 147 times.
147 const ICal::Component localComponent = inputs.local.events.value(key);
464
1/1
✓ Branch 144 → 145 taken 147 times.
147 const ICal::Component serverComponent = inputs.server.events.value(key);
465
2/3
✓ Branch 146 → 147 taken 147 times.
✗ Branch 146 → 148 not taken.
✓ Branch 149 → 150 taken 147 times.
147 addSetItems(&plan, key, name, recurrence, inBase ? &baseComponent : nullptr, &localComponent,
466 &serverComponent);
467 147 continue;
468 147 }
469
3/3
✓ Branch 156 → 157 taken 27239 times.
✓ Branch 159 → 160 taken 27239 times.
✓ Branch 162 → 163 taken 27239 times.
27239 const QStringList b = baseGroups.value(name), l = localGroups.value(name), s = serverGroups.value(name);
470 27239 Merge::Item item;
471
2/2
✓ Branch 167 → 168 taken 27239 times.
✓ Branch 168 → 169 taken 27239 times.
27239 item.id = key + QStringLiteral("::") + name;
472 27239 item.name = name;
473 27239 item.parent = key;
474 27239 item.context = recurrence;
475
1/1
✓ Branch 178 → 179 taken 27239 times.
27239 item.group = groupOfName(name, recurrence.isEmpty());
476
1/1
✓ Branch 181 → 182 taken 27239 times.
27239 item.base = valueOf(b);
477
1/1
✓ Branch 184 → 185 taken 27239 times.
27239 item.local = valueOf(l);
478
1/1
✓ Branch 187 → 188 taken 27239 times.
27239 item.server = valueOf(s);
479 // The pre-Sprint-107 decision: local == base → server; server ==
480 // base or both alike → local; otherwise a conflict.
481
8/8
✓ Branch 190 → 191 taken 27239 times.
✓ Branch 191 → 192 taken 26728 times.
✓ Branch 191 → 195 taken 511 times.
✓ Branch 192 → 193 taken 26728 times.
✓ Branch 193 → 194 taken 26557 times.
✓ Branch 193 → 195 taken 171 times.
✓ Branch 196 → 197 taken 26557 times.
✓ Branch 196 → 198 taken 682 times.
27239 if (l == b && s == b)
482 26557 item.state = Merge::State::Same;
483
3/3
✓ Branch 198 → 199 taken 682 times.
✓ Branch 199 → 200 taken 171 times.
✓ Branch 199 → 201 taken 511 times.
682 else if (l == b)
484 171 item.state = Merge::State::ServerOnly;
485
3/3
✓ Branch 201 → 202 taken 511 times.
✓ Branch 202 → 203 taken 179 times.
✓ Branch 202 → 204 taken 332 times.
511 else if (s == b)
486 179 item.state = Merge::State::LocalOnly;
487
3/3
✓ Branch 204 → 205 taken 332 times.
✓ Branch 205 → 206 taken 17 times.
✓ Branch 205 → 207 taken 315 times.
332 else if (l == s)
488 17 item.state = Merge::State::BothSame;
489 else
490 315 item.state = Merge::State::Conflict;
491
7/10
✓ Branch 209 → 210 taken 105 times.
✓ Branch 209 → 217 taken 27134 times.
✓ Branch 211 → 212 taken 105 times.
✗ Branch 211 → 217 not taken.
✓ Branch 213 → 214 taken 105 times.
✗ Branch 213 → 217 not taken.
✓ Branch 215 → 216 taken 105 times.
✗ Branch 215 → 217 not taken.
✓ Branch 218 → 219 taken 105 times.
✓ Branch 218 → 232 taken 27134 times.
27239 if (isTextName(name) && b.size() <= 1 && l.size() <= 1 && s.size() <= 1) {
492 105 item.kind = Merge::Kind::Text;
493
1/1
✓ Branch 219 → 220 taken 105 times.
105 item.basePlain = plainOf(b);
494
1/1
✓ Branch 222 → 223 taken 105 times.
105 item.localPlain = plainOf(l);
495
1/1
✓ Branch 225 → 226 taken 105 times.
105 item.serverPlain = plainOf(s);
496
1/1
✓ Branch 228 → 229 taken 105 times.
105 item.hunks = TextMerge::diff3(item.basePlain, item.localPlain, item.serverPlain);
497 }
498
1/1
✓ Branch 232 → 233 taken 27239 times.
27239 plan.items.append(item);
499 27239 }
500 5616 }
501
1/1
✓ Branch 267 → 268 taken 465 times.
465 coupleMovedAndCancelled(&plan);
502
1/1
✓ Branch 268 → 269 taken 465 times.
465 Merge::applyDefaults(&plan);
503 465 return plan;
504 532 }
505
506 356 Applied apply(const Merge::Plan &plan) {
507 356 Applied applied;
508
2/2
✓ Branch 16 → 4 taken 26929 times.
✓ Branch 16 → 17 taken 342 times.
27271 for (const Merge::Item &item : plan.items)
509
3/3
✓ Branch 5 → 6 taken 26929 times.
✓ Branch 6 → 7 taken 14 times.
✓ Branch 6 → 14 taken 26915 times.
26929 if (Merge::itemOpen(plan, item)) {
510 14 applied.error = QStringLiteral("Open conflicts remain");
511 14 return applied;
512 }
513
2/2
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 26 taken 336 times.
342 if (!plan.comparable) {
514
1/1
✓ Branch 18 → 19 taken 6 times.
6 const Merge::Item *raw = plan.item(kRawItem);
515
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 6 times.
6 if (!raw) {
516 ✗ applied.error = plan.error;
517 ✗ return applied;
518 }
519 6 applied.ok = true;
520
1/1
✓ Branch 22 → 23 taken 6 times.
6 applied.text = Merge::resultText(*raw);
521 6 return applied;
522 }
523
1/1
✓ Branch 26 → 27 taken 336 times.
336 const Inputs inputs = parseInputs(plan.baseText, plan.localText, plan.serverText);
524
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 336 times.
336 if (!inputs.error.isEmpty()) {
525 ✗ applied.error = inputs.error;
526 ✗ return applied;
527 }
528 336 ICal::Component merged = inputs.server.root;
529
1/1
✓ Branch 35 → 36 taken 336 times.
336 merged.removeComponents(QStringLiteral("VEVENT"));
530
1/1
✓ Branch 41 → 42 taken 336 times.
336 merged.removeComponents(QStringLiteral("VTODO"));
531
5/5
✓ Branch 44 → 45 taken 336 times.
✓ Branch 45 → 46 taken 336 times.
✓ Branch 46 → 47 taken 336 times.
✓ Branch 47 → 48 taken 336 times.
✓ Branch 48 → 49 taken 336 times.
336 QStringList keys = inputs.base.events.keys() + inputs.local.events.keys() + inputs.server.events.keys();
532
1/1
✓ Branch 53 → 54 taken 336 times.
336 keys.removeDuplicates();
533
3/3
✓ Branch 54 → 55 taken 336 times.
✓ Branch 55 → 56 taken 336 times.
✓ Branch 56 → 57 taken 336 times.
336 std::sort(keys.begin(), keys.end());
534 // The items of each component and the component items, indexed once (G11:
535 // a series with 500 overrides has thousands of items).
536 336 QHash<QString, QList<const Merge::Item *>> children;
537 336 QHash<QString, const Merge::Item *> byId;
538
2/2
✓ Branch 65 → 59 taken 26887 times.
✓ Branch 65 → 66 taken 336 times.
27223 for (const Merge::Item &item : plan.items) {
539
2/2
✓ Branch 60 → 61 taken 26887 times.
✓ Branch 61 → 62 taken 26887 times.
26887 children[item.parent].append(&item);
540
1/1
✓ Branch 62 → 63 taken 26887 times.
26887 byId.insert(item.id, &item);
541 }
542
4/4
✓ Branch 66 → 67 taken 336 times.
✓ Branch 67 → 68 taken 336 times.
✓ Branch 185 → 69 taken 5373 times.
✓ Branch 185 → 186 taken 336 times.
5709 for (const QString &key : keys) {
543
2/2
✓ Branch 71 → 72 taken 27 times.
✓ Branch 71 → 87 taken 5346 times.
5373 if (const Merge::Item *component = byId.value(key)) {
544
2/2
✓ Branch 72 → 73 taken 19 times.
✓ Branch 72 → 74 taken 8 times.
35 const QMap<QString, ICal::Component> *side = component->choice == Merge::Choice::Server ? &inputs.server.events
545
1/2
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 8 times.
8 : component->choice == Merge::Choice::Base ? &inputs.base.events
546 : &inputs.local.events;
547
3/3
✓ Branch 78 → 79 taken 27 times.
✓ Branch 79 → 80 taken 12 times.
✓ Branch 79 → 86 taken 15 times.
27 if (side->contains(key))
548
2/2
✓ Branch 81 → 82 taken 12 times.
✓ Branch 82 → 83 taken 12 times.
12 merged.components.append(side->value(key));
549 27 continue;
550 27 }
551
5/8
✓ Branch 87 → 88 taken 5346 times.
✓ Branch 88 → 89 taken 5346 times.
✗ Branch 88 → 91 not taken.
✓ Branch 89 → 90 taken 5346 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 5346 times.
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 95 taken 5346 times.
5346 if (!inputs.local.events.contains(key) || !inputs.server.events.contains(key))
552 ✗ continue; // removed on both sides
553
1/1
✓ Branch 95 → 96 taken 5346 times.
5346 const ICal::Component &ours = inputs.local.events[key];
554
1/1
✓ Branch 96 → 97 taken 5346 times.
5346 const ICal::Component &theirs = inputs.server.events[key];
555
5/9
✓ Branch 97 → 98 taken 5346 times.
✓ Branch 98 → 99 taken 5346 times.
✗ Branch 98 → 101 not taken.
✓ Branch 99 → 100 taken 5346 times.
✓ Branch 100 → 102 taken 5346 times.
✓ Branch 102 → 103 taken 5346 times.
✗ Branch 102 → 104 not taken.
✗ Branch 289 → 290 not taken.
✗ Branch 289 → 291 not taken.
5346 const Groups baseGroups = inputs.base.events.contains(key) ? groupsOf(inputs.base.events[key]) : Groups{};
556
1/1
✓ Branch 104 → 105 taken 5346 times.
5346 const Groups localGroups = groupsOf(ours);
557
1/1
✓ Branch 105 → 106 taken 5346 times.
5346 const Groups serverGroups = groupsOf(theirs);
558 5346 Groups mergedGroups;
559 5346 const QList<const Merge::Item *> siblings = children.value(key);
560
2/2
✓ Branch 153 → 109 taken 26860 times.
✓ Branch 153 → 154 taken 5346 times.
32206 for (const Merge::Item *entry : siblings) {
561 26860 const Merge::Item &item = *entry;
562
2/2
✓ Branch 110 → 111 taken 219 times.
✓ Branch 110 → 118 taken 26641 times.
26860 if (item.kind == Merge::Kind::SetValue) {
563
3/3
✓ Branch 111 → 112 taken 219 times.
✓ Branch 112 → 113 taken 126 times.
✓ Branch 112 → 117 taken 93 times.
219 if (!mergedGroups.contains(item.name))
564
2/2
✓ Branch 113 → 114 taken 126 times.
✓ Branch 114 → 115 taken 126 times.
126 mergedGroups.insert(item.name, setLines(siblings, item.name, localGroups, serverGroups, &ours, &theirs));
565 219 continue;
566 }
567
4/4
✓ Branch 118 → 119 taken 799 times.
✓ Branch 118 → 125 taken 1 time.
✓ Branch 118 → 131 taken 13 times.
✓ Branch 118 → 145 taken 25828 times.
26641 switch (item.choice) {
568 799 case Merge::Choice::Server:
569
2/2
✓ Branch 120 → 121 taken 799 times.
✓ Branch 121 → 122 taken 799 times.
799 mergedGroups.insert(item.name, serverGroups.value(item.name));
570 799 break;
571 1 case Merge::Choice::Base:
572
2/2
✓ Branch 126 → 127 taken 1 time.
✓ Branch 127 → 128 taken 1 time.
1 mergedGroups.insert(item.name, baseGroups.value(item.name));
573 1 break;
574 13 case Merge::Choice::Edited:
575
4/4
✓ Branch 134 → 135 taken 13 times.
✓ Branch 135 → 136 taken 13 times.
✓ Branch 136 → 137 taken 13 times.
✓ Branch 137 → 138 taken 13 times.
13 mergedGroups.insert(item.name, editedLines(Merge::resultText(item), localGroups.value(item.name),
576
1/1
✓ Branch 132 → 133 taken 13 times.
26 serverGroups.value(item.name)));
577 13 break;
578 25828 default:
579
2/2
✓ Branch 146 → 147 taken 25828 times.
✓ Branch 147 → 148 taken 25828 times.
25828 mergedGroups.insert(item.name, localGroups.value(item.name));
580 25828 break;
581 }
582 }
583
2/2
✓ Branch 162 → 163 taken 5346 times.
✓ Branch 163 → 164 taken 5346 times.
10692 const int sequence = qMax(ours.value(QStringLiteral("SEQUENCE")).toInt(),
584
2/2
✓ Branch 157 → 158 taken 5346 times.
✓ Branch 158 → 159 taken 5346 times.
16038 theirs.value(QStringLiteral("SEQUENCE")).toInt());
585
2/2
✓ Branch 171 → 172 taken 5346 times.
✓ Branch 172 → 173 taken 5346 times.
5346 merged.components.append(rebuild(theirs, ours, mergedGroups, sequence));
586 5346 }
587 // A zone the local edit introduced needs its definition next to theirs.
588 336 QSet<QString> zones;
589
5/5
✓ Branch 190 → 191 taken 336 times.
✓ Branch 193 → 194 taken 336 times.
✓ Branch 194 → 195 taken 336 times.
✓ Branch 207 → 196 taken 256 times.
✓ Branch 207 → 208 taken 336 times.
592 for (const ICal::Component &component : merged.componentsNamed(QStringLiteral("VTIMEZONE")))
590
2/2
✓ Branch 200 → 201 taken 256 times.
✓ Branch 201 → 202 taken 256 times.
592 zones.insert(component.value(QStringLiteral("TZID")));
591 336 int insertAt = 0;
592
6/6
✓ Branch 212 → 213 taken 592 times.
✓ Branch 212 → 218 taken 60 times.
✓ Branch 216 → 217 taken 316 times.
✓ Branch 216 → 218 taken 276 times.
✓ Branch 219 → 210 taken 316 times.
✓ Branch 219 → 220 taken 336 times.
652 while (insertAt < merged.components.size() && merged.components.at(insertAt).name != QLatin1String("VEVENT"))
593 316 ++insertAt;
594
5/5
✓ Branch 223 → 224 taken 336 times.
✓ Branch 226 → 227 taken 336 times.
✓ Branch 227 → 228 taken 336 times.
✓ Branch 242 → 229 taken 264 times.
✓ Branch 242 → 243 taken 336 times.
600 for (const ICal::Component &component : inputs.local.root.componentsNamed(QStringLiteral("VTIMEZONE")))
595
3/3
✓ Branch 233 → 234 taken 264 times.
✓ Branch 238 → 239 taken 8 times.
✓ Branch 238 → 240 taken 256 times.
264 if (!zones.contains(component.value(QStringLiteral("TZID"))))
596
1/1
✓ Branch 239 → 240 taken 8 times.
344 merged.components.insert(insertAt++, component);
597 336 applied.ok = true;
598
1/1
✓ Branch 244 → 245 taken 336 times.
336 applied.text = ICal::serialize(merged);
599 336 return applied;
600 336 }
601
602 153 QString conflictName(const Merge::Item &item) {
603
7/11
✓ Branch 3 → 4 taken 146 times.
✓ Branch 3 → 5 taken 7 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 11 taken 146 times.
✓ Branch 11 → 12 taken 7 times.
✓ Branch 11 → 13 taken 146 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
160 const QString label = item.context.isEmpty() ? QString() : QStringLiteral("@") + item.context;
604
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 23 taken 148 times.
153 if (item.parent.isEmpty())
605
1/1
✓ Branch 18 → 19 taken 5 times.
5 return QStringLiteral("VEVENT") + label;
606
3/5
✓ Branch 24 → 25 taken 148 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 148 times.
✗ Branch 26 → 28 not taken.
✓ Branch 28 → 29 taken 148 times.
148 return (item.name.startsWith(QLatin1String("BEGIN:")) ? item.name.mid(6) : item.name) + label;
607 153 }
608
609 } // namespace ICalMergePlan
610