controller/ThreadBuilder.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ThreadBuilder.h" | ||
| 2 | |||
| 3 | #include "util/MessageId.h" | ||
| 4 | |||
| 5 | #include <QHash> | ||
| 6 | #include <QLoggingCategory> | ||
| 7 | #include <QRegularExpression> | ||
| 8 | #include <QSet> | ||
| 9 | |||
| 10 | #include <algorithm> | ||
| 11 | #include <functional> | ||
| 12 | #include <optional> | ||
| 13 | |||
| 14 |
4/7✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 8 taken 924 times.
✓ Branch 4 → 5 taken 16 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 16 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
940 | Q_LOGGING_CATEGORY(lcThreadBuilder, "mailjd.threading") |
| 15 | |||
| 16 | namespace { | ||
| 17 | |||
| 18 | 5353588 | bool mailIdLess(const MailHeader &left, const MailHeader &right) { | |
| 19 |
2/2✓ Branch 2 → 3 taken 559 times.
✓ Branch 2 → 4 taken 5353029 times.
|
5353588 | if (left.folderId != right.folderId) |
| 20 | 559 | return left.folderId < right.folderId; | |
| 21 |
2/2✓ Branch 4 → 5 taken 5353011 times.
✓ Branch 4 → 6 taken 18 times.
|
5353029 | if (left.uid != right.uid) |
| 22 | 5353011 | return left.uid < right.uid; | |
| 23 |
2/2✓ Branch 7 → 8 taken 16 times.
✓ Branch 7 → 10 taken 2 times.
|
18 | if (left.messageId != right.messageId) |
| 24 | 16 | return left.messageId < right.messageId; | |
| 25 | 2 | return left.subject < right.subject; | |
| 26 | } | ||
| 27 | |||
| 28 | 396486 | bool chronologicalLess(const ThreadNode *left, const ThreadNode *right) { | |
| 29 | 396486 | const bool leftValid = left->header.date.isValid(); | |
| 30 | 396486 | const bool rightValid = right->header.date.isValid(); | |
| 31 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 396486 times.
|
396486 | if (leftValid != rightValid) |
| 32 | ✗ | return leftValid; | |
| 33 |
5/6✓ Branch 6 → 7 taken 396486 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 388992 times.
✓ Branch 8 → 10 taken 7494 times.
✓ Branch 11 → 12 taken 388992 times.
✓ Branch 11 → 14 taken 7494 times.
|
396486 | if (leftValid && left->header.date != right->header.date) |
| 34 | 388992 | return left->header.date < right->header.date; | |
| 35 | 7494 | return mailIdLess(left->header, right->header); | |
| 36 | } | ||
| 37 | |||
| 38 | 331910 | bool wouldCreateCycle(const ThreadNode *node, const ThreadNode *candidate) { | |
| 39 |
2/2✓ Branch 6 → 3 taken 1650472 times.
✓ Branch 6 → 7 taken 330406 times.
|
1980878 | for (const ThreadNode *parent = candidate; parent; parent = parent->parent) { |
| 40 |
2/2✓ Branch 3 → 4 taken 1504 times.
✓ Branch 3 → 5 taken 1648968 times.
|
1650472 | if (parent == node) |
| 41 | 1504 | return true; | |
| 42 | } | ||
| 43 | 330406 | return false; | |
| 44 | } | ||
| 45 | |||
| 46 | 755034 | int currentParentDepth(const ThreadNode *node) { | |
| 47 | 755034 | int depth = 0; | |
| 48 |
2/2✓ Branch 6 → 3 taken 3182074 times.
✓ Branch 6 → 7 taken 755034 times.
|
3937108 | for (const ThreadNode *parent = node->parent; parent; |
| 49 | 3182074 | parent = parent->parent) { | |
| 50 | 3182074 | ++depth; | |
| 51 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3182074 times.
|
3182074 | if (depth > ThreadBuilder::MaximumDepth) |
| 52 | ✗ | break; | |
| 53 | } | ||
| 54 | 755034 | return depth; | |
| 55 | } | ||
| 56 | |||
| 57 | 331910 | bool canAttachTo(const ThreadNode *node, const ThreadNode *candidate) { | |
| 58 |
6/8✓ Branch 2 → 3 taken 331910 times.
✗ Branch 2 → 9 not taken.
✓ Branch 3 → 4 taken 331910 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 330406 times.
✓ Branch 5 → 9 taken 1504 times.
✓ Branch 7 → 8 taken 330279 times.
✓ Branch 7 → 9 taken 127 times.
|
662316 | return candidate && node != candidate && !wouldCreateCycle(node, candidate) && |
| 59 | 662316 | currentParentDepth(candidate) < ThreadBuilder::MaximumDepth; | |
| 60 | } | ||
| 61 | |||
| 62 | 645157 | QStringList canonicalReplyIds(const MailHeader &header) { | |
| 63 | 645157 | QStringList ids; | |
| 64 | 645157 | const QStringList source = header.inReplyToIds.isEmpty() | |
| 65 | 1131240 | ? QStringList{header.inReplyTo} | |
| 66 |
6/10✓ Branch 3 → 4 taken 565620 times.
✓ Branch 3 → 6 taken 79537 times.
✓ Branch 7 → 8 taken 565620 times.
✓ Branch 7 → 12 taken 79537 times.
✓ Branch 9 → 10 taken 565620 times.
✓ Branch 9 → 12 taken 565620 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 44 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 44 not taken.
|
2342017 | : header.inReplyToIds; |
| 67 |
2/2✓ Branch 34 → 14 taken 645277 times.
✓ Branch 34 → 35 taken 645157 times.
|
1290434 | for (const QString &value : source) { |
| 68 |
5/5✓ Branch 15 → 16 taken 645277 times.
✓ Branch 16 → 17 taken 645277 times.
✓ Branch 17 → 18 taken 645277 times.
✓ Branch 26 → 19 taken 91932 times.
✓ Branch 26 → 27 taken 645277 times.
|
737209 | for (const QString &token : MessageId::parseTokens(value)) { |
| 69 |
1/2✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 91932 times.
|
91932 | if (ids.size() >= MessageId::MaximumTokens) |
| 70 | ✗ | return ids; | |
| 71 |
1/1✓ Branch 23 → 24 taken 91932 times.
|
91932 | ids.append(token); |
| 72 | 645277 | } | |
| 73 | } | ||
| 74 | 645157 | return ids; | |
| 75 | 1210777 | } | |
| 76 | |||
| 77 | 645157 | QStringList canonicalReferences(const MailHeader &header) { | |
| 78 | 645157 | QStringList ids; | |
| 79 |
2/2✓ Branch 24 → 4 taken 678539 times.
✓ Branch 24 → 25 taken 645157 times.
|
1323696 | for (const QString &value : header.references) { |
| 80 |
5/5✓ Branch 5 → 6 taken 678539 times.
✓ Branch 6 → 7 taken 678539 times.
✓ Branch 7 → 8 taken 678539 times.
✓ Branch 16 → 9 taken 678539 times.
✓ Branch 16 → 17 taken 678539 times.
|
1357078 | for (const QString &token : MessageId::parseTokens(value)) { |
| 81 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 678539 times.
|
678539 | if (ids.size() >= MessageId::MaximumTokens) |
| 82 | ✗ | return ids; | |
| 83 |
1/1✓ Branch 13 → 14 taken 678539 times.
|
678539 | ids.append(token); |
| 84 | 678539 | } | |
| 85 | } | ||
| 86 | 645157 | return ids; | |
| 87 | ✗ | } | |
| 88 | |||
| 89 | 319556 | ThreadNode *chooseDuplicateParent(const QList<ThreadNode *> &candidates, | |
| 90 | const ThreadNode *child) { | ||
| 91 | 319556 | QList<ThreadNode *> eligible; | |
| 92 |
2/2✓ Branch 9 → 4 taken 448208 times.
✓ Branch 9 → 10 taken 319556 times.
|
767764 | for (ThreadNode *candidate : candidates) { |
| 93 |
2/2✓ Branch 5 → 6 taken 446736 times.
✓ Branch 5 → 7 taken 1472 times.
|
448208 | if (candidate != child) |
| 94 |
1/1✓ Branch 6 → 7 taken 446736 times.
|
446736 | eligible.append(candidate); |
| 95 | } | ||
| 96 |
2/2✓ Branch 11 → 12 taken 284 times.
✓ Branch 11 → 13 taken 319272 times.
|
319556 | if (eligible.isEmpty()) |
| 97 | 284 | return nullptr; | |
| 98 | |||
| 99 | // Explicit duplicate policy: prefer the same folder, then the closest | ||
| 100 | // candidate not newer than the child. If every copy is newer, use the | ||
| 101 | // earliest copy. MailId is the final deterministic tie-breaker. | ||
| 102 |
3/3✓ Branch 13 → 14 taken 319272 times.
✓ Branch 14 → 15 taken 319272 times.
✓ Branch 15 → 16 taken 319272 times.
|
319272 | std::sort(eligible.begin(), eligible.end(), [child](const ThreadNode *left, |
| 103 | const ThreadNode *right) { | ||
| 104 | 243388 | const bool leftSame = left->header.folderId == child->header.folderId; | |
| 105 | 243388 | const bool rightSame = right->header.folderId == child->header.folderId; | |
| 106 |
2/2✓ Branch 2 → 3 taken 124 times.
✓ Branch 2 → 4 taken 243264 times.
|
243388 | if (leftSame != rightSame) |
| 107 | 124 | return leftSame; | |
| 108 | 243264 | const bool childDated = child->header.date.isValid(); | |
| 109 |
4/6✓ Branch 5 → 6 taken 243264 times.
✗ Branch 5 → 12 not taken.
✓ Branch 7 → 8 taken 243264 times.
✗ Branch 7 → 12 not taken.
✓ Branch 10 → 11 taken 116692 times.
✓ Branch 10 → 12 taken 126572 times.
|
486528 | const bool leftPrior = childDated && left->header.date.isValid() && |
| 110 | 243264 | left->header.date <= child->header.date; | |
| 111 |
4/6✓ Branch 13 → 14 taken 243264 times.
✗ Branch 13 → 20 not taken.
✓ Branch 15 → 16 taken 243264 times.
✗ Branch 15 → 20 not taken.
✓ Branch 18 → 19 taken 137946 times.
✓ Branch 18 → 20 taken 105318 times.
|
486528 | const bool rightPrior = childDated && right->header.date.isValid() && |
| 112 | 243264 | right->header.date <= child->header.date; | |
| 113 |
2/2✓ Branch 21 → 22 taken 74410 times.
✓ Branch 21 → 23 taken 168854 times.
|
243264 | if (leftPrior != rightPrior) |
| 114 | 74410 | return leftPrior; | |
| 115 |
1/2✓ Branch 24 → 25 taken 168854 times.
✗ Branch 24 → 30 not taken.
|
168854 | if (left->header.date != right->header.date) { |
| 116 |
2/2✓ Branch 25 → 26 taken 90114 times.
✓ Branch 25 → 28 taken 78740 times.
|
168854 | if (leftPrior) |
| 117 | 90114 | return left->header.date > right->header.date; | |
| 118 | 78740 | return chronologicalLess(left, right); | |
| 119 | } | ||
| 120 | ✗ | return mailIdLess(left->header, right->header); | |
| 121 | }); | ||
| 122 |
1/1✓ Branch 16 → 17 taken 319272 times.
|
319272 | return eligible.first(); |
| 123 | 319556 | } | |
| 124 | |||
| 125 | 244065 | QSet<QString> participants(const MailHeader &header) { | |
| 126 | 244065 | QSet<QString> result; | |
| 127 | static const QRegularExpression address( | ||
| 128 | 14 | QStringLiteral(R"(([^\s<>,;\"]+@[^\s<>,;\"]+))"), | |
| 129 |
4/7✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 16 taken 244058 times.
✓ Branch 5 → 6 taken 7 times.
✗ Branch 5 → 16 not taken.
✓ Branch 10 → 11 taken 7 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
|
244079 | QRegularExpression::UseUnicodePropertiesOption); |
| 130 |
2/2✓ Branch 35 → 20 taken 488130 times.
✓ Branch 35 → 36 taken 244065 times.
|
1220325 | for (const QString &field : {header.from, header.to}) { |
| 131 |
1/1✓ Branch 21 → 22 taken 488130 times.
|
488130 | auto matches = address.globalMatch(field); |
| 132 |
3/3✓ Branch 31 → 32 taken 967012 times.
✓ Branch 32 → 23 taken 478882 times.
✓ Branch 32 → 33 taken 488130 times.
|
967012 | while (matches.hasNext()) |
| 133 |
4/4✓ Branch 23 → 24 taken 478882 times.
✓ Branch 24 → 25 taken 478882 times.
✓ Branch 25 → 26 taken 478882 times.
✓ Branch 26 → 27 taken 478882 times.
|
478882 | result.insert(matches.next().captured(1).toCaseFolded()); |
| 134 | 1220325 | } | |
| 135 | 244065 | return result; | |
| 136 | ✗ | } | |
| 137 | |||
| 138 | 14930 | bool participantOverlap(const QSet<QString> &leftParticipants, | |
| 139 | const QSet<QString> &rightParticipants) { | ||
| 140 |
3/6✓ Branch 3 → 4 taken 14930 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 14930 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 14930 times.
|
14930 | if (leftParticipants.isEmpty() || rightParticipants.isEmpty()) |
| 141 | ✗ | return false; | |
| 142 |
2/2✓ Branch 18 → 12 taken 22789 times.
✓ Branch 18 → 19 taken 6452 times.
|
29241 | for (const QString &participant : leftParticipants) { |
| 143 |
2/2✓ Branch 14 → 15 taken 8478 times.
✓ Branch 14 → 16 taken 14311 times.
|
22789 | if (rightParticipants.contains(participant)) |
| 144 | 8478 | return true; | |
| 145 | } | ||
| 146 | 6452 | return false; | |
| 147 | } | ||
| 148 | |||
| 149 | struct NormalizedSubject { | ||
| 150 | QString text; | ||
| 151 | bool replyOrForward = false; | ||
| 152 | }; | ||
| 153 | |||
| 154 | 423393 | NormalizedSubject normalizedSubject(const QString &subject) { | |
| 155 | // Deliberately anchored: a mailing-list tag remains part of the comparison | ||
| 156 | // text. The reviewed marker set covers English and common localized forms. | ||
| 157 | static const QRegularExpression prefix( | ||
| 158 | 32 | QStringLiteral( | |
| 159 | R"(^\s*(?:re|fwd|fw|aw|wg|sv|vs|tr|antw|doorst)\s*(?:\[\d+\])?\s*:\s*)"), | ||
| 160 | QRegularExpression::CaseInsensitiveOption | | ||
| 161 |
4/7✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 15 taken 423377 times.
✓ Branch 4 → 5 taken 16 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 16 times.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
|
423409 | QRegularExpression::UseUnicodePropertiesOption); |
| 162 |
1/1✓ Branch 15 → 16 taken 423393 times.
|
423393 | QString value = subject.normalized(QString::NormalizationForm_KC); |
| 163 | 423393 | bool removed = false; | |
| 164 | while (true) { | ||
| 165 |
1/1✓ Branch 18 → 19 taken 482004 times.
|
482004 | const QRegularExpressionMatch match = prefix.match(value); |
| 166 |
3/3✓ Branch 19 → 20 taken 482004 times.
✓ Branch 20 → 21 taken 423393 times.
✓ Branch 20 → 22 taken 58611 times.
|
482004 | if (!match.hasMatch()) |
| 167 | 423393 | break; | |
| 168 |
2/2✓ Branch 22 → 23 taken 58611 times.
✓ Branch 23 → 24 taken 58611 times.
|
58611 | value.remove(0, match.capturedLength()); |
| 169 | 58611 | removed = true; | |
| 170 | 540615 | } | |
| 171 |
1/1✓ Branch 29 → 30 taken 423393 times.
|
1270179 | return {value.simplified().toCaseFolded(), removed}; |
| 172 | 846786 | } | |
| 173 | |||
| 174 | 14930 | bool insideFallbackWindow(const MailHeader &root, const MailHeader &reply) { | |
| 175 |
4/8✓ Branch 3 → 4 taken 14930 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 14930 times.
✗ Branch 5 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 14930 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 14930 times.
|
14930 | if (!root.date.isValid() || !reply.date.isValid() || root.date > reply.date) |
| 176 | ✗ | return false; | |
| 177 | 14930 | return root.date.secsTo(reply.date) <= ThreadBuilder::FallbackWindowSeconds; | |
| 178 | } | ||
| 179 | |||
| 180 | } // namespace | ||
| 181 | |||
| 182 | std::vector<std::unique_ptr<ThreadNode>> | ||
| 183 | 957 | ThreadBuilder::buildThreads(const QList<MailHeader> &headers, | |
| 184 | const std::function<bool()> &cancelled) { | ||
| 185 |
2/2✓ Branch 3 → 4 taken 17 times.
✓ Branch 3 → 5 taken 940 times.
|
957 | if (headers.isEmpty()) |
| 186 | 17 | return {}; | |
| 187 | |||
| 188 | 940 | std::vector<std::unique_ptr<ThreadNode>> allNodes; | |
| 189 |
1/1✓ Branch 6 → 7 taken 940 times.
|
940 | allNodes.reserve(static_cast<size_t>(headers.size())); |
| 190 |
2/2✓ Branch 49 → 9 taken 424628 times.
✓ Branch 49 → 50 taken 940 times.
|
425568 | for (const MailHeader &header : headers) { |
| 191 |
5/7✓ Branch 11 → 12 taken 219775 times.
✓ Branch 11 → 15 taken 204853 times.
✓ Branch 12 → 13 taken 219775 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 219775 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 192 |
1/1✓ Branch 18 → 19 taken 424628 times.
|
424628 | auto node = std::make_unique<ThreadNode>(); |
| 193 | 424628 | node->header = header; | |
| 194 |
1/1✓ Branch 21 → 22 taken 424628 times.
|
424628 | node->header.messageId = MessageId::canonicalFirst(header.messageId); |
| 195 |
1/1✓ Branch 25 → 26 taken 424628 times.
|
424628 | node->header.inReplyToIds = canonicalReplyIds(header); |
| 196 | 849256 | node->header.inReplyTo = node->header.inReplyToIds.isEmpty() | |
| 197 |
2/2✓ Branch 31 → 32 taken 359347 times.
✓ Branch 31 → 33 taken 65281 times.
|
849256 | ? QString() |
| 198 |
1/1✓ Branch 34 → 35 taken 65281 times.
|
489909 | : node->header.inReplyToIds.first(); |
| 199 |
1/1✓ Branch 39 → 40 taken 424628 times.
|
424628 | node->header.references = canonicalReferences(header); |
| 200 |
1/1✓ Branch 44 → 45 taken 424628 times.
|
424628 | allNodes.push_back(std::move(node)); |
| 201 | 424628 | } | |
| 202 |
1/1✓ Branch 52 → 53 taken 940 times.
|
940 | std::sort(allNodes.begin(), allNodes.end(), |
| 203 | 5341304 | [](const auto &left, const auto &right) { | |
| 204 | 5341304 | return mailIdLess(left->header, right->header); | |
| 205 | }); | ||
| 206 | |||
| 207 | 940 | QHash<QString, QList<ThreadNode *>> nodesById; | |
| 208 |
2/2✓ Branch 73 → 55 taken 424628 times.
✓ Branch 73 → 74 taken 940 times.
|
425568 | for (const auto &node : allNodes) { |
| 209 |
5/7✓ Branch 57 → 58 taken 219775 times.
✓ Branch 57 → 61 taken 204853 times.
✓ Branch 58 → 59 taken 219775 times.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 219775 times.
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 210 |
2/2✓ Branch 66 → 67 taken 418338 times.
✓ Branch 66 → 71 taken 6290 times.
|
424628 | if (!node->header.messageId.isEmpty()) |
| 211 |
2/2✓ Branch 68 → 69 taken 418338 times.
✓ Branch 70 → 71 taken 418338 times.
|
418338 | nodesById[node->header.messageId].append(node.get()); |
| 212 | } | ||
| 213 | |||
| 214 | 940 | QHash<ThreadNode *, bool> hadResolvableReference; | |
| 215 | 940 | QHash<QString, QList<ThreadNode *>> missingAncestorGroups; | |
| 216 |
2/2✓ Branch 123 → 77 taken 424628 times.
✓ Branch 123 → 124 taken 940 times.
|
425568 | for (const auto &ownedNode : allNodes) { |
| 217 |
5/7✓ Branch 79 → 80 taken 219775 times.
✓ Branch 79 → 83 taken 204853 times.
✓ Branch 80 → 81 taken 219775 times.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 83 taken 219775 times.
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 86 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 218 | 424628 | ThreadNode *node = ownedNode.get(); | |
| 219 | 424628 | QStringList candidates = node->header.references; | |
| 220 |
1/1✓ Branch 88 → 89 taken 424628 times.
|
424628 | candidates.append(node->header.inReplyToIds); |
| 221 | 424628 | bool resolvable = false; | |
| 222 |
2/2✓ Branch 107 → 91 taken 375104 times.
✓ Branch 107 → 108 taken 106987 times.
|
482091 | for (int index = candidates.size() - 1; index >= 0; --index) { |
| 223 | 375104 | const auto found = nodesById.constFind(candidates.at(index)); | |
| 224 |
2/2✓ Branch 95 → 96 taken 55548 times.
✓ Branch 95 → 97 taken 319556 times.
|
375104 | if (found == nodesById.cend()) |
| 225 | 55832 | continue; | |
| 226 |
1/1✓ Branch 98 → 99 taken 319556 times.
|
319556 | ThreadNode *parent = chooseDuplicateParent(found.value(), node); |
| 227 |
2/2✓ Branch 99 → 100 taken 284 times.
✓ Branch 99 → 101 taken 319272 times.
|
319556 | if (!parent) |
| 228 | 284 | continue; | |
| 229 | 319272 | resolvable = true; | |
| 230 |
2/2✓ Branch 102 → 103 taken 317641 times.
✓ Branch 102 → 104 taken 1631 times.
|
319272 | if (canAttachTo(node, parent)) { |
| 231 | 317641 | node->parent = parent; | |
| 232 | 317641 | break; | |
| 233 | } | ||
| 234 | } | ||
| 235 |
1/1✓ Branch 108 → 109 taken 424628 times.
|
424628 | hadResolvableReference.insert(node, resolvable); |
| 236 |
8/8✓ Branch 109 → 110 taken 106987 times.
✓ Branch 109 → 114 taken 317641 times.
✓ Branch 110 → 111 taken 105590 times.
✓ Branch 110 → 114 taken 1397 times.
✓ Branch 112 → 113 taken 21864 times.
✓ Branch 112 → 114 taken 83726 times.
✓ Branch 115 → 116 taken 21864 times.
✓ Branch 115 → 119 taken 402764 times.
|
424628 | if (!node->parent && !resolvable && !candidates.isEmpty()) |
| 237 |
3/3✓ Branch 116 → 117 taken 21864 times.
✓ Branch 117 → 118 taken 21864 times.
✓ Branch 118 → 119 taken 21864 times.
|
21864 | missingAncestorGroups[candidates.last()].append(node); |
| 238 | 424628 | } | |
| 239 | |||
| 240 | // JWZ-style missing containers carry no physical mail. The UI tree cannot | ||
| 241 | // render an invisible QModelIndex, so prune each useful container into its | ||
| 242 | // oldest physical child and attach its siblings there. This preserves every | ||
| 243 | // mail exactly once and retains the missing-ancestor grouping without a fake | ||
| 244 | // selectable message. | ||
| 245 |
1/1✓ Branch 124 → 125 taken 940 times.
|
940 | for (auto group = missingAncestorGroups.begin(); |
| 246 |
2/2✓ Branch 170 → 126 taken 17704 times.
✓ Branch 170 → 171 taken 940 times.
|
18644 | group != missingAncestorGroups.end(); ++group) { |
| 247 |
5/7✓ Branch 127 → 128 taken 8219 times.
✓ Branch 127 → 131 taken 9485 times.
✓ Branch 128 → 129 taken 8219 times.
✗ Branch 129 → 130 not taken.
✓ Branch 129 → 131 taken 8219 times.
✗ Branch 132 → 133 not taken.
✓ Branch 132 → 134 taken 17704 times.
|
17704 | if (cancelled && cancelled()) return {}; |
| 248 | 17704 | QList<ThreadNode *> members; | |
| 249 |
4/4✓ Branch 135 → 136 taken 17704 times.
✓ Branch 136 → 137 taken 17704 times.
✓ Branch 143 → 138 taken 21864 times.
✓ Branch 143 → 144 taken 17704 times.
|
39568 | for (ThreadNode *node : group.value()) { |
| 250 |
1/2✓ Branch 139 → 140 taken 21864 times.
✗ Branch 139 → 141 not taken.
|
21864 | if (!node->parent) |
| 251 |
1/1✓ Branch 140 → 141 taken 21864 times.
|
21864 | members.append(node); |
| 252 | } | ||
| 253 |
2/2✓ Branch 145 → 146 taken 13806 times.
✓ Branch 145 → 147 taken 3898 times.
|
17704 | if (members.size() < 2) |
| 254 | 13806 | continue; | |
| 255 |
3/3✓ Branch 147 → 148 taken 3898 times.
✓ Branch 148 → 149 taken 3898 times.
✓ Branch 149 → 150 taken 3898 times.
|
3898 | std::sort(members.begin(), members.end(), chronologicalLess); |
| 256 |
1/1✓ Branch 150 → 151 taken 3898 times.
|
3898 | ThreadNode *anchor = members.first(); |
| 257 |
2/2✓ Branch 159 → 152 taken 4160 times.
✓ Branch 159 → 160 taken 3898 times.
|
8058 | for (int index = 1; index < members.size(); ++index) { |
| 258 |
1/2✓ Branch 154 → 155 taken 4160 times.
✗ Branch 154 → 157 not taken.
|
4160 | if (canAttachTo(members.at(index), anchor)) |
| 259 | 4160 | members.at(index)->parent = anchor; | |
| 260 | } | ||
| 261 | 17704 | } | |
| 262 | |||
| 263 | struct SubjectRoot { | ||
| 264 | ThreadNode *node; | ||
| 265 | std::optional<QSet<QString>> participants; | ||
| 266 | }; | ||
| 267 | 940 | QHash<QString, QList<SubjectRoot>> subjectRoots; | |
| 268 |
2/2✓ Branch 209 → 173 taken 424628 times.
✓ Branch 209 → 210 taken 940 times.
|
425568 | for (const auto &node : allNodes) { |
| 269 |
5/7✓ Branch 175 → 176 taken 219775 times.
✓ Branch 175 → 179 taken 204853 times.
✓ Branch 176 → 177 taken 219775 times.
✗ Branch 177 → 178 not taken.
✓ Branch 177 → 179 taken 219775 times.
✗ Branch 180 → 181 not taken.
✓ Branch 180 → 182 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 270 |
6/6✓ Branch 183 → 184 taken 102827 times.
✓ Branch 183 → 187 taken 321801 times.
✓ Branch 186 → 187 taken 1397 times.
✓ Branch 186 → 188 taken 101430 times.
✓ Branch 189 → 190 taken 323198 times.
✓ Branch 189 → 191 taken 101430 times.
|
424628 | if (node->parent || hadResolvableReference.value(node.get(), false)) |
| 271 | 323198 | continue; | |
| 272 |
1/1✓ Branch 192 → 193 taken 101430 times.
|
101430 | const NormalizedSubject subject = normalizedSubject(node->header.subject); |
| 273 |
6/6✓ Branch 193 → 194 taken 86581 times.
✓ Branch 193 → 197 taken 14849 times.
✓ Branch 195 → 196 taken 86579 times.
✓ Branch 195 → 197 taken 2 times.
✓ Branch 198 → 199 taken 86579 times.
✓ Branch 198 → 204 taken 14851 times.
|
101430 | if (!subject.replyOrForward && !subject.text.isEmpty()) |
| 274 |
2/2✓ Branch 199 → 200 taken 86579 times.
✓ Branch 201 → 202 taken 86579 times.
|
86579 | subjectRoots[subject.text].append(SubjectRoot{node.get(), std::nullopt}); |
| 275 | 101430 | } | |
| 276 |
3/3✓ Branch 210 → 211 taken 940 times.
✓ Branch 220 → 212 taken 64262 times.
✓ Branch 220 → 221 taken 940 times.
|
65202 | for (auto roots = subjectRoots.begin(); roots != subjectRoots.end(); ++roots) |
| 277 |
3/3✓ Branch 213 → 214 taken 64262 times.
✓ Branch 215 → 216 taken 64262 times.
✓ Branch 216 → 217 taken 64262 times.
|
64262 | std::sort(roots.value().begin(), roots.value().end(), |
| 278 | 51352 | [](const SubjectRoot &left, const SubjectRoot &right) { | |
| 279 | 51352 | return chronologicalLess(left.node, right.node); | |
| 280 | }); | ||
| 281 | |||
| 282 |
2/2✓ Branch 316 → 223 taken 424628 times.
✓ Branch 316 → 317 taken 940 times.
|
425568 | for (const auto &ownedNode : allNodes) { |
| 283 |
5/7✓ Branch 225 → 226 taken 219775 times.
✓ Branch 225 → 229 taken 204853 times.
✓ Branch 226 → 227 taken 219775 times.
✗ Branch 227 → 228 not taken.
✓ Branch 227 → 229 taken 219775 times.
✗ Branch 230 → 231 not taken.
✓ Branch 230 → 232 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 284 | 424628 | ThreadNode *node = ownedNode.get(); | |
| 285 |
6/6✓ Branch 233 → 234 taken 102827 times.
✓ Branch 233 → 236 taken 321801 times.
✓ Branch 235 → 236 taken 1397 times.
✓ Branch 235 → 237 taken 101430 times.
✓ Branch 238 → 239 taken 323198 times.
✓ Branch 238 → 240 taken 101430 times.
|
424628 | if (node->parent || hadResolvableReference.value(node, false)) |
| 286 | 410432 | continue; | |
| 287 |
1/1✓ Branch 240 → 241 taken 101430 times.
|
101430 | const NormalizedSubject subject = normalizedSubject(node->header.subject); |
| 288 |
6/6✓ Branch 241 → 242 taken 14849 times.
✓ Branch 241 → 244 taken 86581 times.
✓ Branch 243 → 244 taken 1 time.
✓ Branch 243 → 245 taken 14848 times.
✓ Branch 246 → 247 taken 86582 times.
✓ Branch 246 → 248 taken 14848 times.
|
101430 | if (!subject.replyOrForward || subject.text.isEmpty()) |
| 289 | 86582 | continue; | |
| 290 |
3/3✓ Branch 248 → 249 taken 14848 times.
✓ Branch 249 → 250 taken 1 time.
✓ Branch 249 → 251 taken 14847 times.
|
14848 | if (!node->header.date.isValid()) |
| 291 | 1 | continue; | |
| 292 |
1/1✓ Branch 251 → 252 taken 14847 times.
|
14847 | auto roots = subjectRoots.find(subject.text); |
| 293 |
2/2✓ Branch 254 → 255 taken 650 times.
✓ Branch 254 → 256 taken 14197 times.
|
14847 | if (roots == subjectRoots.end()) |
| 294 | 650 | continue; | |
| 295 |
1/1✓ Branch 256 → 257 taken 14197 times.
|
14197 | const auto replyParticipants = participants(node->header); |
| 296 |
2/2✓ Branch 258 → 259 taken 1 time.
✓ Branch 258 → 260 taken 14196 times.
|
14197 | if (replyParticipants.isEmpty()) |
| 297 | 1 | continue; | |
| 298 | // Roots are ordered by date. The first eligible root must be inside the | ||
| 299 | // same 30-day interval; binary search avoids scanning years of older | ||
| 300 | // messages in a popular subject group for every reply. | ||
| 301 | 14196 | auto &candidates = roots.value(); | |
| 302 |
3/3✓ Branch 262 → 263 taken 14196 times.
✓ Branch 263 → 264 taken 14196 times.
✓ Branch 264 → 265 taken 14196 times.
|
14196 | const auto first = std::lower_bound(candidates.begin(), candidates.end(), |
| 303 |
1/1✓ Branch 261 → 262 taken 14196 times.
|
28392 | node->header.date.addSecs(-ThreadBuilder::FallbackWindowSeconds), |
| 304 | 35727 | [](const SubjectRoot &root, const QDateTime &earliest) { | |
| 305 |
3/4✓ Branch 3 → 4 taken 35727 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 732 times.
✓ Branch 6 → 8 taken 34995 times.
|
35727 | return root.node->header.date.isValid() && root.node->header.date < earliest; |
| 306 | }); | ||
| 307 |
3/3✓ Branch 300 → 301 taken 20648 times.
✓ Branch 302 → 267 taken 20244 times.
✓ Branch 302 → 303 taken 404 times.
|
20648 | for (auto candidate = first; candidate != candidates.end(); ++candidate) { |
| 308 | 20244 | auto &root = *candidate; | |
| 309 |
7/8✓ Branch 268 → 269 taken 20244 times.
✓ Branch 269 → 270 taken 20244 times.
✗ Branch 269 → 273 not taken.
✓ Branch 270 → 271 taken 20244 times.
✓ Branch 272 → 273 taken 5314 times.
✓ Branch 272 → 274 taken 14930 times.
✓ Branch 275 → 276 taken 5314 times.
✓ Branch 275 → 277 taken 14930 times.
|
20244 | if (!root.node->header.date.isValid() || root.node->header.date > node->header.date) |
| 310 | 5314 | break; | |
| 311 |
2/4✓ Branch 277 → 278 taken 14930 times.
✗ Branch 277 → 281 not taken.
✓ Branch 282 → 283 taken 14930 times.
✗ Branch 282 → 299 not taken.
|
29860 | if (root.node != node && |
| 312 |
2/3✓ Branch 278 → 279 taken 14930 times.
✓ Branch 279 → 280 taken 14930 times.
✗ Branch 279 → 281 not taken.
|
14930 | insideFallbackWindow(root.node->header, node->header)) { |
| 313 |
2/2✓ Branch 284 → 285 taken 9341 times.
✓ Branch 284 → 289 taken 5589 times.
|
14930 | if (!root.participants) |
| 314 |
1/1✓ Branch 285 → 286 taken 9341 times.
|
9341 | root.participants.emplace(participants(root.node->header)); |
| 315 |
5/5✓ Branch 290 → 291 taken 14930 times.
✓ Branch 291 → 292 taken 8478 times.
✓ Branch 291 → 294 taken 6452 times.
✓ Branch 296 → 297 taken 6452 times.
✓ Branch 296 → 298 taken 8478 times.
|
23408 | if (!participantOverlap(*root.participants, replyParticipants) || |
| 316 |
1/2✗ Branch 293 → 294 not taken.
✓ Branch 293 → 295 taken 8478 times.
|
8478 | !canAttachTo(node, root.node)) |
| 317 | 6452 | continue; | |
| 318 | 8478 | node->parent = root.node; | |
| 319 | 8478 | break; // roots are oldest first, with MailId tie-breakers | |
| 320 | } | ||
| 321 | } | ||
| 322 | 101431 | } | |
| 323 | |||
| 324 | 940 | int depthLimitedLinks = 0; | |
| 325 |
2/2✓ Branch 335 → 319 taken 424628 times.
✓ Branch 335 → 336 taken 940 times.
|
425568 | for (const auto &node : allNodes) { |
| 326 |
5/7✓ Branch 321 → 322 taken 219775 times.
✓ Branch 321 → 325 taken 204853 times.
✓ Branch 322 → 323 taken 219775 times.
✗ Branch 323 → 324 not taken.
✓ Branch 323 → 325 taken 219775 times.
✗ Branch 326 → 327 not taken.
✓ Branch 326 → 328 taken 424628 times.
|
424628 | if (cancelled && cancelled()) return {}; |
| 327 |
1/2✗ Branch 330 → 331 not taken.
✓ Branch 330 → 333 taken 424628 times.
|
424628 | if (currentParentDepth(node.get()) > MaximumDepth) { |
| 328 | ✗ | node->parent = nullptr; | |
| 329 | ✗ | ++depthLimitedLinks; | |
| 330 | } | ||
| 331 | } | ||
| 332 |
1/2✗ Branch 336 → 338 not taken.
✓ Branch 336 → 352 taken 940 times.
|
940 | if (depthLimitedLinks > 0) { |
| 333 | ✗ | qCWarning(lcThreadBuilder) | |
| 334 | ✗ | << "Detached" << depthLimitedLinks | |
| 335 | ✗ | << "thread links beyond maximum depth" << MaximumDepth; | |
| 336 | } | ||
| 337 | |||
| 338 | 940 | std::vector<std::unique_ptr<ThreadNode>> roots; | |
| 339 |
2/2✓ Branch 364 → 354 taken 424628 times.
✓ Branch 364 → 365 taken 940 times.
|
425568 | for (auto &node : allNodes) { |
| 340 |
2/2✓ Branch 356 → 357 taken 330279 times.
✓ Branch 356 → 360 taken 94349 times.
|
424628 | if (node->parent) |
| 341 |
1/1✓ Branch 359 → 362 taken 330279 times.
|
330279 | node->parent->children.push_back(std::move(node)); |
| 342 | else | ||
| 343 |
1/1✓ Branch 361 → 362 taken 94349 times.
|
94349 | roots.push_back(std::move(node)); |
| 344 | } | ||
| 345 | |||
| 346 | std::function<QDateTime(ThreadNode *, int)> finalize = | ||
| 347 | 426508 | [&](ThreadNode *node, int depth) { | |
| 348 | 424628 | node->depth = depth; | |
| 349 | 424628 | std::sort(node->children.begin(), node->children.end(), | |
| 350 | 259210 | [](const auto &left, const auto &right) { | |
| 351 | 259210 | return chronologicalLess(left.get(), right.get()); | |
| 352 | }); | ||
| 353 | 424628 | QDateTime newest = node->header.date; | |
| 354 |
2/2✓ Branch 25 → 8 taken 330279 times.
✓ Branch 25 → 26 taken 424628 times.
|
754907 | for (auto &child : node->children) { |
| 355 |
1/1✓ Branch 10 → 11 taken 330279 times.
|
330279 | const auto childNewest = finalize(child.get(), depth + 1); |
| 356 |
10/11✓ Branch 11 → 12 taken 330279 times.
✓ Branch 12 → 13 taken 330274 times.
✓ Branch 12 → 19 taken 5 times.
✓ Branch 13 → 14 taken 330274 times.
✓ Branch 14 → 15 taken 330274 times.
✗ Branch 14 → 18 not taken.
✓ Branch 15 → 16 taken 330274 times.
✓ Branch 17 → 18 taken 279783 times.
✓ Branch 17 → 19 taken 50491 times.
✓ Branch 20 → 21 taken 279783 times.
✓ Branch 20 → 22 taken 50496 times.
|
330279 | if (childNewest.isValid() && (!newest.isValid() || childNewest > newest)) |
| 357 | 279783 | newest = childNewest; | |
| 358 | 330279 | } | |
| 359 | 424628 | return newest; | |
| 360 | 940 | }; | |
| 361 | struct DatedRoot { | ||
| 362 | QDateTime newest; | ||
| 363 | std::unique_ptr<ThreadNode> node; | ||
| 364 | }; | ||
| 365 | 940 | std::vector<DatedRoot> datedRoots; | |
| 366 |
1/1✓ Branch 367 → 368 taken 940 times.
|
940 | datedRoots.reserve(roots.size()); |
| 367 |
2/2✓ Branch 379 → 370 taken 94349 times.
✓ Branch 379 → 380 taken 940 times.
|
95289 | for (auto &root : roots) |
| 368 | 94349 | datedRoots.push_back({finalize(root.get(), 0), std::move(root)}); | |
| 369 | 940 | roots.clear(); | |
| 370 |
1/1✓ Branch 383 → 384 taken 940 times.
|
940 | std::sort(datedRoots.begin(), datedRoots.end(), |
| 371 | 928179 | [](const DatedRoot &left, const DatedRoot &right) { | |
| 372 |
2/2✓ Branch 3 → 4 taken 923389 times.
✓ Branch 3 → 6 taken 4790 times.
|
928179 | if (left.newest != right.newest) |
| 373 | 923389 | return left.newest > right.newest; | |
| 374 | 4790 | return mailIdLess(left.node->header, right.node->header); | |
| 375 | }); | ||
| 376 |
2/2✓ Branch 391 → 386 taken 94349 times.
✓ Branch 391 → 392 taken 940 times.
|
95289 | for (auto &dated : datedRoots) |
| 377 |
1/1✓ Branch 388 → 389 taken 94349 times.
|
94349 | roots.push_back(std::move(dated.node)); |
| 378 | |||
| 379 | // P90.6: hot-loop progress only with explicitly enabled debug verbosity; | ||
| 380 | // the default production run stays free of per-batch log lines. | ||
| 381 |
7/7✓ Branch 392 → 393 taken 940 times.
✓ Branch 396 → 397 taken 940 times.
✓ Branch 397 → 398 taken 940 times.
✓ Branch 399 → 400 taken 940 times.
✓ Branch 400 → 401 taken 940 times.
✓ Branch 407 → 394 taken 940 times.
✓ Branch 407 → 408 taken 940 times.
|
1880 | qCDebug(lcThreadBuilder) << "Built" << roots.size() << "threads from" |
| 382 |
2/2✓ Branch 402 → 403 taken 940 times.
✓ Branch 403 → 404 taken 940 times.
|
940 | << headers.size() << "headers"; |
| 383 | 940 | return roots; | |
| 384 | 95289 | } | |
| 385 | |||
| 386 | 220529 | QStringList ThreadBuilder::dependencyKeys(const MailHeader &header) { | |
| 387 | 220529 | QStringList result; | |
| 388 | 488430 | const auto add = [&result](const QString &id) { | |
| 389 |
4/4✓ Branch 3 → 4 taken 482197 times.
✓ Branch 3 → 13 taken 6233 times.
✓ Branch 7 → 8 taken 482197 times.
✓ Branch 8 → 9 taken 482197 times.
|
970627 | if (!id.isEmpty()) result.append(QStringLiteral("id:") + id); |
| 390 | 488430 | }; | |
| 391 |
2/2✓ Branch 2 → 3 taken 220529 times.
✓ Branch 3 → 4 taken 220529 times.
|
220529 | add(MessageId::canonicalFirst(header.messageId)); |
| 392 |
6/6✓ Branch 5 → 6 taken 220529 times.
✓ Branch 6 → 7 taken 220529 times.
✓ Branch 7 → 8 taken 220529 times.
✓ Branch 10 → 11 taken 241370 times.
✓ Branch 13 → 9 taken 241370 times.
✓ Branch 13 → 14 taken 220529 times.
|
461899 | for (const auto &id : canonicalReferences(header)) add(id); |
| 393 |
6/6✓ Branch 15 → 16 taken 220529 times.
✓ Branch 16 → 17 taken 220529 times.
✓ Branch 17 → 18 taken 220529 times.
✓ Branch 20 → 21 taken 26531 times.
✓ Branch 23 → 19 taken 26531 times.
✓ Branch 23 → 24 taken 220529 times.
|
247060 | for (const auto &id : canonicalReplyIds(header)) add(id); |
| 394 |
1/1✓ Branch 25 → 26 taken 220529 times.
|
220529 | result.removeDuplicates(); |
| 395 | 220529 | return result; | |
| 396 | ✗ | } | |
| 397 | |||
| 398 | QList<ThreadBuilder::FallbackCandidate> | ||
| 399 | 220533 | ThreadBuilder::fallbackCandidates(const MailHeader &header) { | |
| 400 |
2/3✓ Branch 2 → 3 taken 220533 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 220533 times.
|
220533 | if (!header.date.isValid()) return {}; |
| 401 |
1/1✓ Branch 5 → 6 taken 220533 times.
|
220533 | const auto subject = normalizedSubject(header.subject); |
| 402 |
2/2✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 9 taken 220527 times.
|
220533 | if (subject.text.isEmpty()) return {}; |
| 403 | 220527 | QList<FallbackCandidate> result; | |
| 404 |
1/1✓ Branch 9 → 10 taken 220527 times.
|
220527 | const auto addresses = participants(header); |
| 405 |
1/1✓ Branch 11 → 12 taken 220527 times.
|
220527 | result.reserve(addresses.size()); |
| 406 |
2/2✓ Branch 22 → 14 taken 415301 times.
✓ Branch 22 → 23 taken 220527 times.
|
635828 | for (const auto &address : addresses) |
| 407 | 415301 | result.append({subject.text, address, header.date.toSecsSinceEpoch(), | |
| 408 | 415301 | subject.replyOrForward}); | |
| 409 | 220527 | return result; | |
| 410 | 635834 | } | |
| 411 |