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: 90.9% 10 / 0 / 11
Functions: 83.3% 5 / 0 / 6
Branches: 73.7% 14 / 0 / 19

data/ThreadProjectionIndex.h
Line Branch Exec Source
1 #pragma once
2
3 #include "MailSort.h"
4 #include "Models.h"
5 #include <QSqlDatabase>
6 #include <QSet>
7 #include <atomic>
8 #include <functional>
9
10 // Position in a thread page: the two leading order values (date newest
11 // first: undated flag and negated newest date; F100-66 other orders: see
12 // threadOrderColumns in the .cpp), then root UID and tree path.
13 struct ThreadProjectionCursor {
14 QVariant high;
15 QVariant low;
16 qint64 rootUid = 0;
17 QByteArray path;
18 bool valid = false;
19 };
20
21 struct ThreadProjectionRow {
22 MailHeader header;
23 qint64 parentUid = 0;
24 int depth = 0;
25 int descendants = 0;
26 int unreadDescendants = 0;
27 ThreadProjectionCursor cursor;
28 };
29
30 struct ThreadProjectionRequest {
31 QString account;
32 qint64 folderId = -1;
33 quint32 uidValidity = 0;
34 qint64 expectedRevision = -1;
35 ThreadProjectionCursor after;
36 bool before = false;
37 qint64 anchorUid = 0;
38 QString filter;
39 bool unread = false, starred = false, attachments = false;
40 QSet<qint64> collapsed;
41 // P90.16: journaled-away identities that must not be re-admitted by a
42 // projection load while their move confirmation is outstanding.
43 QSet<qint64> excludedUids;
44 // Read mails that stay in an unread-filtered view because it already
45 // showed them (the user opened them); ignored without the unread filter.
46 QSet<qint64> retainedUids;
47 // First fill of an empty list: serve the page of the revision this call
48 // just published even if newer headers arrived meanwhile (a running sync
49 // otherwise made every page stale). The caller catches up afterwards.
50 bool acceptOlderRevision = false;
51 int limit = 200;
52 // F100-66: thread order (MailSort::threadOrder); cursors of one order are
53 // meaningless in another.
54 MailSortOrder sort;
55 };
56
57 struct ThreadProjectionPage {
58 enum class Outcome { Ready, Stale, Cancelled, Failed };
59 Outcome outcome = Outcome::Failed;
60 QList<ThreadProjectionRow> rows;
61 qint64 revision = -1;
62 int totalCount = -1;
63 int unreadCount = -1;
64 bool hasMore = false;
65 // Forward pages: visible rows exist before the first returned row
66 // (only an anchored page can start below the top).
67 bool hasBefore = false;
68 QString error;
69 qsizetype rebuiltHeaders = 0;
70 qsizetype largestBuildBatch = 0;
71 // Filtered page read without a materialized match set (dense filter).
72 bool lazyMatches = false;
73 };
74
75 // Worker-only persistent, reconstructible thread projection. Header-change
76 // triggers live in the same SQLite transaction as the mail changes. Rebuild
77 // only connected components affected by those changes, using ThreadBuilder
78 // as the rules implementation. Newer dirty revisions survive publication;
79 // only current pages reach GUI consumers, with no QObject/tree indices.
80 class ThreadProjectionIndex final {
81 public:
82 // P90.4 (v3): fallback candidates use participant/date windows rather than
83 // global subject terms. Existing projections need one full rebuild to
84 // replace their component index; canonical mail content is untouched.
85 static constexpr int Version = 3;
86 static constexpr int MaximumPageRows = 200;
87
1/1
✓ Branch 3 → 4 taken 98 times.
98 ThreadProjectionIndex() = default;
88 ~ThreadProjectionIndex();
89 bool open(const QString &databasePath);
90 ThreadProjectionPage page(const ThreadProjectionRequest &request,
91 const std::atomic_bool &cancelled);
92 ✗ QString error() const { return m_error; }
93 // Diagnostic progress runs on the owning worker, between committed TEMP batches.
94 5 void setBatchObserver(std::function<void(qsizetype)> observer) { m_batchObserver=std::move(observer); }
95 // Filtered pages with at most this many matches read the page from the
96 // match set (sorted) instead of walking the folder's order index. Both
97 // read paths must return identical pages; tests pin one path with
98 // setSparseMatchRows() (-1 forces the order-index walk).
99 static constexpr qint64 DefaultSparseMatchRows = 20000;
100 1 void setSparseMatchRows(qint64 rows) { m_sparseMatchRows = rows; }
101 // Folders with at least this many mails probe the first LazyProbeRows
102 // rows; when at least 1/LazyMinimumShareDivisor of them match, the page
103 // is read without materializing the match set. -1 disables the probe;
104 // tests pass 0 to probe every folder.
105 static constexpr qint64 DefaultLazyMatchFolderRows = 20000;
106 static constexpr int LazyProbeRows = 2048;
107 static constexpr int LazyMinimumShareDivisor = 8;
108 29 void setLazyMatchFolderRows(qint64 rows) { m_lazyMatchFolderRows = rows; }
109 private:
110 ThreadProjectionPage readPage(const ThreadProjectionRequest &request,
111 const std::atomic_bool &cancelled);
112 bool refresh(const ThreadProjectionRequest &request, const std::atomic_bool &cancelled,
113 ThreadProjectionPage &result);
114 bool exec(const QString &sql, const QVariantList &bindings = {});
115 struct MatchScope {
116 QString account;
117 QString filter;
118 qint64 folderId = -1;
119 qint64 revision = -1;
120 quint32 uidValidity = 0;
121 bool unread = false;
122 bool starred = false;
123 bool attachments = false;
124 QSet<qint64> retained;
125 460 bool operator==(const MatchScope &other) const {
126
2/2
✓ Branch 5 → 6 taken 422 times.
✓ Branch 5 → 15 taken 38 times.
920 return account == other.account && filter == other.filter &&
127
3/4
✓ Branch 6 → 7 taken 422 times.
✗ Branch 6 → 15 not taken.
✓ Branch 7 → 8 taken 421 times.
✓ Branch 7 → 15 taken 1 time.
422 folderId == other.folderId && revision == other.revision &&
128
2/4
✓ Branch 8 → 9 taken 421 times.
✗ Branch 8 → 15 not taken.
✓ Branch 9 → 10 taken 421 times.
✗ Branch 9 → 15 not taken.
421 uidValidity == other.uidValidity && unread == other.unread &&
129
6/8
✓ Branch 3 → 4 taken 460 times.
✗ Branch 3 → 15 not taken.
✓ Branch 10 → 11 taken 418 times.
✓ Branch 10 → 15 taken 3 times.
✓ Branch 11 → 12 taken 418 times.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 416 times.
✓ Branch 13 → 15 taken 2 times.
1338 starred == other.starred && attachments == other.attachments &&
130 878 retained == other.retained;
131 }
132 };
133 MatchScope m_matchScope;
134 bool m_matchReady = false;
135 // Rows in the TEMP match set of m_matchScope (-1: unknown).
136 qint64 m_matchCount = -1;
137 qint64 m_sparseMatchRows = DefaultSparseMatchRows;
138 qint64 m_lazyMatchFolderRows = DefaultLazyMatchFolderRows;
139 // The ready scope reads pages without a match set (m_matchCount unused).
140 bool m_matchLazy = false;
141 QSqlDatabase m_db;
142 QString m_connection, m_error;
143 std::function<void(qsizetype)> m_batchObserver;
144 };
145