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: 85.3% 93 / 0 / 109
Functions: 100.0% 7 / 0 / 7
Branches: 66.9% 158 / 0 / 236

data/MailCacheMaintenance.cpp
Line Branch Exec Source
1 // Account and folder maintenance of MailCache (rename, removal, folder
2 // listing for the predictor) and the folder badge cache, split from
3 // MailCache.cpp for its line limit (SPRINT-92.md §8).
4 #include "MailCache.h"
5
6 #include "data/MailCacheSql.h"
7
8 #include <QLoggingCategory>
9 #include <QMutexLocker>
10 #include <QSqlError>
11 #include <QSqlQuery>
12
13 Q_DECLARE_LOGGING_CATEGORY(lcCache)
14
15 using MailCacheSql::beginWriteTransaction;
16
17 // --- Maintenance ---
18
19 // T-138: Path-based convenience overloads
20
21 // T-286: Rename folder path in DB (for folder rename/move operations)
22 1 bool MailCache::renameAccount(const QString &oldName, const QString &newName) {
23
4/8
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
1 if (oldName.isEmpty() || newName.isEmpty() || oldName == newName) return false;
24 1 const auto writeMutex = m_writeMutex;
25 1 QMutexLocker publication(writeMutex.get());
26
2/3
✓ Branch 15 → 16 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 34 taken 1 time.
1 if (!beginWriteTransaction(m_db)) {
27 ✗ m_lastError = m_db.lastError().text();
28 ✗ qCWarning(lcCache) << "Failed to start account rename transaction:" << m_lastError;
29 ✗ return false;
30 }
31
1/1
✓ Branch 34 → 35 taken 1 time.
1 QSqlQuery folders(m_db);
32
1/1
✓ Branch 38 → 39 taken 1 time.
1 folders.prepare(QStringLiteral("UPDATE folders SET account = :new WHERE account = :old"));
33
1/1
✓ Branch 46 → 47 taken 1 time.
2 folders.bindValue(QStringLiteral(":new"), newName);
34
1/1
✓ Branch 55 → 56 taken 1 time.
2 folders.bindValue(QStringLiteral(":old"), oldName);
35
1/1
✓ Branch 59 → 60 taken 1 time.
1 QSqlQuery journal(m_db);
36
1/1
✓ Branch 63 → 64 taken 1 time.
1 journal.prepare(QStringLiteral(
37 "UPDATE mutation_journal SET account_id = :new WHERE account_id = :old"));
38
1/1
✓ Branch 71 → 72 taken 1 time.
2 journal.bindValue(QStringLiteral(":new"), newName);
39
1/1
✓ Branch 80 → 81 taken 1 time.
2 journal.bindValue(QStringLiteral(":old"), oldName);
40
7/11
✓ Branch 84 → 85 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
✗ Branch 85 → 90 not taken.
✓ Branch 86 → 87 taken 1 time.
✓ Branch 87 → 88 taken 1 time.
✗ Branch 87 → 90 not taken.
✓ Branch 88 → 89 taken 1 time.
✗ Branch 89 → 90 not taken.
✓ Branch 89 → 91 taken 1 time.
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 120 taken 1 time.
1 if (!folders.exec() || !journal.exec() || !m_db.commit()) {
41 ✗ m_lastError = folders.lastError().isValid() ? folders.lastError().text()
42 ✗ : m_db.lastError().text();
43 ✗ m_db.rollback();
44 ✗ qCWarning(lcCache) << "Failed to rename account cache rows:" << m_lastError;
45 ✗ return false;
46 }
47 1 m_folderIdentity.clear();
48
8/8
✓ Branch 121 → 122 taken 1 time.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 127 → 128 taken 1 time.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 129 → 130 taken 1 time.
✓ Branch 133 → 123 taken 1 time.
✓ Branch 133 → 134 taken 1 time.
2 qCInfo(lcCache) << "Renamed account cache rows" << oldName << "->" << newName;
49 1 return true;
50 1 }
51
52 5 bool MailCache::removeAccountData(const QString &account) {
53
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 5 times.
5 if (account.isEmpty()) return false;
54 5 const auto writeMutex = m_writeMutex;
55 5 QMutexLocker publication(writeMutex.get());
56
2/3
✓ Branch 8 → 9 taken 5 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 27 taken 5 times.
5 if (!beginWriteTransaction(m_db)) {
57 ✗ m_lastError = m_db.lastError().text();
58 ✗ qCWarning(lcCache) << "Failed to start account removal transaction:" << m_lastError;
59 ✗ return false;
60 }
61
1/1
✓ Branch 27 → 28 taken 5 times.
5 QSqlQuery journal(m_db);
62
1/1
✓ Branch 31 → 32 taken 5 times.
5 journal.prepare(QStringLiteral("DELETE FROM mutation_journal WHERE account_id = :account"));
63
1/1
✓ Branch 39 → 40 taken 5 times.
10 journal.bindValue(QStringLiteral(":account"), account);
64
1/1
✓ Branch 43 → 44 taken 5 times.
5 QSqlQuery folders(m_db);
65
1/1
✓ Branch 47 → 48 taken 5 times.
5 folders.prepare(QStringLiteral("DELETE FROM folders WHERE account = :account"));
66
1/1
✓ Branch 55 → 56 taken 5 times.
10 folders.bindValue(QStringLiteral(":account"), account);
67
7/11
✓ Branch 59 → 60 taken 5 times.
✓ Branch 60 → 61 taken 5 times.
✗ Branch 60 → 65 not taken.
✓ Branch 61 → 62 taken 5 times.
✓ Branch 62 → 63 taken 5 times.
✗ Branch 62 → 65 not taken.
✓ Branch 63 → 64 taken 5 times.
✗ Branch 64 → 65 not taken.
✓ Branch 64 → 66 taken 5 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 95 taken 5 times.
5 if (!journal.exec() || !folders.exec() || !m_db.commit()) {
68 ✗ m_lastError = journal.lastError().isValid() ? journal.lastError().text()
69 ✗ : folders.lastError().text();
70 ✗ m_db.rollback();
71 ✗ qCWarning(lcCache) << "Failed to remove account cache rows:" << m_lastError;
72 ✗ return false;
73 }
74 5 m_folderIdentity.clear();
75
1/1
✓ Branch 96 → 97 taken 5 times.
5 m_payloadLimits.refreshPayloadCacheBytes();
76
6/6
✓ Branch 97 → 98 taken 5 times.
✓ Branch 101 → 102 taken 5 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 103 → 104 taken 5 times.
✓ Branch 107 → 99 taken 5 times.
✓ Branch 107 → 108 taken 5 times.
10 qCInfo(lcCache) << "Removed cached data of account" << account;
77 5 return true;
78 5 }
79
80 7 void MailCache::renameFolderPath(const QString &account,
81 const QString &oldPath,
82 const QString &newPath,
83 const QString &delimiter) {
84 // F-51 (SPRINT-104): the rename carries the folder AND its children —
85 // a moved parent renames every path below it. The prefix comparison
86 // avoids LIKE wildcard surprises in folder names.
87
1/1
✓ Branch 2 → 3 taken 7 times.
7 QSqlQuery q(m_db);
88
1/1
✓ Branch 6 → 7 taken 7 times.
7 q.prepare(QStringLiteral(
89 "UPDATE folders SET path = :newPath || substr(path, :oldLen) "
90 "WHERE account = :account AND "
91 "(path = :oldPath OR substr(path, 1, :oldLen) = :oldPrefix)"));
92
1/1
✓ Branch 14 → 15 taken 7 times.
14 q.bindValue(QStringLiteral(":newPath"), newPath);
93
1/1
✓ Branch 23 → 24 taken 7 times.
14 q.bindValue(QStringLiteral(":account"), account);
94
1/1
✓ Branch 32 → 33 taken 7 times.
14 q.bindValue(QStringLiteral(":oldPath"), oldPath);
95 // S106-112: SQLite's substr() counts code points, QString::size() UTF-16
96 // units — an astral character (emoji) would shift the child match.
97
2/2
✓ Branch 37 → 38 taken 7 times.
✓ Branch 43 → 44 taken 7 times.
14 q.bindValue(QStringLiteral(":oldLen"), oldPath.toUcs4().size() + 1);
98
2/2
✓ Branch 49 → 50 taken 7 times.
✓ Branch 54 → 55 taken 7 times.
14 q.bindValue(QStringLiteral(":oldPrefix"), oldPath + delimiter);
99 7 m_folderIdentity.clear();
100
3/3
✓ Branch 60 → 61 taken 7 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 61 → 80 taken 4 times.
7 if (q.exec()) {
101
8/8
✓ Branch 62 → 63 taken 3 times.
✓ Branch 66 → 67 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 70 → 71 taken 3 times.
✓ Branch 78 → 64 taken 3 times.
✓ Branch 78 → 79 taken 3 times.
6 qCInfo(lcCache) << "Renamed folder path" << oldPath << "->" << newPath
102
4/4
✓ Branch 71 → 72 taken 3 times.
✓ Branch 72 → 73 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✓ Branch 74 → 75 taken 3 times.
3 << "with children (" << q.numRowsAffected() << "rows)";
103 } else {
104
5/5
✓ Branch 80 → 81 taken 4 times.
✓ Branch 84 → 85 taken 4 times.
✓ Branch 85 → 86 taken 4 times.
✓ Branch 94 → 82 taken 4 times.
✓ Branch 94 → 95 taken 4 times.
8 qCWarning(lcCache) << "Failed to rename folder path:"
105
3/3
✓ Branch 86 → 87 taken 4 times.
✓ Branch 87 → 88 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
4 << q.lastError().text();
106 }
107 7 }
108
109 // T-167: Convenience methods for FolderPredictor pre-training
110
111 1647 QStringList MailCache::allFolderPaths(const QString &account, QString *error) const {
112
2/2
✓ Branch 2 → 3 taken 773 times.
✓ Branch 2 → 4 taken 874 times.
1647 if (error) error->clear();
113 1647 QStringList result;
114
1/1
✓ Branch 4 → 5 taken 1647 times.
1647 QSqlQuery q(m_db);
115
1/1
✓ Branch 8 → 9 taken 1647 times.
1647 q.prepare(QStringLiteral(
116 "SELECT path FROM folders WHERE account = :account ORDER BY path"));
117
1/1
✓ Branch 16 → 17 taken 1647 times.
3294 q.bindValue(QStringLiteral(":account"), account);
118
3/3
✓ Branch 20 → 21 taken 1647 times.
✓ Branch 21 → 22 taken 1644 times.
✓ Branch 21 → 31 taken 3 times.
1647 if (q.exec()) {
119
3/3
✓ Branch 29 → 30 taken 5819 times.
✓ Branch 30 → 23 taken 4175 times.
✓ Branch 30 → 31 taken 1644 times.
5819 while (q.next()) {
120
3/3
✓ Branch 23 → 24 taken 4175 times.
✓ Branch 24 → 25 taken 4175 times.
✓ Branch 25 → 26 taken 4175 times.
4175 result.append(q.value(0).toString());
121 }
122 }
123
4/4
✓ Branch 31 → 32 taken 1647 times.
✓ Branch 32 → 33 taken 1647 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 34 → 43 taken 1644 times.
1647 if (q.lastError().isValid()) {
124
1/1
✓ Branch 35 → 36 taken 3 times.
3 result.clear();
125
4/4
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 43 taken 2 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
3 if (error) *error = q.lastError().text();
126 }
127 1647 return result;
128 1647 }
129
130 3 QList<MailHeader> MailCache::headersByFolder(const QString &account,
131 const QString &folderPath) const {
132
1/1
✓ Branch 2 → 3 taken 3 times.
3 QSqlQuery fq(m_db);
133
1/1
✓ Branch 6 → 7 taken 3 times.
3 fq.prepare(QStringLiteral(
134 "SELECT id FROM folders WHERE account = :account AND path = :path"));
135
1/1
✓ Branch 14 → 15 taken 3 times.
6 fq.bindValue(QStringLiteral(":account"), account);
136
1/1
✓ Branch 23 → 24 taken 3 times.
6 fq.bindValue(QStringLiteral(":path"), folderPath);
137
7/8
✓ Branch 27 → 28 taken 3 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 31 taken 2 times.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 1 time.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 1 time.
3 if (!fq.exec() || !fq.next())
138 2 return {};
139
140
2/2
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
1 qint64 folderId = fq.value(0).toLongLong();
141
1/1
✓ Branch 39 → 40 taken 1 time.
1 return headers(folderId);
142 3 }
143
144 // --- Badge caching (T-075) ---
145
146 131 void MailCache::storeBadge(qint64 folderId, int unseenCount) {
147
1/1
✓ Branch 2 → 3 taken 131 times.
131 QSqlQuery q(m_db);
148
1/1
✓ Branch 6 → 7 taken 131 times.
131 q.prepare(QStringLiteral(
149 "INSERT OR REPLACE INTO folder_badges (folder_id, unseen, updated_at) "
150 "VALUES (:fid, :unseen, strftime('%s', 'now'))"));
151
1/1
✓ Branch 14 → 15 taken 131 times.
262 q.bindValue(QStringLiteral(":fid"), folderId);
152
1/1
✓ Branch 23 → 24 taken 131 times.
262 q.bindValue(QStringLiteral(":unseen"), unseenCount);
153
154
3/3
✓ Branch 27 → 28 taken 131 times.
✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 45 taken 127 times.
131 if (!q.exec()) {
155
8/8
✓ Branch 29 → 30 taken 4 times.
✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 36 → 37 taken 4 times.
✓ Branch 37 → 38 taken 4 times.
✓ Branch 43 → 31 taken 4 times.
✓ Branch 43 → 44 taken 4 times.
8 qCWarning(lcCache) << "Failed to store badge:" << q.lastError().text();
156 }
157 131 }
158
159 1634 QMap<QString, int> MailCache::loadAllBadges(const QString &account) const {
160 1634 QMap<QString, int> result;
161
1/1
✓ Branch 2 → 3 taken 1634 times.
1634 QSqlQuery q(m_db);
162
1/1
✓ Branch 6 → 7 taken 1634 times.
1634 q.prepare(QStringLiteral(
163 "SELECT f.path, b.unseen FROM folder_badges b "
164 "JOIN folders f ON f.id = b.folder_id "
165 "WHERE f.account = :account AND b.unseen > 0"));
166
1/1
✓ Branch 14 → 15 taken 1634 times.
3268 q.bindValue(QStringLiteral(":account"), account);
167
168
3/3
✓ Branch 18 → 19 taken 1634 times.
✓ Branch 19 → 20 taken 1631 times.
✓ Branch 19 → 32 taken 3 times.
1634 if (q.exec()) {
169
3/3
✓ Branch 30 → 31 taken 1675 times.
✓ Branch 31 → 21 taken 44 times.
✓ Branch 31 → 48 taken 1631 times.
1675 while (q.next()) {
170
5/5
✓ Branch 21 → 22 taken 44 times.
✓ Branch 22 → 23 taken 44 times.
✓ Branch 23 → 24 taken 44 times.
✓ Branch 24 → 25 taken 44 times.
✓ Branch 25 → 26 taken 44 times.
44 result[q.value(0).toString()] = q.value(1).toInt();
171 }
172 } else {
173
8/8
✓ Branch 32 → 33 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 37 → 38 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
✓ Branch 40 → 41 taken 3 times.
✓ Branch 46 → 34 taken 3 times.
✓ Branch 46 → 47 taken 3 times.
6 qCWarning(lcCache) << "Failed to load badges:" << q.lastError().text();
174 }
175 1634 return result;
176 1634 }
177