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: 97.2% 239 / 0 / 246
Functions: 100.0% 20 / 0 / 20
Branches: 69.9% 452 / 0 / 647

presentation/QuickCalendarIcs.cpp
Line Branch Exec Source
1 #include "QuickCalendarIcs.h"
2
3 #include "CalendarEventDetails.h"
4 #include "app/core/CalendarSyncCoordinator.h"
5 #include "app/core/MailSessionCore.h"
6 #include "data/CalendarStore.h"
7 #include "data/ICalTodoCodec.h"
8 #include "TaskListLayout.h"
9 #include "data/MailCache.h"
10 #include "service/MailPayloadExportService.h"
11
12 #include <QDir>
13 #include <QFile>
14 #include <QFileInfo>
15 #include <QLoggingCategory>
16 #include <QSaveFile>
17 #include <QStandardPaths>
18
19
4/7
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 8 taken 13 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
16 Q_LOGGING_CATEGORY(lcCalendarIcs, "mailjd.quick.ics")
20
21 799 QuickCalendarIcs::QuickCalendarIcs(MailSessionCore &session, Deps deps, QObject *parent)
22 799 : QObject(parent), m_session(session), m_deps(std::move(deps)) {}
23
24 1540 QVariantList QuickCalendarIcs::items() const {
25 1540 QVariantList list;
26
1/2
✓ Branch 3 → 4 taken 1540 times.
✗ Branch 3 → 6 not taken.
1540 CalendarStore *store = m_session.calendarSync() ? m_session.calendarSync()->store() : nullptr;
27
2/2
✓ Branch 87 → 8 taken 14 times.
✓ Branch 87 → 88 taken 1540 times.
1554 for (int i = 0; i < m_entries.size(); ++i) {
28 14 const IcsFile::Entry &entry = m_entries.at(i);
29 14 const CalendarEvent &master = entry.rows.first();
30 14 QStringList existsIn;
31
1/2
✓ Branch 10 → 11 taken 14 times.
✗ Branch 10 → 26 not taken.
14 if (store)
32
5/5
✓ Branch 11 → 12 taken 14 times.
✓ Branch 12 → 13 taken 14 times.
✓ Branch 13 → 14 taken 14 times.
✓ Branch 23 → 15 taken 7 times.
✓ Branch 23 → 24 taken 14 times.
21 for (const auto &[accountId, path] : store->calendarsWithUid(entry.uid))
33
2/2
✓ Branch 18 → 19 taken 7 times.
✓ Branch 19 → 20 taken 7 times.
21 existsIn << calendarSelectionKey(accountId, path);
34
1/1
✓ Branch 64 → 65 taken 14 times.
42 list.append(QVariantMap{{QStringLiteral("uid"), entry.uid},
35
3/3
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 36 taken 12 times.
✓ Branch 35 → 37 taken 2 times.
28 {QStringLiteral("summary"), master.summary.isEmpty() ? tr("(No title)") : master.summary},
36
1/1
✓ Branch 41 → 42 taken 14 times.
28 {QStringLiteral("whenText"), CalendarEventDetails::whenText(master)},
37
3/4
✓ Branch 47 → 48 taken 12 times.
✓ Branch 47 → 50 taken 2 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 12 times.
28 {QStringLiteral("recurring"), !master.rrule.isEmpty() || entry.rows.size() > 1},
38 28 {QStringLiteral("existsIn"), existsIn},
39
3/5
✓ Branch 60 → 61 taken 14 times.
✓ Branch 68 → 69 taken 84 times.
✓ Branch 68 → 70 taken 14 times.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
112 {QStringLiteral("checked"), m_checked.value(i)}});
40 14 }
41
2/2
✓ Branch 183 → 89 taken 2 times.
✓ Branch 183 → 184 taken 1540 times.
1542 for (int i = 0; i < m_todos.size(); ++i) {
42 2 ICalTodoCodec::Resource resource;
43
4/7
✓ Branch 90 → 91 taken 2 times.
✓ Branch 91 → 92 taken 2 times.
✗ Branch 91 → 94 not taken.
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 95 taken 2 times.
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 2 times.
2 if (!ICalTodoCodec::parseResource(m_todos.at(i).resource, &resource) || resource.tasks.isEmpty()) continue;
44
1/1
✓ Branch 98 → 99 taken 2 times.
2 const CalendarTask &task = resource.tasks.first();
45
2/2
✓ Branch 99 → 100 taken 2 times.
✓ Branch 100 → 101 taken 2 times.
2 const QString due = TaskListLayout::dueText(task, QDate::currentDate());
46
1/1
✓ Branch 147 → 148 taken 2 times.
6 list.append(QVariantMap{{QStringLiteral("uid"), task.uid},
47 6 {QStringLiteral("kind"), QStringLiteral("task")},
48
1/3
✗ Branch 116 → 117 not taken.
✓ Branch 116 → 118 taken 2 times.
✗ Branch 117 → 119 not taken.
4 {QStringLiteral("summary"), task.summary.isEmpty() ? tr("(No title)") : task.summary},
49
4/9
✗ Branch 124 → 125 not taken.
✓ Branch 124 → 126 taken 2 times.
✗ Branch 125 → 129 not taken.
✓ Branch 126 → 127 taken 2 times.
✓ Branch 128 → 129 taken 2 times.
✓ Branch 161 → 162 taken 2 times.
✗ Branch 161 → 163 not taken.
✗ Branch 290 → 291 not taken.
✗ Branch 290 → 292 not taken.
4 {QStringLiteral("whenText"), due.isEmpty() ? tr("Task") : tr("Task · due %1").arg(due)},
50 4 {QStringLiteral("recurring"), !task.rrule.isEmpty()},
51 4 {QStringLiteral("existsIn"), QStringList{}},
52
3/5
✓ Branch 143 → 144 taken 2 times.
✓ Branch 151 → 152 taken 14 times.
✓ Branch 151 → 153 taken 2 times.
✗ Branch 261 → 262 not taken.
✗ Branch 261 → 263 not taken.
18 {QStringLiteral("checked"), m_todoChecked.value(i)}});
53 2 }
54 1540 return list;
55 114 }
56
57 3072 int QuickCalendarIcs::checkedCount() const { return int(m_checked.count(true) + m_todoChecked.count(true)); }
58
59 4673 QVariantList QuickCalendarIcs::calendars() const {
60 4673 QVariantList list;
61
5/5
✓ Branch 2 → 3 taken 4673 times.
✓ Branch 3 → 4 taken 4673 times.
✓ Branch 4 → 5 taken 4673 times.
✓ Branch 81 → 6 taken 5298 times.
✓ Branch 81 → 82 taken 4673 times.
9971 for (const QVariant &value : m_deps.calendarChoices()) {
62
1/1
✓ Branch 7 → 8 taken 5298 times.
5298 const QVariantMap choice = value.toMap();
63
4/4
✓ Branch 12 → 13 taken 5298 times.
✓ Branch 13 → 14 taken 5298 times.
✓ Branch 18 → 19 taken 155 times.
✓ Branch 18 → 20 taken 5143 times.
10596 if (choice.value(QStringLiteral("readOnly")).toBool()) continue;
64
2/2
✓ Branch 27 → 28 taken 5143 times.
✓ Branch 49 → 50 taken 5143 times.
20572 list.append(QVariantMap{{QStringLiteral("key"), choice.value(QStringLiteral("key"))},
65
1/1
✓ Branch 36 → 37 taken 5143 times.
15429 {QStringLiteral("displayName"), choice.value(QStringLiteral("displayName"))},
66
3/5
✓ Branch 45 → 46 taken 5143 times.
✓ Branch 53 → 54 taken 15429 times.
✓ Branch 53 → 55 taken 5143 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
30858 {QStringLiteral("color"), choice.value(QStringLiteral("color"))}});
67 9971 }
68 4673 return list;
69 20572 }
70
71 404 QString QuickCalendarIcs::defaultCalendar() const {
72
2/2
✓ Branch 2 → 3 taken 404 times.
✓ Branch 6 → 7 taken 404 times.
808 const QStringList last = m_deps.lastCalendar().split(QLatin1Char('\n'));
73
3/3
✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 13 taken 399 times.
✓ Branch 12 → 14 taken 5 times.
404 const QString lastKey = last.size() == 2 ? calendarSelectionKey(last.at(0), last.at(1)) : QString();
74 404 QString first;
75
5/5
✓ Branch 14 → 15 taken 404 times.
✓ Branch 15 → 16 taken 404 times.
✓ Branch 16 → 17 taken 404 times.
✓ Branch 44 → 18 taken 647 times.
✓ Branch 44 → 45 taken 400 times.
1047 for (const QVariant &value : calendars()) {
76
3/3
✓ Branch 19 → 20 taken 647 times.
✓ Branch 24 → 25 taken 647 times.
✓ Branch 25 → 26 taken 647 times.
1294 const QString key = value.toMap().value(QStringLiteral("key")).toString();
77
2/2
✓ Branch 32 → 33 taken 4 times.
✓ Branch 32 → 34 taken 643 times.
647 if (key == lastKey) return key;
78
2/2
✓ Branch 35 → 36 taken 400 times.
✓ Branch 35 → 37 taken 243 times.
643 if (first.isEmpty()) first = key;
79 1047 }
80 400 return first;
81 404 }
82
83 6 bool QuickCalendarIcs::resolveCalendar(const QString &key, QString *accountId, QString *path) const {
84
5/5
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 4 → 5 taken 6 times.
✓ Branch 75 → 6 taken 16 times.
✓ Branch 75 → 76 taken 1 time.
17 for (const QVariant &value : m_deps.calendarChoices()) {
85
1/1
✓ Branch 7 → 8 taken 16 times.
16 const QVariantMap choice = value.toMap();
86
23/46
✓ Branch 12 → 13 taken 16 times.
✓ Branch 13 → 14 taken 16 times.
✓ Branch 15 → 16 taken 6 times.
✓ Branch 15 → 23 taken 10 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 5 times.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 25 → 27 taken 10 times.
✓ Branch 27 → 28 taken 6 times.
✓ Branch 27 → 29 taken 10 times.
✓ Branch 29 → 30 taken 6 times.
✓ Branch 29 → 31 taken 10 times.
✓ Branch 31 → 32 taken 6 times.
✓ Branch 31 → 33 taken 10 times.
✓ Branch 33 → 34 taken 16 times.
✗ Branch 33 → 35 not taken.
✓ Branch 35 → 36 taken 16 times.
✗ Branch 35 → 37 not taken.
✓ Branch 37 → 38 taken 16 times.
✗ Branch 37 → 39 not taken.
✓ Branch 39 → 40 taken 16 times.
✗ Branch 39 → 41 not taken.
✓ Branch 41 → 42 taken 16 times.
✗ Branch 41 → 43 not taken.
✓ Branch 43 → 44 taken 11 times.
✓ Branch 43 → 45 taken 5 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
✗ Branch 103 → 104 not taken.
✗ Branch 103 → 105 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 113 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
38 if (choice.value(QStringLiteral("key")).toString() != key || choice.value(QStringLiteral("readOnly")).toBool())
87 11 continue;
88
2/2
✓ Branch 49 → 50 taken 5 times.
✓ Branch 50 → 51 taken 5 times.
10 *accountId = choice.value(QStringLiteral("accountId")).toString();
89
2/2
✓ Branch 61 → 62 taken 5 times.
✓ Branch 62 → 63 taken 5 times.
10 *path = choice.value(QStringLiteral("path")).toString();
90 5 return true;
91 22 }
92 1 return false;
93 }
94
95 8 bool QuickCalendarIcs::openText(const QString &text, const QString &name) {
96 8 QString error;
97
1/1
✓ Branch 2 → 3 taken 8 times.
8 QList<IcsFile::Entry> entries = IcsFile::read(text, &error);
98
1/1
✓ Branch 3 → 4 taken 8 times.
8 QList<IcsFile::TodoEntry> todos = IcsFile::readTodos(text);
99
6/6
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 9 taken 6 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 30 taken 7 times.
8 if (entries.isEmpty() && todos.isEmpty()) {
100
7/7
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 13 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
2 qCWarning(lcCalendarIcs) << "No appointments or tasks in" << name << error;
101
3/3
✓ Branch 23 → 24 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
2 m_deps.setStatus(tr("%1 contains no appointments or tasks.").arg(name));
102 1 return false;
103 }
104 7 m_entries = std::move(entries);
105
1/1
✓ Branch 33 → 34 taken 7 times.
7 m_checked = QList<bool>(m_entries.size(), true);
106 7 m_todos = std::move(todos);
107
1/1
✓ Branch 39 → 40 taken 7 times.
7 m_todoChecked = QList<bool>(m_todos.size(), true);
108 7 m_sourceName = name;
109
1/1
✓ Branch 43 → 44 taken 7 times.
7 emit changed();
110 7 return true;
111 8 }
112
113 3 bool QuickCalendarIcs::openFile(const QUrl &url) {
114 // S106-407: a file opened now supersedes an attachment read still in
115 // flight — its late answer must not replace this preview.
116 3 ++m_readGeneration;
117
3/5
✓ Branch 2 → 3 taken 3 times.
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 7 taken 3 times.
✗ Branch 6 → 7 not taken.
3 const QString path = url.isLocalFile() ? url.toLocalFile() : url.toString();
118
1/1
✓ Branch 7 → 8 taken 3 times.
3 QFile file(path);
119
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
3 const QString name = QFileInfo(path).fileName();
120
3/3
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 20 taken 2 times.
3 if (file.size() > IcsFile::MaxBytes) {
121
3/3
✓ Branch 13 → 14 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
2 m_deps.setStatus(tr("%1 is too large for a calendar file.").arg(name));
122 1 return false;
123 }
124
3/3
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 44 taken 1 time.
2 if (!file.open(QIODevice::ReadOnly)) {
125
8/8
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 36 → 25 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
2 qCWarning(lcCalendarIcs) << "Cannot read" << path << file.errorString();
126
3/3
✓ Branch 37 → 38 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
2 m_deps.setStatus(tr("%1 could not be read.").arg(name));
127 1 return false;
128 }
129
3/3
✓ Branch 44 → 45 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
1 return openText(QString::fromUtf8(file.read(IcsFile::MaxBytes)), name);
130 3 }
131
132 2 bool QuickCalendarIcs::openAttachment(qint64 attachmentId, const QString &name) {
133
1/1
✓ Branch 2 → 3 taken 2 times.
2 const MailId mail = m_deps.currentMail();
134 2 auto *cache = m_session.cache();
135
6/8
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
2 if (!mail.isValid() || attachmentId <= 0 || !cache) return false;
136 3 MailPayloadRequest request{cache->databasePath(), mail, cache->uidValidity(mail.folderId),
137
1/5
✓ Branch 14 → 15 taken 1 time.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
1 MailPayloadRequest::Kind::Attachment, attachmentId, 0, IcsFile::MaxBytes};
138 1 const quint64 generation = ++m_readGeneration;
139
6/6
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 25 → 17 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
2 qCInfo(lcCalendarIcs) << "Reading calendar attachment" << attachmentId;
140
3/3
✓ Branch 26 → 27 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
2 MailPayloadExportService::shared()->readWindow(
141 2 this, request, [this, generation, name](QByteArray bytes, MailPayloadReadResult result) {
142
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (generation != m_readGeneration) return;
143
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 18 taken 1 time.
1 if (!result.completed) {
144 ✗ qCWarning(lcCalendarIcs) << "Calendar attachment unreadable:" << result.error;
145 ✗ m_deps.setStatus(result.error);
146 ✗ return;
147 }
148
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
1 openText(QString::fromUtf8(bytes), name);
149 });
150 1 return true;
151 2 }
152
153 4 void QuickCalendarIcs::setChecked(int index, bool checked) {
154 // Items list appointments first, then tasks.
155
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 1 time.
4 QList<bool> &flags = index < m_checked.size() ? m_checked : m_todoChecked;
156
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 10 taken 3 times.
4 const int at = index < m_checked.size() ? index : index - int(m_checked.size());
157
7/8
✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 16 not taken.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 16 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 2 times.
4 if (index < 0 || at >= flags.size() || flags.at(at) == checked) return;
158 2 flags[at] = checked;
159 2 emit changed();
160 }
161
162 3 void QuickCalendarIcs::setAllChecked(bool checked) {
163 3 m_checked.fill(checked);
164 3 m_todoChecked.fill(checked);
165 3 emit changed();
166 3 }
167
168 10 void QuickCalendarIcs::close() {
169 10 ++m_readGeneration;
170
6/6
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 7 taken 6 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 7 times.
10 if (m_entries.isEmpty() && m_todos.isEmpty()) return;
171 7 m_entries.clear();
172 7 m_checked.clear();
173 7 m_todos.clear();
174 7 m_todoChecked.clear();
175 7 m_sourceName.clear();
176 7 emit changed();
177 }
178
179 8 QVariantMap QuickCalendarIcs::importChecked(const QString &calendarKey, bool updateExisting) {
180
2/4
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 8 times.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
32 QVariantMap result{{QStringLiteral("ok"), false}};
181 8 auto *sync = m_session.calendarSync();
182 8 QString accountId, path;
183 8 const bool needsCalendar = m_checked.contains(true);
184
9/11
✓ Branch 14 → 15 taken 8 times.
✗ Branch 14 → 20 not taken.
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 20 not taken.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 21 taken 2 times.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 5 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 35 taken 7 times.
8 if (!sync || !sync->store() || (needsCalendar && !resolveCalendar(calendarKey, &accountId, &path))) {
185
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
2 result[QStringLiteral("text")] = tr("Choose a writable calendar.");
186 1 return result;
187 }
188
3/3
✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 38 taken 2 times.
✓ Branch 37 → 39 taken 5 times.
7 const QString color = needsCalendar ? sync->store()->calendarColor(accountId, path) : QString();
189 7 int imported = 0, updated = 0, skipped = 0, failed = 0;
190
2/2
✓ Branch 76 → 40 taken 17 times.
✓ Branch 76 → 77 taken 7 times.
24 for (int i = 0; i < m_entries.size(); ++i) {
191
3/3
✓ Branch 40 → 41 taken 17 times.
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 43 taken 13 times.
19 if (!m_checked.value(i)) continue;
192 13 const IcsFile::Entry &entry = m_entries.at(i);
193
1/1
✓ Branch 45 → 46 taken 13 times.
13 const bool exists = !sync->store()->eventSeries(entry.uid, accountId, path).isEmpty();
194
4/4
✓ Branch 48 → 49 taken 6 times.
✓ Branch 48 → 51 taken 7 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 4 times.
13 if (exists && !updateExisting) {
195 2 ++skipped;
196 2 continue;
197 }
198 11 QList<CalendarEvent> rows = entry.rows;
199
4/4
✓ Branch 52 → 53 taken 11 times.
✓ Branch 53 → 54 taken 11 times.
✓ Branch 61 → 55 taken 14 times.
✓ Branch 61 → 62 taken 11 times.
25 for (CalendarEvent &row : rows) {
200 14 row.accountId = accountId;
201 14 row.calendarPath = path;
202 14 row.color = color;
203 }
204
2/3
✓ Branch 62 → 63 taken 11 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 11 times.
11 if (!sync->restoreEventSeries(entry.uid, accountId, path, rows)) {
205 ✗ ++failed;
206 ✗ continue;
207 }
208
2/2
✓ Branch 65 → 66 taken 4 times.
✓ Branch 65 → 67 taken 7 times.
11 ++(exists ? updated : imported);
209 11 }
210 7 int taskUpdated = 0, taskSkipped = 0, taskFailed = 0;
211
1/1
✓ Branch 77 → 78 taken 7 times.
7 const int tasksImported = importTodos(calendarKey, updateExisting, &taskUpdated, &taskSkipped, &taskFailed);
212 7 updated += taskUpdated;
213 7 skipped += taskSkipped;
214 7 failed += taskFailed;
215
9/9
✓ Branch 78 → 79 taken 7 times.
✓ Branch 82 → 83 taken 7 times.
✓ Branch 83 → 84 taken 7 times.
✓ Branch 84 → 85 taken 7 times.
✓ Branch 85 → 86 taken 7 times.
✓ Branch 86 → 87 taken 7 times.
✓ Branch 87 → 88 taken 7 times.
✓ Branch 96 → 80 taken 7 times.
✓ Branch 96 → 97 taken 7 times.
14 qCInfo(lcCalendarIcs) << "Imported" << imported << "appointments and" << tasksImported << "tasks; updated" << updated
216
5/5
✓ Branch 88 → 89 taken 7 times.
✓ Branch 89 → 90 taken 7 times.
✓ Branch 90 → 91 taken 7 times.
✓ Branch 91 → 92 taken 7 times.
✓ Branch 92 → 93 taken 7 times.
7 << "skipped" << skipped << "failed" << failed;
217 7 QStringList parts;
218
10/12
✓ Branch 97 → 98 taken 3 times.
✓ Branch 97 → 108 taken 4 times.
✓ Branch 98 → 99 taken 1 time.
✓ Branch 98 → 100 taken 2 times.
✓ Branch 99 → 103 taken 1 time.
✓ Branch 100 → 101 taken 2 times.
✓ Branch 102 → 103 taken 2 times.
✓ Branch 103 → 104 taken 3 times.
✓ Branch 105 → 106 taken 2 times.
✓ Branch 105 → 107 taken 1 time.
✗ Branch 256 → 257 not taken.
✗ Branch 256 → 258 not taken.
7 if (imported) parts << (imported == 1 ? tr("1 appointment imported") : tr("%1 appointments imported").arg(imported));
219
6/12
✓ Branch 108 → 109 taken 1 time.
✓ Branch 108 → 119 taken 6 times.
✓ Branch 109 → 110 taken 1 time.
✗ Branch 109 → 111 not taken.
✓ Branch 110 → 114 taken 1 time.
✗ Branch 111 → 112 not taken.
✗ Branch 113 → 114 not taken.
✓ Branch 114 → 115 taken 1 time.
✗ Branch 116 → 117 not taken.
✓ Branch 116 → 118 taken 1 time.
✗ Branch 264 → 265 not taken.
✗ Branch 264 → 266 not taken.
7 if (tasksImported) parts << (tasksImported == 1 ? tr("1 task imported") : tr("%1 tasks imported").arg(tasksImported));
220
10/12
✓ Branch 119 → 120 taken 2 times.
✓ Branch 119 → 130 taken 5 times.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 122 taken 1 time.
✓ Branch 121 → 125 taken 1 time.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 124 → 125 taken 1 time.
✓ Branch 125 → 126 taken 2 times.
✓ Branch 127 → 128 taken 1 time.
✓ Branch 127 → 129 taken 1 time.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 274 not taken.
7 if (updated) parts << (updated == 1 ? tr("1 updated") : tr("%1 updated").arg(updated));
221
7/12
✓ Branch 130 → 131 taken 1 time.
✓ Branch 130 → 141 taken 6 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 133 taken 1 time.
✗ Branch 132 → 136 not taken.
✓ Branch 133 → 134 taken 1 time.
✓ Branch 135 → 136 taken 1 time.
✓ Branch 136 → 137 taken 1 time.
✓ Branch 138 → 139 taken 1 time.
✗ Branch 138 → 140 not taken.
✗ Branch 280 → 281 not taken.
✗ Branch 280 → 282 not taken.
7 if (skipped) parts << (skipped == 1 ? tr("1 already existed") : tr("%1 already existed").arg(skipped));
222
1/12
✗ Branch 141 → 142 not taken.
✓ Branch 141 → 152 taken 7 times.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
✗ Branch 143 → 147 not taken.
✗ Branch 144 → 145 not taken.
✗ Branch 146 → 147 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
✗ Branch 288 → 289 not taken.
✗ Branch 288 → 290 not taken.
7 if (failed) parts << (failed == 1 ? tr("1 could not be saved") : tr("%1 could not be saved").arg(failed));
223
11/17
✓ Branch 153 → 154 taken 1 time.
✓ Branch 153 → 155 taken 6 times.
✓ Branch 154 → 162 taken 1 time.
✓ Branch 160 → 161 taken 6 times.
✓ Branch 161 → 162 taken 6 times.
✓ Branch 162 → 163 taken 6 times.
✓ Branch 162 → 164 taken 1 time.
✓ Branch 164 → 165 taken 6 times.
✓ Branch 164 → 166 taken 1 time.
✓ Branch 166 → 167 taken 6 times.
✓ Branch 166 → 168 taken 1 time.
✗ Branch 293 → 294 not taken.
✗ Branch 293 → 295 not taken.
✗ Branch 297 → 298 not taken.
✗ Branch 297 → 299 not taken.
✗ Branch 301 → 302 not taken.
✗ Branch 301 → 303 not taken.
13 const QString text = parts.isEmpty() ? tr("Nothing was imported.") : parts.join(QStringLiteral(", ")) + QLatin1Char('.');
224 14 result = {{QStringLiteral("ok"), failed == 0},
225 14 {QStringLiteral("imported"), imported},
226 14 {QStringLiteral("updated"), updated},
227 14 {QStringLiteral("skipped"), skipped},
228
2/4
✓ Branch 192 → 193 taken 35 times.
✓ Branch 192 → 194 taken 7 times.
✗ Branch 308 → 309 not taken.
✗ Branch 308 → 310 not taken.
49 {QStringLiteral("text"), text}};
229
1/1
✓ Branch 204 → 205 taken 7 times.
7 m_deps.setStatus(text);
230
2/3
✓ Branch 205 → 206 taken 7 times.
✗ Branch 205 → 207 not taken.
✓ Branch 206 → 207 taken 7 times.
7 if (failed == 0) close();
231 7 return result;
232 74 }
233
234 8 QList<QList<CalendarEvent>> QuickCalendarIcs::seriesOf(const QVariantMap &occurrence) const {
235 8 auto *sync = m_session.calendarSync();
236
3/6
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 8 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 8 times.
8 if (!sync || !sync->store()) return {};
237
2/2
✓ Branch 27 → 28 taken 8 times.
✓ Branch 28 → 29 taken 8 times.
24 const QList<CalendarEvent> rows = sync->store()->eventSeries(occurrence.value(QStringLiteral("uid")).toString(),
238
2/2
✓ Branch 21 → 22 taken 8 times.
✓ Branch 22 → 23 taken 8 times.
24 occurrence.value(QStringLiteral("accountId")).toString(),
239
3/3
✓ Branch 15 → 16 taken 8 times.
✓ Branch 16 → 17 taken 8 times.
✓ Branch 29 → 30 taken 8 times.
32 occurrence.value(QStringLiteral("calendarPath")).toString());
240
6/10
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 6 times.
✓ Branch 50 → 51 taken 6 times.
✓ Branch 50 → 54 taken 2 times.
✓ Branch 52 → 53 taken 6 times.
✓ Branch 52 → 54 taken 6 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 106 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
20 return rows.isEmpty() ? QList<QList<CalendarEvent>>{} : QList<QList<CalendarEvent>>{rows};
241 14 }
242
243 8 QString QuickCalendarIcs::writeFile(const QString &directory, const QString &name, const QByteArray &bytes,
244 bool unique) {
245
9/16
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 8 times.
✓ Branch 6 → 7 taken 8 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 6 times.
✓ Branch 10 → 11 taken 8 times.
✗ Branch 10 → 12 not taken.
✓ Branch 12 → 13 taken 8 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 6 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
✗ Branch 97 → 98 not taken.
✗ Branch 97 → 99 not taken.
8 if (directory.isEmpty() || !QDir().mkpath(directory)) return {};
246
2/2
✓ Branch 16 → 17 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
6 QString target = QDir(directory).filePath(name);
247 // An existing download keeps its name: "Title (2).ics".
248
8/9
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 46 taken 3 times.
✓ Branch 42 → 43 taken 4 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 46 taken 3 times.
✓ Branch 44 → 45 taken 1 time.
✗ Branch 44 → 46 not taken.
✓ Branch 47 → 20 taken 1 time.
✓ Branch 47 → 48 taken 6 times.
7 for (int n = 2; unique && QFileInfo::exists(target) && n < 1000; ++n)
249
6/6
✓ Branch 20 → 21 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
4 target = QDir(directory).filePath(QStringLiteral("%1 (%2).ics").arg(QFileInfo(name).completeBaseName()).arg(n));
250
1/1
✓ Branch 48 → 49 taken 6 times.
6 QSaveFile file(target);
251
7/11
✓ Branch 50 → 51 taken 6 times.
✓ Branch 51 → 52 taken 6 times.
✗ Branch 51 → 57 not taken.
✓ Branch 52 → 53 taken 6 times.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 54 → 57 not taken.
✓ Branch 55 → 56 taken 6 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 6 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 75 taken 6 times.
6 if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
252 ✗ qCWarning(lcCalendarIcs) << "Cannot write" << target << file.errorString();
253 ✗ return {};
254 }
255
7/7
✓ Branch 75 → 76 taken 6 times.
✓ Branch 79 → 80 taken 6 times.
✓ Branch 80 → 81 taken 6 times.
✓ Branch 82 → 83 taken 6 times.
✓ Branch 83 → 84 taken 6 times.
✓ Branch 87 → 77 taken 6 times.
✓ Branch 87 → 88 taken 6 times.
12 qCInfo(lcCalendarIcs) << "Wrote calendar file of" << bytes.size() << "bytes";
256 6 return target;
257 6 }
258
259 4 bool QuickCalendarIcs::exportOccurrence(const QVariantMap &occurrence) {
260
1/1
✓ Branch 2 → 3 taken 4 times.
4 const auto series = seriesOf(occurrence);
261
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 3 times.
4 if (series.isEmpty()) return false;
262 3 const QString directory = m_deps.downloadDirectory.isEmpty()
263
1/3
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
✗ Branch 8 → 10 not taken.
3 ? QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) : m_deps.downloadDirectory;
264
3/3
✓ Branch 10 → 11 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
3 const QString path = writeFile(directory, IcsFile::fileName(series.first().first().summary), IcsFile::write(series), true);
265
5/5
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 2 times.
✓ Branch 19 → 25 taken 1 time.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 3 times.
7 m_deps.setStatus(path.isEmpty() ? tr("The calendar file could not be saved.")
266
9/15
✓ Branch 20 → 21 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 1 time.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 1 time.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 1 time.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
7 : tr("Saved %1.").arg(QFileInfo(path).fileName()));
267 3 return !path.isEmpty();
268 4 }
269
270 2 bool QuickCalendarIcs::exportCalendar(const QString &accountId, const QString &calendarPath) {
271 2 auto *sync = m_session.calendarSync();
272
3/6
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
2 if (!sync || !sync->store()) return false;
273 2 QList<QList<CalendarEvent>> series;
274 2 QStringList seen;
275
5/5
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 27 → 15 taken 8 times.
✓ Branch 27 → 28 taken 2 times.
10 for (const CalendarEvent &event : sync->store()->eventsForCalendar(accountId, calendarPath)) {
276
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 6 times.
8 if (seen.contains(event.uid)) continue;
277
1/1
✓ Branch 19 → 20 taken 6 times.
6 seen << event.uid;
278
2/2
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
6 series.append(sync->store()->eventSeries(event.uid, accountId, calendarPath));
279 2 }
280
1/1
✓ Branch 32 → 33 taken 2 times.
2 QString name = calendarPath.section(QLatin1Char('/'), -2, -1, QString::SectionSkipEmpty);
281
5/5
✓ Branch 33 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 94 → 37 taken 3 times.
✓ Branch 94 → 95 taken 2 times.
5 for (const QVariant &value : m_deps.calendarChoices()) {
282
1/1
✓ Branch 38 → 39 taken 3 times.
3 const QVariantMap choice = value.toMap();
283
19/50
✓ Branch 44 → 45 taken 3 times.
✓ Branch 45 → 46 taken 3 times.
✓ Branch 46 → 47 taken 3 times.
✗ Branch 46 → 56 not taken.
✓ Branch 52 → 53 taken 3 times.
✓ Branch 53 → 54 taken 3 times.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 54 → 56 taken 2 times.
✓ Branch 57 → 58 taken 3 times.
✗ Branch 57 → 59 not taken.
✓ Branch 59 → 60 taken 3 times.
✗ Branch 59 → 61 not taken.
✓ Branch 61 → 62 taken 3 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 3 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 3 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 3 times.
✗ Branch 67 → 69 not taken.
✓ Branch 69 → 70 taken 3 times.
✗ Branch 69 → 71 not taken.
✓ Branch 71 → 72 taken 3 times.
✗ Branch 71 → 73 not taken.
✓ Branch 73 → 74 taken 3 times.
✗ Branch 73 → 75 not taken.
✓ Branch 75 → 76 taken 3 times.
✗ Branch 75 → 77 not taken.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 91 taken 2 times.
✗ Branch 157 → 158 not taken.
✗ Branch 157 → 159 not taken.
✗ Branch 161 → 162 not taken.
✗ Branch 161 → 163 not taken.
✗ Branch 165 → 166 not taken.
✗ Branch 165 → 167 not taken.
✗ Branch 169 → 170 not taken.
✗ Branch 169 → 171 not taken.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
✗ Branch 177 → 178 not taken.
✗ Branch 177 → 179 not taken.
✗ Branch 181 → 182 not taken.
✗ Branch 181 → 183 not taken.
✗ Branch 185 → 186 not taken.
✗ Branch 185 → 187 not taken.
✗ Branch 189 → 190 not taken.
✗ Branch 189 → 191 not taken.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
9 if (choice.value(QStringLiteral("accountId")) == accountId && choice.value(QStringLiteral("path")) == calendarPath)
284
2/2
✓ Branch 82 → 83 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
2 name = choice.value(QStringLiteral("displayName")).toString();
285 5 }
286 2 const QString directory = m_deps.downloadDirectory.isEmpty()
287
1/3
✗ Branch 97 → 98 not taken.
✓ Branch 97 → 99 taken 2 times.
✗ Branch 98 → 100 not taken.
2 ? QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) : m_deps.downloadDirectory;
288
3/3
✓ Branch 100 → 101 taken 2 times.
✓ Branch 101 → 102 taken 2 times.
✓ Branch 102 → 103 taken 2 times.
2 const QString path = writeFile(directory, IcsFile::fileName(name), IcsFile::write(series), true);
289
5/6
✓ Branch 106 → 107 taken 1 time.
✓ Branch 106 → 108 taken 1 time.
✓ Branch 107 → 123 taken 1 time.
✗ Branch 114 → 123 not taken.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 123 → 124 taken 2 times.
3 m_deps.setStatus(path.isEmpty() ? tr("The calendar file could not be saved.")
290
3/15
✗ Branch 109 → 110 not taken.
✓ Branch 109 → 115 taken 1 time.
✗ Branch 110 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 135 → 136 not taken.
✓ Branch 135 → 137 taken 2 times.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 139 taken 2 times.
✗ Branch 242 → 243 not taken.
✗ Branch 242 → 244 not taken.
✗ Branch 246 → 247 not taken.
✗ Branch 246 → 248 not taken.
✗ Branch 251 → 252 not taken.
✗ Branch 251 → 253 not taken.
3 : series.size() == 1 ? tr("Saved %1 with 1 appointment.").arg(QFileInfo(path).fileName())
291
13/22
✓ Branch 115 → 116 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
✓ Branch 118 → 119 taken 1 time.
✓ Branch 119 → 120 taken 1 time.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 127 taken 1 time.
✓ Branch 127 → 128 taken 1 time.
✓ Branch 127 → 129 taken 1 time.
✓ Branch 129 → 130 taken 1 time.
✓ Branch 129 → 131 taken 1 time.
✓ Branch 131 → 132 taken 1 time.
✓ Branch 131 → 133 taken 1 time.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 135 taken 2 times.
✗ Branch 225 → 226 not taken.
✗ Branch 225 → 227 not taken.
✗ Branch 229 → 230 not taken.
✗ Branch 229 → 231 not taken.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
✗ Branch 238 → 239 not taken.
✗ Branch 238 → 240 not taken.
3 : tr("Saved %1 with %2 appointments.").arg(QFileInfo(path).fileName()).arg(series.size()));
292 2 return !path.isEmpty();
293 2 }
294
295 4 QString QuickCalendarIcs::occurrenceFile(const QVariantMap &occurrence) {
296
1/1
✓ Branch 2 → 3 taken 4 times.
4 const auto series = seriesOf(occurrence);
297
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 3 times.
4 if (series.isEmpty()) return {};
298 // One file per appointment; a newer export replaces it.
299
6/6
✓ Branch 6 → 7 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
3 const QString directory = QDir(m_deps.exportDirectory).filePath(series.first().first().uid.toUtf8().toHex().left(32));
300
3/3
✓ Branch 19 → 20 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
3 return writeFile(directory, IcsFile::fileName(series.first().first().summary), IcsFile::write(series), false);
301 4 }
302