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 )