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.4% 184 / 0 / 195
Functions: 100.0% 18 / 0 / 18
Branches: 85.5% 359 / 0 / 420

data/RecurrenceExpansion.cpp
Line Branch Exec Source
1 #include "RecurrenceExpansion.h"
2
3 #include "ICalTime.h"
4 #include "RecurrenceRule.h"
5 #include "util/SecurityLog.h"
6
7 #include <QLoggingCategory>
8 #include <QSet>
9 #include <QTimeZone>
10 #include <algorithm>
11
12
4/7
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 8 taken 9 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
14 Q_LOGGING_CATEGORY(lcCalendar, "mailjd.calendar")
13
14 namespace {
15
16 // All-day values are calendar dates stored at UTC midnight; timed values
17 // are shown on the local calendar.
18 224346 QDate displayDate(const QDateTime &value, bool allDay) {
19
10/14
✓ Branch 2 → 3 taken 2824 times.
✓ Branch 2 → 5 taken 221522 times.
✓ Branch 3 → 4 taken 2824 times.
✓ Branch 4 → 7 taken 2824 times.
✓ Branch 5 → 6 taken 221522 times.
✓ Branch 6 → 7 taken 221522 times.
✓ Branch 8 → 9 taken 221522 times.
✓ Branch 8 → 10 taken 2824 times.
✓ Branch 10 → 11 taken 2824 times.
✓ Branch 10 → 12 taken 221522 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
448692 return allDay ? value.toUTC().date() : value.toLocalTime().date();
20 }
21
22 // T-79.D3: an occurrence is excluded when its start matches one of the
23 // event's EXDATEs (date-level match for all-day events, exact instant
24 // otherwise). The keys are built once per series: comparing every
25 // occurrence with every EXDATE grew with their product (Sprint 102 fuzzing).
26 70056 qint64 exclusionKey(const QDateTime &start, bool allDay) {
27
7/9
✓ Branch 2 → 3 taken 564 times.
✓ Branch 2 → 6 taken 69492 times.
✓ Branch 3 → 4 taken 564 times.
✓ Branch 4 → 5 taken 564 times.
✓ Branch 6 → 7 taken 69492 times.
✓ Branch 9 → 10 taken 564 times.
✓ Branch 9 → 11 taken 69492 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
70056 return allDay ? start.toUTC().date().toJulianDay() : start.toMSecsSinceEpoch();
28 }
29
30 1377 QSet<qint64> exclusionKeys(const CalendarEvent &event) {
31 1377 QSet<qint64> keys;
32
2/2
✓ Branch 13 → 5 taken 60 times.
✓ Branch 13 → 14 taken 1377 times.
1437 for (const QDateTime &exdate : event.exdates)
33
2/3
✓ Branch 6 → 7 taken 60 times.
✓ Branch 7 → 8 taken 60 times.
✗ Branch 7 → 11 not taken.
60 if (exdate.isValid())
34
2/2
✓ Branch 8 → 9 taken 60 times.
✓ Branch 9 → 10 taken 60 times.
60 keys.insert(exclusionKey(exdate, event.allDay));
35 1377 return keys;
36 ✗ }
37
38 74785 QDate displayEndDate(const CalendarEvent &event) {
39
1/1
✓ Branch 2 → 3 taken 74785 times.
74785 const QDate startDate = displayDate(event.dtStart, event.allDay);
40
3/3
✓ Branch 3 → 4 taken 74785 times.
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 6 taken 74776 times.
74785 if (!event.dtEnd.isValid())
41 9 return startDate;
42
2/2
✓ Branch 6 → 7 taken 938 times.
✓ Branch 6 → 12 taken 73838 times.
74776 if (event.allDay) {
43 // iCal all-day DTEND is exclusive → last day is the day before. Clamp
44 // to the start date so legacy events stored with an INCLUSIVE end
45 // (pre-Sprint-65 EventEditDialog) still render instead of vanishing.
46
2/2
✓ Branch 7 → 8 taken 938 times.
✓ Branch 8 → 9 taken 938 times.
938 return qMax(startDate, displayDate(event.dtEnd, true).addDays(-1));
47 }
48 // F-11 (SPRINT-104): a zero-length event exactly at midnight
49 // (DTSTART == DTEND == 00:00) is shown on ITS day — the −1 ms that
50 // makes an ending exclusive moved it to the previous day and left an
51 // empty day range, so the event vanished.
52
3/3
✓ Branch 12 → 13 taken 73838 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 16 taken 73836 times.
73838 if (event.dtEnd == event.dtStart)
53
1/1
✓ Branch 14 → 15 taken 2 times.
2 return displayDate(event.dtEnd, false);
54
2/2
✓ Branch 16 → 17 taken 73836 times.
✓ Branch 17 → 18 taken 73836 times.
73836 return displayDate(event.dtEnd.addMSecs(-1), false);
55 }
56
57 74786 void appendDisplayOccurrence(QList<CalendarEvent> *expanded, const CalendarEvent &event,
58 const QDate &rangeStart, const QDate &rangeEnd, bool splitMultiDay) {
59
3/3
✓ Branch 2 → 3 taken 74786 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 74785 times.
74786 if (!event.isSchedulable())
60 74718 return;
61
1/1
✓ Branch 5 → 6 taken 74785 times.
74785 const QDate startDate = displayDate(event.dtStart, event.allDay);
62
1/1
✓ Branch 6 → 7 taken 74785 times.
74785 const QDate endDate = displayEndDate(event);
63 74785 QDate day = qMax(startDate, rangeStart);
64 74785 const QDate lastDay = qMin(endDate, rangeEnd);
65
2/2
✓ Branch 9 → 10 taken 74717 times.
✓ Branch 9 → 20 taken 68 times.
74785 if (!splitMultiDay) {
66
6/7
✓ Branch 10 → 11 taken 74717 times.
✓ Branch 11 → 12 taken 74717 times.
✗ Branch 11 → 16 not taken.
✓ Branch 14 → 15 taken 60713 times.
✓ Branch 14 → 16 taken 14004 times.
✓ Branch 17 → 18 taken 60713 times.
✓ Branch 17 → 19 taken 14004 times.
74717 if (day.isValid() && day <= lastDay)
67
1/1
✓ Branch 18 → 19 taken 60713 times.
60713 expanded->append(event);
68 74717 return;
69 }
70
6/7
✓ Branch 68 → 69 taken 133 times.
✓ Branch 69 → 70 taken 133 times.
✗ Branch 69 → 74 not taken.
✓ Branch 72 → 73 taken 65 times.
✓ Branch 72 → 74 taken 68 times.
✓ Branch 75 → 21 taken 65 times.
✓ Branch 75 → 76 taken 68 times.
133 while (day.isValid() && day <= lastDay) {
71 65 CalendarEvent displayEvent = event;
72
2/2
✓ Branch 23 → 24 taken 6 times.
✓ Branch 23 → 38 taken 59 times.
65 if (day != startDate) {
73
6/8
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 28 taken 2 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 27 → 31 taken 4 times.
✓ Branch 30 → 31 taken 2 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
20 displayEvent.dtStart = event.allDay ? QDateTime(day, QTime(0, 0), QTimeZone::utc())
74
6/8
✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 37 taken 2 times.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
14 : QDateTime(day, QTime(0, 0), QTimeZone::systemTimeZone());
75 }
76
6/7
✓ Branch 39 → 40 taken 6 times.
✓ Branch 39 → 43 taken 59 times.
✓ Branch 40 → 41 taken 6 times.
✓ Branch 41 → 42 taken 6 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 6 times.
✓ Branch 44 → 61 taken 59 times.
65 if (day != endDate && event.dtEnd.isValid()) {
77 6 displayEvent.dtEnd = event.allDay
78
8/10
✓ Branch 45 → 46 taken 4 times.
✓ Branch 45 → 50 taken 2 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 48 → 49 taken 4 times.
✓ Branch 49 → 54 taken 4 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
20 ? QDateTime(day.addDays(1), QTime(0, 0), QTimeZone::utc())
79
6/8
✓ Branch 50 → 51 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 4 times.
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 2 times.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
14 : QDateTime(day.addDays(1), QTime(0, 0), QTimeZone::systemTimeZone());
80 }
81
2/2
✓ Branch 62 → 63 taken 8 times.
✓ Branch 62 → 64 taken 57 times.
65 if (startDate != endDate)
82 8 displayEvent.allDay = true;
83
1/1
✓ Branch 64 → 65 taken 65 times.
65 expanded->append(displayEvent);
84
1/1
✓ Branch 65 → 66 taken 65 times.
65 day = day.addDays(1);
85 65 }
86 }
87
88 69949 CalendarEvent shiftedOccurrence(const CalendarEvent &event, const QDateTime &start) {
89 69949 CalendarEvent shifted = event;
90 69949 shifted.dtStart = start;
91
3/3
✓ Branch 4 → 5 taken 69949 times.
✓ Branch 5 → 6 taken 69943 times.
✓ Branch 5 → 14 taken 6 times.
69949 if (event.dtEnd.isValid()) {
92
5/5
✓ Branch 6 → 7 taken 560 times.
✓ Branch 6 → 9 taken 69383 times.
✓ Branch 7 → 8 taken 560 times.
✓ Branch 8 → 11 taken 560 times.
✓ Branch 10 → 11 taken 69383 times.
209269 shifted.dtEnd = event.allDay ? start.addDays(event.dtStart.daysTo(event.dtEnd))
93
1/1
✓ Branch 9 → 10 taken 69383 times.
139326 : start.addSecs(event.dtStart.secsTo(event.dtEnd));
94 }
95 69949 return shifted;
96 ✗ }
97
98 // The zone the event's wall clock lives in: its TZID zone, UTC, or invalid
99 // for floating (local) time.
100 1515 QTimeZone wallClockZone(const QDateTime &value) {
101
2/5
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1315 times.
✗ Branch 3 → 6 not taken.
✓ Branch 3 → 8 taken 200 times.
✗ Branch 3 → 9 not taken.
1515 switch (value.timeSpec()) {
102 ✗ case Qt::UTC: return QTimeZone::utc();
103 1315 case Qt::TimeZone: return value.timeZone();
104 ✗ case Qt::OffsetFromUTC: return QTimeZone(value.offsetFromUtc());
105 200 case Qt::LocalTime: return {};
106 }
107 ✗ return {};
108 }
109
110 70113 QDateTime toInstant(const QDateTime &naive, bool allDay, const QTimeZone &zone) {
111
2/2
✓ Branch 2 → 3 taken 569 times.
✓ Branch 2 → 9 taken 69544 times.
70113 if (allDay)
112
4/4
✓ Branch 3 → 4 taken 569 times.
✓ Branch 4 → 5 taken 569 times.
✓ Branch 5 → 6 taken 569 times.
✓ Branch 6 → 7 taken 569 times.
569 return QDateTime(naive.date(), QTime(0, 0), QTimeZone::utc());
113
3/3
✓ Branch 9 → 10 taken 69544 times.
✓ Branch 10 → 11 taken 69544 times.
✓ Branch 11 → 12 taken 69544 times.
69544 return ICalTime::fromWallClock(naive.date(), naive.time(), zone);
114 }
115
116 1614 QDateTime toNaive(const QDateTime &instant, bool allDay, const QTimeZone &zone) {
117
2/2
✓ Branch 2 → 3 taken 65 times.
✓ Branch 2 → 12 taken 1549 times.
1614 if (allDay)
118
5/5
✓ Branch 3 → 4 taken 65 times.
✓ Branch 4 → 5 taken 65 times.
✓ Branch 5 → 6 taken 65 times.
✓ Branch 6 → 7 taken 65 times.
✓ Branch 7 → 8 taken 65 times.
65 return QDateTime(instant.toUTC().date(), QTime(0, 0), QTimeZone::utc());
119
5/5
✓ Branch 12 → 13 taken 1549 times.
✓ Branch 13 → 14 taken 1335 times.
✓ Branch 13 → 15 taken 214 times.
✓ Branch 14 → 16 taken 1335 times.
✓ Branch 15 → 16 taken 214 times.
1549 const QDateTime local = zone.isValid() ? instant.toTimeZone(zone) : instant.toLocalTime();
120
4/4
✓ Branch 16 → 17 taken 1549 times.
✓ Branch 17 → 18 taken 1549 times.
✓ Branch 18 → 19 taken 1549 times.
✓ Branch 19 → 20 taken 1549 times.
1549 return QDateTime(local.date(), local.time(), QTimeZone::utc());
121 1549 }
122
123 // UNTIL on the event's wall clock (RFC 5545: inclusive bound).
124 1374 QDateTime untilOnWallClock(const RecurrenceRule &rule, bool allDay, const QTimeZone &zone) {
125
2/2
✓ Branch 3 → 4 taken 1293 times.
✓ Branch 3 → 5 taken 81 times.
1374 if (!rule.hasUntil())
126 1293 return {};
127
2/2
✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 13 taken 74 times.
81 if (rule.untilIsDate())
128
5/5
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 8 → 10 taken 6 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 7 times.
7 return rule.untilNaive(allDay ? QTime(0, 0) : QTime(23, 59, 59));
129
2/2
✓ Branch 13 → 14 taken 74 times.
✓ Branch 14 → 15 taken 74 times.
74 const QDateTime naive = rule.untilNaive(QTime(0, 0));
130
2/3
✓ Branch 15 → 16 taken 74 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 74 times.
74 if (!rule.untilIsUtc())
131 ✗ return naive; // floating UNTIL: already a wall clock
132
5/5
✓ Branch 18 → 19 taken 74 times.
✓ Branch 19 → 20 taken 74 times.
✓ Branch 20 → 21 taken 74 times.
✓ Branch 21 → 22 taken 74 times.
✓ Branch 22 → 23 taken 74 times.
148 return toNaive(QDateTime(naive.date(), naive.time(), QTimeZone::utc()), allDay, zone);
133 74 }
134
135 // Sprint 93 S93.1.3/S93.1.4: occurrence starts of a recurring master in
136 // [rangeStart, rangeEnd] (with margin), stepped on the event's wall clock.
137 1377 QList<QDateTime> occurrenceStarts(const CalendarEvent &event, const QDate &rangeStart,
138 const QDate &rangeEnd, bool *partial, int maxStarts, bool *limited) {
139
2/3
✓ Branch 2 → 3 taken 1377 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1377 times.
1377 if (!event.isSchedulable())
140 ✗ return {};
141
1/1
✓ Branch 5 → 6 taken 1377 times.
1377 const QTimeZone zone = wallClockZone(event.dtStart);
142
1/1
✓ Branch 6 → 7 taken 1377 times.
1377 const QDateTime naiveStart = toNaive(event.dtStart, event.allDay, zone);
143 // Occurrences that start before the range but last into it are found by
144 // looking back by the duration, capped at a year: a series whose
145 // occurrences last 1 000 years made every display expand from year 1026
146 // (Sprint 102 fuzzing). Longer occurrences show from the year before.
147 1377 constexpr qint64 kMaxLookBackDays = 366;
148 1377 const qint64 durationDays = qMin<qint64>(
149
4/4
✓ Branch 7 → 8 taken 1377 times.
✓ Branch 8 → 9 taken 1375 times.
✓ Branch 8 → 12 taken 2 times.
✓ Branch 9 → 10 taken 1375 times.
1377 kMaxLookBackDays, event.dtEnd.isValid() ? qMax<qint64>(0, event.dtStart.daysTo(event.dtEnd)) : 0);
150 // Margins: occurrences that start before the range but still overlap it,
151 // and the offset between the event zone and the display zone.
152
4/4
✓ Branch 14 → 15 taken 1377 times.
✓ Branch 15 → 16 taken 1377 times.
✓ Branch 16 → 17 taken 1377 times.
✓ Branch 17 → 18 taken 1377 times.
1377 const QDateTime windowStart(rangeStart.addDays(-durationDays - 2), QTime(0, 0), QTimeZone::utc());
153
4/4
✓ Branch 19 → 20 taken 1377 times.
✓ Branch 20 → 21 taken 1377 times.
✓ Branch 21 → 22 taken 1377 times.
✓ Branch 22 → 23 taken 1377 times.
1377 const QDateTime windowEnd(rangeEnd.addDays(2), QTime(23, 59, 59), QTimeZone::utc());
154 1377 QList<QDateTime> starts;
155
3/3
✓ Branch 24 → 25 taken 1377 times.
✓ Branch 27 → 28 taken 1374 times.
✓ Branch 27 → 71 taken 3 times.
1377 if (!event.rrule.trimmed().isEmpty()) {
156
1/1
✓ Branch 28 → 29 taken 1374 times.
1374 const RecurrenceRule rule = RecurrenceRule::parse(event.rrule);
157 // The limit counts from the range start (minus the duration), not from
158 // the zone margin: a minutely series would spend it before the range.
159
4/4
✓ Branch 29 → 30 taken 1374 times.
✓ Branch 30 → 31 taken 1374 times.
✓ Branch 31 → 32 taken 1374 times.
✓ Branch 32 → 33 taken 1374 times.
1374 const QDateTime countFrom(rangeStart.addDays(-durationDays), QTime(0, 0), QTimeZone::utc());
160 const NaiveExpansion naive = expandNaive(rule, naiveStart, windowStart, windowEnd,
161
2/2
✓ Branch 34 → 35 taken 1374 times.
✓ Branch 35 → 36 taken 1374 times.
1374 untilOnWallClock(rule, event.allDay, zone), maxStarts, countFrom);
162 1374 *limited = naive.limited;
163
4/4
✓ Branch 37 → 38 taken 1368 times.
✓ Branch 37 → 39 taken 6 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 38 → 58 taken 1360 times.
1374 if (naive.partial || naive.truncated) {
164 14 *partial = true;
165
7/7
✓ Branch 39 → 40 taken 14 times.
✓ Branch 43 → 44 taken 14 times.
✓ Branch 44 → 45 taken 14 times.
✓ Branch 45 → 46 taken 14 times.
✓ Branch 46 → 47 taken 14 times.
✓ Branch 56 → 41 taken 14 times.
✓ Branch 56 → 57 taken 14 times.
28 qCWarning(lcCalendar) << "RRULE shown incompletely" << event.rrule << "for event"
166
4/4
✓ Branch 47 → 48 taken 14 times.
✓ Branch 48 → 49 taken 13 times.
✓ Branch 48 → 50 taken 1 time.
✓ Branch 51 → 52 taken 14 times.
14 << event.uid << (rule.valid ? QString() : rule.error);
167 }
168
2/2
✓ Branch 66 → 60 taken 69975 times.
✓ Branch 66 → 67 taken 1374 times.
71349 for (const QDateTime &stamp : naive.starts)
169
2/2
✓ Branch 61 → 62 taken 69975 times.
✓ Branch 62 → 63 taken 69975 times.
69975 starts.append(toInstant(stamp, event.allDay, zone));
170
5/8
✓ Branch 71 → 72 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✗ Branch 73 → 78 not taken.
✓ Branch 74 → 75 taken 3 times.
✓ Branch 76 → 77 taken 3 times.
✗ Branch 76 → 78 not taken.
✓ Branch 79 → 80 taken 3 times.
✗ Branch 79 → 81 not taken.
1377 } else if (naiveStart >= windowStart && naiveStart <= windowEnd) {
171
1/1
✓ Branch 80 → 81 taken 3 times.
3 starts.append(event.dtStart);
172 }
173 1377 QSet<qint64> seen;
174
4/4
✓ Branch 82 → 83 taken 1377 times.
✓ Branch 83 → 84 taken 1377 times.
✓ Branch 90 → 85 taken 69978 times.
✓ Branch 90 → 91 taken 1377 times.
71355 for (const QDateTime &start : starts)
175
2/2
✓ Branch 86 → 87 taken 69978 times.
✓ Branch 87 → 88 taken 69978 times.
69978 seen.insert(start.toSecsSinceEpoch());
176
2/2
✓ Branch 141 → 93 taken 25 times.
✓ Branch 141 → 142 taken 1377 times.
1402 for (const QDateTime &rdate : event.rdates) {
177
1/8
✗ Branch 95 → 96 not taken.
✗ Branch 97 → 98 not taken.
✗ Branch 101 → 102 not taken.
✓ Branch 101 → 103 taken 25 times.
✗ Branch 195 → 196 not taken.
✗ Branch 195 → 197 not taken.
✗ Branch 200 → 201 not taken.
✗ Branch 200 → 202 not taken.
25 const QDateTime instant = event.allDay ? QDateTime(rdate.toUTC().date(), QTime(0, 0), QTimeZone::utc())
178
2/7
✗ Branch 94 → 95 not taken.
✓ Branch 94 → 100 taken 25 times.
✗ Branch 96 → 97 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 99 → 101 not taken.
✗ Branch 103 → 104 not taken.
✓ Branch 103 → 105 taken 25 times.
50 : rdate;
179
1/1
✓ Branch 105 → 106 taken 25 times.
25 const QDateTime naive = toNaive(instant, event.allDay, zone);
180
10/11
✓ Branch 106 → 107 taken 25 times.
✓ Branch 108 → 109 taken 21 times.
✓ Branch 108 → 115 taken 4 times.
✓ Branch 109 → 110 taken 21 times.
✓ Branch 111 → 112 taken 21 times.
✗ Branch 111 → 115 not taken.
✓ Branch 112 → 113 taken 21 times.
✓ Branch 114 → 115 taken 3 times.
✓ Branch 114 → 116 taken 18 times.
✓ Branch 117 → 118 taken 7 times.
✓ Branch 117 → 119 taken 18 times.
25 if (naive < windowStart || naive > windowEnd || seen.contains(instant.toSecsSinceEpoch()))
181 7 continue;
182
2/2
✓ Branch 119 → 120 taken 18 times.
✓ Branch 120 → 121 taken 18 times.
18 seen.insert(instant.toSecsSinceEpoch());
183 // Keep the series zone so later wall-clock math stays in it.
184
6/7
✓ Branch 121 → 122 taken 18 times.
✓ Branch 122 → 123 taken 17 times.
✓ Branch 122 → 126 taken 1 time.
✓ Branch 123 → 124 taken 17 times.
✗ Branch 123 → 126 not taken.
✓ Branch 124 → 125 taken 17 times.
✓ Branch 127 → 128 taken 18 times.
18 starts.append(zone.isValid() && !event.allDay ? instant.toTimeZone(zone) : instant);
185 32 }
186
3/3
✓ Branch 142 → 143 taken 1377 times.
✓ Branch 143 → 144 taken 1377 times.
✓ Branch 144 → 145 taken 1377 times.
1377 std::sort(starts.begin(), starts.end());
187 1377 return starts;
188 1377 }
189
190 108 bool matchesAnchor(const CalendarEvent &occurrence, const CalendarEvent &exception) {
191
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 108 times.
108 if (occurrence.uid != exception.uid)
192 ✗ return false;
193
2/4
✓ Branch 5 → 6 taken 108 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 15 taken 108 times.
108 if (occurrence.allDay || exception.allDay)
194 ✗ return occurrence.dtStart.toUTC().date() == exception.recurrenceId.toUTC().date();
195
3/3
✓ Branch 15 → 16 taken 108 times.
✓ Branch 16 → 17 taken 108 times.
✓ Branch 17 → 18 taken 108 times.
108 return occurrence.dtStart.toUTC() == exception.recurrenceId.toUTC();
196 }
197
198 } // namespace
199
200 2040 QDateTime seriesEnd(const CalendarEvent &event) {
201
3/3
✓ Branch 2 → 3 taken 2040 times.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 2034 times.
2040 if (!event.isSchedulable())
202 6 return {};
203
4/4
✓ Branch 5 → 6 taken 2034 times.
✓ Branch 6 → 7 taken 2014 times.
✓ Branch 6 → 10 taken 20 times.
✓ Branch 7 → 8 taken 2014 times.
2034 const qint64 duration = event.dtEnd.isValid() ? qMax<qint64>(0, event.dtStart.secsTo(event.dtEnd)) : 0;
204
12/14
✓ Branch 11 → 12 taken 2034 times.
✓ Branch 12 → 13 taken 2023 times.
✓ Branch 12 → 18 taken 11 times.
✓ Branch 13 → 14 taken 2023 times.
✓ Branch 15 → 16 taken 1837 times.
✓ Branch 15 → 19 taken 186 times.
✓ Branch 17 → 18 taken 1836 times.
✓ Branch 17 → 19 taken 1 time.
✓ Branch 20 → 21 taken 2023 times.
✓ Branch 20 → 22 taken 11 times.
✓ Branch 22 → 23 taken 1847 times.
✓ Branch 22 → 25 taken 187 times.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
2034 if (event.recurrenceId.isValid() || (event.rrule.trimmed().isEmpty() && event.rdates.isEmpty()))
205
1/1
✓ Branch 23 → 24 taken 1847 times.
1847 return event.dtStart.addSecs(duration);
206 187 QDateTime last = event.dtStart;
207
3/3
✓ Branch 26 → 27 taken 187 times.
✓ Branch 29 → 30 taken 186 times.
✓ Branch 29 → 111 taken 1 time.
187 if (!event.rrule.trimmed().isEmpty()) {
208
1/1
✓ Branch 30 → 31 taken 186 times.
186 const RecurrenceRule rule = RecurrenceRule::parse(event.rrule);
209
11/11
✓ Branch 31 → 32 taken 183 times.
✓ Branch 31 → 37 taken 3 times.
✓ Branch 32 → 33 taken 183 times.
✓ Branch 33 → 34 taken 182 times.
✓ Branch 33 → 37 taken 1 time.
✓ Branch 34 → 35 taken 44 times.
✓ Branch 34 → 38 taken 138 times.
✓ Branch 36 → 37 taken 34 times.
✓ Branch 36 → 38 taken 10 times.
✓ Branch 39 → 40 taken 38 times.
✓ Branch 39 → 42 taken 148 times.
186 if (!rule.valid || !rule.fullySupported() || (rule.count == 0 && !rule.hasUntil()))
210 38 return {};
211
2/2
✓ Branch 43 → 44 taken 10 times.
✓ Branch 43 → 66 taken 138 times.
148 if (rule.hasUntil()) {
212 // UNTIL is an upper bound already: its wall clock read as UTC plus a
213 // day covers every zone offset - no expansion (a daily series over
214 // five years would cost ~2 ms per stored row).
215
2/2
✓ Branch 44 → 45 taken 10 times.
✓ Branch 45 → 46 taken 10 times.
10 const QDateTime until = rule.untilNaive(QTime(23, 59, 59));
216 // S106-125: an UNTIL of the right shape that is no real date
217 // ("UNTIL=99999999") gives no bound. Before, the invalid date became an
218 // epoch-0 end that hid the whole series from every range query.
219
2/3
✓ Branch 46 → 47 taken 10 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 50 taken 10 times.
10 if (!until.isValid())
220 ✗ return {};
221
6/6
✓ Branch 50 → 51 taken 10 times.
✓ Branch 51 → 52 taken 10 times.
✓ Branch 52 → 53 taken 10 times.
✓ Branch 53 → 54 taken 10 times.
✓ Branch 54 → 55 taken 10 times.
✓ Branch 55 → 56 taken 10 times.
10 last = qMax(last, QDateTime(until.date(), QTime(23, 59, 59), QTimeZone::utc()).addDays(1));
222
1/2
✓ Branch 62 → 63 taken 10 times.
✗ Branch 62 → 65 not taken.
10 } else {
223 // COUNT: the last of the counted occurrences.
224
1/1
✓ Branch 66 → 67 taken 138 times.
138 const QTimeZone zone = wallClockZone(event.dtStart);
225
1/1
✓ Branch 67 → 68 taken 138 times.
138 const QDateTime naiveStart = toNaive(event.dtStart, event.allDay, zone);
226 276 const NaiveExpansion naive = expandNaive(rule, naiveStart, naiveStart,
227
5/5
✓ Branch 70 → 71 taken 138 times.
✓ Branch 71 → 72 taken 138 times.
✓ Branch 72 → 73 taken 138 times.
✓ Branch 73 → 74 taken 138 times.
✓ Branch 74 → 75 taken 138 times.
138 QDateTime(QDate(9999, 12, 31), QTime(0, 0), QTimeZone::utc()));
228
2/4
✓ Branch 79 → 80 taken 138 times.
✗ Branch 79 → 81 not taken.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 83 taken 138 times.
138 if (naive.partial || naive.truncated)
229 ✗ return {};
230
1/2
✓ Branch 84 → 85 taken 138 times.
✗ Branch 84 → 91 not taken.
138 if (!naive.starts.isEmpty())
231
2/2
✓ Branch 86 → 87 taken 138 times.
✓ Branch 87 → 88 taken 138 times.
138 last = qMax(last, toInstant(naive.starts.last(), event.allDay, zone));
232 138 }
233 186 }
234
2/2
✓ Branch 118 → 113 taken 3 times.
✓ Branch 118 → 119 taken 149 times.
152 for (const QDateTime &rdate : event.rdates)
235
1/1
✓ Branch 114 → 115 taken 3 times.
3 last = qMax(last, rdate);
236
2/2
✓ Branch 119 → 120 taken 149 times.
✓ Branch 120 → 121 taken 149 times.
149 return last.addSecs(duration).addDays(1);
237 187 }
238
239 1380 QString seriesKey(const CalendarEvent &event) {
240
4/4
✓ Branch 6 → 7 taken 1380 times.
✓ Branch 7 → 8 taken 1380 times.
✓ Branch 8 → 9 taken 1380 times.
✓ Branch 9 → 10 taken 1380 times.
1380 return event.accountId + QLatin1Char('\n') + event.calendarPath + QLatin1Char('\n') + event.uid;
241 }
242
243 5438 QList<CalendarEvent> expandEventsForDisplay(const QList<CalendarEvent> &events,
244 const QDate &rangeStart, const QDate &rangeEnd,
245 bool splitMultiDay, const DisplayLimit &limit) {
246 5438 QList<CalendarEvent> expanded;
247 // Exceptions replace the master occurrence at their RECURRENCE-ID anchor;
248 // collect them first so the master pass can skip those anchors before
249 // splitting multi-day occurrences into display days.
250 5438 QList<CalendarEvent> exceptions;
251
2/2
✓ Branch 10 → 4 taken 6234 times.
✓ Branch 10 → 11 taken 5438 times.
11672 for (const auto &event : events)
252
3/3
✓ Branch 5 → 6 taken 6234 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 8 taken 6214 times.
6234 if (event.recurrenceId.isValid())
253
1/1
✓ Branch 7 → 8 taken 20 times.
20 exceptions.append(event);
254
255
2/2
✓ Branch 66 → 13 taken 6234 times.
✓ Branch 66 → 67 taken 5438 times.
11672 for (const auto &event : events) {
256
3/3
✓ Branch 14 → 15 taken 6234 times.
✓ Branch 15 → 16 taken 20 times.
✓ Branch 15 → 17 taken 6214 times.
6234 if (event.recurrenceId.isValid())
257 4857 continue;
258
8/11
✓ Branch 17 → 18 taken 6214 times.
✓ Branch 19 → 20 taken 4840 times.
✓ Branch 19 → 23 taken 1374 times.
✓ Branch 21 → 22 taken 4837 times.
✓ Branch 21 → 23 taken 3 times.
✓ Branch 24 → 25 taken 6214 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 4837 times.
✓ Branch 26 → 29 taken 1377 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
6214 if (event.rrule.trimmed().isEmpty() && event.rdates.isEmpty()) {
259
1/1
✓ Branch 27 → 28 taken 4837 times.
4837 appendDisplayOccurrence(&expanded, event, rangeStart, rangeEnd, splitMultiDay);
260 4837 continue;
261 }
262 1377 bool partial = false, limited = false;
263
3/3
✓ Branch 29 → 30 taken 1377 times.
✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 33 taken 1372 times.
1377 const int maxStarts = limit.unlimitedSeries.contains(seriesKey(event)) ? 0 : limit.perSeries;
264
1/1
✓ Branch 35 → 36 taken 1377 times.
1377 const QList<QDateTime> starts = occurrenceStarts(event, rangeStart, rangeEnd, &partial, maxStarts, &limited);
265
1/1
✓ Branch 36 → 37 taken 1377 times.
1377 const QSet<qint64> excluded = exclusionKeys(event);
266
2/2
✓ Branch 59 → 39 taken 69996 times.
✓ Branch 59 → 60 taken 1377 times.
71373 for (const QDateTime &start : starts) {
267 // T-79.D3: EXDATE removes the occurrence (it still consumed COUNT).
268
7/8
✓ Branch 40 → 41 taken 69996 times.
✓ Branch 41 → 42 taken 69996 times.
✗ Branch 41 → 46 not taken.
✓ Branch 42 → 43 taken 69996 times.
✓ Branch 44 → 45 taken 47 times.
✓ Branch 44 → 46 taken 69949 times.
✓ Branch 47 → 48 taken 47 times.
✓ Branch 47 → 49 taken 69949 times.
69996 if (start.isValid() && excluded.contains(exclusionKey(start, event.allDay)))
269 47 continue;
270
1/1
✓ Branch 49 → 50 taken 69949 times.
69949 CalendarEvent occurrence = shiftedOccurrence(event, start);
271 69949 occurrence.partialRecurrence = partial;
272 69949 occurrence.occurrencesLimited = limited;
273
1/1
✓ Branch 52 → 53 taken 69949 times.
69949 const bool replaced = std::any_of(exceptions.cbegin(), exceptions.cend(),
274 108 [&occurrence](const CalendarEvent &exception) {
275 108 return matchesAnchor(occurrence, exception);
276 });
277
2/2
✓ Branch 53 → 54 taken 69929 times.
✓ Branch 53 → 55 taken 20 times.
69949 if (!replaced)
278
1/1
✓ Branch 54 → 55 taken 69929 times.
69929 appendDisplayOccurrence(&expanded, occurrence, rangeStart, rangeEnd, splitMultiDay);
279 69949 }
280 1377 }
281
282 // Q11: recurrence exceptions render at their own (moved) start.
283
4/4
✓ Branch 67 → 68 taken 5438 times.
✓ Branch 68 → 69 taken 5438 times.
✓ Branch 74 → 70 taken 20 times.
✓ Branch 74 → 75 taken 5438 times.
5458 for (const auto &exception : exceptions)
284
1/1
✓ Branch 71 → 72 taken 20 times.
20 appendDisplayOccurrence(&expanded, exception, rangeStart, rangeEnd, splitMultiDay);
285
286
3/3
✓ Branch 75 → 76 taken 5438 times.
✓ Branch 76 → 77 taken 5438 times.
✓ Branch 77 → 78 taken 5438 times.
5438 std::sort(expanded.begin(), expanded.end(),
287 666526 [](const CalendarEvent &a, const CalendarEvent &b) {
288 666526 return a.dtStart < b.dtStart;
289 });
290 5438 return expanded;
291 5438 }
292