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 |