File indexing completed on 2024-11-24 04:53:35
0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net> 0002 0003 This file is part of the Trojita Qt IMAP e-mail client, 0004 http://trojita.flaska.net/ 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License as 0008 published by the Free Software Foundation; either version 2 of 0009 the License or (at your option) version 3 or any later version 0010 accepted by the membership of KDE e.V. (or its successor approved 0011 by the membership of KDE e.V.), which shall act as a proxy 0012 defined in Section 14 of version 3 of the license. 0013 0014 This program is distributed in the hope that it will be useful, 0015 but WITHOUT ANY WARRANTY; without even the implied warranty of 0016 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0017 GNU General Public License for more details. 0018 0019 You should have received a copy of the GNU General Public License 0020 along with this program. If not, see <http://www.gnu.org/licenses/>. 0021 */ 0022 0023 #include <algorithm> 0024 #include <QtTest> 0025 #include "test_Imap_Threading.h" 0026 #include "Imap/Model/MsgListModel.h" 0027 #include "Imap/Model/ThreadingMsgListModel.h" 0028 #include "Streams/FakeSocket.h" 0029 #include "Utils/FakeCapabilitiesInjector.h" 0030 0031 #if defined(__has_feature) 0032 # if __has_feature(address_sanitizer) 0033 # define ASAN_BUILD 0034 # endif 0035 #endif 0036 0037 Q_DECLARE_METATYPE(Mapping); 0038 0039 /** @short Test that the ThreadingMsgListModel can process a static THREAD response */ 0040 void ImapModelThreadingTest::testStaticThreading() 0041 { 0042 QFETCH(uint, messageCount); 0043 QFETCH(QByteArray, response); 0044 QFETCH(Mapping, mapping); 0045 initialMessages(messageCount); 0046 QCOMPARE(SOCK->writtenStuff(), t.mk("UID THREAD REFS utf-8 ALL\r\n")); 0047 SOCK->fakeReading(QByteArray("* THREAD ") + response + QByteArray("\r\n") + t.last("OK thread\r\n")); 0048 QCoreApplication::processEvents(); 0049 QCoreApplication::processEvents(); 0050 QCoreApplication::processEvents(); 0051 QCoreApplication::processEvents(); 0052 verifyMapping(mapping); 0053 QVERIFY(SOCK->writtenStuff().isEmpty()); 0054 QVERIFY(errorSpy->isEmpty()); 0055 } 0056 0057 /** @short Data for the testStaticThreading */ 0058 void ImapModelThreadingTest::testStaticThreading_data() 0059 { 0060 QTest::addColumn<uint>("messageCount"); 0061 QTest::addColumn<QByteArray>("response"); 0062 QTest::addColumn<Mapping>("mapping"); 0063 0064 Mapping m; 0065 0066 // A linear subset of messages 0067 m[QStringLiteral("0")] = 1; // index 0: UID 0068 m[QStringLiteral("0.0")] = 0; // index 0.0: invalid 0069 m[QStringLiteral("0.1")] = 0; // index 0.1: invalid 0070 m[QStringLiteral("1")] = 2; 0071 0072 QTest::newRow("no-threads") 0073 << (uint)2 0074 << QByteArray("(1)(2)") 0075 << m; 0076 0077 // No threading at all; just an unthreaded list of all messages 0078 m[QStringLiteral("2")] = 3; 0079 m[QStringLiteral("3")] = 4; 0080 m[QStringLiteral("4")] = 5; 0081 m[QStringLiteral("5")] = 6; 0082 m[QStringLiteral("6")] = 7; 0083 m[QStringLiteral("7")] = 8; 0084 m[QStringLiteral("8")] = 9; 0085 m[QStringLiteral("9")] = 10; // index 9: UID 10 0086 m[QStringLiteral("10")] = 0; // index 10: invalid 0087 0088 QTest::newRow("no-threads-ten") 0089 << (uint)10 0090 << QByteArray("(1)(2)(3)(4)(5)(6)(7)(8)(9)(10)") 0091 << m; 0092 0093 // A flat list of threads, but now with some added fake nodes for complexity. 0094 // The expected result is that they get cleared as redundant and useless nodes. 0095 QTest::newRow("extra-parentheses") 0096 << (uint)10 0097 << QByteArray("(1)((2))(((3)))((((4))))(((((5)))))(6)(7)(8)(9)(((10)))") 0098 << m; 0099 0100 // A liner nested list (ie. a message is a child of the previous one) 0101 m.clear(); 0102 m[QStringLiteral("0")] = 1; 0103 m[QStringLiteral("1")] = 0; 0104 m[QStringLiteral("0.0")] = 2; 0105 m[QStringLiteral("0.1")] = 0; 0106 m[QStringLiteral("0.0.0")] = 0; 0107 m[QStringLiteral("0.0.1")] = 0; 0108 QTest::newRow("linear-threading-just-two") 0109 << (uint)2 0110 << QByteArray("(1 2)") 0111 << m; 0112 0113 // The same, but with three messages 0114 m[QStringLiteral("0.0.0")] = 3; 0115 m[QStringLiteral("0.0.0.0")] = 0; 0116 QTest::newRow("linear-threading-just-three") 0117 << (uint)3 0118 << QByteArray("(1 2 3)") 0119 << m; 0120 0121 // The same, but with some added parentheses 0122 m[QStringLiteral("0.0.0")] = 3; 0123 m[QStringLiteral("0.0.0.0")] = 0; 0124 QTest::newRow("linear-threading-just-three-extra-parentheses-outside") 0125 << (uint)3 0126 << QByteArray("((((1 2 3))))") 0127 << m; 0128 // The same, but with the extra parentheses in the innermost item 0129 QTest::newRow("linear-threading-just-three-extra-parentheses-inside") 0130 << (uint)3 0131 << QByteArray("(1 2 (((3))))") 0132 << m; 0133 0134 // The same, but with the extra parentheses in the middle item 0135 // This is actually a server's bug, as the fake node should've been eliminated 0136 // by the IMAP server. 0137 m.clear(); 0138 m[QStringLiteral("0")] = 1; 0139 m[QStringLiteral("1")] = 0; 0140 m[QStringLiteral("0.0")] = 2; 0141 m[QStringLiteral("0.0.0")] = 0; 0142 m[QStringLiteral("0.1")] = 3; 0143 m[QStringLiteral("0.1.0")] = 0; 0144 m[QStringLiteral("0.2")] = 0; 0145 QTest::newRow("linear-threading-just-extra-parentheses-middle") 0146 << (uint)3 0147 << QByteArray("(1 (2) 3)") 0148 << m; 0149 0150 // A complex nested hierarchy with nodes to be promoted 0151 QByteArray response; 0152 complexMapping(m, response); 0153 QTest::newRow("complex-threading") 0154 << (uint)10 0155 << response 0156 << m; 0157 } 0158 0159 void ImapModelThreadingTest::testThreadDeletionsAdditions() 0160 { 0161 QFETCH(uint, exists); 0162 QFETCH(QByteArray, response); 0163 QFETCH(QStringList, operations); 0164 Q_ASSERT(operations.size() % 2 == 0); 0165 0166 initialMessages(exists); 0167 0168 QCOMPARE(SOCK->writtenStuff(), t.mk("UID THREAD REFS utf-8 ALL\r\n")); 0169 SOCK->fakeReading(QByteArray("* THREAD ") + response + QByteArray("\r\n") + t.last("OK thread\r\n")); 0170 QCoreApplication::processEvents(); 0171 QCoreApplication::processEvents(); 0172 QCoreApplication::processEvents(); 0173 QCoreApplication::processEvents(); 0174 QCOMPARE(response, treeToThreading(QModelIndex())); 0175 QVERIFY(SOCK->writtenStuff().isEmpty()); 0176 QVERIFY(errorSpy->isEmpty()); 0177 0178 for (int i = 0; i < operations.size(); i += 2) { 0179 QString whichOne = operations[i]; 0180 QString expectedRes = operations[i+1]; 0181 if (whichOne[0] == QLatin1Char('-')) { 0182 // Removing messages. The number specifies a *sequence number* 0183 Q_ASSERT(whichOne.size() > 1); 0184 SOCK->fakeReading(QStringLiteral("* %1 EXPUNGE\r\n").arg(whichOne.mid(1)).toUtf8()); 0185 QCoreApplication::processEvents(); 0186 QCoreApplication::processEvents(); 0187 QCoreApplication::processEvents(); 0188 QCoreApplication::processEvents(); 0189 QVERIFY(SOCK->writtenStuff().isEmpty()); 0190 QVERIFY(errorSpy->isEmpty()); 0191 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), expectedRes); 0192 } else if (whichOne[0] == QLatin1Char('+')) { 0193 // New additions. The number specifies the number of new arrivals. 0194 Q_ASSERT(whichOne.size() > 1); 0195 int newArrivals = whichOne.midRef(1).toInt(); 0196 Q_ASSERT(newArrivals > 0); 0197 0198 for (int i = 0; i < newArrivals; ++i) { 0199 uidMapA.append(uidNextA + i); 0200 } 0201 0202 existsA += newArrivals; 0203 0204 // Send information about the new arrival 0205 SOCK->fakeReading(QStringLiteral("* %1 EXISTS\r\n").arg(QString::number(existsA)).toUtf8()); 0206 QCoreApplication::processEvents(); 0207 QCoreApplication::processEvents(); 0208 0209 // At this point, the threading code should have asked for new threading and the generic IMAP model code for flags 0210 QByteArray expected = t.mk("UID FETCH ") + QStringLiteral("%1:* (FLAGS)\r\n").arg(QString::number(uidNextA)).toUtf8(); 0211 uidNextA += newArrivals; 0212 QByteArray uidFetchResponse; 0213 for (int i = 0; i < newArrivals; ++i) { 0214 int offset = existsA - newArrivals + i; 0215 uidFetchResponse += QStringLiteral("* %1 FETCH (UID %2 FLAGS ())\r\n").arg( 0216 // the sequqnce number is one-based, not zero-based 0217 QString::number(offset + 1), 0218 QString::number(uidMapA[offset]) 0219 ).toUtf8(); 0220 } 0221 0222 // See LibMailboxSync::helperSyncFlags for why have to do this 0223 for (int i = 0; i < (newArrivals / 100) + 1; ++i) 0224 QCoreApplication::processEvents(); 0225 0226 uidFetchResponse += t.last("OK fetch\r\n"); 0227 QCOMPARE(SOCK->writtenStuff(), expected); 0228 SOCK->fakeReading(uidFetchResponse); 0229 QCoreApplication::processEvents(); 0230 QCoreApplication::processEvents(); 0231 QCoreApplication::processEvents(); 0232 QCoreApplication::processEvents(); 0233 QByteArray expectedThread = t.mk("UID THREAD REFS utf-8 ALL\r\n"); 0234 QByteArray uidThreadResponse = QByteArray("* THREAD ") + expectedRes.toUtf8() + QByteArray("\r\n") + t.last("OK thread\r\n"); 0235 QCOMPARE(SOCK->writtenStuff(), expectedThread); 0236 SOCK->fakeReading(uidThreadResponse); 0237 QCoreApplication::processEvents(); 0238 QCoreApplication::processEvents(); 0239 0240 QVERIFY(SOCK->writtenStuff().isEmpty()); 0241 QVERIFY(errorSpy->isEmpty()); 0242 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), expectedRes); 0243 } else { 0244 Q_ASSERT(false); 0245 } 0246 } 0247 } 0248 0249 void ImapModelThreadingTest::testThreadDeletionsAdditions_data() 0250 { 0251 QTest::addColumn<uint>("exists"); 0252 QTest::addColumn<QByteArray>("response"); 0253 QTest::addColumn<QStringList>("operations"); 0254 0255 // Just test that dumping works; no deletions yet 0256 QTest::newRow("basic-flat-list") << (uint)2 << QByteArray("(1)(2)") << QStringList(); 0257 // Simple tests for flat lists 0258 QTest::newRow("flat-list-two-delete-first") << (uint)2 << QByteArray("(1)(2)") << (QStringList() << QStringLiteral("-1") << QStringLiteral("(2)")); 0259 QTest::newRow("flat-list-two-delete-last") << (uint)2 << QByteArray("(1)(2)") << (QStringList() << QStringLiteral("-2") << QStringLiteral("(1)")); 0260 QTest::newRow("flat-list-three-delete-first") << (uint)3 << QByteArray("(1)(2)(3)") << (QStringList() << QStringLiteral("-1") << QStringLiteral("(2)(3)")); 0261 QTest::newRow("flat-list-three-delete-middle") << (uint)3 << QByteArray("(1)(2)(3)") << (QStringList() << QStringLiteral("-2") << QStringLiteral("(1)(3)")); 0262 QTest::newRow("flat-list-three-delete-last") << (uint)3 << QByteArray("(1)(2)(3)") << (QStringList() << QStringLiteral("-3") << QStringLiteral("(1)(2)")); 0263 // Try to test a single thread 0264 QTest::newRow("simple-three-delete-first") << (uint)3 << QByteArray("(1 2 3)") << (QStringList() << QStringLiteral("-1") << QStringLiteral("(2 3)")); 0265 QTest::newRow("simple-three-delete-middle") << (uint)3 << QByteArray("(1 2 3)") << (QStringList() << QStringLiteral("-2") << QStringLiteral("(1 3)")); 0266 QTest::newRow("simple-three-delete-last") << (uint)3 << QByteArray("(1 2 3)") << (QStringList() << QStringLiteral("-3") << QStringLiteral("(1 2)")); 0267 // A thread with a fork: 0268 // 1 0269 // +- 2 0270 // +- 3 0271 // +- 4 0272 // +- 5 0273 QTest::newRow("fork") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << QStringList(); 0274 QTest::newRow("fork-delete-first") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << (QStringList() << QStringLiteral("-1") << QStringLiteral("(2 (3)(4 5))")); 0275 QTest::newRow("fork-delete-second") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << (QStringList() << QStringLiteral("-2") << QStringLiteral("(1 (3)(4 5))")); 0276 QTest::newRow("fork-delete-third") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << (QStringList() << QStringLiteral("-3") << QStringLiteral("(1 (2)(4 5))")); 0277 // Remember, we're using EXPUNGE which use sequence numbers, not UIDs 0278 QTest::newRow("fork-delete-two-three") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << 0279 (QStringList() << QStringLiteral("-2") << QStringLiteral("(1 (3)(4 5))") << QStringLiteral("-2") << QStringLiteral("(1 4 5)")); 0280 QTest::newRow("fork-delete-two-four") << (uint)5 << QByteArray("(1 (2 3)(4 5))") << 0281 (QStringList() << QStringLiteral("-2") << QStringLiteral("(1 (3)(4 5))") << QStringLiteral("-3") << QStringLiteral("(1 (3)(5))")); 0282 0283 // Test new arrivals 0284 QTest::newRow("flat-list-new") << (uint)2 << QByteArray("(1)(2)") << (QStringList() << QStringLiteral("+1") << QStringLiteral("(1)(2)(3)")); 0285 0286 } 0287 0288 /** @short Test deletion of several nodes at once resulting in child promotion 0289 0290 There was a bug in https://gerrit.vesnicky.cesnet.cz/r/#/c/153/1, an assert failure at src/Imap/Model/ThreadingMsgListModel.cpp:1117. 0291 The bug happened because an initial version of that patch failed to fix the other part of an if branch, a place where the old code 0292 assumed that all other thread nodes had their offsets already fixed. 0293 0294 Testing this with Qt5 is not easy, though, due to the random iteration order of QHash which is used within the ThreadingMsgListModel. 0295 What we're looking for is a situation where a non-leaf node is processed by the code *after* some of its preceding siblings which 0296 happen to be leaves were already removed. 0297 */ 0298 void ImapModelThreadingTest::testVanishedHierarchyReplacement() 0299 { 0300 // Initialize the same data structure as the one which is used within the ThreadingMsgListModel. 0301 // We're doing this in order to be able to prepare such a sequence of keys which will trigger that 0302 // particular sequence of hash traversal which we need from here. 0303 decltype(threadingModel->threading) dummy; 0304 for (int i = 0; i < 5; ++i) { 0305 dummy[i]; 0306 } 0307 auto keys = dummy.keys(); 0308 keys.removeOne(0); // but it's important that 0 was there for the actual hash iteration order 0309 QCOMPARE(keys.size(), 4); 0310 0311 initialMessages(4); 0312 0313 // The threading will have to look like this one: 0314 // 1 0315 // 2 0316 // 3 0317 // +- 4 0318 // 0319 // ..except that the numbers above correspond to the indexes in the list of keys of the hash 0320 // in the hash's iteration order, not actual UIDs. 0321 QCOMPARE(SOCK->writtenStuff(), t.mk("UID THREAD REFS utf-8 ALL\r\n")); 0322 SOCK->fakeReading("* THREAD (" + QByteArray::number(keys[0]) + ")(" + QByteArray::number(keys[1]) + ")(" + 0323 QByteArray::number(keys[2]) + ' ' + QByteArray::number(keys[3]) + ")\r\n" + t.last("OK thread\r\n")); 0324 cEmpty(); 0325 QVERIFY(errorSpy->isEmpty()); 0326 0327 cServer("* VANISHED " + QByteArray::number(keys[0]) + ',' + QByteArray::number(keys[1]) + ',' + QByteArray::number(keys[2]) + "\r\n"); 0328 cEmpty(); 0329 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), QString::fromUtf8("(%1)").arg(keys[3])); 0330 cEmpty(); 0331 QVERIFY(errorSpy->isEmpty()); 0332 } 0333 0334 /** @short Test deletion of one message */ 0335 void ImapModelThreadingTest::testDynamicThreading() 0336 { 0337 initialMessages(10); 0338 0339 // A complex nested hierarchy with nodes to be promoted 0340 Mapping mapping; 0341 QByteArray response; 0342 complexMapping(mapping, response); 0343 0344 QCOMPARE(SOCK->writtenStuff(), t.mk("UID THREAD REFS utf-8 ALL\r\n")); 0345 SOCK->fakeReading(QByteArray("* THREAD ") + response + QByteArray("\r\n") + t.last("OK thread\r\n")); 0346 QCoreApplication::processEvents(); 0347 QCoreApplication::processEvents(); 0348 QCoreApplication::processEvents(); 0349 QCoreApplication::processEvents(); 0350 verifyMapping(mapping); 0351 QCOMPARE(threadingModel->rowCount(QModelIndex()), 4); 0352 IndexMapping indexMap = buildIndexMap(mapping); 0353 verifyIndexMap(indexMap, mapping); 0354 // The response is actually slightly different, but never mind (extra parentheses around 7) 0355 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2 3)(4 (5)(6))(7 (8)(9 10))")); 0356 0357 // this one will be deleted 0358 QPersistentModelIndex delete10 = findItem(QStringLiteral("3.1.0")); 0359 QVERIFY(delete10.isValid()); 0360 0361 // its parent 0362 QPersistentModelIndex msg9 = findItem(QStringLiteral("3.1")); 0363 QCOMPARE(QPersistentModelIndex(delete10.parent()), msg9); 0364 QCOMPARE(threadingModel->rowCount(msg9), 1); 0365 0366 { 0367 // Qt5 bug: there is now QAsbtractProxyModel::sibling which does not do the right thing anymore. 0368 // Make sure our workaround is in place. 0369 Q_ASSERT(msg9.isValid()); 0370 auto sibling1 = msg9.sibling(msg9.row(), msg9.column() + 1); 0371 QVERIFY(sibling1.isValid()); 0372 QCOMPARE(sibling1.row(), msg9.row()); 0373 auto sibling2 = sibling1.sibling(sibling1.row(), sibling1.column() - 1); 0374 QVERIFY(sibling2.isValid()); 0375 QCOMPARE(sibling2, QModelIndex(msg9)); 0376 } 0377 0378 // Delete the last message; it's some leaf 0379 SOCK->fakeReading("* 10 EXPUNGE\r\n"); 0380 QCoreApplication::processEvents(); 0381 QCoreApplication::processEvents(); 0382 QCoreApplication::processEvents(); 0383 QCoreApplication::processEvents(); 0384 --existsA; 0385 QCOMPARE(msgListModel->rowCount(QModelIndex()), static_cast<int>(existsA)); 0386 QCOMPARE(threadingModel->rowCount(msg9), 0); 0387 QVERIFY(!delete10.isValid()); 0388 mapping.remove(QStringLiteral("3.1.0.0")); 0389 mapping[QStringLiteral("3.1.0")] = 0; 0390 indexMap.remove(QStringLiteral("3.1.0.0")); 0391 verifyMapping(mapping); 0392 verifyIndexMap(indexMap, mapping); 0393 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2 3)(4 (5)(6))(7 (8)(9))")); 0394 0395 QPersistentModelIndex msg2 = findItem(QStringLiteral("1")); 0396 QVERIFY(msg2.isValid()); 0397 QPersistentModelIndex msg3 = findItem(QStringLiteral("1.0")); 0398 QVERIFY(msg3.isValid()); 0399 0400 // Delete the root of the second thread 0401 SOCK->fakeReading("* 2 EXPUNGE\r\n"); 0402 QCoreApplication::processEvents(); 0403 QCoreApplication::processEvents(); 0404 QCoreApplication::processEvents(); 0405 QCoreApplication::processEvents(); 0406 --existsA; 0407 QCOMPARE(msgListModel->rowCount(QModelIndex()), static_cast<int>(existsA)); 0408 QCOMPARE(threadingModel->rowCount(QModelIndex()), 4); 0409 QPersistentModelIndex newMsg3 = findItem(QStringLiteral("1")); 0410 QVERIFY(!msg2.isValid()); 0411 QVERIFY(msg3.isValid()); 0412 QCOMPARE(msg3, newMsg3); 0413 mapping.remove(QStringLiteral("1.0.0")); 0414 mapping[QStringLiteral("1.0")] = 0; 0415 mapping[QStringLiteral("1")] = 3; 0416 verifyMapping(mapping); 0417 // Check the changed persistent indexes 0418 indexMap.remove(QStringLiteral("1.0.0")); 0419 indexMap[QStringLiteral("1")] = indexMap[QStringLiteral("1.0")]; 0420 indexMap.remove(QStringLiteral("1.0")); 0421 verifyIndexMap(indexMap, mapping); 0422 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(3)(4 (5)(6))(7 (8)(9))")); 0423 0424 // Push a new message, but with an unknown UID so far 0425 ++existsA; 0426 ++uidNextA; 0427 QCOMPARE(existsA, 9u); 0428 QCOMPARE(uidNextA, 12u); 0429 SOCK->fakeReading("* 9 EXISTS\r\n"); 0430 QCoreApplication::processEvents(); 0431 QCoreApplication::processEvents(); 0432 QCoreApplication::processEvents(); 0433 QCoreApplication::processEvents(); 0434 // There should be a message with zero UID at the end 0435 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(3)(4 (5)(6))(7 (8)(9))()")); 0436 0437 QByteArray fetchCommand1 = t.mk("UID FETCH ") + QStringLiteral("%1:* (FLAGS)\r\n").arg(QString::number(uidNextA - 1)).toUtf8(); 0438 QByteArray delayedFetchResponse1 = t.last("OK uid fetch\r\n"); 0439 QByteArray threadCommand1 = t.mk("UID THREAD REFS utf-8 ALL\r\n"); 0440 QByteArray delayedThreadResponse1 = t.last("OK threading\r\n"); 0441 QCOMPARE(SOCK->writtenStuff(), fetchCommand1); 0442 0443 QByteArray fetchUntagged1("* 9 FETCH (UID 66 FLAGS (\\Recent))\r\n"); 0444 QByteArray threadUntagged1("* THREAD (1)(3)(4 (5)(6))((7)(8)(9)(66))\r\n"); 0445 0446 // Check that we've registered that change 0447 QCOMPARE(msgListModel->rowCount(QModelIndex()), static_cast<int>(existsA)); 0448 0449 // The UID haven't arrived yet 0450 cEmpty(); 0451 0452 if (1) { 0453 // Make the UID known 0454 cServer(fetchUntagged1 + delayedFetchResponse1); 0455 // After the UID got known, it is now time to ask for threading 0456 cClient(threadCommand1); 0457 // In the meanwhile, the message is temporarily visible as a standalone thread 0458 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), QString::fromUtf8("(1)(3)(4 (5)(6))(7 (8)(9))(66)")); 0459 mapping[QStringLiteral("4")] = 66; 0460 indexMap[QStringLiteral("4")] = findItem(QStringLiteral("4")); 0461 verifyMapping(mapping); 0462 verifyIndexMap(indexMap, mapping); 0463 0464 // Move the message into its proper place now 0465 cServer(threadUntagged1 + delayedThreadResponse1); 0466 indexMap[QStringLiteral("3.2")] = indexMap[QStringLiteral("4")]; 0467 indexMap.remove(QStringLiteral("4")); 0468 mapping[QStringLiteral("3.2")] = mapping[QStringLiteral("4")]; 0469 mapping[QStringLiteral("3.2.0")] = 0; 0470 mapping[QStringLiteral("3.3")] = 0; 0471 mapping[QStringLiteral("4")] = 0; 0472 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), QString::fromUtf8("(1)(3)(4 (5)(6))(7 (8)(9)(66))")); 0473 verifyMapping(mapping); 0474 verifyIndexMap(indexMap, mapping); 0475 } 0476 0477 cEmpty(); 0478 QVERIFY(errorSpy->isEmpty()); 0479 } 0480 0481 /** @short Create a tuple of (mapping, string)*/ 0482 void ImapModelThreadingTest::complexMapping(Mapping &m, QByteArray &response) 0483 { 0484 m.clear(); 0485 m[QStringLiteral("0")] = 1; 0486 m[QStringLiteral("0.0")] = 0; 0487 m[QStringLiteral("1")] = 2; 0488 m[QStringLiteral("1.0")] = 3; 0489 m[QStringLiteral("1.0.0")] = 0; 0490 m[QStringLiteral("2")] = 4; 0491 m[QStringLiteral("2.0")] = 5; 0492 m[QStringLiteral("2.0.0")] = 0; 0493 m[QStringLiteral("2.1")] = 6; 0494 m[QStringLiteral("2.1.0")] = 0; 0495 m[QStringLiteral("3")] = 7; 0496 m[QStringLiteral("3.0")] = 8; 0497 m[QStringLiteral("3.0.0")] = 0; 0498 m[QStringLiteral("3.1")] = 9; 0499 m[QStringLiteral("3.1.0")] = 10; 0500 m[QStringLiteral("3.1.0.0")] = 0; 0501 m[QStringLiteral("3.2")] = 0; 0502 m[QStringLiteral("4")] = 0; 0503 response = QByteArray("(1)(2 3)(4 (5)(6))((7)(8)(9 10))"); 0504 } 0505 0506 /** @short Prepare an index to a threaded message based on a compressed text index description */ 0507 QModelIndex ImapModelThreadingTest::findItem(const QString &where) 0508 { 0509 return findIndexByPosition(threadingModel, where); 0510 } 0511 0512 /** @short Make sure that the specified indexes resolve to proper UIDs */ 0513 void ImapModelThreadingTest::verifyMapping(const Mapping &mapping) 0514 { 0515 for(Mapping::const_iterator it = mapping.begin(); it != mapping.end(); ++it) { 0516 QModelIndex index = findItem(it.key()); 0517 if (it.value()) { 0518 // it's a supposedly valid index 0519 if (!index.isValid()) { 0520 qDebug() << "Invalid index at" << it.key(); 0521 } 0522 QVERIFY(index.isValid()); 0523 int got = index.data(Imap::Mailbox::RoleMessageUid).toInt(); 0524 if (got != it.value()) { 0525 qDebug() << "Index" << it.key(); 0526 } 0527 QCOMPARE(got, it.value()); 0528 } else { 0529 // we expect this one to be a fake 0530 if (index.isValid()) { 0531 qDebug() << "Valid index at" << it.key(); 0532 } 0533 QVERIFY(!index.isValid()); 0534 } 0535 } 0536 } 0537 0538 /** @short Create a map of (position -> persistent_index) based on the current state of the model */ 0539 IndexMapping ImapModelThreadingTest::buildIndexMap(const Mapping &mapping) 0540 { 0541 IndexMapping res; 0542 Q_FOREACH(const QString &key, mapping.keys()) { 0543 // only include real indexes 0544 res[key] = findItem(key); 0545 } 0546 return res; 0547 } 0548 0549 void ImapModelThreadingTest::verifyIndexMap(const IndexMapping &indexMap, const Mapping &map) 0550 { 0551 Q_FOREACH(const QString &key, indexMap.keys()) { 0552 if (!map.contains(key)) { 0553 qDebug() << "Table contains an index for" << key << ", but mapping to UIDs indicates that the index should not be there. Bug in the unit test, I'd say."; 0554 QFAIL("Extra index found in the map"); 0555 } 0556 const QPersistentModelIndex &idx = indexMap[key]; 0557 int expected = map[key]; 0558 if (expected) { 0559 if (!idx.isValid()) { 0560 qDebug() << "Invalid persistent index for" << key; 0561 } 0562 QVERIFY(idx.isValid()); 0563 QCOMPARE(idx.data(Imap::Mailbox::RoleMessageUid).toInt(), expected); 0564 } else { 0565 if (idx.isValid()) { 0566 qDebug() << "Persistent index for" << key << "should not be valid"; 0567 } 0568 QVERIFY(!idx.isValid()); 0569 } 0570 } 0571 } 0572 0573 void ImapModelThreadingTest::init() 0574 { 0575 LibMailboxSync::init(); 0576 0577 // Got to pretend that we support threads. Well, we really do :). 0578 FakeCapabilitiesInjector injector(model); 0579 injector.injectCapability(QStringLiteral("THREAD=REFS")); 0580 0581 // Setup the threading model 0582 threadingModel->setUserWantsThreading(true); 0583 0584 // Deactivate the helper_multipleExpunges slot. We don't want QTestLib to run it, 0585 // but I'm too lazy to add an extra QObject just for the slot. 0586 helper_multipleExpunges_hit = -1; 0587 } 0588 0589 /** @short Walk the model and output a THREAD-like responsde with the UIDs */ 0590 QByteArray ImapModelThreadingTest::treeToThreading(QModelIndex index) 0591 { 0592 QByteArray res = index.data(Imap::Mailbox::RoleMessageUid).toString().toUtf8(); 0593 for (int i = 0; i < threadingModel->rowCount(index); ++i) { 0594 // We're the first child of something 0595 bool shallAddSpace = (i == 0) && index.isValid(); 0596 // If there are multiple siblings (or at the top level), we're always enclosed in parentheses 0597 bool shallEncloseInParenteses = threadingModel->rowCount(index) > 1 || !index.isValid(); 0598 if (shallAddSpace) { 0599 res += ' '; 0600 } 0601 if (shallEncloseInParenteses) { 0602 res += '('; 0603 } 0604 QModelIndex child = threadingModel->index(i, 0, index); 0605 res += treeToThreading(child); 0606 if (shallEncloseInParenteses) { 0607 res += ')'; 0608 } 0609 } 0610 return res; 0611 } 0612 0613 #define checkUidMapFromThreading(MAPPING) \ 0614 { \ 0615 QCOMPARE(threadingModel->rowCount(), MAPPING.size()); \ 0616 Imap::Uids actual; \ 0617 for (int i = 0; i < MAPPING.size(); ++i) { \ 0618 QModelIndex messageIndex = threadingModel->index(i, 0); \ 0619 QVERIFY(messageIndex.isValid()); \ 0620 actual << messageIndex.data(Imap::Mailbox::RoleMessageUid).toUInt(); \ 0621 } \ 0622 QCOMPARE(actual, MAPPING); \ 0623 } 0624 0625 QByteArray ImapModelThreadingTest::numListToString(const Imap::Uids &seq) 0626 { 0627 QStringList res; 0628 Q_FOREACH(const uint num, seq) 0629 res << QString::number(num); 0630 return res.join(QStringLiteral(" ")).toUtf8(); 0631 } 0632 0633 /** @short Test how sorting reacts to dynamic mailbox updates and the initial sync */ 0634 void ImapModelThreadingTest::testDynamicSorting() 0635 { 0636 // keep preloading active 0637 0638 FakeCapabilitiesInjector injector(model); 0639 injector.injectCapability(QStringLiteral("QRESYNC")); 0640 injector.injectCapability(QStringLiteral("SORT=DISPLAY")); 0641 injector.injectCapability(QStringLiteral("SORT")); 0642 0643 threadingModel->setUserWantsThreading(false); 0644 0645 Imap::Mailbox::SyncState sync; 0646 sync.setExists(3); 0647 sync.setUidValidity(666); 0648 sync.setUidNext(15); 0649 sync.setHighestModSeq(33); 0650 sync.setUnSeenCount(3); 0651 sync.setRecent(0); 0652 Imap::Uids uidMap; 0653 uidMap << 6 << 9 << 10; 0654 model->cache()->setMailboxSyncState(QStringLiteral("a"), sync); 0655 model->cache()->setUidMapping(QStringLiteral("a"), uidMap); 0656 model->cache()->setMsgFlags(QStringLiteral("a"), 6, QStringList() << QStringLiteral("x")); 0657 model->cache()->setMsgFlags(QStringLiteral("a"), 9, QStringList() << QStringLiteral("y")); 0658 model->cache()->setMsgFlags(QStringLiteral("a"), 10, QStringList() << QStringLiteral("z")); 0659 msgListModel->setMailbox(QStringLiteral("a")); 0660 cClient(t.mk("SELECT a (QRESYNC (666 33 (2 9)))\r\n")); 0661 cServer("* 3 EXISTS\r\n" 0662 "* OK [UIDVALIDITY 666] .\r\n" 0663 "* OK [UIDNEXT 15] .\r\n" 0664 "* OK [HIGHESTMODSEQ 33] .\r\n" 0665 ); 0666 cServer(t.last("OK selected\r\n")); 0667 cEmpty(); 0668 QCOMPARE(model->cache()->mailboxSyncState("a"), sync); 0669 QCOMPARE(static_cast<int>(model->cache()->mailboxSyncState("a").exists()), uidMap.size()); 0670 QCOMPARE(model->cache()->uidMapping("a"), uidMap); 0671 QCOMPARE(model->cache()->msgFlags("a", 6), QStringList() << "x"); 0672 QCOMPARE(model->cache()->msgFlags("a", 9), QStringList() << "y"); 0673 QCOMPARE(model->cache()->msgFlags("a", 10), QStringList() << "z"); 0674 0675 checkUidMapFromThreading(uidMap); 0676 0677 // A persistent index to make sure that these get updated properly 0678 QPersistentModelIndex msgUid6 = threadingModel->index(0, 0); 0679 QPersistentModelIndex msgUid9 = threadingModel->index(1, 0); 0680 QPersistentModelIndex msgUid10 = threadingModel->index(2, 0); 0681 QVERIFY(msgUid6.isValid()); 0682 QVERIFY(msgUid9.isValid()); 0683 QVERIFY(msgUid10.isValid()); 0684 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0685 QCOMPARE(msgUid9.data(Imap::Mailbox::RoleMessageUid).toUInt(), 9u); 0686 QCOMPARE(msgUid10.data(Imap::Mailbox::RoleMessageUid).toUInt(), 10u); 0687 QCOMPARE(msgUid6.row(), 0); 0688 QCOMPARE(msgUid9.row(), 1); 0689 QCOMPARE(msgUid10.row(), 2); 0690 0691 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT); 0692 0693 Imap::Uids expectedUidOrder; 0694 0695 // suppose subjects are "qt", "trojita" and "mail" 0696 expectedUidOrder << 10 << 6 << 9; 0697 0698 // A ery basic sorting example 0699 cClient(t.mk("UID SORT (SUBJECT) utf-8 ALL\r\n")); 0700 cServer("* SORT " + numListToString(expectedUidOrder) + "\r\n"); 0701 cServer(t.last("OK sorted\r\n")); 0702 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0703 checkUidMapFromThreading(expectedUidOrder); 0704 QCOMPARE(msgUid6.row(), 1); 0705 QCOMPARE(msgUid9.row(), 2); 0706 QCOMPARE(msgUid10.row(), 0); 0707 0708 // Sort by the same criteria, but in a reversed order 0709 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT, Qt::DescendingOrder); 0710 cEmpty(); 0711 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0712 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0713 checkUidMapFromThreading(expectedUidOrder); 0714 QCOMPARE(msgUid6.row(), 1); 0715 QCOMPARE(msgUid9.row(), 0); 0716 QCOMPARE(msgUid10.row(), 2); 0717 0718 // Revert back to ascending sort 0719 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT, Qt::AscendingOrder); 0720 cEmpty(); 0721 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0722 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0723 checkUidMapFromThreading(expectedUidOrder); 0724 QCOMPARE(msgUid6.row(), 1); 0725 QCOMPARE(msgUid9.row(), 2); 0726 QCOMPARE(msgUid10.row(), 0); 0727 0728 // Sort in a native order, reverse direction 0729 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_NONE, Qt::DescendingOrder); 0730 cEmpty(); 0731 expectedUidOrder = uidMap; 0732 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0733 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0734 checkUidMapFromThreading(expectedUidOrder); 0735 QCOMPARE(msgUid6.row(), 2); 0736 QCOMPARE(msgUid9.row(), 1); 0737 QCOMPARE(msgUid10.row(), 0); 0738 0739 // Let a new message arrive 0740 cServer("* 4 EXISTS\r\n"); 0741 cClient(t.mk("UID FETCH 15:* (FLAGS)\r\n")); 0742 cServer("* 4 FETCH (UID 15 FLAGS ())\r\n" + t.last("ok fetched\r\n")); 0743 uidMap << 15; 0744 expectedUidOrder = uidMap; 0745 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0746 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0747 checkUidMapFromThreading(expectedUidOrder); 0748 QCOMPARE(msgUid6.row(), 3); 0749 QCOMPARE(msgUid9.row(), 2); 0750 QCOMPARE(msgUid10.row(), 1); 0751 // ...and delete it again 0752 cServer("* VANISHED 15\r\n"); 0753 uidMap.remove(uidMap.indexOf(15)); 0754 expectedUidOrder = uidMap; 0755 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0756 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0757 checkUidMapFromThreading(expectedUidOrder); 0758 QCOMPARE(msgUid6.row(), 2); 0759 QCOMPARE(msgUid9.row(), 1); 0760 QCOMPARE(msgUid10.row(), 0); 0761 0762 // Check dynamic updates when some sorting criteria are active 0763 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT, Qt::AscendingOrder); 0764 expectedUidOrder.clear(); 0765 expectedUidOrder << 10 << 6 << 9; 0766 cClient(t.mk("UID SORT (SUBJECT) utf-8 ALL\r\n")); 0767 cServer("* SORT " + numListToString(expectedUidOrder) + "\r\n"); 0768 cServer(t.last("OK sorted\r\n")); 0769 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0770 checkUidMapFromThreading(expectedUidOrder); 0771 QCOMPARE(msgUid6.row(), 1); 0772 QCOMPARE(msgUid9.row(), 2); 0773 QCOMPARE(msgUid10.row(), 0); 0774 0775 // ...new arrivals 0776 cServer("* 4 EXISTS\r\n"); 0777 cClient(t.mk("UID FETCH 16:* (FLAGS)\r\n")); 0778 QByteArray delayedUidFetch = "* 4 FETCH (UID 16 FLAGS ())\r\n" + t.last("ok fetched\r\n"); 0779 // ... their UID remains unknown for a while; the model won't request SORT just yet 0780 cEmpty(); 0781 // that new arrival shall be visible immediately 0782 expectedUidOrder << 0; 0783 checkUidMapFromThreading(expectedUidOrder); 0784 cServer(delayedUidFetch); 0785 uidMap << 16; 0786 // as soon as the new UID arrives, the sorting order gets thrown out of the window 0787 expectedUidOrder = uidMap; 0788 checkUidMapFromThreading(expectedUidOrder); 0789 // at this point, the SORT gets issued 0790 expectedUidOrder.clear(); 0791 expectedUidOrder << 10 << 6 << 16 << 9; 0792 cClient(t.mk("UID SORT (SUBJECT) utf-8 ALL\r\n")); 0793 cServer("* SORT " + numListToString(expectedUidOrder) + "\r\n"); 0794 cServer(t.last("OK sorted\r\n")); 0795 0796 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0797 checkUidMapFromThreading(expectedUidOrder); 0798 QCOMPARE(msgUid6.row(), 1); 0799 QCOMPARE(msgUid9.row(), 3); 0800 QCOMPARE(msgUid10.row(), 0); 0801 // ...and delete it again 0802 cServer("* VANISHED 16\r\n"); 0803 uidMap.remove(uidMap.indexOf(16)); 0804 expectedUidOrder.remove(expectedUidOrder.indexOf(16)); 0805 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0806 checkUidMapFromThreading(expectedUidOrder); 0807 QCOMPARE(msgUid6.row(), 1); 0808 QCOMPARE(msgUid9.row(), 2); 0809 QCOMPARE(msgUid10.row(), 0); 0810 0811 // A new message arrives and the user requests a completely different sort order 0812 // Make it a bit more interesting, suddenly support ESORT as well 0813 injector.injectCapability(QStringLiteral("ESORT")); 0814 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_FROM, Qt::AscendingOrder); 0815 cServer("* 4 EXISTS\r\n"); 0816 QByteArray sortReq = t.mk("UID SORT RETURN (ALL) (DISPLAYFROM) utf-8 ALL\r\n"); 0817 QByteArray sortResp = t.last("OK sorted\r\n"); 0818 QByteArray uidFetchReq = t.mk("UID FETCH 17:* (FLAGS)\r\n"); 0819 delayedUidFetch = "* 4 FETCH (UID 17 FLAGS ())\r\n" + t.last("ok fetched\r\n"); 0820 uidMap << 0; 0821 expectedUidOrder = uidMap; 0822 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0823 checkUidMapFromThreading(expectedUidOrder); 0824 QCOMPARE(msgUid6.row(), 0); 0825 QCOMPARE(msgUid9.row(), 1); 0826 QCOMPARE(msgUid10.row(), 2); 0827 0828 cClient(sortReq + uidFetchReq); 0829 expectedUidOrder.clear(); 0830 expectedUidOrder << 9 << 17 << 6 << 10; 0831 cServer("* SORT " + numListToString(expectedUidOrder) + "\r\n" + sortResp); 0832 // in this situation, the new arrival is not visible, unfortunately 0833 expectedUidOrder.remove(expectedUidOrder.indexOf(17)); 0834 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0835 checkUidMapFromThreading(expectedUidOrder); 0836 QCOMPARE(msgUid6.row(), 1); 0837 QCOMPARE(msgUid9.row(), 0); 0838 QCOMPARE(msgUid10.row(), 2); 0839 // Deliver the UID; it will get listed as the last item. Now because we do cache the raw (UID-based) form of SORT/SEARCH, 0840 // the last SORT result will be reused. 0841 // Previously (when SORT responses weren't cached), this would require a asking for SORT once again; that is no longer 0842 // necessary. 0843 cServer(delayedUidFetch); 0844 uidMap.remove(uidMap.indexOf(0)); 0845 uidMap << 17; 0846 // The sorted result previously didn't contain the missing UID, so we'll refill the expected order now 0847 expectedUidOrder.clear(); 0848 expectedUidOrder << 9 << 17 << 6 << 10; 0849 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0850 checkUidMapFromThreading(expectedUidOrder); 0851 QCOMPARE(msgUid6.row(), 2); 0852 QCOMPARE(msgUid9.row(), 0); 0853 QCOMPARE(msgUid10.row(), 3); 0854 0855 cEmpty(); 0856 justKeepTask(); 0857 } 0858 0859 void ImapModelThreadingTest::testDynamicSortingContext() 0860 { 0861 // keep preloading active 0862 0863 FakeCapabilitiesInjector injector(model); 0864 injector.injectCapability(QStringLiteral("QRESYNC")); 0865 injector.injectCapability(QStringLiteral("SORT")); 0866 injector.injectCapability(QStringLiteral("ESORT")); 0867 injector.injectCapability(QStringLiteral("CONTEXT=SORT")); 0868 0869 threadingModel->setUserWantsThreading(false); 0870 0871 Imap::Mailbox::SyncState sync; 0872 sync.setExists(3); 0873 sync.setUidValidity(666); 0874 sync.setUidNext(15); 0875 sync.setHighestModSeq(33); 0876 sync.setUnSeenCount(3); 0877 sync.setRecent(0); 0878 Imap::Uids uidMap; 0879 uidMap << 6 << 9 << 10; 0880 model->cache()->setMailboxSyncState(QStringLiteral("a"), sync); 0881 model->cache()->setUidMapping(QStringLiteral("a"), uidMap); 0882 model->cache()->setMsgFlags(QStringLiteral("a"), 6, QStringList() << QStringLiteral("x")); 0883 model->cache()->setMsgFlags(QStringLiteral("a"), 9, QStringList() << QStringLiteral("y")); 0884 model->cache()->setMsgFlags(QStringLiteral("a"), 10, QStringList() << QStringLiteral("z")); 0885 msgListModel->setMailbox(QStringLiteral("a")); 0886 cClient(t.mk("SELECT a (QRESYNC (666 33 (2 9)))\r\n")); 0887 cServer("* 3 EXISTS\r\n" 0888 "* OK [UIDVALIDITY 666] .\r\n" 0889 "* OK [UIDNEXT 15] .\r\n" 0890 "* OK [HIGHESTMODSEQ 33] .\r\n" 0891 ); 0892 cServer(t.last("OK selected\r\n")); 0893 cEmpty(); 0894 QCOMPARE(model->cache()->mailboxSyncState("a"), sync); 0895 QCOMPARE(static_cast<int>(model->cache()->mailboxSyncState("a").exists()), uidMap.size()); 0896 QCOMPARE(model->cache()->uidMapping("a"), uidMap); 0897 QCOMPARE(model->cache()->msgFlags("a", 6), QStringList() << "x"); 0898 QCOMPARE(model->cache()->msgFlags("a", 9), QStringList() << "y"); 0899 QCOMPARE(model->cache()->msgFlags("a", 10), QStringList() << "z"); 0900 0901 checkUidMapFromThreading(uidMap); 0902 0903 // A persistent index to make sure that these get updated properly 0904 QPersistentModelIndex msgUid6 = threadingModel->index(0, 0); 0905 QPersistentModelIndex msgUid9 = threadingModel->index(1, 0); 0906 QPersistentModelIndex msgUid10 = threadingModel->index(2, 0); 0907 QVERIFY(msgUid6.isValid()); 0908 QVERIFY(msgUid9.isValid()); 0909 QVERIFY(msgUid10.isValid()); 0910 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0911 QCOMPARE(msgUid9.data(Imap::Mailbox::RoleMessageUid).toUInt(), 9u); 0912 QCOMPARE(msgUid10.data(Imap::Mailbox::RoleMessageUid).toUInt(), 10u); 0913 QCOMPARE(msgUid6.row(), 0); 0914 QCOMPARE(msgUid9.row(), 1); 0915 QCOMPARE(msgUid10.row(), 2); 0916 0917 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT); 0918 0919 Imap::Uids expectedUidOrder; 0920 0921 // suppose subjects are "qt", "trojita" and "mail" 0922 expectedUidOrder << 10 << 6 << 9; 0923 0924 // A ery basic sorting example 0925 cClient(t.mk("UID SORT RETURN (ALL UPDATE) (SUBJECT) utf-8 ALL\r\n")); 0926 QByteArray sortTag(t.last()); 0927 cServer("* SORT " + numListToString(expectedUidOrder) + "\r\n"); 0928 cServer(t.last("OK sorted\r\n")); 0929 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0930 checkUidMapFromThreading(expectedUidOrder); 0931 QCOMPARE(msgUid6.row(), 1); 0932 QCOMPARE(msgUid9.row(), 2); 0933 QCOMPARE(msgUid10.row(), 0); 0934 0935 // Sort by the same criteria, but in a reversed order 0936 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_SUBJECT, Qt::DescendingOrder); 0937 cEmpty(); 0938 std::reverse(expectedUidOrder.begin(), expectedUidOrder.end()); 0939 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0940 checkUidMapFromThreading(expectedUidOrder); 0941 QCOMPARE(msgUid6.row(), 1); 0942 QCOMPARE(msgUid9.row(), 0); 0943 QCOMPARE(msgUid10.row(), 2); 0944 0945 // Delivery of a new item 0946 cServer("* 4 EXISTS\r\n* ESEARCH (TAG \"" + sortTag + "\") UID ADDTO (0 15)\r\n"); 0947 cClient(t.mk("UID FETCH 15:* (FLAGS)\r\n")); 0948 cServer("* 4 FETCH (UID 15 FLAGS ())\r\n" + t.last("ok fetched\r\n")); 0949 cEmpty(); 0950 0951 // We're still showing the reversed list 0952 expectedUidOrder << 15; 0953 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0954 checkUidMapFromThreading(expectedUidOrder); 0955 QCOMPARE(msgUid6.row(), 1); 0956 QCOMPARE(msgUid9.row(), 0); 0957 QCOMPARE(msgUid10.row(), 2); 0958 0959 // Remove one message 0960 cServer("* ESEARCH (TAG \"" + sortTag + "\") UID REMOVEFROM (4 9)\r\n"); 0961 cServer("* VANISHED 9\r\n"); 0962 expectedUidOrder.remove(expectedUidOrder.indexOf(9)); 0963 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0964 checkUidMapFromThreading(expectedUidOrder); 0965 QCOMPARE(msgUid6.row(), 0); 0966 QVERIFY(!msgUid9.isValid()); 0967 QCOMPARE(msgUid10.row(), 1); 0968 0969 // Deliver a few more messages to give removals a decent test 0970 cServer("* 6 EXISTS\r\n"); 0971 cClient(t.mk("UID FETCH 16:* (FLAGS)\r\n")); 0972 QByteArray uidFetchResp = "* 4 FETCH (UID 16 FLAGS ())\r\n" 0973 "* 6 FETCH (UID 18 FLAGS ())\r\n" 0974 "* 5 FETCH (UID 17 FLAGS ())\r\n" + t.last("OK fetched\r\n"); 0975 0976 // At the same time, request a different sorting criteria 0977 threadingModel->setUserSearchingSortingPreference(QStringList(), Imap::Mailbox::ThreadingMsgListModel::SORT_CC, Qt::AscendingOrder); 0978 0979 QByteArray cancelReq = t.mk(QByteArray("CANCELUPDATE \"" + sortTag + "\"\r\n")); 0980 QByteArray cancelResponse = t.last("OK no more updates for you\r\n"); 0981 cClient(cancelReq + t.mk("UID SORT RETURN (ALL UPDATE) (CC) utf-8 ALL\r\n")); 0982 sortTag = t.last(); 0983 cServer("* ESEARCH (TAG \"" + sortTag + "\") UID ALL 15:17,6,18,10\r\n"); 0984 cServer(cancelResponse + t.last("OK sorted\r\n")); 0985 cEmpty(); 0986 0987 expectedUidOrder.clear(); 0988 expectedUidOrder << 15 << 6 << 10; 0989 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 0990 checkUidMapFromThreading(expectedUidOrder); 0991 0992 // deliver the UIDs 0993 cServer(uidFetchResp); 0994 expectedUidOrder.clear(); 0995 expectedUidOrder << 15 << 16 << 17 << 6 << 18 << 10; 0996 checkUidMapFromThreading(expectedUidOrder); 0997 0998 // Remove a message, now through a response without an explicit offset 0999 cServer("* ESEARCH (TAG \"" + sortTag + "\") UID REMOVEFROM (0 17)\r\n"); 1000 expectedUidOrder.remove(expectedUidOrder.indexOf(17)); 1001 checkUidMapFromThreading(expectedUidOrder); 1002 1003 // Try to push it back now 1004 cServer("* ESEARCH (TAG \"" + sortTag + "\") UID ADDTO (2 17)\r\n"); 1005 expectedUidOrder.clear(); 1006 expectedUidOrder << 15 << 17 << 16 << 6 << 18 << 10; 1007 checkUidMapFromThreading(expectedUidOrder); 1008 1009 // Insert one message at the end 1010 cServer("* ESEARCH (TAG \"" + sortTag + "\") UID ADDTO (7 18)\r\n"); 1011 expectedUidOrder << 18; 1012 checkUidMapFromThreading(expectedUidOrder); 1013 1014 model->switchToMailbox(idxB); 1015 cClient(t.mk(QByteArray("CANCELUPDATE \"" + sortTag + "\"\r\n"))); 1016 cServer(t.last("OK no further updates\r\n")); 1017 cClient(t.mk("SELECT b\r\n")); 1018 cServer("* OK [CLOSED] previous mailbox closed\r\n* 0 EXISTS\r\n" + t.last("OK selected\r\n")); 1019 1020 cEmpty(); 1021 justKeepTask(); 1022 1023 // FIXME: finalize me -- test the incrmeental updates 1024 // FIXME: also test behavior when we get "* NO [NOUPDATE "tag"] ..." 1025 } 1026 1027 /** @short Test how filtering (searching) works */ 1028 void ImapModelThreadingTest::testDynamicSearch() 1029 { 1030 // keep preloading active 1031 1032 FakeCapabilitiesInjector injector(model); 1033 injector.injectCapability(QStringLiteral("QRESYNC")); 1034 1035 threadingModel->setUserWantsThreading(false); 1036 1037 Imap::Mailbox::SyncState sync; 1038 sync.setExists(3); 1039 sync.setUidValidity(666); 1040 sync.setUidNext(15); 1041 sync.setHighestModSeq(33); 1042 sync.setUnSeenCount(3); 1043 sync.setRecent(0); 1044 Imap::Uids uidMap; 1045 uidMap << 6 << 9 << 10; 1046 model->cache()->setMailboxSyncState(QStringLiteral("a"), sync); 1047 model->cache()->setUidMapping(QStringLiteral("a"), uidMap); 1048 model->cache()->setMsgFlags(QStringLiteral("a"), 6, QStringList() << QStringLiteral("x")); 1049 model->cache()->setMsgFlags(QStringLiteral("a"), 9, QStringList() << QStringLiteral("y")); 1050 model->cache()->setMsgFlags(QStringLiteral("a"), 10, QStringList() << QStringLiteral("z")); 1051 msgListModel->setMailbox(QStringLiteral("a")); 1052 cClient(t.mk("SELECT a (QRESYNC (666 33 (2 9)))\r\n")); 1053 cServer("* 3 EXISTS\r\n" 1054 "* OK [UIDVALIDITY 666] .\r\n" 1055 "* OK [UIDNEXT 15] .\r\n" 1056 "* OK [HIGHESTMODSEQ 33] .\r\n" 1057 ); 1058 cServer(t.last("OK selected\r\n")); 1059 cEmpty(); 1060 QCOMPARE(model->cache()->mailboxSyncState("a"), sync); 1061 QCOMPARE(static_cast<int>(model->cache()->mailboxSyncState("a").exists()), uidMap.size()); 1062 QCOMPARE(model->cache()->uidMapping("a"), uidMap); 1063 QCOMPARE(model->cache()->msgFlags("a", 6), QStringList() << "x"); 1064 QCOMPARE(model->cache()->msgFlags("a", 9), QStringList() << "y"); 1065 QCOMPARE(model->cache()->msgFlags("a", 10), QStringList() << "z"); 1066 1067 checkUidMapFromThreading(uidMap); 1068 1069 // A persistent index to make sure that these get updated properly 1070 QPersistentModelIndex msgUid6 = threadingModel->index(0, 0); 1071 QPersistentModelIndex msgUid9 = threadingModel->index(1, 0); 1072 QPersistentModelIndex msgUid10 = threadingModel->index(2, 0); 1073 QVERIFY(msgUid6.isValid()); 1074 QVERIFY(msgUid9.isValid()); 1075 QVERIFY(msgUid10.isValid()); 1076 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 1077 QCOMPARE(msgUid9.data(Imap::Mailbox::RoleMessageUid).toUInt(), 9u); 1078 QCOMPARE(msgUid10.data(Imap::Mailbox::RoleMessageUid).toUInt(), 10u); 1079 QCOMPARE(msgUid6.row(), 0); 1080 QCOMPARE(msgUid9.row(), 1); 1081 QCOMPARE(msgUid10.row(), 2); 1082 1083 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("foo"), 1084 threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); 1085 1086 Imap::Uids expectedUidOrder; 1087 1088 expectedUidOrder << 6 << 9 << 10; 1089 1090 // A very basic searching example 1091 cClient(t.mk("UID SEARCH CHARSET utf-8 SUBJECT foo\r\n")); 1092 cServer("* SEARCH " + numListToString(expectedUidOrder) + "\r\n"); 1093 cServer(t.last("OK sorted\r\n")); 1094 QCOMPARE(msgUid6.data(Imap::Mailbox::RoleMessageUid).toUInt(), 6u); 1095 checkUidMapFromThreading(expectedUidOrder); 1096 QCOMPARE(msgUid6.row(), 0); 1097 QCOMPARE(msgUid9.row(), 1); 1098 QCOMPARE(msgUid10.row(), 2); 1099 1100 // Enable ESEARCH 1101 injector.injectCapability(QStringLiteral("ESEARCH")); 1102 1103 // Try a "different" search 1104 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("blah"), 1105 threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); 1106 expectedUidOrder.clear(); 1107 expectedUidOrder << 9; 1108 1109 cClient(t.mk("UID SEARCH RETURN (ALL) CHARSET utf-8 SUBJECT blah\r\n")); 1110 QByteArray searchTag = t.last(); 1111 cServer("* ESEARCH (TAG \"" + searchTag + "\") UID ALL 9\r\n" + t.last("OK searched\r\n")); 1112 checkUidMapFromThreading(expectedUidOrder); 1113 1114 // Try yet another search, this time something which yields an empty result 1115 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("foobar"), 1116 threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); 1117 expectedUidOrder.clear(); 1118 cClient(t.mk("UID SEARCH RETURN (ALL) CHARSET utf-8 SUBJECT foobar\r\n")); 1119 searchTag = t.last(); 1120 cServer("* ESEARCH (TAG \"" + searchTag + "\") UID\r\n" + t.last("OK searched\r\n")); 1121 checkUidMapFromThreading(expectedUidOrder); 1122 1123 // Try to go back to no filtering 1124 threadingModel->setUserSearchingSortingPreference(QStringList(), threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); 1125 expectedUidOrder = uidMap; 1126 checkUidMapFromThreading(expectedUidOrder); 1127 1128 // Let's check threading & searching combo 1129 threadingModel->setUserWantsThreading(true); 1130 QByteArray threading; 1131 QByteArray ordering; 1132 1133 // testing simple threading functionality for sure 1134 threading = QByteArray("(6)(9 10)"); 1135 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1136 cServer(QByteArray("* THREAD ") + threading + QByteArray("\r\n") + t.last("OK thread\r\n")); 1137 QCOMPARE(treeToThreading(QModelIndex()), threading); 1138 1139 #define _letsThreadingSearch(CONDITIONS, ORDERING, THREADING, reversed_answer) \ 1140 { \ 1141 threadingModel->setUserSearchingSortingPreference(CONDITIONS, threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); \ 1142 QString imapConds = CONDITIONS.join(' '); \ 1143 auto q = t.mk(QString("UID SEARCH RETURN (ALL) CHARSET utf-8 " + imapConds + "\r\n").toLocal8Bit().data()); \ 1144 searchTag = t.last(); \ 1145 q.append(t.mk(QString("UID THREAD REFS utf-8 " + imapConds + "\r\n").toLocal8Bit().data())); \ 1146 cClient(q); \ 1147 if (!reversed_answer) { \ 1148 cServer("* ESEARCH (TAG \"" + searchTag + "\") UID ALL " + ORDERING + "\r\n" + t.prev("OK searched\r\n")); \ 1149 cServer(QByteArray("* THREAD ") + THREADING + QByteArray("\r\n") + t.last("OK thread\r\n")); \ 1150 } else { \ 1151 cServer(QByteArray("* THREAD ") + THREADING + QByteArray("\r\n") + t.last("OK thread\r\n")); \ 1152 cServer("* ESEARCH (TAG \"" + searchTag + "\") UID ALL " + ORDERING + "\r\n" + t.prev("OK searched\r\n")); \ 1153 } \ 1154 cEmpty(); \ 1155 } 1156 #define letsThreadingSearch(CONDITIONS, ORDERING, THREADING) _letsThreadingSearch(CONDITIONS, ORDERING, THREADING, false) 1157 #define letsTrickyThreadingSearch(CONDITIONS, ORDERING, THREADING) _letsThreadingSearch(CONDITIONS, ORDERING, THREADING, true) 1158 1159 QStringList conditions; 1160 1161 threading = QByteArray("(9)"); 1162 ordering = QByteArray("9"); 1163 conditions = QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("foo1"); 1164 letsThreadingSearch(conditions, ordering, threading); 1165 QCOMPARE(treeToThreading(QModelIndex()), threading); 1166 1167 threading = QByteArray("(9 10)"); 1168 ordering = QByteArray("9,10"); 1169 conditions = QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("foo2"); 1170 letsThreadingSearch(conditions, ordering, threading); 1171 QCOMPARE(treeToThreading(QModelIndex()), threading); 1172 // redundant testing against treeToThreading() bugs for sure 1173 QCOMPARE(threadingModel->rowCount(QModelIndex()), 1); 1174 QCOMPARE(threadingModel->rowCount(threadingModel->index(1, 0)), 1); 1175 1176 threading = QByteArray("(9 10)"); 1177 ordering = QByteArray("9,10"); 1178 conditions = QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("fooOOO2"); 1179 letsTrickyThreadingSearch(conditions, ordering, threading); 1180 QCOMPARE(treeToThreading(QModelIndex()), threading); 1181 1182 injector.injectCapability(QStringLiteral("CONTEXT=SEARCH")); 1183 1184 #define letsThreadingSearchUpdate(CONDITIONS, ORDERING, THREADING) \ 1185 { \ 1186 threadingModel->setUserSearchingSortingPreference(CONDITIONS, threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); \ 1187 QString imapConds = CONDITIONS.join(' '); \ 1188 auto q = t.mk(QString("UID SEARCH RETURN (ALL UPDATE) CHARSET utf-8 " + imapConds + "\r\n").toLocal8Bit().data()); \ 1189 searchTag = t.last(); \ 1190 q.append(t.mk(QString("UID THREAD REFS utf-8 " + imapConds + "\r\n").toLocal8Bit().data())); \ 1191 cClient(q); \ 1192 cServer("* ESEARCH (TAG \"" + searchTag + "\") UID ALL " + ORDERING + "\r\n" + t.prev("OK searched\r\n")); \ 1193 cServer(QByteArray("* THREAD ") + THREADING + QByteArray("\r\n") + t.last("OK thread\r\n")); \ 1194 cEmpty(); \ 1195 } 1196 1197 threading = QByteArray("(10)"); 1198 ordering = QByteArray("10"); 1199 conditions = QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("foo3"); 1200 letsThreadingSearchUpdate(conditions, ordering, threading); 1201 QCOMPARE(treeToThreading(QModelIndex()), threading); 1202 // getting incremental update with active search 1203 threading = QByteArray("(10 15)"); 1204 ordering = QByteArray("10 15"); 1205 cServer("* 4 EXISTS\r\n* ESEARCH (TAG \"" + searchTag + "\") UID ADDTO (0 15)\r\n"); 1206 auto qFoo3 = t.mk("UID FETCH 15:* (FLAGS)\r\n"); 1207 qFoo3.append(t.mk("UID THREAD REFS utf-8 SUBJECT foo3\r\n")); 1208 cClient(qFoo3); 1209 cServer("* 4 FETCH (UID 15 FLAGS ())\r\n" + t.prev("ok fetched\r\n")); 1210 cServer(QByteArray("* THREAD ") + threading + QByteArray("\r\n") + t.last("OK thread\r\n")); 1211 cEmpty(); 1212 QCOMPARE(treeToThreading(QModelIndex()), threading); 1213 1214 // exiting search 1215 threading = QByteArray("(6)(9 (10)(15))"); 1216 ordering = QByteArray("6,9:10,15"); 1217 conditions = QStringList(); 1218 threadingModel->setUserSearchingSortingPreference(conditions, threadingModel->currentSortCriterium(), threadingModel->currentSortOrder()); 1219 auto q = t.mk(QString("CANCELUPDATE \"" + searchTag + "\"\r\n").toLocal8Bit().data()); 1220 q.append(t.mk(QString("UID THREAD REFS utf-8 ALL\r\n").toLocal8Bit().data())); 1221 cClient(q); 1222 cServer("* OK Updates cancelled\r\n" + t.prev("OK searched\r\n")); 1223 cServer(QByteArray("* THREAD ") + threading + QByteArray("\r\n") + t.last("OK thread\r\n")); 1224 cEmpty(); 1225 justKeepTask(); 1226 QCOMPARE(treeToThreading(QModelIndex()), threading); 1227 1228 // FIXME: check sorting & searching combo 1229 // FIXME: check threading & sorting & searching combo 1230 // FIXME: check incremental updates 1231 // FIXME: finalize me 1232 1233 cEmpty(); 1234 justKeepTask(); 1235 } 1236 1237 QByteArray ImapModelThreadingTest::prepareHugeUntaggedThread(const uint num) 1238 { 1239 QString sampleThread = QStringLiteral("(%1 (%2 %3 (%4)(%5 %6 %7))(%8 %9 %10))"); 1240 QString linearThread = QStringLiteral("(%1 %2 %3 %4 %5 %6 %7 %8 %9 %10)"); 1241 QString flatThread = QStringLiteral("(%1 (%2)(%3)(%4)(%5)(%6)(%7)(%8)(%9)(%10))"); 1242 Q_ASSERT(num % 10 == 0); 1243 QString response = QStringLiteral("* THREAD "); 1244 for (uint i = 1; i < num; i += 10) { 1245 QString *format = 0; 1246 switch (i % 100) { 1247 case 1: 1248 case 11: 1249 case 21: 1250 case 31: 1251 case 41: 1252 format = &sampleThread; 1253 break; 1254 case 51: 1255 case 61: 1256 format = &linearThread; 1257 break; 1258 case 71: 1259 case 81: 1260 case 91: 1261 format = &flatThread; 1262 break; 1263 } 1264 Q_ASSERT(format); 1265 response += format->arg(QString::number(i), QString::number(i+1), QString::number(i+2), QString::number(i+3), 1266 QString::number(i+4), QString::number(i+5), QString::number(i+6), QString::number(i+7), 1267 QString::number(i+8)).arg(QString::number(i+9)); 1268 } 1269 response += QLatin1String("\r\n"); 1270 return response.toUtf8(); 1271 } 1272 1273 void ImapModelThreadingTest::testThreadingPerformance() 1274 { 1275 #ifdef ASAN_BUILD 1276 qDebug() << "ASAN build detected, benchmarking with fewer items"; 1277 const uint num = 6660; 1278 #else 1279 const uint num = 100000; 1280 #endif 1281 initialMessages(num); 1282 QByteArray untaggedThread = prepareHugeUntaggedThread(num); 1283 QBENCHMARK_ONCE { 1284 QCOMPARE(SOCK->writtenStuff(), t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1285 SOCK->fakeReading(untaggedThread + t.last("OK thread\r\n")); 1286 QCoreApplication::processEvents(); 1287 QCoreApplication::processEvents(); 1288 QCoreApplication::processEvents(); 1289 QCoreApplication::processEvents(); 1290 model->cache()->setMessageThreading(QStringLiteral("a"), QVector<Imap::Responses::ThreadingNode>()); 1291 threadingModel->wantThreading(); 1292 QCoreApplication::processEvents(); 1293 QCoreApplication::processEvents(); 1294 } 1295 } 1296 1297 void ImapModelThreadingTest::testSortingPerformance() 1298 { 1299 threadingModel->setUserWantsThreading(false); 1300 1301 using namespace Imap::Mailbox; 1302 1303 #ifdef ASAN_BUILD 1304 qDebug() << "ASAN build detected, benchmarking with fewer items"; 1305 const int num = 6660; 1306 #else 1307 const int num = 100000; 1308 #endif 1309 initialMessages(num); 1310 1311 FakeCapabilitiesInjector injector(model); 1312 injector.injectCapability(QStringLiteral("QRESYNC")); 1313 injector.injectCapability(QStringLiteral("SORT=DISPLAY")); 1314 injector.injectCapability(QStringLiteral("SORT")); 1315 1316 QStringList sortOrder; 1317 int i = 0; 1318 while (i < num / 2) { 1319 sortOrder << QString::number(num / 2 + 1 + i); 1320 ++i; 1321 } 1322 while (i < num) { 1323 sortOrder << QString::number(i - num / 2); 1324 ++i; 1325 } 1326 QCOMPARE(sortOrder.size(), num); 1327 QByteArray resp = ("* SORT " + sortOrder.join(QStringLiteral(" ")) + "\r\n").toUtf8(); 1328 1329 QBENCHMARK_ONCE { 1330 threadingModel->setUserSearchingSortingPreference(QStringList(), ThreadingMsgListModel::SORT_NONE, Qt::AscendingOrder); 1331 threadingModel->setUserSearchingSortingPreference(QStringList(), ThreadingMsgListModel::SORT_NONE, Qt::DescendingOrder); 1332 } 1333 1334 bool flag = false; 1335 QBENCHMARK_ONCE { 1336 ThreadingMsgListModel::SortCriterium criterium = flag ? ThreadingMsgListModel::SORT_SUBJECT : ThreadingMsgListModel::SORT_CC; 1337 Qt::SortOrder order = flag ? Qt::AscendingOrder : Qt::DescendingOrder; 1338 threadingModel->setUserSearchingSortingPreference(QStringList(), criterium, order); 1339 if (flag) { 1340 cClient(t.mk("UID SORT (SUBJECT) utf-8 ALL\r\n")); 1341 } else { 1342 cClient(t.mk("UID SORT (CC) utf-8 ALL\r\n")); 1343 } 1344 flag = !flag; 1345 cServer(resp); 1346 cServer(t.last("OK sorted\r\n")); 1347 } 1348 } 1349 1350 void ImapModelThreadingTest::testSearchingPerformance() 1351 { 1352 threadingModel->setUserWantsThreading(false); 1353 1354 using namespace Imap::Mailbox; 1355 1356 #ifdef ASAN_BUILD 1357 qDebug() << "ASAN build detected, benchmarking with fewer items"; 1358 const int num = 6660; 1359 #else 1360 const int num = 100000; 1361 #endif 1362 initialMessages(num); 1363 1364 FakeCapabilitiesInjector injector(model); 1365 injector.injectCapability(QStringLiteral("QRESYNC")); 1366 1367 threadingModel->setUserSearchingSortingPreference(QStringList(), ThreadingMsgListModel::SORT_NONE, Qt::DescendingOrder); 1368 /*cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1369 QByteArray untaggedThread = prepareHugeUntaggedThread(num); 1370 cServer(untaggedThread + t.last("OK thread\r\n"));*/ 1371 1372 Imap::Uids result; 1373 result << 1 << 5 << 59 << 666; 1374 QStringList buf; 1375 Q_FOREACH(const int uid, result) 1376 buf << QString::number(uid); 1377 QByteArray sortResult = buf.join(QStringLiteral(" ")).toUtf8(); 1378 1379 QBENCHMARK_ONCE { 1380 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("x"), 1381 ThreadingMsgListModel::SORT_NONE, Qt::AscendingOrder); 1382 cClient(t.mk("UID SEARCH CHARSET utf-8 SUBJECT x\r\n")); 1383 cServer("* SEARCH " + sortResult + "\r\n"); 1384 cServer(t.last("OK sorted\r\n")); 1385 } 1386 QCOMPARE(threadingModel->rowCount(), result.size()); 1387 for (int i = 0; i < result.size(); ++i) { 1388 QModelIndex index = threadingModel->index(i, 0); 1389 QVERIFY(index.isValid()); 1390 QCOMPARE(index.data(Imap::Mailbox::RoleMessageUid).toUInt(), result[i]); 1391 } 1392 } 1393 1394 /** @short Test that the INCTHREAD extension works as advertized */ 1395 void ImapModelThreadingTest::testIncrementalThreading() 1396 { 1397 initialMessages(10); 1398 1399 // A complex nested hierarchy with nodes to be promoted 1400 Mapping mapping; 1401 QByteArray response; 1402 complexMapping(mapping, response); 1403 1404 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1405 cServer(QByteArray("* THREAD ") + response + QByteArray("\r\n") + t.last("OK thread\r\n")); 1406 verifyMapping(mapping); 1407 QCOMPARE(threadingModel->rowCount(QModelIndex()), 4); 1408 IndexMapping indexMap = buildIndexMap(mapping); 1409 verifyIndexMap(indexMap, mapping); 1410 // The response is actually slightly different, but never mind (extra parentheses around 7) 1411 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2 3)(4 (5)(6))(7 (8)(9 10))")); 1412 1413 // Activate support for the INCTHREAD extension 1414 FakeCapabilitiesInjector injector(model); 1415 injector.injectCapability(QStringLiteral("ETHREAD")); 1416 injector.injectCapability(QStringLiteral("INCTHREAD")); 1417 1418 // Fake delivery of one new message 1419 cServer("* 11 EXISTS\r\n"); 1420 // Ask for the UID and deliver it immediately 1421 cClient(t.mk("UID FETCH 11:* (FLAGS)\r\n")); 1422 cServer("* 11 FETCH (UID 11 FLAGS ())\r\n" + t.last("OK fetch\r\n")); 1423 1424 // Test the incremental threading 1425 cClient(t.mk("UID THREAD RETURN (INCTHREAD) REFS utf-8 INTHREAD REFS UID 11:*\r\n")); 1426 // Yes, it's a rather funky response 1427 cServer("* ESEARCH (TAG \"" + t.last() + "\") UID INCTHREAD 2 (7 (8 9 11)(10))\r\n"); 1428 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2 3)(7 (8 9 11)(10))(4 (5)(6))")); 1429 cServer(t.last("OK done\r\n")); 1430 1431 cEmpty(); 1432 } 1433 1434 /** Test what happens when a thread root ceases to exist while the THREAD response is in flight */ 1435 void ImapModelThreadingTest::testRemovingRootWithThreadingInFlight() 1436 { 1437 // At first, add two standalone threads 1438 initialMessages(2); 1439 Mapping mapping; 1440 mapping.clear(); 1441 mapping[QStringLiteral("0")] = 1; 1442 mapping[QStringLiteral("0.0")] = 0; 1443 mapping[QStringLiteral("1")] = 2; 1444 mapping[QStringLiteral("1.0")] = 0; 1445 mapping[QStringLiteral("2")] = 0; 1446 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1447 cServer(QByteArray("* THREAD (1)(2)\r\n") + t.last("OK thread\r\n")); 1448 verifyMapping(mapping); 1449 QCOMPARE(threadingModel->rowCount(QModelIndex()), 2); 1450 verifyIndexMap(buildIndexMap(mapping), mapping); 1451 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2)")); 1452 1453 // Now let one more message arrive 1454 cServer("* 3 EXISTS\r\n"); 1455 cClient(t.mk("UID FETCH 3:* (FLAGS)\r\n")); 1456 QByteArray fetchUntagged("* 3 FETCH (UID 3 FLAGS ())\r\n"); 1457 QByteArray fetchTagged(t.last("OK fetched\r\n")); 1458 cServer(fetchUntagged); 1459 cServer(fetchTagged); 1460 // While the threading is requested, one thread root gets removed 1461 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1462 QByteArray threadUntagged("* THREAD (1)(2 3)\r\n"); 1463 QByteArray threadTagged(t.last("OK thread\r\n")); 1464 cServer(threadUntagged); 1465 // The important bit is EXPUNGE prior to the tagged OK for the threading. This is a difference which matters. 1466 cServer("* 2 EXPUNGE\r\n"); 1467 cServer(threadTagged); 1468 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), QString::fromUtf8("(1)(3)")); 1469 mapping.clear(); 1470 mapping[QStringLiteral("0")] = 1; 1471 mapping[QStringLiteral("0.0")] = 0; 1472 mapping[QStringLiteral("1")] = 3; 1473 mapping[QStringLiteral("1.0")] = 0; 1474 mapping[QStringLiteral("2")] = 0; 1475 verifyMapping(mapping); 1476 QCOMPARE(threadingModel->rowCount(QModelIndex()), 2); 1477 verifyIndexMap(buildIndexMap(mapping), mapping); 1478 cEmpty(); 1479 } 1480 1481 /** @short Check that multiple messages being removed at once doesn't break stuff */ 1482 void ImapModelThreadingTest::testMultipleExpunges() 1483 { 1484 initialMessages(4); 1485 Mapping mapping; 1486 mapping[QStringLiteral("0")] = 1; 1487 mapping[QStringLiteral("0.0")] = 0; 1488 mapping[QStringLiteral("1")] = 2; 1489 mapping[QStringLiteral("1.0")] = 0; 1490 mapping[QStringLiteral("2")] = 3; 1491 mapping[QStringLiteral("2.0")] = 0; 1492 mapping[QStringLiteral("3")] = 4; 1493 mapping[QStringLiteral("3.0")] = 0; 1494 mapping[QStringLiteral("4")] = 0; 1495 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1496 cServer(QByteArray("* THREAD (1)(2)(3)(4)\r\n") + t.last("OK thread\r\n")); 1497 verifyMapping(mapping); 1498 QCOMPARE(threadingModel->rowCount(QModelIndex()), 4); 1499 verifyIndexMap(buildIndexMap(mapping), mapping); 1500 QCOMPARE(treeToThreading(QModelIndex()), QByteArray("(1)(2)(3)(4)")); 1501 1502 QPersistentModelIndex m1 = findItem(QStringLiteral("0")); 1503 QPersistentModelIndex m2 = findItem(QStringLiteral("1")); 1504 QPersistentModelIndex m4 = findItem(QStringLiteral("3")); 1505 helper_multipleExpunges_hit = 0; 1506 1507 QVERIFY(m1.isValid()); 1508 QVERIFY(m2.isValid()); 1509 QVERIFY(!helper_indexMultipleExpunges_1.isValid()); 1510 QVERIFY(m4.isValid()); 1511 1512 // The tricky part is here. The bug we want to test for was this: some code within QItemSelectionModel (?) 1513 // grabbed a new persistent index most likely within a slot tied to layoutAboutToBeChanged signal. This persistent 1514 // index was, however, not updated by the delayedPrune method, and therefore it was left dangling. In the GUI, this 1515 // was apparent because some items were suddenly getting selected after some other messages were removed, and the visual 1516 // position of the now bogous selection was conspicuously similar to the expunged messages. 1517 connect(threadingModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &ImapModelThreadingTest::helper_multipleExpunges); 1518 cServer("* 2 EXPUNGE\r\n* 2 EXPUNGE\r\n"); 1519 disconnect(threadingModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &ImapModelThreadingTest::helper_multipleExpunges); 1520 QCOMPARE(helper_multipleExpunges_hit, 1); 1521 1522 QCOMPARE(QString::fromUtf8(treeToThreading(QModelIndex())), QString::fromUtf8("(1)(4)")); 1523 QCOMPARE(threadingModel->rowCount(QModelIndex()), 2); 1524 QVERIFY(m1.isValid()); 1525 QVERIFY(!m2.isValid()); 1526 QVERIFY(!helper_indexMultipleExpunges_1.isValid()); 1527 QVERIFY(m4.isValid()); 1528 1529 cEmpty(); 1530 } 1531 1532 void ImapModelThreadingTest::helper_multipleExpunges() 1533 { 1534 if (helper_multipleExpunges_hit == -1) { 1535 // hack: don't let the QTestLib "run" this method in a standalone manner 1536 return; 1537 } 1538 helper_indexMultipleExpunges_1 = findItem(QStringLiteral("2")); 1539 QVERIFY(helper_indexMultipleExpunges_1.isValid()); 1540 ++helper_multipleExpunges_hit; 1541 } 1542 1543 /** @short Check how fast it is to delete a substantial number of e-mails from the mailbox using flat threading */ 1544 void ImapModelThreadingTest::testFlatThreadDeletionPerformance() 1545 { 1546 threadingModel->setUserWantsThreading(false); 1547 // only send NOOPs after a day; the default timeout of two minutes is way too short for valgrind's callgrind 1548 model->setProperty("trojita-imap-noop-period", 24 * 60 * 60 * 1000); 1549 1550 #ifdef ASAN_BUILD 1551 qDebug() << "ASAN build detected, benchmarking with fewer items"; 1552 const int num = 6660; 1553 #else 1554 const int num = 30000; // 30k messages translate into roughly 3-5s, which is acceptable 1555 #endif 1556 initialMessages(num); 1557 auto numDeletes = num / 2; 1558 1559 // perform the deletes in the middle 1560 QByteArray deletes = QByteArray("* " + QByteArray::number(num - numDeletes - 5) + " EXPUNGE\r\n").repeated(numDeletes); 1561 1562 QSignalSpy layoutChanged(threadingModel, SIGNAL(layoutChanged())); 1563 1564 QBENCHMARK_ONCE { 1565 cServer(deletes); 1566 // make sure all events are delivered; the model scheduler processes them on a 100-sized chunks 1567 // plus add one for actual processing of the delayed delete 1568 for (auto i = 0; i < numDeletes / 100 + 1; ++i) { 1569 QCoreApplication::processEvents(); 1570 } 1571 } 1572 QCOMPARE(model->rowCount(msgListB), 0); 1573 QVERIFY(layoutChanged.size() >= 1); 1574 QCOMPARE(threadingModel->rowCount(), num - numDeletes); 1575 QCOMPARE(model->rowCount(msgListA), num - numDeletes); 1576 1577 // Make sure that the mailbox switchover won't cause any events to get lost. 1578 // This should flush the delayed timer unconditionally. 1579 cClient(t.mk("SELECT b\r\n")); 1580 cServer("* 0 EXISTS\r\n* OK [UIDVALIDITY 666] \r\n* OK [UIDNEXT 1] \r\n" + t.last("OK selected\r\n")); 1581 1582 QCOMPARE(static_cast<int>(model->cache()->mailboxSyncState(QLatin1String("a")).exists()), num - numDeletes); 1583 justKeepTask(); 1584 cEmpty(); 1585 } 1586 1587 /** @short Make sure that changes of a yet unsynced message doesn't confuse us */ 1588 void ImapModelThreadingTest::testDataChangedUnknownUid() 1589 { 1590 initialMessages(1); 1591 cServer("* 1 FETCH (FLAGS ())\r\n"); 1592 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1593 cServer("* THREAD (1)\r\n" + t.last("OK thread\r\n")); 1594 justKeepTask(); 1595 cEmpty(); 1596 1597 model->markMailboxAsRead(idxA); 1598 cClient(t.mk("STORE 1:* +FLAGS.SILENT \\Seen\r\n")); 1599 cServer("* 2 EXISTS\r\n* 2 FETCH (FLAGS (pwn))\r\n" + t.last("OK marked\r\n")); 1600 cClient(t.mk("UID FETCH 2:* (FLAGS)\r\n")); 1601 cServer("* 2 FETCH (UID 1002 FLAGS (ahoy))\r\n" + t.last("OK done\r\n")); 1602 cClient(t.mk("UID THREAD REFS utf-8 ALL\r\n")); 1603 cServer("* THREAD (1)(1002)\r\n" + t.last("OK thread\r\n")); 1604 QCOMPARE(model->cache()->msgFlags(QLatin1String("a"), 1002), QStringList() << QLatin1String("ahoy")); 1605 justKeepTask(); 1606 cEmpty(); 1607 } 1608 1609 /** @short Verify parsing of various ESEARCH return results */ 1610 void ImapModelThreadingTest::testESearchResults() 1611 { 1612 using namespace Imap::Mailbox; 1613 threadingModel->setUserWantsThreading(false); 1614 initialMessages(1); 1615 FakeCapabilitiesInjector injector(model); 1616 injector.injectCapability(QStringLiteral("QRESYNC")); 1617 injector.injectCapability(QStringLiteral("ESEARCH")); 1618 1619 // An empty result, Dovecot style 1620 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("x"), 1621 ThreadingMsgListModel::SORT_NONE, Qt::AscendingOrder); 1622 cClient(t.mk("UID SEARCH RETURN (ALL) CHARSET utf-8 SUBJECT x\r\n")); 1623 // Dovecot sends the UID response, as expected 1624 cServer("* ESEARCH (TAG \"" + t.last() + "\") UID \r\n"); 1625 cServer(t.last("OK searched\r\n")); 1626 QCOMPARE(threadingModel->rowCount(), 0); 1627 1628 // Some extra data in the ESEARCH response -- just to make sure that the code doesn't expect a fixed position 1629 // of the ALL data set 1630 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("y"), 1631 ThreadingMsgListModel::SORT_NONE, Qt::AscendingOrder); 1632 cClient(t.mk("UID SEARCH RETURN (ALL) CHARSET utf-8 SUBJECT y\r\n")); 1633 // Check if random crap in these resplies doesn't break stuff 1634 cServer("* ESEARCH (TAG \"" + t.last() + "\") UID random0 0 Random00 0 ALL 1 random1 666 RANDOM2 333\r\n"); 1635 cServer(t.last("OK searched\r\n")); 1636 QCOMPARE(threadingModel->rowCount(), 1); 1637 1638 // Empty result, Cyrus 2.9.17-style 1639 threadingModel->setUserSearchingSortingPreference(QStringList() << QStringLiteral("SUBJECT") << QStringLiteral("z"), 1640 ThreadingMsgListModel::SORT_NONE, Qt::AscendingOrder); 1641 cClient(t.mk("UID SEARCH RETURN (ALL) CHARSET utf-8 SUBJECT z\r\n")); 1642 // Cyrus, however, omits the UID part of the response 1643 // https://bugs.kde.org/show_bug.cgi?id=350698 1644 // https://bugzilla.cyrusimap.org/show_bug.cgi?id=3900 1645 cServer("* ESEARCH (TAG \"" + t.last() + "\")\r\n"); 1646 cServer(t.last("OK searched\r\n")); 1647 QCOMPARE(threadingModel->rowCount(), 0); 1648 } 1649 1650 QTEST_GUILESS_MAIN( ImapModelThreadingTest )