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: 89.0% 89 / 0 / 100
Functions: 100.0% 7 / 0 / 7
Branches: 67.3% 111 / 0 / 165

controller/MailSelectionStateCommands.cpp
Line Branch Exec Source
1 #include "MailSelectionStateCommands.h"
2
3 #include "controller/MailMutationPipeline.h"
4 #include "controller/UndoManager.h"
5 #include "data/MailCache.h"
6 #include "service/MailSelectionMutationService.h"
7 #include "ui/MailListModel.h"
8 #include "ui/MailThreadModel.h"
9
10 #include <QLoggingCategory>
11 #include <QTimer>
12
13 Q_DECLARE_LOGGING_CATEGORY(lcController)
14
15 1050 MailSelectionStateCommands::MailSelectionStateCommands(MailCache *cache, MailListModel *model,
16 MailThreadModel *const &threadModel, FolderContext &folder, MailMutationPipeline &mutations,
17 1050 const QPointer<UndoManager> &undoManager, Deps deps, QObject *parent)
18 1050 : QObject(parent), m_cache(cache), m_model(model), m_threadModel(threadModel), m_folder(folder),
19 1050 m_mutations(mutations), m_undoManager(undoManager), m_deps(std::move(deps)) {}
20
21 18 QString MailSelectionStateCommands::describe(MutationKind kind, const QString &label, bool desired, int count) {
22
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 7 taken 12 times.
18 if (kind == MutationKind::Seen)
23
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 2 times.
6 return desired ? tr("%n message(s) marked as read", nullptr, count)
24 6 : tr("%n message(s) marked as unread", nullptr, count);
25
2/2
✓ Branch 7 → 8 taken 11 times.
✓ Branch 7 → 12 taken 1 time.
12 if (kind == MutationKind::Flagged)
26
2/2
✓ Branch 8 → 9 taken 10 times.
✓ Branch 8 → 10 taken 1 time.
11 return desired ? tr("%n message(s) marked as important", nullptr, count)
27 11 : tr("Important mark removed from %n message(s)", nullptr, count);
28
1/3
✓ Branch 13 → 14 taken 1 time.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
2 return desired ? tr("Label “%1” added to %n message(s)", nullptr, count).arg(label)
29
4/11
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 16 not taken.
✓ Branch 15 → 19 taken 1 time.
✗ Branch 16 → 17 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
2 : tr("Label “%1” removed from %n message(s)", nullptr, count).arg(label);
30 }
31
32 19 void MailSelectionStateCommands::setState(std::shared_ptr<const MailSelectionSnapshot> selection,
33 qint64 folderId, const QList<qint64> &uids, MutationKind kind, const QString &label, bool desired,
34 bool undoable, std::function<void(bool journaled)> done) {
35 19 MailSelectionStateRequest request;
36 19 request.accountId = m_folder.accountId;
37
2/2
✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 7 taken 7 times.
19 request.folderId = selection ? selection->folderId : folderId;
38
1/1
✓ Branch 8 → 9 taken 19 times.
19 request.folderPath = m_cache->folderPath(request.folderId);
39 // F-67 (SPRINT-104): a captured selection acts under ITS OWN epoch —
40 // the request must never silently rebase old UIDs onto a newer mailbox
41 // identity (a reused UID would change a different message). `pinned`
42 // carries the epoch an undo entry was created under.
43
2/2
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 15 taken 7 times.
26 request.uidValidity = selection ? selection->uidValidity
44
2/2
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 17 taken 4 times.
7 : m_epoch != 0 ? m_epoch
45
1/1
✓ Branch 17 → 18 taken 4 times.
4 : m_cache->uidValidity(request.folderId);
46 19 request.selection = std::move(selection);
47 19 request.uids = uids;
48 19 request.kind = kind;
49
2/2
✓ Branch 23 → 24 taken 7 times.
✓ Branch 23 → 27 taken 12 times.
64 request.flag = kind == MutationKind::Seen ? QStringLiteral("\\Seen")
50
6/6
✓ Branch 27 → 28 taken 11 times.
✓ Branch 27 → 31 taken 1 time.
✓ Branch 34 → 35 taken 11 times.
✓ Branch 34 → 36 taken 8 times.
✓ Branch 36 → 37 taken 7 times.
✓ Branch 36 → 38 taken 12 times.
50 : kind == MutationKind::Flagged ? QStringLiteral("\\Flagged") : label;
51 19 request.desiredState = desired;
52 19 const QString databasePath = m_cache->databasePath();
53
9/9
✓ Branch 39 → 40 taken 19 times.
✓ Branch 43 → 44 taken 19 times.
✓ Branch 44 → 45 taken 19 times.
✓ Branch 45 → 46 taken 19 times.
✓ Branch 46 → 47 taken 19 times.
✓ Branch 47 → 48 taken 19 times.
✓ Branch 48 → 49 taken 19 times.
✓ Branch 58 → 41 taken 19 times.
✓ Branch 58 → 59 taken 19 times.
38 qCDebug(lcController) << "selection state: kind" << int(kind) << "desired" << desired << "count"
54
3/3
✓ Branch 50 → 51 taken 12 times.
✓ Branch 50 → 53 taken 7 times.
✓ Branch 54 → 55 taken 19 times.
19 << (request.selection ? request.selection->count : qint64(uids.size()));
55 // F-69: the completion guard compares the ACCOUNT THIS REQUEST was built
56 // for — result.request is only populated on success, and a failed round
57 // must still reach `done` so an asynchronous Undo entry can keep itself
58 // retryable instead of hanging the manager.
59 19 const QString requestAccount = request.accountId;
60
1/1
✓ Branch 60 → 61 taken 19 times.
19 const QPointer<MailSelectionStateCommands> owner(this);
61
2/2
✓ Branch 61 → 62 taken 19 times.
✓ Branch 71 → 72 taken 19 times.
38 MailSelectionMutationService::shared()->setSelectionState(this, databasePath, std::move(request),
62
1/7
✓ Branch 67 → 68 taken 19 times.
✗ Branch 97 → 98 not taken.
✗ Branch 97 → 99 not taken.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 103 → 104 not taken.
✗ Branch 103 → 105 not taken.
38 [owner, databasePath, requestAccount, undoable, done = std::move(done)](MailSelectionStateResult result) {
63 19 const bool journaled = result.outcome == MailSelectionStateResult::Outcome::Journaled;
64 // An action accepted for another profile or account is durable there;
65 // only the account it belongs to shows it.
66
4/8
✓ Branch 3 → 4 taken 19 times.
✗ Branch 3 → 11 not taken.
✓ Branch 7 → 8 taken 19 times.
✗ Branch 7 → 11 not taken.
✓ Branch 13 → 14 taken 19 times.
✗ Branch 13 → 15 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 31 taken 19 times.
38 if (!owner || owner->m_cache->databasePath() != databasePath ||
67
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 19 times.
19 requestAccount != owner->m_folder.accountId) {
68 ✗ if (owner && !journaled)
69 ✗ owner->m_deps.actionError(tr("The selected messages could not be changed. Nothing was changed."));
70 ✗ if (done) done(journaled);
71 ✗ return;
72 }
73 19 owner->applyResult(result, undoable);
74
2/2
✓ Branch 34 → 35 taken 5 times.
✓ Branch 34 → 36 taken 14 times.
19 if (done) done(journaled);
75 });
76 19 }
77
78 19 void MailSelectionStateCommands::applyResult(const MailSelectionStateResult &result, bool undoable) {
79
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 20 taken 18 times.
19 if (result.outcome != MailSelectionStateResult::Outcome::Journaled) {
80
6/6
✓ Branch 3 → 4 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 5 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcController) << "selection state rejected:" << result.error;
81
1/1
✓ Branch 17 → 18 taken 1 time.
1 m_deps.actionError(result.outcome == MailSelectionStateResult::Outcome::Limited
82
2/4
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 15 → 17 not taken.
✓ Branch 16 → 17 taken 1 time.
2 ? tr("The selection is too large for one action. Nothing was changed.")
83 : tr("The selected messages could not be changed. Nothing was changed."));
84 4 return;
85 }
86 18 const auto &request = result.request;
87 18 const bool seen = request.kind == MutationKind::Seen;
88 18 QList<qint64> changed;
89 18 bool outsideWindow = false;
90
2/2
✓ Branch 51 → 22 taken 282 times.
✓ Branch 51 → 52 taken 18 times.
300 for (const auto &change : result.changes) {
91
1/1
✓ Branch 23 → 24 taken 282 times.
282 changed.append(change.uid);
92 282 const MailId id{request.accountId, request.folderId, change.uid};
93
2/2
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 29 taken 279 times.
282 if (request.kind == MutationKind::Keyword) {
94
1/1
✓ Branch 26 → 27 taken 3 times.
3 m_deps.applyVisibleLabel(change.uid, request.folderId, request.flag, request.desiredState);
95
1/1
✓ Branch 27 → 28 taken 3 times.
3 m_cache->announceLabelCommitted(request.folderId, change.uid, request.flag, request.desiredState);
96 3 continue;
97 }
98
1/1
✓ Branch 30 → 31 taken 279 times.
279 m_mutations.intents().markFlag(id);
99
3/3
✓ Branch 31 → 32 taken 279 times.
✓ Branch 33 → 34 taken 246 times.
✓ Branch 33 → 37 taken 33 times.
279 if (m_model->flagsFor(change.uid, request.folderId)) {
100
1/1
✓ Branch 34 → 35 taken 246 times.
246 m_model->updateFlags(change.uid, change.flags, request.folderId);
101
2/3
✓ Branch 35 → 36 taken 246 times.
✗ Branch 35 → 40 not taken.
✓ Branch 36 → 40 taken 246 times.
246 if (m_threadModel) m_threadModel->updateFlags(change.uid, change.flags, request.folderId);
102
3/4
✓ Branch 37 → 38 taken 5 times.
✓ Branch 37 → 40 taken 28 times.
✓ Branch 38 → 39 taken 5 times.
✗ Branch 38 → 40 not taken.
33 } else if (seen && request.folderId == m_folder.currentFolderId) {
103 5 outsideWindow = true;
104 }
105
3/3
✓ Branch 40 → 41 taken 12 times.
✓ Branch 40 → 42 taken 267 times.
✓ Branch 41 → 43 taken 12 times.
279 if (seen) m_deps.seenVisualChanged(request.folderId, change.uid, request.desiredState);
106
1/1
✓ Branch 42 → 43 taken 267 times.
267 else m_deps.starVisualChanged(request.folderId, change.uid, request.desiredState);
107 282 }
108 // S106-196: rows outside the loaded window changed only the cache. Its
109 // committed folder total (one indexed point read) replaces the count — a
110 // ±1 per row corrupted a total that did not include such a row.
111
2/2
✓ Branch 52 → 53 taken 3 times.
✓ Branch 52 → 55 taken 15 times.
18 if (outsideWindow)
112
2/2
✓ Branch 53 → 54 taken 3 times.
✓ Branch 54 → 55 taken 3 times.
3 m_model->setAuthoritativeUnread(m_cache->unreadCount(m_folder.currentFolderId));
113
3/4
✓ Branch 55 → 56 taken 6 times.
✓ Branch 55 → 59 taken 12 times.
✓ Branch 56 → 57 taken 6 times.
✗ Branch 56 → 59 not taken.
18 if (seen && request.folderId == m_folder.currentFolderId)
114
2/2
✓ Branch 57 → 58 taken 6 times.
✓ Branch 58 → 59 taken 6 times.
6 m_deps.unreadCountChanged(m_folder.currentFolder, m_model->unreadCount());
115
2/3
✓ Branch 60 → 61 taken 18 times.
✗ Branch 60 → 63 not taken.
✓ Branch 61 → 62 taken 18 times.
18 if (!changed.isEmpty()) QTimer::singleShot(0, &m_mutations, &MailMutationPipeline::replayMutationJournal);
116
1/1
✓ Branch 64 → 65 taken 18 times.
18 const QString text = describe(request.kind, request.flag, request.desiredState, int(changed.size()));
117
1/1
✓ Branch 65 → 66 taken 18 times.
18 m_deps.statusMessage(text);
118
6/8
✓ Branch 66 → 67 taken 15 times.
✓ Branch 66 → 71 taken 3 times.
✓ Branch 68 → 69 taken 15 times.
✗ Branch 68 → 71 not taken.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 15 times.
✓ Branch 73 → 74 taken 3 times.
✓ Branch 73 → 75 taken 15 times.
18 if (!undoable || changed.isEmpty() || !m_undoManager) return;
119
1/1
✓ Branch 75 → 76 taken 15 times.
15 const QPointer<MailSelectionStateCommands> owner(this);
120 15 const qint64 folderId = request.folderId;
121 15 const MutationKind kind = request.kind;
122 15 const QString label = request.flag;
123 15 const bool desired = request.desiredState;
124 // F-67: the inverse carries the epoch the forward action was journaled
125 // under; a mailbox reset since then fails the undo visibly instead of
126 // changing whatever message now owns the UID.
127 // F-69: the entry is asynchronous — the inverse write finishes on the
128 // durable worker, so a FAILED write keeps the entry retryable instead
129 // of consuming it with "Undo performed". Acceptance is local durable
130 // journal admission, not server confirmation.
131 15 const quint32 epoch = result.request.uidValidity;
132
1/1
✓ Branch 82 → 83 taken 15 times.
30 m_undoManager->pushAsync(text,
133
1/7
✓ Branch 81 → 82 taken 15 times.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
30 [owner, folderId, changed, kind, label, desired, epoch](UndoManager::Completion finish) {
134
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 3 times.
3 if (!owner) {
135 ✗ finish(false);
136 ✗ return;
137 }
138
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 26 taken 3 times.
3 if (owner->m_cache->uidValidity(folderId) != epoch) {
139 ✗ qCWarning(lcController) << "Undo of a selection state change refused:"
140 ✗ << "mailbox identity changed since the action";
141 ✗ owner->m_deps.actionError(MailSelectionStateCommands::tr(
142 "The mailbox changed in the meantime; the change can no longer be undone."));
143 ✗ finish(false);
144 ✗ return;
145 }
146 3 owner->m_epoch = epoch;
147
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
6 owner->setState(nullptr, folderId, changed, kind, label, !desired, false,
148
1/1
✓ Branch 28 → 29 taken 3 times.
9 [finish](bool journaled) { finish(journaled); });
149 3 owner->m_epoch = 0;
150 });
151 21 }
152