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.3% 207 / 0 / 215
Functions: 100.0% 14 / 0 / 14
Branches: 68.5% 315 / 0 / 460

presentation/QuickComposeBackendMeeting.cpp
Line Branch Exec Source
1 // Sprint 108 S108.8.1/8.2 (D-22): *Suggest times* — free slots over the
2 // user's shown calendars and the block that offers them in the message;
3 // S108.8.6 (D-25): the scheduling grid's busy times and the next time
4 // everyone is free; the event of an invitation that left (D-23).
5 #include "QuickComposeBackend.h"
6 #include "ComposeLog.h"
7 #include "ComposeSession.h"
8
9 #include "app/core/MailSessionCore.h"
10 #include "data/CalendarModels.h"
11 #include "data/CalendarStore.h"
12 #include "data/FreeSlots.h"
13 #include "data/ICalEventCodec.h"
14 #include "data/ICalTime.h"
15 #include "data/ICalendar.h"
16 #include "data/MessageLanguageText.h"
17 #include "data/RecurrenceExpansion.h"
18 #include "data/ImipInvitation.h"
19 #include "data/MailOutboxStore.h"
20 #include "app/core/CalendarSyncCoordinator.h"
21 #include "service/MimeParser.h"
22 #include "RecipientChipModel.h"
23
24 #include <QPointer>
25
26 #include <QJsonDocument>
27 #include <QJsonObject>
28
29 #include <QElapsedTimer>
30 #include <QLocale>
31
32 namespace {
33
34 // The stored VEVENT text — written from the event's fields when the event
35 // has none yet (created in MailJD and not synced), as the event editor does.
36 1 QString storedComponentText(const CalendarEvent &event) {
37
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (!event.icalRaw.isEmpty()) return event.icalRaw;
38 const ICal::Component calendar =
39
3/11
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
1 event.resourceIcal.isEmpty() ? ICal::Component{} : ICal::parseFirst(event.resourceIcal, QStringLiteral("VCALENDAR"));
40
3/3
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
1 return ICalEventCodec::componentText(ICalEventCodec::eventComponent(event, ICalTime::ZoneResolver(calendar)));
41 1 }
42
43 23 QVariantList intervalList(const QList<FreeSlots::Interval> &intervals) {
44 23 QVariantList out;
45
2/2
✓ Branch 27 → 4 taken 11 times.
✓ Branch 27 → 28 taken 23 times.
34 for (const FreeSlots::Interval &interval : intervals)
46
3/5
✓ Branch 15 → 16 taken 11 times.
✓ Branch 19 → 20 taken 22 times.
✓ Branch 19 → 21 taken 11 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
55 out.append(QVariantMap{{QStringLiteral("start"), interval.start}, {QStringLiteral("end"), interval.end}});
47 23 return out;
48 33 }
49
50 43 QList<FreeSlots::Interval> intervalsFrom(const QVariantList &list) {
51 43 QList<FreeSlots::Interval> out;
52
2/2
✓ Branch 46 → 4 taken 44 times.
✓ Branch 46 → 47 taken 43 times.
87 for (const QVariant &value : list) {
53
1/1
✓ Branch 5 → 6 taken 44 times.
44 const QVariantMap map = value.toMap();
54
2/2
✓ Branch 10 → 11 taken 44 times.
✓ Branch 11 → 12 taken 44 times.
88 const QDateTime start = map.value(QStringLiteral("start")).toDateTime();
55
2/2
✓ Branch 20 → 21 taken 44 times.
✓ Branch 21 → 22 taken 44 times.
88 const QDateTime end = map.value(QStringLiteral("end")).toDateTime();
56
7/11
✓ Branch 26 → 27 taken 44 times.
✓ Branch 27 → 28 taken 44 times.
✗ Branch 27 → 34 not taken.
✓ Branch 28 → 29 taken 44 times.
✓ Branch 29 → 30 taken 44 times.
✗ Branch 29 → 34 not taken.
✓ Branch 30 → 31 taken 44 times.
✓ Branch 32 → 33 taken 44 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 44 times.
✗ Branch 35 → 41 not taken.
88 if (start.isValid() && end.isValid() && end > start) out.append({start, end});
57 44 }
58 43 return out;
59 44 }
60
61 } // namespace
62
63 // The user's timed occurrences in the shown calendars (D-25: transparent,
64 // all-day and hidden-calendar entries do not block).
65 23 QList<FreeSlots::Interval> QuickComposeBackend::ownBusyIntervals(const QDate &from, int days,
66 const QStringList &hiddenCalendars) const {
67 23 QList<FreeSlots::Interval> busy;
68 23 CalendarStore *store = m_session.calendarStore();
69
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 23 times.
23 if (!store) return busy;
70
1/1
✓ Branch 5 → 6 taken 23 times.
23 const QDate end = from.addDays(days);
71
3/3
✓ Branch 6 → 7 taken 23 times.
✓ Branch 7 → 8 taken 23 times.
✓ Branch 8 → 9 taken 23 times.
23 const QDateTime rangeStart(from.addDays(-1), QTime(0, 0));
72
3/3
✓ Branch 9 → 10 taken 23 times.
✓ Branch 10 → 11 taken 23 times.
✓ Branch 11 → 12 taken 23 times.
23 const QDateTime rangeEnd(end.addDays(1), QTime(23, 59, 59));
73
1/1
✓ Branch 12 → 13 taken 23 times.
23 const auto events = store->eventsForDateRange(rangeStart, rangeEnd);
74
1/1
✓ Branch 13 → 14 taken 23 times.
23 const QSet<qint64> transparent = store->transparentEventIds(rangeStart, rangeEnd);
75 23 QSet<QString> hidden;
76
3/3
✓ Branch 18 → 19 taken 1 time.
✓ Branch 21 → 17 taken 1 time.
✓ Branch 21 → 22 taken 23 times.
24 for (const QString &key : hiddenCalendars) hidden.insert(key);
77 23 for (const CalendarEvent &event :
78
5/5
✓ Branch 23 → 24 taken 23 times.
✓ Branch 25 → 26 taken 23 times.
✓ Branch 26 → 27 taken 23 times.
✓ Branch 54 → 28 taken 34 times.
✓ Branch 54 → 55 taken 23 times.
80 expandEventsForDisplay(events, from, end, false, {kDisplayOccurrencesPerSeries, {}})) {
79
4/7
✓ Branch 29 → 30 taken 34 times.
✗ Branch 29 → 32 not taken.
✓ Branch 30 → 31 taken 34 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 34 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 34 times.
34 if (event.allDay || !event.dtStart.isValid()) continue;
80
3/3
✓ Branch 36 → 37 taken 34 times.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 33 times.
34 if (hidden.contains(calendarSelectionKey(event.accountId, event.calendarPath))) continue;
81
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 44 taken 32 times.
33 if (transparent.contains(event.id)) continue;
82
1/2
✓ Branch 46 → 47 taken 32 times.
✗ Branch 46 → 48 not taken.
64 busy.append({event.dtStart, event.dtEnd.isValid() ? event.dtEnd : event.dtStart.addSecs(3600)});
83 23 }
84 23 return busy;
85 110 }
86
87 8 QVariantList QuickComposeBackend::ownBusy(const QStringList &hiddenCalendars, int days) const {
88
4/4
✓ Branch 2 → 3 taken 8 times.
✓ Branch 3 → 4 taken 8 times.
✓ Branch 4 → 5 taken 8 times.
✓ Branch 5 → 6 taken 8 times.
8 return intervalList(ownBusyIntervals(QDate::currentDate(), qBound(1, days, 60), hiddenCalendars));
89 }
90
91 7 void QuickComposeBackend::requestAttendeeBusy(QObject *sessionObject, int days) {
92
1/1
✓ Branch 2 → 3 taken 7 times.
7 auto *session = qobject_cast<ComposeSession *>(sessionObject);
93 7 auto *sync = m_session.calendarSync();
94
2/4
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 7 times.
7 if (!session || !sync) return;
95 7 const QString sessionId = session->sessionId();
96
2/2
✓ Branch 13 → 14 taken 7 times.
✓ Branch 14 → 15 taken 7 times.
14 const QString account = session->meeting().value(QStringLiteral("calendarAccount")).toString();
97
3/3
✓ Branch 20 → 21 taken 7 times.
✓ Branch 25 → 26 taken 7 times.
✓ Branch 26 → 27 taken 7 times.
14 const QString organizer = session->identityMap().value(QStringLiteral("email")).toString();
98 7 QStringList attendees;
99 7 QHash<QString, QString> names;
100
2/2
✓ Branch 89 → 36 taken 14 times.
✓ Branch 89 → 90 taken 7 times.
21 for (RecipientChipModel *field : {&session->to(), &session->cc()})
101
2/2
✓ Branch 87 → 37 taken 15 times.
✓ Branch 87 → 88 taken 14 times.
29 for (int row = 0; row < field->count(); ++row) {
102
1/1
✓ Branch 37 → 38 taken 15 times.
15 const QVariantMap chip = field->chipAt(row);
103
4/4
✓ Branch 42 → 43 taken 15 times.
✓ Branch 43 → 44 taken 15 times.
✓ Branch 44 → 45 taken 15 times.
✓ Branch 45 → 46 taken 15 times.
30 const QString email = chip.value(QStringLiteral("address")).toString().trimmed().toLower();
104
4/7
✓ Branch 54 → 55 taken 15 times.
✓ Branch 55 → 56 taken 15 times.
✗ Branch 55 → 58 not taken.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 59 taken 15 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 15 times.
15 if (!email.contains(QLatin1Char('@')) || attendees.contains(email)) continue;
105
1/1
✓ Branch 62 → 63 taken 15 times.
15 attendees.append(email);
106
3/3
✓ Branch 67 → 68 taken 15 times.
✓ Branch 68 → 69 taken 15 times.
✓ Branch 69 → 70 taken 15 times.
30 names.insert(email, chip.value(QStringLiteral("name")).toString());
107 15 }
108
3/3
✓ Branch 90 → 91 taken 7 times.
✓ Branch 91 → 92 taken 7 times.
✓ Branch 92 → 93 taken 7 times.
7 const QDateTime from(QDate::currentDate(), QTime(0, 0));
109
2/2
✓ Branch 93 → 94 taken 7 times.
✓ Branch 94 → 95 taken 7 times.
7 const QDateTime to = from.addDays(qBound(1, days, 60));
110
1/1
✓ Branch 95 → 96 taken 7 times.
7 QPointer<QuickComposeBackend> self(this);
111
3/3
✓ Branch 101 → 102 taken 7 times.
✓ Branch 102 → 103 taken 7 times.
✓ Branch 103 → 104 taken 7 times.
7 sync->requestFreeBusy(account, organizer, attendees, from.toUTC(), to.toUTC(),
112
1/7
✓ Branch 100 → 101 taken 7 times.
✗ Branch 203 → 204 not taken.
✗ Branch 203 → 205 not taken.
✗ Branch 206 → 207 not taken.
✗ Branch 206 → 208 not taken.
✗ Branch 209 → 210 not taken.
✗ Branch 209 → 211 not taken.
14 [self, sessionId, names, attendees](const QList<CalDavXml::FreeBusy> &answers) {
113
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
7 if (!self) return;
114 7 QVariantList rows;
115
2/2
✓ Branch 100 → 7 taken 15 times.
✓ Branch 100 → 101 taken 7 times.
22 for (const QString &email : attendees) {
116 30 QVariantMap row{{QStringLiteral("email"), email},
117 30 {QStringLiteral("name"), names.value(email)},
118 30 {QStringLiteral("known"), false},
119 30 {QStringLiteral("noAnswer"), false},
120
2/4
✓ Branch 32 → 33 taken 75 times.
✓ Branch 32 → 34 taken 15 times.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
105 {QStringLiteral("busy"), QVariantList{}}};
121
2/2
✓ Branch 93 → 48 taken 33 times.
✓ Branch 93 → 94 taken 15 times.
48 for (const CalDavXml::FreeBusy &answer : answers) {
122
2/2
✓ Branch 50 → 51 taken 18 times.
✓ Branch 50 → 52 taken 15 times.
33 if (answer.recipient != email) continue;
123
1/1
✓ Branch 56 → 57 taken 15 times.
30 row.insert(QStringLiteral("noAnswer"), answer.noAnswer);
124 15 QList<FreeSlots::Interval> busy;
125
1/2
✗ Branch 69 → 62 not taken.
✓ Branch 69 → 70 taken 15 times.
15 for (const auto &interval : answer.busy)
126 ✗ busy.append({interval.first.toLocalTime(), interval.second.toLocalTime()});
127
1/1
✓ Branch 74 → 75 taken 15 times.
30 row.insert(QStringLiteral("known"), answer.known);
128
2/2
✓ Branch 78 → 79 taken 15 times.
✓ Branch 83 → 84 taken 15 times.
30 row.insert(QStringLiteral("busy"), intervalList(busy));
129 15 }
130
1/1
✓ Branch 95 → 96 taken 15 times.
15 rows.append(row);
131 15 }
132
1/1
✓ Branch 102 → 103 taken 7 times.
7 emit self->attendeeBusyReady(sessionId, rows);
133
1/10
✓ Branch 30 → 31 taken 15 times.
✗ Branch 63 → 64 not taken.
✗ Branch 64 → 65 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 153 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 153 not taken.
✗ Branch 166 → 167 not taken.
✗ Branch 166 → 168 not taken.
127 });
134 7 }
135
136 42 QVariantList QuickComposeBackend::daySpans(const QVariantList &busy, const QDateTime &day, int dayStartHour,
137 int dayEndHour) const {
138 42 QVariantList out;
139
6/11
✓ Branch 2 → 3 taken 42 times.
✓ Branch 3 → 4 taken 42 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 42 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 42 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 42 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 42 times.
42 if (!day.isValid() || dayStartHour < 0 || dayEndHour > 24 || dayEndHour <= dayStartHour) return out;
140
2/4
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 42 times.
✗ Branch 12 → 14 not taken.
✓ Branch 13 → 14 taken 42 times.
42 const QTime end = dayEndHour == 24 ? QTime(23, 59, 59, 999) : QTime(dayEndHour, 0);
141 42 for (const FreeSlots::Span &span :
142
9/9
✓ Branch 15 → 16 taken 42 times.
✓ Branch 16 → 17 taken 42 times.
✓ Branch 17 → 18 taken 42 times.
✓ Branch 18 → 19 taken 42 times.
✓ Branch 19 → 20 taken 42 times.
✓ Branch 23 → 24 taken 42 times.
✓ Branch 24 → 25 taken 42 times.
✓ Branch 49 → 26 taken 20 times.
✓ Branch 49 → 50 taken 42 times.
104 FreeSlots::daySpans(intervalsFrom(busy), day.toLocalTime().date(), QTime(dayStartHour, 0), end))
143
3/5
✓ Branch 37 → 38 taken 20 times.
✓ Branch 41 → 42 taken 40 times.
✓ Branch 41 → 43 taken 20 times.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
142 out.append(QVariantMap{{QStringLiteral("from"), span.from}, {QStringLiteral("to"), span.to}});
144 42 return out;
145 60 }
146
147 21 QString QuickComposeBackend::busyDescription(const QVariantList &busy, const QDateTime &day, int dayStartHour,
148 int dayEndHour) const {
149
1/1
✓ Branch 2 → 3 taken 21 times.
21 const QVariantList spans = daySpans(busy, day, dayStartHour, dayEndHour);
150
3/3
✓ Branch 4 → 5 taken 14 times.
✓ Branch 4 → 7 taken 7 times.
✓ Branch 5 → 6 taken 14 times.
21 if (spans.isEmpty()) return tr("free all day");
151
1/1
✓ Branch 8 → 9 taken 7 times.
7 const QVariantMap only = spans.first().toMap();
152
13/22
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 26 taken 3 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 26 taken 2 times.
✓ Branch 37 → 38 taken 4 times.
✓ Branch 37 → 39 taken 3 times.
✓ Branch 39 → 40 taken 4 times.
✓ Branch 39 → 41 taken 3 times.
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 43 taken 3 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 46 taken 7 times.
✗ Branch 126 → 127 not taken.
✗ Branch 126 → 128 not taken.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
20 if (spans.size() == 1 && only.value(QStringLiteral("from")).toDouble() <= 0.0 &&
153
13/22
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 5 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 5 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 5 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 5 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 37 taken 3 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.
11 only.value(QStringLiteral("to")).toDouble() >= 1.0)
154 ✗ return tr("busy all day");
155
4/4
✓ Branch 46 → 47 taken 7 times.
✓ Branch 47 → 48 taken 7 times.
✓ Branch 48 → 49 taken 7 times.
✓ Branch 49 → 50 taken 7 times.
7 const QDateTime start(day.toLocalTime().date(), QTime(dayStartHour, 0));
156 7 const qint64 length = qint64(dayEndHour - dayStartHour) * 3600 * 1000;
157
1/1
✓ Branch 51 → 52 taken 7 times.
7 const QLocale locale;
158 // Fractions back to wall times, on the minute (13:00, not 12:59).
159
2/2
✓ Branch 3 → 4 taken 20 times.
✓ Branch 4 → 5 taken 20 times.
20 const auto at = [&](double fraction) { return start.addSecs(qRound64(fraction * length / 60000.0) * 60).time(); };
160 7 QStringList stretches;
161
2/2
✓ Branch 90 → 54 taken 10 times.
✓ Branch 90 → 91 taken 7 times.
17 for (const QVariant &value : spans) {
162
1/1
✓ Branch 55 → 56 taken 10 times.
10 const QVariantMap span = value.toMap();
163
3/3
✓ Branch 60 → 61 taken 10 times.
✓ Branch 61 → 62 taken 10 times.
✓ Branch 62 → 63 taken 10 times.
20 const QTime from = at(span.value(QStringLiteral("from")).toDouble());
164
3/3
✓ Branch 71 → 72 taken 10 times.
✓ Branch 72 → 73 taken 10 times.
✓ Branch 73 → 74 taken 10 times.
20 const QTime to = at(span.value(QStringLiteral("to")).toDouble());
165
5/5
✓ Branch 78 → 79 taken 10 times.
✓ Branch 79 → 80 taken 10 times.
✓ Branch 80 → 81 taken 10 times.
✓ Branch 81 → 82 taken 10 times.
✓ Branch 82 → 83 taken 10 times.
10 stretches.append(tr("%1 to %2").arg(locale.toString(from, QLocale::ShortFormat), locale.toString(to, QLocale::ShortFormat)));
166 10 }
167
3/3
✓ Branch 91 → 92 taken 7 times.
✓ Branch 96 → 97 taken 7 times.
✓ Branch 97 → 98 taken 7 times.
14 return tr("busy %1, free otherwise").arg(stretches.join(QStringLiteral(", ")));
168 21 }
169
170 1 QVariantMap QuickComposeBackend::nextFreeForAll(const QVariantList &busy, const QDateTime &after, int minutes,
171 int dayStartHour, int dayEndHour, const QVariantList &workdays) const {
172
1/1
✓ Branch 2 → 3 taken 1 time.
1 FreeSlots::Options options;
173
2/4
✓ Branch 3 → 4 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
✗ Branch 6 → 7 not taken.
1 const QDateTime start = after.isValid() ? after : QDateTime::currentDateTime();
174
1/1
✓ Branch 7 → 8 taken 1 time.
1 options.from = start.date();
175 1 options.days = 14;
176
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
1 options.dayStart = QTime(qBound(0, dayStartHour, 23), 0);
177
3/5
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
✗ Branch 11 → 14 not taken.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
1 options.dayEnd = dayEndHour >= 24 ? QTime(23, 59) : QTime(qBound(1, dayEndHour, 23), 0);
178
1/1
✓ Branch 14 → 15 taken 1 time.
1 options.durationMinutes = qBound(5, minutes, 480);
179
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
1 options.notBefore = qMax(start, QDateTime::currentDateTime());
180 1 options.maximum = 1;
181 1 options.workingDays.clear();
182
4/4
✓ Branch 23 → 24 taken 7 times.
✓ Branch 24 → 25 taken 7 times.
✓ Branch 27 → 22 taken 7 times.
✓ Branch 27 → 28 taken 1 time.
8 for (const QVariant &day : workdays) options.workingDays.insert(day.toInt());
183
1/3
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 34 taken 1 time.
✗ Branch 30 → 31 not taken.
1 if (options.workingDays.isEmpty()) options.workingDays = {1, 2, 3, 4, 5};
184
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
1 const auto found = FreeSlots::find(intervalsFrom(busy), options);
185
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 1 time.
1 if (found.isEmpty()) return {};
186
2/4
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 54 taken 1 time.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
5 return {{QStringLiteral("start"), found.first().start}, {QStringLiteral("end"), found.first().end}};
187 4 }
188
189 15 QVariantList QuickComposeBackend::freeSlots(int durationMinutes, int dayStartHour, int dayEndHour,
190 const QVariantList &workdays, const QStringList &hiddenCalendars) const {
191 15 QElapsedTimer timer;
192 15 timer.start();
193
1/1
✓ Branch 3 → 4 taken 15 times.
15 FreeSlots::Options options;
194
1/1
✓ Branch 4 → 5 taken 15 times.
15 options.from = QDate::currentDate();
195 15 options.days = 14;
196
2/2
✓ Branch 5 → 6 taken 15 times.
✓ Branch 6 → 7 taken 15 times.
15 options.dayStart = QTime(qBound(0, dayStartHour, 23), 0);
197
5/5
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 13 times.
✓ Branch 8 → 11 taken 2 times.
✓ Branch 9 → 10 taken 13 times.
✓ Branch 10 → 11 taken 13 times.
15 options.dayEnd = dayEndHour >= 24 ? QTime(23, 59) : QTime(qBound(1, dayEndHour, 23), 0);
198 15 options.durationMinutes = durationMinutes;
199
1/1
✓ Branch 11 → 12 taken 15 times.
15 options.notBefore = QDateTime::currentDateTime();
200 15 options.workingDays.clear();
201
4/4
✓ Branch 18 → 19 taken 101 times.
✓ Branch 19 → 20 taken 101 times.
✓ Branch 22 → 17 taken 101 times.
✓ Branch 22 → 23 taken 15 times.
116 for (const QVariant &day : workdays) options.workingDays.insert(day.toInt());
202
1/3
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 29 taken 15 times.
✗ Branch 25 → 26 not taken.
15 if (options.workingDays.isEmpty()) options.workingDays = {1, 2, 3, 4, 5};
203
1/1
✓ Branch 29 → 30 taken 15 times.
15 const QList<FreeSlots::Interval> busy = ownBusyIntervals(options.from, options.days, hiddenCalendars);
204 15 QVariantList out;
205
1/1
✓ Branch 30 → 31 taken 15 times.
15 const QLocale locale;
206 // Hours and minutes only (the short format of some locales has seconds).
207
1/1
✓ Branch 31 → 32 taken 15 times.
15 QString timeFormat = locale.timeFormat(QLocale::ShortFormat);
208
1/1
✓ Branch 35 → 36 taken 15 times.
15 timeFormat.remove(QStringLiteral(":ss"));
209
5/5
✓ Branch 38 → 39 taken 15 times.
✓ Branch 39 → 40 taken 15 times.
✓ Branch 40 → 41 taken 15 times.
✓ Branch 100 → 42 taken 600 times.
✓ Branch 100 → 101 taken 15 times.
615 for (const FreeSlots::Interval &slot : FreeSlots::find(busy, options))
210
1/1
✓ Branch 75 → 76 taken 600 times.
1800 out.append(QVariantMap{{QStringLiteral("start"), slot.start},
211 1200 {QStringLiteral("end"), slot.end},
212
2/2
✓ Branch 57 → 58 taken 600 times.
✓ Branch 58 → 59 taken 600 times.
1800 {QStringLiteral("day"), locale.toString(slot.start.date(), QStringLiteral("ddd d MMM"))},
213
3/3
✓ Branch 68 → 69 taken 600 times.
✓ Branch 69 → 70 taken 600 times.
✓ Branch 70 → 71 taken 600 times.
2400 {QStringLiteral("time"), locale.toString(slot.start.time(), timeFormat) + QStringLiteral("–") +
214
5/7
✓ Branch 63 → 64 taken 600 times.
✓ Branch 64 → 65 taken 600 times.
✓ Branch 71 → 72 taken 600 times.
✓ Branch 79 → 80 taken 2400 times.
✓ Branch 79 → 81 taken 600 times.
✗ Branch 148 → 149 not taken.
✗ Branch 148 → 150 not taken.
5415 locale.toString(slot.end.time(), timeFormat)}});
215
2/10
✓ Branch 102 → 103 taken 15 times.
✗ Branch 106 → 107 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 109 → 110 not taken.
✗ Branch 110 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 119 → 104 not taken.
✓ Branch 119 → 120 taken 15 times.
15 qCInfo(lcCompose) << "free slots" << out.size() << "busy" << busy.size() << "ms" << timer.elapsed();
216 15 return out;
217 3015 }
218
219 2 bool QuickComposeBackend::insertSuggestedTimes(QObject *sessionObject, const QVariantList &chosen) {
220
1/1
✓ Branch 2 → 3 taken 2 times.
2 auto *session = qobject_cast<ComposeSession *>(sessionObject);
221
4/8
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
2 if (!session || chosen.isEmpty() || !session->document()) return false;
222
1/1
✓ Branch 13 → 14 taken 2 times.
2 const MessageLanguageText language(session->language());
223
2/4
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 2 times.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
4 QStringList lines{language.suggestTimesIntro()};
224
5/5
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 61 → 24 taken 5 times.
✓ Branch 61 → 62 taken 2 times.
7 for (const QVariant &value : chosen.mid(0, 5)) {
225
1/1
✓ Branch 25 → 26 taken 5 times.
5 const QVariantMap slot = value.toMap();
226
4/4
✓ Branch 36 → 37 taken 5 times.
✓ Branch 37 → 38 taken 5 times.
✓ Branch 38 → 39 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
20 lines.append(QStringLiteral("• ") + language.timeSlot(slot.value(QStringLiteral("start")).toDateTime(),
227
3/3
✓ Branch 30 → 31 taken 5 times.
✓ Branch 31 → 32 taken 5 times.
✓ Branch 42 → 43 taken 5 times.
20 slot.value(QStringLiteral("end")).toDateTime()));
228 7 }
229
3/3
✓ Branch 63 → 64 taken 2 times.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
2 lines.append(language.timeZoneNote(QTimeZone::systemTimeZone()));
230
3/3
✓ Branch 73 → 74 taken 2 times.
✓ Branch 74 → 75 taken 2 times.
✓ Branch 75 → 76 taken 2 times.
2 session->editor().insertPlainText(lines.join(QLatin1Char('\n')) + QLatin1Char('\n'));
231
2/6
✓ Branch 78 → 79 taken 2 times.
✗ Branch 82 → 83 not taken.
✗ Branch 83 → 84 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 90 → 80 not taken.
✓ Branch 90 → 91 taken 2 times.
2 qCInfo(lcCompose) << "suggested times inserted" << qMin(int(chosen.size()), 5);
232 2 return true;
233 4 }
234
235 // D-23: the invitation left — its event goes into the chosen calendar now
236 // (so Undo never had anything to remove), read back from the sent iMIP
237 // part: the organizer's copy with every attendee waiting for an answer.
238 2 bool QuickComposeBackend::writeOrganizedEvent(const OutboxEntry &entry, const QByteArray &sentBytes,
239 bool serverSchedules) {
240
3/3
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
2 const QJsonObject target = QJsonDocument::fromJson(entry.replyReference.toUtf8()).object();
241
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
2 const QString accountId = target.value(QStringLiteral("calendarAccount")).toString();
242
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
2 const QString path = target.value(QStringLiteral("calendarPath")).toString();
243 2 auto *sync = m_session.calendarSync();
244
4/8
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 29 not taken.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 29 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 2 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 44 taken 2 times.
2 if (!sync || accountId.isEmpty() || path.isEmpty()) {
245 ✗ qCWarning(lcCompose) << "invitation sent without a calendar" << entry.id;
246 ✗ return false;
247 }
248 2 QString calendarText;
249
4/5
✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 58 → 48 taken 2 times.
✗ Branch 58 → 59 not taken.
2 for (const MimePart &part : MimeParser::parse(sentBytes).attachments)
250
1/2
✓ Branch 51 → 52 taken 2 times.
✗ Branch 51 → 56 not taken.
2 if (part.contentType.compare(QLatin1String("text/calendar"), Qt::CaseInsensitive) == 0) {
251
1/1
✓ Branch 52 → 53 taken 2 times.
2 calendarText = QString::fromUtf8(part.body);
252 2 break;
253 2 }
254
1/1
✓ Branch 60 → 61 taken 2 times.
2 const auto invitation = Imip::parse(calendarText);
255
3/6
✓ Branch 62 → 63 taken 2 times.
✗ Branch 62 → 67 not taken.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 2 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 82 taken 2 times.
2 if (!invitation || invitation->method != QLatin1String("REQUEST")) {
256 ✗ qCWarning(lcCompose) << "invitation without a readable calendar part" << entry.id;
257 ✗ return false;
258 }
259 // S108.8.9: an invitation started from the event editor names a stored
260 // event: it takes the organizer, attendees and SEQUENCE and keeps the
261 // rest (reminders, people, recurrence) as the user saved it.
262
3/3
✓ Branch 83 → 84 taken 2 times.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 124 taken 1 time.
2 if (const auto stored = sync->eventFor(invitation->uid, accountId, path)) {
263 1 CalendarEvent updated = *stored;
264
2/2
✓ Branch 93 → 94 taken 1 time.
✓ Branch 94 → 95 taken 1 time.
2 updated.icalRaw = Imip::withMeetingAttendees(storedComponentText(*stored), invitation->organizerEmail,
265 1 invitation->organizerName, invitation->attendees, invitation->sequence,
266 1 serverSchedules);
267
3/5
✓ Branch 99 → 100 taken 1 time.
✗ Branch 99 → 103 not taken.
✓ Branch 100 → 101 taken 1 time.
✓ Branch 101 → 102 taken 1 time.
✗ Branch 101 → 103 not taken.
1 const bool saved = !updated.icalRaw.isEmpty() && sync->saveEvent(updated, false);
268
2/7
✓ Branch 104 → 105 taken 1 time.
✗ Branch 108 → 109 not taken.
✗ Branch 109 → 110 not taken.
✗ Branch 110 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 116 → 106 not taken.
✓ Branch 116 → 117 taken 1 time.
1 qCInfo(lcCompose) << "organized event updated" << saved << invitation->uid;
269
1/4
✗ Branch 117 → 118 not taken.
✓ Branch 117 → 122 taken 1 time.
✗ Branch 118 → 119 not taken.
✗ Branch 119 → 120 not taken.
1 if (!saved) m_deps.reportError(tr("The invitation was sent, but the appointment could not be saved to the calendar."));
270 1 return saved;
271 3 }
272
1/1
✓ Branch 132 → 134 taken 1 time.
1 QList<CalendarEvent> rows = Imip::eventsToStore(*invitation, QString(), QString(), QString());
273
4/4
✓ Branch 137 → 138 taken 1 time.
✓ Branch 138 → 139 taken 1 time.
✓ Branch 154 → 140 taken 1 time.
✓ Branch 154 → 155 taken 1 time.
2 for (CalendarEvent &row : rows) {
274 1 row.accountId = accountId;
275 1 row.calendarPath = path;
276 // Server mode (D-24): the server schedules — no SCHEDULE-AGENT=CLIENT.
277
1/2
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 152 taken 1 time.
1 if (serverSchedules)
278 ✗ row.icalRaw = Imip::withMeetingAttendees(row.icalRaw, invitation->organizerEmail, invitation->organizerName,
279 ✗ invitation->attendees, invitation->sequence, true);
280 }
281
1/1
✓ Branch 156 → 157 taken 1 time.
1 const bool stored = sync->restoreEventSeries(invitation->uid, accountId, path, rows);
282
2/8
✓ Branch 157 → 158 taken 1 time.
✗ Branch 161 → 162 not taken.
✗ Branch 162 → 163 not taken.
✗ Branch 163 → 164 not taken.
✗ Branch 164 → 165 not taken.
✗ Branch 166 → 167 not taken.
✗ Branch 170 → 159 not taken.
✓ Branch 170 → 171 taken 1 time.
1 qCInfo(lcCompose) << "organized event written" << stored << "rows" << rows.size();
283
1/4
✗ Branch 171 → 172 not taken.
✓ Branch 171 → 176 taken 1 time.
✗ Branch 172 → 173 not taken.
✗ Branch 173 → 174 not taken.
1 if (!stored) m_deps.reportError(tr("The invitation was sent, but the appointment could not be saved to the calendar."));
284 1 return stored;
285 2 }
286