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: 93.9% 354 / 0 / 377
Functions: 96.5% 55 / 0 / 57
Branches: 73.6% 510 / 0 / 693

app/core/SettingsSyncSession.cpp
Line Branch Exec Source
1 #include "SettingsSyncSession.h"
2 #include "AccountSession.h"
3 #include "data/CalendarStore.h"
4 #include "data/CredentialStore.h"
5 #include "data/SettingsCollector.h"
6 #include "data/SettingsMergePlan.h"
7 #include "service/SettingsSyncService.h"
8 #include <QFile>
9 #include <QJsonDocument>
10 #include <QSaveFile>
11 #include <QCryptographicHash>
12 #include <QLoggingCategory>
13 #include <QPointer>
14 #include <QSettings>
15 #include <QTimer>
16 #include <QUuid>
17 #include <optional>
18
19
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 9 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
11 Q_LOGGING_CATEGORY(lcSettingsSyncSession, "mailjd.sync.session")
20
21 namespace {
22 77 QByteArray contentIdentity(SyncPayload payload) {
23 77 payload.clientId.clear();
24 77 payload.lastModified = {};
25
3/3
✓ Branch 6 → 7 taken 77 times.
✓ Branch 7 → 8 taken 77 times.
✓ Branch 8 → 9 taken 77 times.
77 return QJsonDocument(payload.toJson()).toJson(QJsonDocument::Compact);
26 }
27 15 QByteArray remoteIdentity(const SyncPayload &payload) {
28
2/2
✓ Branch 2 → 3 taken 15 times.
✓ Branch 4 → 5 taken 15 times.
15 return QCryptographicHash::hash(payload.toRfcMessage(), QCryptographicHash::Sha256);
29 }
30 23 QString payloadJson(const SyncPayload &payload) {
31
4/4
✓ Branch 2 → 3 taken 23 times.
✓ Branch 3 → 4 taken 23 times.
✓ Branch 4 → 5 taken 23 times.
✓ Branch 5 → 6 taken 23 times.
23 return QString::fromUtf8(QJsonDocument(payload.toJson()).toJson(QJsonDocument::Compact));
32 }
33 // Sprint 107 D-13/D-21: the last payload both installations agreed on
34 // (the merge base) and the local payload a resolution replaced. Owner-only
35 // files, written atomically; a missing or unreadable base means two-way.
36 28 bool writePayloadFile(const QString &path, const SyncPayload &payload) {
37
1/1
✓ Branch 2 → 3 taken 28 times.
28 QSaveFile file(path);
38
2/3
✓ Branch 4 → 5 taken 28 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 28 times.
28 if (!file.open(QIODevice::WriteOnly)) return false;
39
4/4
✓ Branch 7 → 8 taken 28 times.
✓ Branch 8 → 9 taken 28 times.
✓ Branch 9 → 10 taken 28 times.
✓ Branch 10 → 11 taken 28 times.
28 file.write(QJsonDocument(payload.toJson()).toJson(QJsonDocument::Compact));
40
2/3
✓ Branch 14 → 15 taken 28 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 28 times.
28 if (!file.commit()) return false;
41
1/1
✓ Branch 18 → 19 taken 28 times.
28 return QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
42 28 }
43 10 std::optional<SyncPayload> readPayloadFile(const QString &path) {
44
1/1
✓ Branch 2 → 3 taken 10 times.
10 QFile file(path);
45
7/8
✓ Branch 4 → 5 taken 10 times.
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 9 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 9 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 9 times.
10 if (!file.open(QIODevice::ReadOnly) || file.size() > SyncPayload::MaxRfcMessageBytes) return std::nullopt;
46
2/2
✓ Branch 12 → 13 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
9 const auto document = QJsonDocument::fromJson(file.readAll());
47
2/3
✓ Branch 15 → 16 taken 9 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 9 times.
9 if (!document.isObject()) return std::nullopt;
48
2/2
✓ Branch 18 → 19 taken 9 times.
✓ Branch 19 → 20 taken 9 times.
9 return SyncPayload::fromJson(document.object());
49 10 }
50 }
51
52 class SettingsSyncWorker final : public QObject {
53 public:
54 11 SettingsSyncWorker(SettingsSyncSession *session,
55 std::shared_ptr<std::atomic<quint64>> epoch)
56
1/1
✓ Branch 9 → 10 taken 11 times.
11 : m_session(session), m_epoch(std::move(epoch)) {}
57 22 ~SettingsSyncWorker() override { stop(); }
58
59 37 void stop() {
60
2/2
✓ Branch 2 → 3 taken 12 times.
✓ Branch 2 → 4 taken 25 times.
37 delete m_debounce; m_debounce = nullptr;
61
2/2
✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 6 taken 25 times.
37 delete m_service; m_service = nullptr;
62 37 m_calendar.reset();
63 37 m_remote.reset();
64 37 m_conflictBase.reset();
65 37 m_baseline.clear();
66 37 m_uploaded.clear();
67 37 }
68 290 bool current() const { return m_epoch->load() == m_generation; }
69 98 void report(const QString &state, const QString &error = {},
70 const QStringList &differences = {}, bool success = false,
71 const QStringList &applied = {}) {
72 98 const auto generation = m_generation;
73
3/3
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 6 taken 90 times.
✓ Branch 5 → 7 taken 8 times.
98 const auto remote = m_remote ? remoteIdentity(*m_remote) : QByteArray{};
74 // The resolver (Sprint 107) builds the per-key plan on the GUI thread
75 // from the three payloads of the conflict shown to the user.
76 98 SettingsSyncSession::ConflictInputs inputs;
77
5/6
✓ Branch 9 → 10 taken 8 times.
✓ Branch 9 → 13 taken 90 times.
✓ Branch 11 → 12 taken 8 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 8 times.
✓ Branch 14 → 30 taken 90 times.
98 if (state == QLatin1String("conflict") && m_remote) {
78
1/1
✓ Branch 15 → 16 taken 8 times.
8 inputs.local = payloadJson(m_conflictLocal);
79
1/1
✓ Branch 19 → 20 taken 8 times.
8 inputs.remote = payloadJson(*m_remote);
80
3/3
✓ Branch 23 → 24 taken 7 times.
✓ Branch 23 → 26 taken 1 time.
✓ Branch 25 → 27 taken 7 times.
8 inputs.base = m_conflictBase ? payloadJson(*m_conflictBase) : QString();
81 }
82 // The session remains alive until its worker has stopped. The queued
83 // receiver is automatically removed at session destruction.
84
1/11
✓ Branch 36 → 37 taken 98 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 51 not taken.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
98 QMetaObject::invokeMethod(m_session, [session = m_session, generation, state,
85 error, differences, success, applied, remote, inputs] {
86
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 98 times.
196 if (session->m_epoch->load() != generation) return;
87 98 session->m_conflictInputs = inputs;
88 98 session->m_state = state;
89 98 session->m_error = error;
90 98 session->m_differences = differences;
91 98 session->m_remoteIdentity = remote;
92
4/4
✓ Branch 31 → 32 taken 21 times.
✓ Branch 31 → 38 taken 77 times.
✓ Branch 32 → 33 taken 21 times.
✓ Branch 33 → 34 taken 21 times.
98 if (success) session->m_lastSuccess = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
93 98 emit session->changed();
94
2/2
✓ Branch 40 → 41 taken 7 times.
✓ Branch 40 → 42 taken 91 times.
98 if (!applied.isEmpty()) emit session->settingsApplied(applied);
95 }, Qt::QueuedConnection);
96 98 }
97 54 SyncPayload collect() {
98 54 return SettingsCollector::collectLocal(nullptr, m_directory, m_calendar.get());
99 }
100 // The password was read by the session on the GUI thread (keyring
101 // invariant, see SettingsSyncSession.h); it is wiped after configure().
102 15 void begin(quint64 generation, const std::optional<AccountConfig> &account, QByteArray password,
103 const QString &folder, const QString &directory, const QString &calendarDatabase) {
104
1/1
✓ Branch 2 → 3 taken 15 times.
15 stop();
105 15 m_generation = generation;
106 15 m_directory = directory;
107
5/6
✓ Branch 5 → 6 taken 15 times.
✗ Branch 5 → 8 not taken.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 12 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 12 times.
15 if (!current() || !account) return;
108
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 33 taken 11 times.
12 if (!calendarDatabase.isEmpty()) {
109
1/1
✓ Branch 14 → 15 taken 1 time.
1 m_calendar = std::make_unique<CalendarStore>();
110
2/3
✓ Branch 18 → 19 taken 1 time.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 33 taken 1 time.
1 if (!m_calendar->open(calendarDatabase)) {
111 ✗ report(QStringLiteral("error"), SettingsSyncSession::tr("The calendar settings could not be opened."));
112 ✗ return;
113 }
114 }
115
3/6
✓ Branch 33 → 34 taken 12 times.
✓ Branch 34 → 35 taken 12 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 12 times.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 113 not taken.
12 m_debounce = new QTimer(this);
116
1/1
✓ Branch 37 → 38 taken 12 times.
12 m_debounce->setSingleShot(true);
117
1/1
✓ Branch 38 → 39 taken 12 times.
12 m_debounce->setInterval(500);
118
1/1
✓ Branch 39 → 40 taken 12 times.
12 connect(m_debounce, &QTimer::timeout, this, [this] {
119
5/10
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 9 not taken.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 9 not taken.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 3 times.
3 if (!current() || !m_service || m_remote || m_baseline.isEmpty()) return;
120
2/4
✓ Branch 13 → 14 taken 3 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 3 times.
✗ Branch 15 → 16 not taken.
3 if (!m_service->readyForOperation()) { m_debounce->start(); return; }
121
1/1
✓ Branch 17 → 18 taken 3 times.
3 const auto local = collect();
122
3/4
✓ Branch 19 → 20 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
✓ Branch 24 → 25 taken 3 times.
3 if (contentIdentity(local) != m_baseline) upload(local);
123 3 });
124
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 56 taken 12 times.
12 if (password.isEmpty()) {
125 ✗ report(QStringLiteral("error"), SettingsSyncSession::tr("The account password is unavailable. Unlock the keyring or edit the account, then retry."));
126 ✗ return;
127 }
128 12 ImapConfig config = account->imap;
129 12 config.password = password;
130
1/1
✓ Branch 59 → 60 taken 12 times.
12 password.fill('\0');
131
3/6
✓ Branch 60 → 61 taken 12 times.
✓ Branch 61 → 62 taken 12 times.
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 12 times.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
12 m_service = new SettingsSyncService(this);
132
1/1
✓ Branch 64 → 65 taken 12 times.
12 connect(m_service, &SettingsSyncService::syncError, this, [this](const QString &error) {
133
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
1 if (m_debounce) m_debounce->stop();
134
2/3
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 17 not taken.
✓ Branch 11 → 12 taken 1 time.
2 if (current()) report(QStringLiteral("error"), error);
135 1 });
136
1/1
✓ Branch 66 → 67 taken 12 times.
12 connect(m_service, &SettingsSyncService::connected, this, [this] {
137
2/3
✓ Branch 3 → 4 taken 12 times.
✗ Branch 3 → 17 not taken.
✓ Branch 10 → 11 taken 12 times.
24 if (current()) report(QStringLiteral("checking"));
138 12 });
139
1/1
✓ Branch 68 → 69 taken 12 times.
12 connect(m_service, &SettingsSyncService::connectionLost, this, [this] {
140 ✗ if (m_debounce) m_debounce->stop();
141 ✗ if (current()) report(QStringLiteral("offline"));
142 ✗ });
143 12 connect(m_service, &SettingsSyncService::settingsReceived, this,
144
1/1
✓ Branch 70 → 71 taken 12 times.
38 [this](const SyncPayload &remote) { received(remote); });
145
1/1
✓ Branch 72 → 73 taken 12 times.
12 connect(m_service, &SettingsSyncService::fetchComplete, this, [this](bool found) {
146
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 36 times.
36 if (!current()) return;
147
4/4
✓ Branch 5 → 6 taken 10 times.
✓ Branch 5 → 10 taken 26 times.
✓ Branch 6 → 7 taken 10 times.
✓ Branch 7 → 8 taken 10 times.
36 if (!found) upload(collect());
148 });
149
1/1
✓ Branch 74 → 75 taken 12 times.
12 connect(m_service, &SettingsSyncService::uploadComplete, this, [this] {
150
2/3
✓ Branch 3 → 4 taken 16 times.
✗ Branch 3 → 17 not taken.
✓ Branch 10 → 11 taken 16 times.
32 if (current()) report(QStringLiteral("checking")); // readback owns completion
151 16 });
152
1/1
✓ Branch 76 → 77 taken 12 times.
12 m_service->configure(config, folder);
153
1/1
✓ Branch 83 → 84 taken 12 times.
24 report(QStringLiteral("connecting"));
154
1/1
✓ Branch 89 → 90 taken 12 times.
12 m_service->setEnabled(true);
155
1/1
✓ Branch 90 → 91 taken 12 times.
12 config.password.fill('\0');
156 12 }
157
1/1
✓ Branch 5 → 6 taken 31 times.
62 QString basePath() const { return m_directory + QStringLiteral("/settings-sync-base.json"); }
158
1/1
✓ Branch 5 → 6 taken 7 times.
14 QString previousPath() const { return m_directory + QStringLiteral("/settings-sync-previous.json"); }
159 21 void agreed(const SyncPayload &payload) {
160
3/4
✓ Branch 2 → 3 taken 21 times.
✓ Branch 3 → 4 taken 21 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 17 taken 21 times.
21 if (!writePayloadFile(basePath(), payload))
161 ✗ qCWarning(lcSettingsSyncSession) << "The settings sync base could not be written";
162 21 }
163 7 void keepPrevious(const SyncPayload &local) {
164
3/4
✓ Branch 2 → 3 taken 7 times.
✓ Branch 3 → 4 taken 7 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 17 taken 7 times.
7 if (!writePayloadFile(previousPath(), local))
165 ✗ qCWarning(lcSettingsSyncSession) << "The replaced settings could not be kept";
166 7 }
167 26 void received(const SyncPayload &remote) {
168
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 26 times.
51 if (!current()) return;
169 // S106-182: the readback of our own upload is no remote change — a local
170 // edit made meanwhile is uploaded next instead of shown as a conflict.
171
10/15
✓ Branch 6 → 7 taken 16 times.
✓ Branch 6 → 12 taken 10 times.
✓ Branch 8 → 9 taken 16 times.
✓ Branch 10 → 11 taken 16 times.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 16 times.
✓ Branch 13 → 15 taken 10 times.
✓ Branch 15 → 16 taken 16 times.
✓ Branch 15 → 17 taken 10 times.
✓ Branch 17 → 18 taken 16 times.
✓ Branch 17 → 67 taken 10 times.
✗ Branch 217 → 218 not taken.
✗ Branch 217 → 219 not taken.
✗ Branch 221 → 222 not taken.
✗ Branch 221 → 223 not taken.
26 if (!m_uploaded.isEmpty() && contentIdentity(remote) == m_uploaded) {
172
1/1
✓ Branch 18 → 19 taken 16 times.
16 m_uploaded.clear();
173 16 m_remote.reset();
174
1/1
✓ Branch 21 → 22 taken 16 times.
16 m_baseline = contentIdentity(remote);
175
1/1
✓ Branch 25 → 26 taken 16 times.
16 agreed(remote);
176
4/4
✓ Branch 26 → 27 taken 16 times.
✓ Branch 27 → 28 taken 16 times.
✓ Branch 28 → 29 taken 16 times.
✓ Branch 34 → 35 taken 16 times.
32 QSettings().setValue(QStringLiteral("sync/lastSync"), QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
177
1/1
✓ Branch 47 → 48 taken 16 times.
32 report(QStringLiteral("ready"), {}, {}, true);
178
10/17
✓ Branch 53 → 54 taken 16 times.
✗ Branch 53 → 59 not taken.
✓ Branch 54 → 55 taken 16 times.
✓ Branch 55 → 56 taken 16 times.
✓ Branch 57 → 58 taken 1 time.
✓ Branch 57 → 59 taken 15 times.
✓ Branch 60 → 61 taken 16 times.
✗ Branch 60 → 62 not taken.
✓ Branch 62 → 63 taken 16 times.
✗ Branch 62 → 64 not taken.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 66 taken 15 times.
✓ Branch 65 → 66 taken 1 time.
✗ Branch 263 → 264 not taken.
✗ Branch 263 → 265 not taken.
✗ Branch 267 → 268 not taken.
✗ Branch 267 → 269 not taken.
16 if (m_debounce && contentIdentity(collect()) != m_baseline) m_debounce->start();
179 16 return;
180 }
181 // Sprint 107 D-13: per-key three-way comparison against the stored
182 // base; only keys changed on both installations are a conflict.
183
1/1
✓ Branch 67 → 68 taken 10 times.
10 const auto local = collect();
184
2/2
✓ Branch 68 → 69 taken 10 times.
✓ Branch 69 → 70 taken 10 times.
10 const auto base = readPayloadFile(basePath());
185
1/1
✓ Branch 71 → 72 taken 10 times.
10 const Merge::Plan plan = SettingsMergePlan::analyze(base, local, remote);
186
1/1
✓ Branch 72 → 73 taken 10 times.
10 const QStringList differences = SettingsMergePlan::conflictCategories(plan);
187
8/8
✓ Branch 73 → 74 taken 10 times.
✓ Branch 77 → 78 taken 10 times.
✓ Branch 78 → 79 taken 10 times.
✓ Branch 80 → 81 taken 9 times.
✓ Branch 80 → 82 taken 1 time.
✓ Branch 83 → 84 taken 10 times.
✓ Branch 96 → 75 taken 10 times.
✓ Branch 96 → 97 taken 10 times.
20 qCInfo(lcSettingsSyncSession) << "settings compared" << (base ? "three-way" : "two-way")
188
5/5
✓ Branch 84 → 85 taken 10 times.
✓ Branch 86 → 87 taken 10 times.
✓ Branch 87 → 88 taken 10 times.
✓ Branch 88 → 89 taken 10 times.
✓ Branch 89 → 90 taken 10 times.
10 << "items" << plan.items.size() << "combined" << plan.combinedCount()
189
3/3
✓ Branch 90 → 91 taken 10 times.
✓ Branch 91 → 92 taken 10 times.
✓ Branch 92 → 93 taken 10 times.
10 << "conflicts" << plan.conflictCount();
190
2/2
✓ Branch 98 → 99 taken 8 times.
✓ Branch 98 → 115 taken 2 times.
10 if (!differences.isEmpty()) {
191 8 m_remote = remote;
192 8 m_conflictLocal = local;
193
1/1
✓ Branch 101 → 102 taken 8 times.
8 m_conflictBase = base;
194 // Freeze the locally admitted categories with this decision. Later
195 // local policy edits may narrow them, but cannot expand an old consent.
196
1/1
✓ Branch 103 → 104 taken 8 times.
8 m_remote->enabledCategories.removeIf([&local](const QString &category) {
197 14 return !local.enabledCategories.contains(category);
198 });
199
1/1
✓ Branch 109 → 110 taken 8 times.
16 report(QStringLiteral("conflict"), {}, differences);
200 8 return;
201 }
202 2 m_remote.reset();
203 // One-sided changes: the server's apply here, ours go up.
204
1/1
✓ Branch 116 → 117 taken 2 times.
2 const SyncPayload merged = SettingsMergePlan::apply(plan);
205 2 QStringList applied;
206
4/4
✓ Branch 118 → 119 taken 2 times.
✓ Branch 120 → 121 taken 2 times.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 133 taken 1 time.
2 if (contentIdentity(merged) != contentIdentity(local)) {
207
1/1
✓ Branch 127 → 128 taken 1 time.
1 keepPrevious(local);
208
1/1
✓ Branch 129 → 130 taken 1 time.
1 applied = SettingsCollector::applyRemote(merged, nullptr, m_directory, m_calendar.get());
209 }
210
1/1
✓ Branch 133 → 134 taken 2 times.
2 const auto now = collect();
211
4/4
✓ Branch 135 → 136 taken 2 times.
✓ Branch 136 → 137 taken 2 times.
✓ Branch 139 → 140 taken 1 time.
✓ Branch 139 → 155 taken 1 time.
2 if (SettingsMergePlan::analyze(std::nullopt, now, remote).conflictCount() > 0) {
212
1/1
✓ Branch 140 → 141 taken 1 time.
1 upload(now);
213
1/3
✗ Branch 142 → 143 not taken.
✓ Branch 142 → 154 taken 1 time.
✗ Branch 148 → 149 not taken.
1 if (!applied.isEmpty()) report(QStringLiteral("uploading"), {}, {}, false, applied);
214 1 return;
215 }
216
1/1
✓ Branch 156 → 157 taken 1 time.
1 m_baseline = contentIdentity(now);
217
1/1
✓ Branch 160 → 161 taken 1 time.
1 agreed(now);
218
4/4
✓ Branch 161 → 162 taken 1 time.
✓ Branch 162 → 163 taken 1 time.
✓ Branch 163 → 164 taken 1 time.
✓ Branch 169 → 170 taken 1 time.
2 QSettings().setValue(QStringLiteral("sync/lastSync"), QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
219
1/1
✓ Branch 181 → 182 taken 1 time.
2 report(QStringLiteral("ready"), {}, {}, true, applied);
220 40 }
221 17 void upload(const SyncPayload &local) {
222
4/8
✓ Branch 3 → 4 taken 17 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 17 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 17 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 17 times.
17 if (!current() || !m_service || !m_service->readyForOperation()) return;
223 17 m_remote.reset();
224
1/1
✓ Branch 13 → 14 taken 17 times.
17 m_uploaded = contentIdentity(local);
225
1/1
✓ Branch 23 → 24 taken 17 times.
34 report(QStringLiteral("uploading"));
226 17 m_service->uploadSettings(local);
227 }
228 17 void command(quint64 generation, int action, const QByteArray &expectedRemote) {
229
4/8
✓ Branch 2 → 3 taken 17 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 17 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 17 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 17 times.
17 if (generation != m_generation || !current() || !m_service) return;
230
3/4
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 14 taken 14 times.
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 13 not taken.
17 if (action == 3) { if (m_debounce) m_debounce->start(); return; }
231
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 14 times.
14 if (!m_service->readyForOperation()) return;
232 // Consent concerns the version displayed to the user, never a newer
233 // payload received while the cross-thread command was queued.
234
5/6
✓ Branch 17 → 18 taken 10 times.
✓ Branch 17 → 20 taken 4 times.
✓ Branch 18 → 19 taken 9 times.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 27 taken 9 times.
19 if ((action == 1 || action == 2 || action == 4) &&
235
6/11
✓ Branch 21 → 22 taken 5 times.
✗ Branch 21 → 26 not taken.
✓ Branch 23 → 24 taken 5 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 5 times.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 30 taken 9 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 14 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
19 (!m_remote || expectedRemote != remoteIdentity(*m_remote))) return;
236
3/3
✓ Branch 32 → 33 taken 9 times.
✓ Branch 32 → 46 taken 5 times.
✓ Branch 39 → 40 taken 9 times.
23 if (action == 0) { report(QStringLiteral("checking")); m_service->fetchLatestSettings(); }
237
5/6
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 50 taken 1 time.
✓ Branch 48 → 49 taken 4 times.
✗ Branch 48 → 50 not taken.
✓ Branch 51 → 52 taken 4 times.
✓ Branch 51 → 79 taken 1 time.
5 else if (action == 1 && m_remote) {
238 // Explicit profile-scoped user admission is the linearization point.
239 // A queued admission from an old account/configuration is discarded.
240
2/2
✓ Branch 52 → 53 taken 4 times.
✓ Branch 53 → 54 taken 4 times.
4 keepPrevious(collect());
241
1/1
✓ Branch 57 → 58 taken 4 times.
4 const auto applied = SettingsCollector::applyRemote(*m_remote, nullptr, m_directory, m_calendar.get());
242 4 m_remote.reset();
243
1/1
✓ Branch 59 → 60 taken 4 times.
4 const auto now = collect();
244
1/1
✓ Branch 61 → 62 taken 4 times.
4 m_baseline = contentIdentity(now);
245
1/1
✓ Branch 65 → 66 taken 4 times.
4 agreed(now);
246
1/1
✓ Branch 71 → 72 taken 4 times.
8 report(QStringLiteral("ready"), {}, {}, true, applied);
247
3/4
✓ Branch 79 → 80 taken 1 time.
✗ Branch 79 → 84 not taken.
✓ Branch 80 → 81 taken 1 time.
✓ Branch 81 → 82 taken 1 time.
5 } else if (action == 2) upload(collect());
248 }
249 // Sprint 107 S107.3.5: the per-key resolution. Adopted locally through
250 // the same guarded apply as "Use server settings" (only the categories of
251 // the frozen consent), then uploaded; the readback stores the new base.
252 2 void resolve(quint64 generation, const QByteArray &expectedRemote, const SyncPayload &resolved) {
253
4/8
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 12 not taken.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 12 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 12 not taken.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 12 not taken.
4 if (generation != m_generation || !current() || !m_service || !m_remote ||
254
4/9
✓ Branch 9 → 10 taken 2 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 16 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 28 taken 2 times.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
4 expectedRemote != remoteIdentity(*m_remote)) {
255 ✗ qCInfo(lcSettingsSyncSession) << "settings resolution dropped: the server version changed";
256 ✗ return;
257 }
258
2/3
✓ Branch 28 → 29 taken 2 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 2 times.
2 if (!m_service->readyForOperation()) return;
259 2 SyncPayload admitted = resolved;
260
1/1
✓ Branch 32 → 33 taken 2 times.
2 admitted.enabledCategories.removeIf([this](const QString &category) {
261 4 return !m_remote->enabledCategories.contains(category);
262 });
263
2/2
✓ Branch 33 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
2 keepPrevious(collect());
264
1/1
✓ Branch 37 → 38 taken 2 times.
2 const auto applied = SettingsCollector::applyRemote(admitted, nullptr, m_directory, m_calendar.get());
265 2 m_remote.reset();
266
1/1
✓ Branch 44 → 45 taken 2 times.
4 report(QStringLiteral("uploading"), {}, {}, false, applied);
267
2/2
✓ Branch 49 → 50 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
2 upload(collect());
268 2 }
269 private:
270 SettingsSyncSession *m_session;
271 std::shared_ptr<std::atomic<quint64>> m_epoch;
272 quint64 m_generation = 0;
273 QString m_directory;
274 QTimer *m_debounce = nullptr;
275 QByteArray m_uploaded; // content identity of the upload awaiting its readback
276 SettingsSyncService *m_service = nullptr;
277 std::unique_ptr<CalendarStore> m_calendar;
278 std::optional<SyncPayload> m_remote;
279 SyncPayload m_conflictLocal; // the local side of the shown conflict
280 std::optional<SyncPayload> m_conflictBase; // its base (none: two-way)
281 QByteArray m_baseline;
282 };
283
284 832 SettingsSyncSession::SettingsSyncSession(AccountSession &accounts, QString configDirectory,
285 832 QString calendarDatabase, QObject *parent)
286 832 : QObject(parent), m_accounts(accounts), m_configDirectory(std::move(configDirectory)),
287 832 m_calendarDatabase(std::move(calendarDatabase)),
288
2/2
✓ Branch 20 → 21 taken 832 times.
✓ Branch 21 → 22 taken 832 times.
2496 m_epoch(std::make_shared<std::atomic<quint64>>(0)) {
289
1/1
✓ Branch 22 → 23 taken 832 times.
832 QSettings settings;
290
2/2
✓ Branch 28 → 29 taken 832 times.
✓ Branch 29 → 30 taken 832 times.
1664 m_enabled = settings.value(QStringLiteral("sync/enabled"), false).toBool();
291
2/2
✓ Branch 38 → 39 taken 832 times.
✓ Branch 39 → 40 taken 832 times.
832 m_accountId = settings.value(QStringLiteral("sync/accountId")).toString();
292
2/2
✓ Branch 53 → 54 taken 832 times.
✓ Branch 54 → 55 taken 832 times.
1664 m_folder = settings.value(QStringLiteral("sync/folder"), QStringLiteral("MailJD-Settings")).toString();
293 // Stored by any client version: the form round-trips this list on save,
294 // so legacy or unknown names must not reach configure() unmapped.
295 const QStringList storedCategories =
296
3/3
✓ Branch 63 → 64 taken 832 times.
✓ Branch 69 → 70 taken 832 times.
✓ Branch 70 → 71 taken 832 times.
1664 settings.value(QStringLiteral("sync/categories"), SyncPayload::allCategories()).toStringList();
297
1/1
✓ Branch 76 → 77 taken 832 times.
832 m_categories = SyncPayload::normalizedCategories(storedCategories);
298
3/3
✓ Branch 79 → 80 taken 832 times.
✓ Branch 80 → 81 taken 1 time.
✓ Branch 80 → 102 taken 831 times.
832 if (m_categories != storedCategories)
299
5/5
✓ Branch 81 → 82 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 87 → 88 taken 1 time.
✓ Branch 100 → 83 taken 1 time.
✓ Branch 100 → 101 taken 1 time.
2 qCInfo(lcSettingsSyncSession).noquote() << "normalized stored categories"
300
5/5
✓ Branch 89 → 90 taken 1 time.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 91 → 92 taken 1 time.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 94 → 95 taken 1 time.
1 << storedCategories.join(u',') << "->" << m_categories.join(u',');
301
2/2
✓ Branch 106 → 107 taken 832 times.
✓ Branch 107 → 108 taken 832 times.
832 m_lastSuccess = settings.value(QStringLiteral("sync/lastSync")).toString();
302
1/1
✓ Branch 116 → 117 taken 832 times.
1664 m_thread.setObjectName(QStringLiteral("settings-sync"));
303
1/1
✓ Branch 119 → 120 taken 832 times.
839 connect(&accounts, &AccountSession::availableAccountsChanged, this, [this] { restart(); });
304
1/1
✓ Branch 121 → 122 taken 832 times.
1654 connect(&accounts, &AccountSession::accountsLoaded, this, [this] { restart(); });
305
1/1
✓ Branch 123 → 124 taken 832 times.
832 connect(&accounts, &AccountSession::configurationChanged, this, [this](const QString &id) {
306
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
9 if (id == m_accountId) restart(true);
307 9 });
308
1/1
✓ Branch 125 → 126 taken 832 times.
832 restart();
309 832 }
310
311 1656 SettingsSyncSession::~SettingsSyncSession() {
312 832 ++*m_epoch;
313
2/2
✓ Branch 4 → 5 taken 821 times.
✓ Branch 4 → 6 taken 11 times.
832 if (!m_worker) return;
314 22 QMetaObject::invokeMethod(m_worker, [worker = m_worker] { worker->stop(); }, Qt::BlockingQueuedConnection);
315 11 m_thread.quit();
316 11 m_thread.wait();
317 13150 }
318
319 26 QVariantMap SettingsSyncSession::configure(bool enabled, const QString &accountId,
320 const QString &folder, const QStringList &categories) {
321
3/9
✓ Branch 10 → 11 taken 4 times.
✓ Branch 12 → 13 taken 8 times.
✓ Branch 12 → 14 taken 4 times.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 41 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
20 auto reject = [](const QString &error) { return QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), error}}; };
322
11/13
✓ Branch 2 → 3 taken 22 times.
✓ Branch 2 → 7 taken 4 times.
✓ Branch 3 → 4 taken 22 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 21 times.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 4 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 25 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
26 if (enabled && !m_accounts.configuration(accountId)) return reject(tr("Choose an existing mail account."));
323
1/1
✓ Branch 16 → 17 taken 25 times.
25 const auto path = folder.trimmed();
324
3/8
✓ Branch 18 → 19 taken 25 times.
✗ Branch 18 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 25 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 29 taken 25 times.
✗ Branch 24 → 25 not taken.
✗ Branch 25 → 26 not taken.
25 if (path.isEmpty() || path.size() > 255) return reject(tr("Enter a settings folder name (1–255 characters)."));
325
2/2
✓ Branch 39 → 31 taken 350 times.
✓ Branch 39 → 40 taken 24 times.
374 for (const auto ch : path)
326
4/4
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 38 taken 349 times.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
350 if (ch.category() == QChar::Other_Control) return reject(tr("The folder name contains a control character."));
327 24 QStringList selected;
328
2/2
✓ Branch 63 → 42 taken 31 times.
✓ Branch 63 → 64 taken 23 times.
54 for (const auto &raw : categories) {
329
1/1
✓ Branch 43 → 44 taken 31 times.
31 const auto category = SyncPayload::normalizedCategory(raw);
330
5/5
✓ Branch 44 → 45 taken 31 times.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 53 taken 30 times.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
31 if (!SyncPayload::allCategories().contains(category)) return reject(tr("Unknown settings category."));
331
2/3
✓ Branch 54 → 55 taken 30 times.
✗ Branch 54 → 56 not taken.
✓ Branch 55 → 56 taken 30 times.
30 if (!selected.contains(category)) selected.append(category);
332 31 }
333
8/8
✓ Branch 64 → 65 taken 19 times.
✓ Branch 64 → 68 taken 4 times.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 68 taken 18 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 75 taken 22 times.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
23 if (enabled && selected.isEmpty()) return reject(tr("Choose at least one settings category."));
334 // Retire the old transport's publication authority before changing any
335 // shared policy key. A collection already running must fail its final
336 // generation check instead of exporting new categories to the old account.
337 22 ++*m_epoch;
338
1/1
✓ Branch 77 → 78 taken 22 times.
22 QSettings settings;
339 22 const QStringList keys{QStringLiteral("sync/enabled"), QStringLiteral("sync/accountId"),
340
2/4
✓ Branch 95 → 96 taken 110 times.
✓ Branch 95 → 97 taken 22 times.
✗ Branch 244 → 245 not taken.
✗ Branch 244 → 246 not taken.
132 QStringLiteral("sync/folder"), QStringLiteral("sync/categories"), QStringLiteral("sync/lastSync")};
341 22 QVariantMap previous;
342
7/7
✓ Branch 106 → 107 taken 110 times.
✓ Branch 107 → 108 taken 45 times.
✓ Branch 107 → 113 taken 65 times.
✓ Branch 109 → 110 taken 45 times.
✓ Branch 110 → 111 taken 45 times.
✓ Branch 115 → 104 taken 110 times.
✓ Branch 115 → 116 taken 22 times.
132 for (const auto &key : keys) if (settings.contains(key)) previous.insert(key, settings.value(key));
343
1/1
✓ Branch 121 → 122 taken 22 times.
44 settings.setValue(QStringLiteral("sync/enabled"), enabled);
344
1/1
✓ Branch 130 → 131 taken 22 times.
44 settings.setValue(QStringLiteral("sync/accountId"), accountId);
345
1/1
✓ Branch 139 → 140 taken 22 times.
44 settings.setValue(QStringLiteral("sync/folder"), path);
346
1/1
✓ Branch 148 → 149 taken 22 times.
44 settings.setValue(QStringLiteral("sync/categories"), selected);
347
3/4
✓ Branch 153 → 154 taken 7 times.
✓ Branch 153 → 156 taken 15 times.
✗ Branch 155 → 156 not taken.
✓ Branch 155 → 157 taken 7 times.
22 const bool targetChanged = accountId != m_accountId || path != m_folder;
348
3/3
✓ Branch 158 → 159 taken 15 times.
✓ Branch 158 → 167 taken 7 times.
✓ Branch 163 → 164 taken 15 times.
37 if (targetChanged) settings.remove(QStringLiteral("sync/lastSync"));
349
1/1
✓ Branch 167 → 168 taken 22 times.
22 settings.sync();
350
2/3
✓ Branch 168 → 169 taken 22 times.
✗ Branch 169 → 170 not taken.
✓ Branch 169 → 194 taken 22 times.
22 if (settings.status() != QSettings::NoError) {
351 ✗ for (const auto &key : keys) {
352 ✗ if (previous.contains(key)) settings.setValue(key, previous.value(key));
353 ✗ else settings.remove(key);
354 }
355 ✗ restart(true);
356 ✗ return reject(tr("The synchronization settings could not be saved."));
357 }
358 22 m_enabled = enabled; m_accountId = accountId; m_folder = path; m_categories = selected;
359
2/2
✓ Branch 197 → 198 taken 15 times.
✓ Branch 197 → 199 taken 7 times.
22 if (targetChanged) m_lastSuccess.clear();
360
1/1
✓ Branch 199 → 200 taken 22 times.
22 restart(true);
361
2/4
✓ Branch 206 → 207 taken 22 times.
✓ Branch 206 → 208 taken 22 times.
✗ Branch 334 → 335 not taken.
✗ Branch 334 → 336 not taken.
88 return {{QStringLiteral("ok"), true}};
362 223 }
363
364 1684 void SettingsSyncSession::restart(bool force) {
365
1/1
✓ Branch 2 → 3 taken 1684 times.
1684 const auto account = m_accounts.configuration(m_accountId);
366 // Metadata carries no secrets. Display-name changes also invalidate legacy
367 // credential fallback, while switching the active mail account alone does not.
368 ✗ QJsonObject identity{{"enabled", m_enabled}, {"network", m_accounts.networkEnabled()},
369
2/4
✓ Branch 10 → 11 taken 6736 times.
✓ Branch 10 → 12 taken 1684 times.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
8420 {"account", m_accountId}, {"folder", m_folder}};
370
2/2
✓ Branch 13 → 14 taken 114 times.
✓ Branch 13 → 51 taken 1570 times.
1684 if (account) {
371
6/6
✓ Branch 15 → 16 taken 114 times.
✓ Branch 16 → 17 taken 114 times.
✓ Branch 17 → 18 taken 114 times.
✓ Branch 21 → 22 taken 114 times.
✓ Branch 22 → 23 taken 114 times.
✓ Branch 23 → 24 taken 114 times.
114 identity.insert("host", account->imap.host); identity.insert("port", account->imap.port);
372
6/6
✓ Branch 27 → 28 taken 114 times.
✓ Branch 28 → 29 taken 114 times.
✓ Branch 29 → 30 taken 114 times.
✓ Branch 33 → 34 taken 114 times.
✓ Branch 34 → 35 taken 114 times.
✓ Branch 35 → 36 taken 114 times.
114 identity.insert("security", account->imap.security); identity.insert("user", account->imap.username);
373
6/6
✓ Branch 39 → 40 taken 114 times.
✓ Branch 40 → 41 taken 114 times.
✓ Branch 41 → 42 taken 114 times.
✓ Branch 45 → 46 taken 114 times.
✓ Branch 46 → 47 taken 114 times.
✓ Branch 47 → 48 taken 114 times.
114 identity.insert("legacy", account->legacyKeyringName); identity.insert("name", account->name);
374 }
375
2/2
✓ Branch 51 → 52 taken 1684 times.
✓ Branch 52 → 53 taken 1684 times.
1684 const auto serialized = QJsonDocument(identity).toJson(QJsonDocument::Compact);
376
6/6
✓ Branch 54 → 55 taken 1661 times.
✓ Branch 54 → 58 taken 23 times.
✓ Branch 56 → 57 taken 735 times.
✓ Branch 56 → 58 taken 926 times.
✓ Branch 59 → 60 taken 735 times.
✓ Branch 59 → 61 taken 949 times.
1684 if (!force && serialized == m_configurationIdentity) return;
377 949 m_configurationIdentity = serialized;
378 949 const auto generation = ++*m_epoch;
379
1/1
✓ Branch 65 → 66 taken 949 times.
949 m_error.clear(); m_differences.clear();
380
1/1
✓ Branch 66 → 67 taken 949 times.
949 m_remoteIdentity.clear();
381 949 m_conflictInputs = {};
382
6/6
✓ Branch 69 → 70 taken 928 times.
✓ Branch 69 → 73 taken 21 times.
✓ Branch 74 → 75 taken 2 times.
✓ Branch 74 → 78 taken 19 times.
✓ Branch 94 → 95 taken 928 times.
✓ Branch 94 → 96 taken 21 times.
3796 m_state = !m_enabled ? QStringLiteral("disabled") : !account ? QStringLiteral("account-required")
383
8/8
✓ Branch 79 → 80 taken 6 times.
✓ Branch 79 → 83 taken 13 times.
✓ Branch 88 → 89 taken 13 times.
✓ Branch 88 → 90 taken 936 times.
✓ Branch 90 → 91 taken 6 times.
✓ Branch 90 → 92 taken 943 times.
✓ Branch 92 → 93 taken 2 times.
✓ Branch 92 → 94 taken 947 times.
1917 : !m_accounts.networkEnabled() ? QStringLiteral("offline") : QStringLiteral("connecting");
384
6/6
✓ Branch 96 → 97 taken 21 times.
✓ Branch 96 → 102 taken 928 times.
✓ Branch 98 → 99 taken 19 times.
✓ Branch 98 → 102 taken 2 times.
✓ Branch 100 → 101 taken 13 times.
✓ Branch 100 → 102 taken 6 times.
949 const bool run = m_enabled && account && m_accounts.networkEnabled();
385
2/2
✓ Branch 103 → 104 taken 13 times.
✓ Branch 103 → 132 taken 936 times.
949 if (run) {
386
1/1
✓ Branch 104 → 105 taken 13 times.
13 QSettings settings;
387
4/4
✓ Branch 109 → 110 taken 13 times.
✓ Branch 110 → 111 taken 13 times.
✓ Branch 116 → 117 taken 11 times.
✓ Branch 116 → 130 taken 2 times.
13 if (settings.value(QStringLiteral("sync/clientId")).toString().isEmpty())
388
3/3
✓ Branch 117 → 118 taken 11 times.
✓ Branch 118 → 119 taken 11 times.
✓ Branch 124 → 125 taken 11 times.
22 settings.setValue(QStringLiteral("sync/clientId"), QUuid::createUuid().toString(QUuid::WithoutBraces));
389 13 }
390
4/4
✓ Branch 132 → 133 taken 13 times.
✓ Branch 132 → 144 taken 936 times.
✓ Branch 133 → 134 taken 11 times.
✓ Branch 133 → 144 taken 2 times.
949 if (run && !m_worker) {
391
3/6
✓ Branch 134 → 135 taken 11 times.
✓ Branch 136 → 137 taken 11 times.
✗ Branch 138 → 139 not taken.
✓ Branch 138 → 140 taken 11 times.
✗ Branch 253 → 254 not taken.
✗ Branch 253 → 255 not taken.
11 m_worker = new SettingsSyncWorker(this, m_epoch);
392
1/1
✓ Branch 140 → 141 taken 11 times.
11 m_worker->moveToThread(&m_thread);
393
1/1
✓ Branch 141 → 142 taken 11 times.
11 connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
394
1/1
✓ Branch 143 → 144 taken 11 times.
11 m_thread.start();
395 }
396
1/1
✓ Branch 144 → 145 taken 949 times.
949 cancelPasswordRead();
397
2/2
✓ Branch 145 → 146 taken 13 times.
✓ Branch 145 → 153 taken 936 times.
949 if (run) {
398 13 m_state = QStringLiteral("credentials");
399
1/1
✓ Branch 152 → 156 taken 13 times.
13 readPasswordThenBegin(generation);
400
2/2
✓ Branch 153 → 154 taken 2 times.
✓ Branch 153 → 156 taken 934 times.
936 } else if (m_worker) {
401
1/1
✓ Branch 154 → 155 taken 2 times.
2 QMetaObject::invokeMethod(m_worker, [worker = m_worker, generation] {
402
1/1
✓ Branch 7 → 8 taken 2 times.
2 worker->begin(generation, std::nullopt, {}, {}, {}, {});
403 2 });
404 }
405
1/1
✓ Branch 156 → 157 taken 949 times.
949 emit changed();
406 4838 }
407
408 // S106-182: without a password for the new start the previous transport
409 // (the old account's IMAP connection and its password) is stopped too.
410 1 void SettingsSyncSession::retireTransport(quint64 generation) {
411
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (!m_worker) return;
412
1/1
✓ Branch 4 → 5 taken 1 time.
1 QMetaObject::invokeMethod(m_worker, [worker = m_worker, generation] {
413
1/1
✓ Branch 7 → 8 taken 1 time.
1 worker->begin(generation, std::nullopt, {}, {}, {}, {});
414 1 });
415 }
416
417 962 void SettingsSyncSession::cancelPasswordRead() {
418 // Also reached from the store's own completion callback: delete later.
419
2/2
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 4 taken 949 times.
962 if (m_credentials) m_credentials->deleteLater();
420 962 m_credentials = nullptr;
421
2/2
✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 7 taken 949 times.
962 if (m_credentialDeadline) {
422 13 m_credentialDeadline->stop();
423 13 m_credentialDeadline->deleteLater();
424 }
425 962 m_credentialDeadline = nullptr;
426 962 }
427
428 13 void SettingsSyncSession::readPasswordThenBegin(quint64 generation) {
429
1/1
✓ Branch 2 → 3 taken 13 times.
13 const auto account = m_accounts.configuration(m_accountId);
430
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 13 times.
13 if (!account) return;
431
3/6
✓ Branch 6 → 7 taken 13 times.
✓ Branch 7 → 8 taken 13 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 13 times.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
13 m_credentials = new CredentialStore(this);
432
3/6
✓ Branch 10 → 11 taken 13 times.
✓ Branch 11 → 12 taken 13 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 13 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
13 m_credentialDeadline = new QTimer(this);
433
1/1
✓ Branch 14 → 15 taken 13 times.
13 m_credentialDeadline->setSingleShot(true);
434
1/1
✓ Branch 15 → 16 taken 13 times.
13 m_credentialDeadline->setInterval(30000);
435
1/1
✓ Branch 16 → 17 taken 13 times.
13 connect(m_credentialDeadline, &QTimer::timeout, this, [this, generation] {
436 ✗ cancelPasswordRead();
437 ✗ if (m_epoch->load() != generation) return;
438 ✗ retireTransport(generation);
439 ✗ m_state = QStringLiteral("error");
440 ✗ m_error = tr("Reading the account password timed out. Unlock the keyring and retry.");
441 ✗ emit changed();
442 });
443
1/1
✓ Branch 18 → 19 taken 13 times.
13 m_credentialDeadline->start();
444 13 const auto begin = [this, generation, account = *account](bool ok, const QByteArray &password) {
445
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 13 times.
26 if (m_epoch->load() != generation) return;
446 13 cancelPasswordRead();
447
5/6
✓ Branch 27 → 28 taken 12 times.
✓ Branch 27 → 30 taken 1 time.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 12 times.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 45 taken 12 times.
13 if (!ok || password.isEmpty()) {
448 1 retireTransport(generation);
449 1 m_state = QStringLiteral("error");
450
1/1
✓ Branch 40 → 41 taken 1 time.
1 m_error = tr("The account password is unavailable. Unlock the keyring or edit the account, then retry.");
451 1 emit changed();
452 1 return;
453 }
454
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 12 times.
12 if (!m_worker) return;
455
1/9
✓ Branch 52 → 53 taken 12 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
24 QMetaObject::invokeMethod(m_worker, [worker = m_worker, generation, account, password,
456 12 folder = m_folder, directory = m_configDirectory,
457 12 database = m_calendarDatabase] {
458
1/1
✓ Branch 4 → 5 taken 12 times.
12 worker->begin(generation, account, password, folder, directory, database);
459 12 });
460 13 };
461
1/2
✓ Branch 23 → 24 taken 13 times.
✗ Branch 23 → 26 not taken.
13 const auto legacy = account->legacyKeyringName.isEmpty() ? account->name : account->legacyKeyringName;
462
2/2
✓ Branch 33 → 34 taken 13 times.
✓ Branch 37 → 38 taken 13 times.
26 m_credentials->readPassword(QStringLiteral("imap"), AccountConfigLoader::imapCredentialName(*account),
463
1/3
✓ Branch 31 → 32 taken 13 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
26 [this, generation, legacy, begin](bool ok, const QByteArray &password) {
464
3/6
✓ Branch 24 → 25 taken 13 times.
✗ Branch 24 → 26 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 13 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 13 times.
26 if (m_epoch->load() != generation || !m_credentials) return;
465
5/6
✓ Branch 30 → 31 taken 12 times.
✓ Branch 30 → 34 taken 1 time.
✓ Branch 32 → 33 taken 12 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 12 times.
✓ Branch 35 → 37 taken 1 time.
13 if (ok && !password.isEmpty()) begin(true, password);
466
2/2
✓ Branch 37 → 38 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
2 else m_credentials->readPassword(QStringLiteral("imap"), legacy, begin);
467 });
468 13 }
469
470 263 void SettingsSyncSession::command(int action) {
471
2/2
✓ Branch 2 → 3 taken 246 times.
✓ Branch 2 → 4 taken 17 times.
263 if (!m_worker) return;
472 17 const auto generation = m_epoch->load();
473
1/1
✓ Branch 27 → 28 taken 17 times.
17 QMetaObject::invokeMethod(m_worker, [worker = m_worker, generation, action, remote = m_remoteIdentity] {
474 17 worker->command(generation, action, remote);
475 17 });
476 }
477 11 void SettingsSyncSession::synchronize() {
478
5/6
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 10 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 10 times.
11 if (m_state == QLatin1String("error") || m_state == QLatin1String("offline")) restart(true);
479 10 else command(0);
480 11 }
481
1/2
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
4 void SettingsSyncSession::useRemote() { if (m_state == QLatin1String("conflict")) command(1); }
482 3 bool SettingsSyncSession::resolveWith(const SyncPayload &resolved, const QByteArray &expectedRemote) {
483
6/8
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 2 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
3 if (m_state != QLatin1String("conflict") || !m_worker || expectedRemote != m_remoteIdentity) return false;
484 2 const auto generation = m_epoch->load();
485
1/3
✓ Branch 36 → 37 taken 2 times.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
2 QMetaObject::invokeMethod(m_worker, [worker = m_worker, generation, expectedRemote, resolved] {
486 2 worker->resolve(generation, expectedRemote, resolved);
487 2 });
488 2 return true;
489 }
490
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 void SettingsSyncSession::useLocal() { if (m_state == QLatin1String("conflict")) command(2); }
491 248 void SettingsSyncSession::localSettingsChanged() { command(3); }
492