MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 83.6% 92 / 0 / 110
Functions: 100.0% 7 / 0 / 7
Branches: 46.3% 145 / 0 / 313

data/CalendarSeries.cpp
Line Branch Exec Source
1 #include "CalendarSeries.h"
2
3 #include "ICalendar.h"
4 #include "RecurrenceExpansion.h"
5 #include "RecurrenceRule.h"
6
7 #include <QTimeZone>
8
9 namespace CalendarSeries {
10
11 namespace {
12
13 16 QList<QDateTime> keepFrom(const QList<QDateTime> &values, const QDateTime &anchor, bool allDay,
14 bool atOrAfter) {
15 16 QList<QDateTime> kept;
16
2/2
✓ Branch 30 → 4 taken 3 times.
✓ Branch 30 → 31 taken 16 times.
19 for (const QDateTime &value : values) {
17
2/12
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 13 taken 3 times.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 3 times.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
6 const bool after = allDay ? value.toUTC().date() >= anchor.toUTC().date()
18
6/13
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 22 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 3 times.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
3 : value.toUTC() >= anchor.toUTC();
19
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 3 times.
3 if (after == atOrAfter)
20 ✗ kept.append(value);
21 }
22 16 return kept;
23 ✗ }
24
25 } // namespace
26
27 111 bool isRecurring(const CalendarEvent &event) {
28
5/9
✓ Branch 2 → 3 taken 111 times.
✓ Branch 4 → 5 taken 77 times.
✓ Branch 4 → 7 taken 34 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 77 times.
✓ Branch 10 → 11 taken 111 times.
✗ Branch 10 → 12 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
111 return !event.rrule.trimmed().isEmpty() || !event.rdates.isEmpty();
29 }
30
31 12 std::optional<int> occurrencesBefore(const CalendarEvent &master, const QDateTime &anchor) {
32
7/11
✓ Branch 2 → 3 taken 12 times.
✓ Branch 3 → 4 taken 12 times.
✗ Branch 3 → 9 not taken.
✓ Branch 4 → 5 taken 12 times.
✓ Branch 5 → 6 taken 12 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 12 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 12 times.
12 if (!master.isSchedulable() || !anchor.isValid() || anchor <= master.dtStart)
33 ✗ return 0;
34 12 CalendarEvent probe = master;
35
1/1
✓ Branch 16 → 17 taken 12 times.
12 probe.exdates.clear(); // COUNT counts excluded occurrences too
36
1/1
✓ Branch 17 → 18 taken 12 times.
12 probe.rdates.clear();
37
5/14
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 12 times.
✗ Branch 19 → 20 not taken.
✗ Branch 20 → 23 not taken.
✓ Branch 21 → 22 taken 12 times.
✓ Branch 22 → 23 taken 12 times.
✓ Branch 23 → 24 taken 12 times.
✗ Branch 23 → 25 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 12 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
12 const QDate from = master.allDay ? master.dtStart.toUTC().date() : master.dtStart.toLocalTime().date();
38
5/14
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 12 times.
✗ Branch 28 → 29 not taken.
✗ Branch 29 → 32 not taken.
✓ Branch 30 → 31 taken 12 times.
✓ Branch 31 → 32 taken 12 times.
✓ Branch 32 → 33 taken 12 times.
✗ Branch 32 → 34 not taken.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 12 times.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
12 const QDate to = master.allDay ? anchor.toUTC().date() : anchor.toLocalTime().date();
39 12 int count = 0;
40
7/9
✓ Branch 39 → 40 taken 12 times.
✓ Branch 42 → 43 taken 12 times.
✓ Branch 42 → 44 taken 12 times.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 46 → 47 taken 12 times.
✓ Branch 78 → 48 taken 38 times.
✓ Branch 78 → 79 taken 10 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
60 for (const CalendarEvent &occurrence : expandEventsForDisplay({probe}, from, to, false)) {
41 // S106-145: a cut-off expansion (work cap) undercounts; COUNT
42 // arithmetic on it would be wrong, so the caller learns it is unknown.
43
2/2
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 53 taken 36 times.
38 if (occurrence.partialRecurrence)
44 2 return std::nullopt;
45
4/14
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 61 taken 36 times.
✗ Branch 54 → 55 not taken.
✗ Branch 55 → 56 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 74 taken 36 times.
✓ Branch 74 → 75 taken 27 times.
✓ Branch 74 → 76 taken 9 times.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
72 if (master.allDay ? occurrence.dtStart.toUTC().date() < anchor.toUTC().date()
46
6/13
✓ Branch 61 → 62 taken 36 times.
✓ Branch 62 → 63 taken 36 times.
✓ Branch 63 → 64 taken 36 times.
✓ Branch 66 → 67 taken 36 times.
✗ Branch 66 → 68 not taken.
✓ Branch 68 → 69 taken 36 times.
✗ Branch 68 → 70 not taken.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 36 times.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
✗ Branch 126 → 127 not taken.
✗ Branch 126 → 128 not taken.
36 : occurrence.dtStart.toUTC() < anchor.toUTC())
47 27 ++count;
48 12 }
49 10 return count;
50 24 }
51
52 4 bool splittableAt(const CalendarEvent &master, const QDateTime &anchor) {
53
2/3
✓ Branch 2 → 3 taken 4 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 4 times.
4 if (master.rrule.trimmed().isEmpty())
54 ✗ return true;
55
1/1
✓ Branch 7 → 8 taken 4 times.
4 const RecurrenceRule rule = RecurrenceRule::parse(master.rrule);
56
5/7
✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 13 not taken.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 9 → 13 not taken.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 14 taken 1 time.
4 return !rule.valid || rule.count == 0 || occurrencesBefore(master, anchor).has_value();
57 4 }
58
59 namespace {
60
61 // S106-131: true when the rule's own end (UNTIL or COUNT) already lies
62 // before `anchor` — then "end before the anchor" must not move it later.
63 6 bool naturallyEndsBefore(const RecurrenceRule &rule, const CalendarEvent &master,
64 const QDateTime &anchor) {
65
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 44 taken 5 times.
6 if (rule.hasUntil()) {
66
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
1 const QDateTime naive = rule.untilNaive(QTime(23, 59, 59));
67
2/3
✓ Branch 6 → 7 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (!naive.isValid())
68 ✗ return false;
69
3/6
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 12 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 31 taken 1 time.
1 if (master.allDay || rule.untilIsDate())
70 ✗ return naive.date() < (master.allDay ? anchor.toUTC().date() : anchor.toTimeZone(master.dtStart.timeZone()).date());
71
3/5
✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 34 not taken.
✓ Branch 33 → 35 taken 1 time.
✗ Branch 34 → 35 not taken.
1 const QTimeZone zone = rule.untilIsUtc() ? QTimeZone::utc() : master.dtStart.timeZone();
72
4/4
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
1 return QDateTime(naive.date(), naive.time(), zone) < anchor;
73 1 }
74
2/2
✓ Branch 44 → 45 taken 4 times.
✓ Branch 44 → 54 taken 1 time.
5 if (rule.count > 0) {
75
1/1
✓ Branch 45 → 46 taken 4 times.
4 const auto before = occurrencesBefore(master, anchor);
76
3/4
✓ Branch 47 → 48 taken 4 times.
✗ Branch 47 → 51 not taken.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 3 times.
4 return before && *before >= rule.count;
77 }
78 1 return false;
79 }
80
81 } // namespace
82
83 6 bool endBefore(CalendarEvent *master, const QDateTime &anchor) {
84
4/8
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 if (!master || !anchor.isValid() || !master->dtStart.isValid())
85 ✗ return false;
86
2/12
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 19 taken 6 times.
✗ Branch 12 → 13 not taken.
✗ Branch 13 → 14 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 15 → 16 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 6 times.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
12 const bool afterStart = master->allDay ? anchor.toUTC().date() > master->dtStart.toUTC().date()
87
6/13
✓ Branch 19 → 20 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 24 → 25 taken 6 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 6 times.
✗ Branch 26 → 28 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 6 times.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
6 : anchor.toUTC() > master->dtStart.toUTC();
88
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 6 times.
6 if (!afterStart)
89 ✗ return false;
90
2/3
✓ Branch 34 → 35 taken 6 times.
✓ Branch 37 → 38 taken 6 times.
✗ Branch 37 → 83 not taken.
6 if (!master->rrule.trimmed().isEmpty()) {
91
1/1
✓ Branch 38 → 39 taken 6 times.
6 RecurrenceRule rule = RecurrenceRule::parse(master->rrule);
92
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 6 times.
6 if (!rule.valid)
93 ✗ return false;
94
3/3
✓ Branch 41 → 42 taken 6 times.
✓ Branch 42 → 43 taken 4 times.
✓ Branch 42 → 77 taken 2 times.
6 if (!naturallyEndsBefore(rule, *master, anchor)) {
95 4 rule.count = 0;
96 4 rule.until = master->allDay
97
4/17
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 51 taken 4 times.
✗ Branch 44 → 45 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 46 → 47 not taken.
✗ Branch 50 → 57 not taken.
✓ Branch 56 → 57 taken 4 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 4 times.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 4 times.
✗ Branch 126 → 127 not taken.
✗ Branch 126 → 128 not taken.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
20 ? anchor.toUTC().date().addDays(-1).toString(QStringLiteral("yyyyMMdd"))
98
7/20
✓ Branch 51 → 52 taken 4 times.
✓ Branch 52 → 53 taken 4 times.
✓ Branch 59 → 60 taken 4 times.
✗ Branch 59 → 61 not taken.
✓ Branch 61 → 62 taken 4 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 4 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 4 times.
✗ Branch 65 → 67 not taken.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 4 times.
✗ Branch 110 → 111 not taken.
✗ Branch 110 → 112 not taken.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
16 : anchor.toUTC().addSecs(-1).toString(QStringLiteral("yyyyMMdd'T'HHmmss'Z'"));
99
1/1
✓ Branch 73 → 74 taken 4 times.
4 master->rrule = rule.toString();
100 }
101 6 }
102
1/1
✓ Branch 83 → 84 taken 6 times.
6 master->exdates = keepFrom(master->exdates, anchor, master->allDay, false);
103
1/1
✓ Branch 86 → 87 taken 6 times.
6 master->rdates = keepFrom(master->rdates, anchor, master->allDay, false);
104 6 return true;
105 }
106
107 2 CalendarEvent followingPart(const CalendarEvent &master, const QDateTime &anchor,
108 const QString &newUid) {
109 2 CalendarEvent part = master;
110 2 part.id = 0;
111 2 part.uid = newUid;
112 2 part.recurrenceId = {};
113 2 part.etag.clear();
114 2 part.resourceHref.clear();
115 2 part.resourceIcal.clear();
116 2 part.syncState.clear();
117
3/4
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 2 times.
2 const qint64 duration = master.dtEnd.isValid() ? master.dtStart.secsTo(master.dtEnd) : -1;
118
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 27 taken 2 times.
2 if (master.allDay) {
119 ✗ part.dtStart = QDateTime(anchor.toUTC().date(), QTime(0, 0), QTimeZone::utc());
120
3/3
✓ Branch 27 → 28 taken 2 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 35 taken 1 time.
2 } else if (master.dtStart.timeSpec() == Qt::TimeZone) {
121
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
1 part.dtStart = anchor.toTimeZone(master.dtStart.timeZone());
122 } else {
123
3/5
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✗ Branch 36 → 38 not taken.
✓ Branch 37 → 39 taken 1 time.
✗ Branch 38 → 39 not taken.
1 part.dtStart = master.dtStart.timeSpec() == Qt::LocalTime ? anchor.toLocalTime() : anchor.toUTC();
124 }
125
1/2
✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 50 not taken.
2 if (duration >= 0)
126
2/5
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 46 taken 2 times.
✗ Branch 44 → 45 not taken.
✗ Branch 45 → 47 not taken.
✓ Branch 46 → 47 taken 2 times.
4 part.dtEnd = master.allDay ? part.dtStart.addDays(master.dtStart.daysTo(master.dtEnd))
127 2 : part.dtStart.addSecs(duration);
128
2/3
✓ Branch 50 → 51 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 53 → 69 not taken.
2 if (!master.rrule.trimmed().isEmpty()) {
129
1/1
✓ Branch 54 → 55 taken 2 times.
2 RecurrenceRule rule = RecurrenceRule::parse(master.rrule);
130
2/4
✓ Branch 55 → 56 taken 2 times.
✗ Branch 55 → 67 not taken.
✓ Branch 56 → 57 taken 2 times.
✗ Branch 56 → 67 not taken.
2 if (rule.valid && rule.count > 0) {
131 // splittableAt() is checked first; an unknown count keeps the rule.
132
2/3
✓ Branch 57 → 58 taken 2 times.
✓ Branch 59 → 60 taken 2 times.
✗ Branch 59 → 66 not taken.
2 if (const auto before = occurrencesBefore(master, anchor)) {
133 2 rule.count = qMax(1, rule.count - *before);
134
1/1
✓ Branch 62 → 63 taken 2 times.
2 part.rrule = rule.toString();
135 }
136 }
137 2 }
138
1/1
✓ Branch 69 → 70 taken 2 times.
2 part.exdates = keepFrom(master.exdates, anchor, master.allDay, true);
139
1/1
✓ Branch 72 → 73 taken 2 times.
2 part.rdates = keepFrom(master.rdates, anchor, master.allDay, true);
140
1/1
✓ Branch 78 → 79 taken 2 times.
2 ICal::Component component = ICal::parseFirst(master.icalRaw, QStringLiteral("VEVENT"));
141
1/2
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 112 taken 2 times.
2 if (!component.name.isEmpty()) {
142 ✗ component.setPropertyValue(QStringLiteral("UID"), ICal::escapeText(newUid));
143 ✗ component.removeProperties(QStringLiteral("RELATED-TO"));
144 ✗ ICal::Property related;
145 ✗ related.name = QStringLiteral("RELATED-TO");
146 ✗ related.value = ICal::escapeText(master.uid);
147 ✗ component.appendProperty(related);
148 ✗ part.icalRaw = ICal::serialize(component);
149 ✗ } else {
150 2 part.icalRaw.clear();
151 }
152 2 return part;
153 2 }
154
155 } // namespace CalendarSeries
156