MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 96.8% 30 / 0 / 31
Functions: 95.7% 22 / 0 / 23
Branches: 91.2% 31 / 0 / 34

presentation/QuickThreadListModel.h
Line Branch Exec Source
1 #pragma once
2
3 #include "MailPresentationModel.h"
4 #include "ui/MailThreadModel.h"
5 #include "data/MailRequest.h"
6 #include "data/MailSelectionSnapshot.h"
7 #include "data/ThreadProjectionIndex.h"
8 #include <QCache>
9 #include <QThreadPool>
10 #include <QAbstractListModel>
11 #include <QHash>
12 #include <QSet>
13 #include <QVector>
14 #include <functional>
15
16 // QML thread projection: persistent worker-owned pages for large folders;
17 // the shared MailThreadModel supplies bounded small-folder/design fixtures.
18 // Expansion, filters and viewport anchors belong to this view owner. Role numbers and names deliberately match
19 // MailPresentationModel so one QML delegate serves both presentations; the
20 // thread-only roles extend the same block.
21 class QuickThreadListModel final : public QAbstractListModel {
22 ✗ Q_OBJECT
23 public:
24 enum Role {
25 Identity = Qt::UserRole + 100, Subject, Sender, Date, Unread, Starred,
26 Labels, Attachment, Selected, Folder, Suggestion,
27 Depth, ThreadCount, HasChildren, Expanded, LabelChips, OpenTask, RuleMatch,
28 Recipient, SizeText /* F100-30: list columns To and Size */,
29 Period /* search rows only; no role name here — keeps the numbers equal (S106-306) */,
30 MatchedInText /* S101.1.10: search rows only; always false here */,
31 Answered /* status column: the IMAP Answered flag */
32 };
33 explicit QuickThreadListModel(MailThreadModel *source, QString accountId, QObject *parent = nullptr);
34 ~QuickThreadListModel() override;
35 // Large folders use persistent pages. The legacy source remains the
36 // bounded small-folder/design-fixture implementation of the same roles.
37 void configureIndexedFolder(const QString &database, qint64 folder, quint32 epoch,
38 qint64 revision, qint64 anchorUid = 0,
39 const QSet<qint64> &excludedUids = {});
40 void leaveIndexedFolder();
41 // Cold thread-index build (P90 "first usable page"): while the first
42 // indexed page of `folder` is still pending and nothing is shown, plain
43 // cached rows (date order, no threading) are shown provisionally. The
44 // first indexed page replaces them. False when the state no longer fits.
45 bool showProvisionalPage(qint64 folder, quint32 epoch, const QList<MailHeader> &headers);
46 44 bool provisionalPageWanted(qint64 folder) const {
47
6/8
✓ Branch 2 → 3 taken 44 times.
✗ Branch 2 → 10 not taken.
✓ Branch 3 → 4 taken 42 times.
✓ Branch 3 → 10 taken 2 times.
✓ Branch 5 → 6 taken 32 times.
✓ Branch 5 → 10 taken 10 times.
✓ Branch 7 → 8 taken 32 times.
✗ Branch 7 → 10 not taken.
76 return m_indexed && m_indexPending && m_indexRows.isEmpty() && !filterActive() &&
48
2/2
✓ Branch 8 → 9 taken 31 times.
✓ Branch 8 → 10 taken 1 time.
76 m_indexRequest.folderId == folder;
49 }
50 void cancelIndexedRequest();
51 // Folder switch continuity: the visible rows are copied and stay on
52 // screen, inert (no identity, key or row lookup), while the model
53 // silently clears and loads the next folder. The first non-empty
54 // content (indexed page, provisional page, rebuilt thread list) or
55 // releaseHeldRows() replaces them in one reset, so no empty frame is
56 // rendered between two folders. Invariant: while holding, the
57 // underlying rows are empty.
58 void holdRowsForFolderSwitch();
59 void releaseHeldRows();
60 58 bool holdingPreviousFolder() const { return m_holding; }
61 // Estimated bytes of the published indexed rows (the bounded window).
62 qint64 retainedRowBytes() const;
63 private:
64 // Ends a hold once the underlying rows have content: one reset swaps
65 // the held rows for them.
66 void swapHeldRowsForContent();
67 public:
68 // P90.16: force a Replace reload even when identity and revision are
69 // unchanged (journal phase transitions change the pending-move
70 // exclusion set, not the revision).
71 void refreshIndexedExclusions(const QSet<qint64> &excludedUids);
72 // Fence an in-flight page and hide a newly pending move immediately;
73 // a coalesced journal read later replaces the full exclusion set.
74 void markIndexedMovePending(qint64 folder, qint64 uid);
75 quint64 indexedExclusionGeneration() const { return m_indexExclusionGeneration; }
76 void abandonIndexedMoveRefresh();
77 // S106-304: entries of the per-identity role caches (tests).
78 2 qsizetype identityCacheSize() const { return m_selectedCache.size() + m_suggestionCache.size(); }
79 5247 bool indexed() const { return m_indexed; }
80 63 qint64 indexedFolder() const { return m_indexRequest.folderId; }
81 547 bool projectionPending() const { return m_indexPending; }
82 // A pending page the user waits for: nothing is shown yet, or the user
83 // replaces the content (filter, jump). Background refreshes (revision
84 // catch-ups, move exclusions, paging, expand) keep the published rows.
85 202 bool projectionBlocking() const {
86
6/6
✓ Branch 2 → 3 taken 94 times.
✓ Branch 2 → 7 taken 108 times.
✓ Branch 4 → 5 taken 47 times.
✓ Branch 4 → 6 taken 47 times.
✓ Branch 5 → 6 taken 16 times.
✓ Branch 5 → 7 taken 31 times.
202 return m_indexPending && (m_indexRows.isEmpty() || m_indexLoadAnnounced);
87 }
88 Q_PROPERTY(bool projectionPending READ projectionPending NOTIFY projectionChanged)
89 187 bool hasMoreIndexed(bool before = false) const {
90
10/10
✓ Branch 2 → 3 taken 137 times.
✓ Branch 2 → 8 taken 50 times.
✓ Branch 3 → 4 taken 81 times.
✓ Branch 3 → 8 taken 56 times.
✓ Branch 4 → 5 taken 52 times.
✓ Branch 4 → 6 taken 29 times.
✓ Branch 5 → 7 taken 18 times.
✓ Branch 5 → 8 taken 34 times.
✓ Branch 6 → 7 taken 27 times.
✓ Branch 6 → 8 taken 2 times.
187 return m_indexed && !m_indexPending && (before ? m_indexHasBefore : m_indexHasAfter);
91 }
92 void fetchIndexed(bool before = false);
93 void jumpIndexed(bool last);
94 void revealIndexed(qint64 uid, bool before);
95 Q_INVOKABLE void rememberViewport(int row, qreal offset);
96 // P90.0 trace wiring: the revision the indexed projection currently
97 // exposes (page or catch-up state), -1 when none is loaded.
98 19 qint64 indexRevision() const { return m_indexRevision; }
99 10 int indexedTotalCount() const { return m_indexTotalCount; }
100 9 int indexedUnreadCount() const { return m_indexUnreadCount; }
101 void updateIndexedFlags(qint64 folder, qint64 uid, quint32 flags);
102 void updateIndexedLabel(qint64 folder, qint64 uid, const QString &label, bool present);
103 // P90.15: visual seen/unseen commit for the indexed projection. Applies
104 // only the Seen bit of the loaded row (no SQL); the durable cache write
105 // later re-applies the authoritative flags through updateIndexedFlags.
106 void applyIndexedSeen(qint64 folder, qint64 uid, bool seen);
107 void applyIndexedStar(qint64 folder, qint64 uid, bool starred);
108 // P90.16: optimistic visual removal for journaled moves. The canonical
109 // header stays until the mutation is confirmed remotely; the projection
110 // refresh later publishes the authoritative page. The freed visual slot
111 // is taken over by the successor row without a model reset.
112 void removeIndexedUid(qint64 folder, qint64 uid);
113 void removeIndexedUids(qint64 folder, const QSet<qint64> &uids);
114 // Identity only (account, folder, epoch; no revision): decides whether a
115 // page of an older revision may fill an empty list during a fast sync.
116 797 void setProjectionIdentityValidator(std::function<bool(const ThreadProjectionRequest &)> validator) {
117 797 m_acceptIndexedIdentity = std::move(validator);
118 797 }
119 798 void setProjectionValidator(std::function<bool(const ThreadProjectionRequest &, qint64)> validator) {
120 798 m_acceptIndexed = std::move(validator);
121 798 }
122 340 QString filterText() const { return m_filterText; }
123 340 bool showUnreadOnly() const { return m_showUnread; }
124 340 bool showStarredOnly() const { return m_showStarred; }
125 340 bool showWithAttachments() const { return m_showAttachments; }
126 QHash<int, QByteArray> roleNames() const override;
127 int rowCount(const QModelIndex &parent = {}) const override;
128 QVariant data(const QModelIndex &index, int role) const override;
129 void setAccountId(const QString &accountId);
130 // Filter semantics mirror MailFilterProxyModel (text against subject and
131 // sender, unread-only, starred-only). A thread stays visible when only a
132 // descendant matches; ancestors of a kept row are always kept, and a
133 // collapsed ancestor still hides its whole subtree.
134 Q_INVOKABLE void filter(const QString &text, bool unreadOnly, bool starredOnly = false, bool withAttachments = false);
135 Q_INVOKABLE void clearFilters();
136 // F100-66: threads ordered by their newest mail (the ThreadBuilder
137 // order) or by their first mail's sender or subject, either direction
138 // (MailSort::threadOrder); replies stay in tree order. A change reloads
139 // the first page (indexed) or rebuilds the rows.
140 void setSortOrder(const MailSortOrder &order);
141 const MailSortOrder &sortOrder() const { return m_sort; }
142 // Expansion is in-memory view state keyed by (folderId, uid), like the
143 // Widgets tree view. New threads start expanded (Widgets expandAll).
144 Q_INVOKABLE void toggleExpanded(int row);
145 Q_INVOKABLE bool isExpanded(int row) const;
146 Q_INVOKABLE QVariantMap mailAt(int row) const;
147 Q_INVOKABLE int rowForIdentity(const QVariantMap &identity) const;
148 MailSelectionKey keyAt(int row) const;
149 // Borrow the already-published preview header for an indexed selection.
150 // The caller must copy it synchronously; page publication can replace it.
151 const MailHeader *indexedHeader(qint64 folder, qint64 uid) const;
152 798 void setSelectionProvider(std::function<bool(qint64, qint64)> provider) { m_selected = std::move(provider); }
153 void selectionChanged();
154 void singleSelectionChanged(MailSelectionKey previous, MailSelectionKey current);
155 797 void setSuggestionProvider(std::function<QString(const MailId &)> provider) { m_suggestion = std::move(provider); }
156 void suggestionsChanged();
157 // S94.5.5: whether a raw Message-ID header has an open linked task.
158 795 void setTaskLinkProvider(std::function<bool(const QString &)> provider) { m_taskLink = std::move(provider); }
159 // F95-14: whether an enabled server rule matches the row.
160 795 void setRuleMatchProvider(std::function<bool(const MailHeader &)> provider) { m_ruleMatch = std::move(provider); }
161 void ruleMatchesChanged();
162 void taskLinksChanged();
163 signals:
164 void projectionChanged();
165 void viewportRestoreRequested(const QVariantMap &identity, qreal offset);
166 void projectionReady();
167 void projectionFailed(const QString &error);
168 void projectionStale();
169 void mailFilterChanged();
170 private:
171 enum class PageMode { Replace, Append, Prepend, Last };
172 // P90.1: bounded catch-up chain per pending sequence. Folder-open reads
173 // absorb mid-read revision changes without dropping pending state;
174 // beyond the bound the explicit stale contract applies again.
175 static constexpr int MaximumCatchUpAttempts = 8;
176 static constexpr int MaximumDiffMoves = 64;
177 // Matches the ThreadProjectionIndex request limit for retained uids.
178 static constexpr int MaximumRetainedUnread = 4096;
179 void loadIndexed(PageMode mode, qint64 anchorUid = 0, bool restoreViewport = true);
180 // loadIndexed() steps (S92.4.2), in their former order: request, worker
181 // read, GUI-side completion (catch-up decision, Replace or rolling
182 // window publication, viewport re-pin) and the orphaned-page snapshot.
183 ThreadProjectionRequest indexedPageRequest(PageMode mode, qint64 anchorUid);
184 static ThreadProjectionPage buildIndexedPage(const ThreadProjectionRequest &request,
185 PageMode mode, const QString &database,
186 std::atomic_bool &cancelled, int &catchUps);
187 void finishIndexedPage(quint64 generation, const ThreadProjectionRequest &request,
188 PageMode mode, qreal viewportOffset, bool restoreViewport,
189 ThreadProjectionPage result, qint64 elapsedMicros, int catchUps,
190 const QString &database);
191 void retainOrphanedPage(const ThreadProjectionRequest &request, PageMode mode,
192 const ThreadProjectionPage &result, const QString &database);
193 bool publishReplacePage(const ThreadProjectionRequest &request, PageMode mode,
194 ThreadProjectionPage &result, bool revisionChanged);
195 void publishWindowPage(PageMode mode, const ThreadProjectionPage &result);
196 void repinViewportAfterReset(qreal viewportOffset, bool restoreViewport);
197 // configureIndexedFolder() steps (S92.4.2): the new-folder path.
198 bool enterIndexedFolder(const QString &database, qint64 folder, quint32 epoch,
199 qint64 revision, const QSet<qint64> &excludedUids);
200 QList<ThreadProjectionRow> provisionalIndexRows(qint64 folder,
201 const QSet<qint64> &excludedUids) const;
202 qint64 catchUpAnchor(qint64 requestedAnchor) const;
203 void publishIndexedContent(QList<ThreadProjectionRow> next);
204 bool applyIndexedRowDiff(const QList<ThreadProjectionRow> &next);
205 void toggleIndexed(int row);
206 void retainIndexedSnapshot();
207 QString indexedKey() const;
208 static QString snapshotKey(const QString &database, const ThreadProjectionRequest &request);
209 // P90.2: bounded orphan accounting for background-completing page loads.
210 void forgetActiveRequest(const std::shared_ptr<std::atomic_bool> &token);
211 struct IndexSnapshot {
212 QList<ThreadProjectionRow> rows;
213 ThreadProjectionRequest request;
214 qint64 revision = -1;
215 int totalCount = -1, unreadCount = -1;
216 bool before = false, after = false;
217 qint64 viewportUid = 0;
218 qreal viewportOffset = 0;
219 };
220 bool m_indexed = false, m_indexPending = false;
221 bool m_indexExclusionsDirty = false;
222 quint64 m_indexExclusionGeneration = 0;
223 bool m_indexHasBefore = false, m_indexHasAfter = false;
224 QString m_indexDatabase;
225 ThreadProjectionRequest m_indexRequest;
226 qint64 m_indexRevision = -1;
227 // Newest revision configureIndexedFolder() asked for; a page that lands
228 // behind it is published and followed by a bounded catch-up.
229 qint64 m_indexWantedRevision = -1;
230 int m_indexTotalCount = -1, m_indexUnreadCount = -1;
231 qint64 m_viewportUid = 0;
232 qreal m_viewportOffset = 0;
233 quint64 m_indexGeneration = 0;
234 int m_indexCatchUps = 0;
235 QList<ThreadProjectionRow> m_indexRows;
236 // P90.12: collapsed subtree spans retained for differential expand.
237 // Revision-fenced: any structural change discards them so a re-insert
238 // can never publish stale rows; bounded and cleared on folder changes.
239 struct CollapsedSpan {
240 qint64 revision = -1;
241 QList<ThreadProjectionRow> rows;
242 };
243 QHash<qint64, CollapsedSpan> m_collapsedSpans;
244 qsizetype m_collapsedSpanRows = 0;
245 QThreadPool m_indexPool;
246 std::shared_ptr<std::atomic_bool> m_indexCancelled;
247 QList<std::shared_ptr<std::atomic_bool>> m_activeRequests;
248 QCache<QString, IndexSnapshot> m_indexSnapshots{3*1024}; // KiB, in addition to the active window
249 std::function<bool(const ThreadProjectionRequest &, qint64)> m_acceptIndexed;
250 std::function<bool(const ThreadProjectionRequest &)> m_acceptIndexedIdentity;
251 using Row = MailThreadModel::VisibleRow;
252 using MailKey = MailThreadModel::MailKey;
253 void rebuild();
254 // Compute descendant visibility once, then append in the existing DFS
255 // order without rescanning each subtree for every visible ancestor.
256 bool collectMatches(const ThreadNode *node,
257 QSet<const ThreadNode *> *matching) const;
258 void appendVisible(ThreadNode *node, int depth, QVector<Row> *rows,
259 const QSet<const ThreadNode *> *matching = nullptr) const;
260 bool matches(const MailHeader &header) const;
261 void forgetShownUnread();
262
7/8
✓ Branch 3 → 4 taken 4262 times.
✓ Branch 3 → 7 taken 10 times.
✓ Branch 4 → 5 taken 4249 times.
✓ Branch 4 → 7 taken 13 times.
✓ Branch 5 → 6 taken 4247 times.
✓ Branch 5 → 7 taken 2 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 4247 times.
4272 bool filterActive() const { return !m_filterText.isEmpty() || m_showUnread || m_showStarred || m_showAttachments; }
263 const MailHeader *headerAtRow(int row) const;
264 MailKey keyAtRow(int row) const;
265 void reindexRows(int from = 0);
266 // Remove a published indexed range without rebuilding the lookup and
267 // every role cache for surviving rows.
268 void removeIndexedRows(int first, int last);
269 MailThreadModel *m_source;
270 QString m_accountId;
271 QVector<Row> m_rows;
272 QHash<MailKey, int> m_rowByKey;
273 QSet<MailKey> m_collapsed;
274 QString m_filterText;
275 MailSortOrder m_sort; // MailSort::threadOrder
276 // Legacy (small-folder) roots in m_sort order.
277 QList<ThreadNode *> orderedRoots(const std::vector<std::unique_ptr<ThreadNode>> &roots) const;
278 bool m_showUnread = false;
279 // Mails this unread-filtered view already showed stay visible after they
280 // are read, until the filter or the folder changes (one folder at a time).
281 mutable QSet<MailKey> m_shownUnread;
282 bool m_indexLoadAnnounced = false;
283 bool m_holding = false;
284 QList<ThreadProjectionRow> m_heldRows;
285 QVector<bool> m_heldExpanded;
286 bool m_nextLoadAnnounced = false;
287 mutable qint64 m_shownUnreadFolder = -1;
288 bool m_showStarred = false;
289 bool m_showAttachments = false;
290 std::function<bool(qint64, qint64)> m_selected;
291 std::function<QString(const MailId &)> m_suggestion;
292 std::function<bool(const QString &)> m_taskLink;
293 std::function<bool(const MailHeader &)> m_ruleMatch;
294 // P90.12: last published per-identity role values so selection/suggestion
295 // changes invalidate only rows whose value actually flipped.
296 // Last value each view read through data() (or a published diff). A row
297 // absent here was never read, so a change needs no notification.
298 mutable QHash<MailKey, bool> m_selectedCache;
299 mutable QHash<MailKey, QString> m_suggestionCache;
300 // P90.12: materialized per-identity display roles — locale date text and
301 // label chips are computed once per header revision instead of on every
302 // delegate evaluation. Lazily filled from const data().
303 mutable QHash<MailKey, QString> m_dateTextCache;
304 mutable QHash<MailKey, QVariantList> m_chipCache;
305 };
306