From 49b83e87e4da54cdd18ec04b10fdb4624389bd80 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 19 Jun 2018 11:00:39 +0200 Subject: Fixed the thread index. * Modifications could result in index changes because we lost the threadId due to remove + add. A modify was necessary (although we can ignore it for the email case). * The ThreadIndexer would try to lookup and potentially index threads for empty parent ids, which is clearly wrong. --- common/index.cpp | 8 +++ common/index.h | 1 + common/indexer.h | 5 ++ common/mail/threadindexer.cpp | 22 ++++-- common/mail/threadindexer.h | 3 +- common/storage/entitystore.cpp | 3 +- common/typeindex.cpp | 151 ++++++++++++++--------------------------- common/typeindex.h | 24 +++++-- tests/mailthreadtest.cpp | 78 +++++++++++++++++++++ tests/mailthreadtest.h | 1 + 10 files changed, 182 insertions(+), 114 deletions(-) diff --git a/common/index.cpp b/common/index.cpp index 13ca6ed..86a2dd5 100644 --- a/common/index.cpp +++ b/common/index.cpp @@ -2,6 +2,14 @@ #include "log.h" +Index::Index(const QString &storageRoot, const QString &dbName, const QString &indexName, Sink::Storage::DataStore::AccessMode mode) + : mTransaction(Sink::Storage::DataStore(storageRoot, dbName, mode).createTransaction(mode)), + mDb(mTransaction.openDatabase(indexName.toLatin1(), std::function(), true)), + mName(indexName), + mLogCtx("index." + indexName.toLatin1()) +{ +} + Index::Index(const QString &storageRoot, const QString &name, Sink::Storage::DataStore::AccessMode mode) : mTransaction(Sink::Storage::DataStore(storageRoot, name, mode).createTransaction(mode)), mDb(mTransaction.openDatabase(name.toLatin1(), std::function(), true)), diff --git a/common/index.h b/common/index.h index 043cc90..492319e 100644 --- a/common/index.h +++ b/common/index.h @@ -29,6 +29,7 @@ public: int code; }; + Index(const QString &storageRoot, const QString &dbName, const QString &indexName, Sink::Storage::DataStore::AccessMode mode = Sink::Storage::DataStore::ReadOnly); Index(const QString &storageRoot, const QString &name, Sink::Storage::DataStore::AccessMode mode = Sink::Storage::DataStore::ReadOnly); Index(const QString &storageRoot, const Sink::Storage::DbLayout &layout, Sink::Storage::DataStore::AccessMode mode = Sink::Storage::DataStore::ReadOnly); Index(const QByteArray &name, Sink::Storage::DataStore::Transaction &); diff --git a/common/indexer.h b/common/indexer.h index f0b32f5..b5b5422 100644 --- a/common/indexer.h +++ b/common/indexer.h @@ -33,6 +33,11 @@ public: virtual ~Indexer() = default; typedef QSharedPointer Ptr; virtual void add(const ApplicationDomain::ApplicationDomainType &entity) = 0; + virtual void modify(const ApplicationDomain::ApplicationDomainType &oldEntity, const ApplicationDomain::ApplicationDomainType &newEntity) + { + remove(oldEntity); + add(newEntity); + } virtual void remove(const ApplicationDomain::ApplicationDomainType &entity) = 0; virtual void commitTransaction() {}; virtual void abortTransaction() {}; diff --git a/common/mail/threadindexer.cpp b/common/mail/threadindexer.cpp index fb47118..c1d1aa8 100644 --- a/common/mail/threadindexer.cpp +++ b/common/mail/threadindexer.cpp @@ -25,24 +25,25 @@ using namespace Sink; using namespace Sink::ApplicationDomain; -void ThreadIndexer::updateThreadingIndex(const QByteArray &identifier, const ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction) +void ThreadIndexer::updateThreadingIndex(const ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction) { auto messageId = entity.getProperty(Mail::MessageId::name); auto parentMessageId = entity.getProperty(Mail::ParentMessageId::name); if (messageId.toByteArray().isEmpty()) { - SinkWarning() << "Found an email without messageId. This is illegal and threading will break. Entity id: " << identifier; + SinkWarning() << "Found an email without messageId. This is illegal and threading will break. Entity id: " << entity.identifier(); } - QVector thread; + SinkTrace() << "Indexing thread. Entity: " << entity.identifier() << "Messageid: " << messageId << "ParentMessageId: " << parentMessageId; //check if a child already registered our thread. - thread = index().secondaryLookup(messageId); + QVector thread = index().secondaryLookup(messageId); - if (!thread.isEmpty()) { + if (!thread.isEmpty() && parentMessageId.isValid()) { //A child already registered our thread so we merge the childs thread //* check if we have a parent thread, if not just continue as usual //* get all messages that have the same threadid as the child //* switch all to the parents thread + Q_ASSERT(!parentMessageId.toByteArray().isEmpty()); auto parentThread = index().secondaryLookup(parentMessageId); if (!parentThread.isEmpty()) { auto childThreadId = thread.first(); @@ -95,13 +96,22 @@ void ThreadIndexer::updateThreadingIndex(const QByteArray &identifier, const App void ThreadIndexer::add(const ApplicationDomain::ApplicationDomainType &entity) { - updateThreadingIndex(entity.identifier(), entity, transaction()); + updateThreadingIndex(entity, transaction()); +} + +void ThreadIndexer::modify(const ApplicationDomain::ApplicationDomainType &oldEntity, const ApplicationDomain::ApplicationDomainType &newEntity) +{ + //FIXME Implement to support thread changes. + //Emails are immutable (for everything threading relevant), so we don't care about it so far. } void ThreadIndexer::remove(const ApplicationDomain::ApplicationDomainType &entity) { auto messageId = entity.getProperty(Mail::MessageId::name); auto thread = index().secondaryLookup(messageId); + if (thread.isEmpty()) { + SinkWarning() << "Failed to find the threadId for the entity " << entity.identifier() << messageId; + } index().unindex(messageId.toByteArray(), thread.first(), transaction()); index().unindex(thread.first(), messageId.toByteArray(), transaction()); } diff --git a/common/mail/threadindexer.h b/common/mail/threadindexer.h index b2e939a..747ba77 100644 --- a/common/mail/threadindexer.h +++ b/common/mail/threadindexer.h @@ -27,10 +27,11 @@ class ThreadIndexer : public Indexer public: typedef QSharedPointer Ptr; virtual void add(const ApplicationDomain::ApplicationDomainType &entity) Q_DECL_OVERRIDE; + virtual void modify(const ApplicationDomain::ApplicationDomainType &oldEntity, const ApplicationDomain::ApplicationDomainType &newEntity) Q_DECL_OVERRIDE; virtual void remove(const ApplicationDomain::ApplicationDomainType &entity) Q_DECL_OVERRIDE; static QMap databases(); private: - void updateThreadingIndex(const QByteArray &identifier, const ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction); + void updateThreadingIndex(const ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction); }; } diff --git a/common/storage/entitystore.cpp b/common/storage/entitystore.cpp index 18c788a..c5b5ffc 100644 --- a/common/storage/entitystore.cpp +++ b/common/storage/entitystore.cpp @@ -283,8 +283,7 @@ bool EntityStore::modify(const QByteArray &type, const ApplicationDomainType &cu { SinkTraceCtx(d->logCtx) << "Modified entity: " << newEntity; - d->typeIndex(type).remove(current.identifier(), current, d->transaction, d->resourceContext.instanceId()); - d->typeIndex(type).add(newEntity.identifier(), newEntity, d->transaction, d->resourceContext.instanceId()); + d->typeIndex(type).modify(newEntity.identifier(), current, newEntity, d->transaction, d->resourceContext.instanceId()); const qint64 newRevision = DataStore::maxRevision(d->transaction) + 1; diff --git a/common/typeindex.cpp b/common/typeindex.cpp index 6aa3796..b18791f 100644 --- a/common/typeindex.cpp +++ b/common/typeindex.cpp @@ -120,82 +120,34 @@ static unsigned int bucketOf(const QVariant &value) } } -template <> -void TypeIndex::addProperty(const QByteArray &property) -{ - auto indexer = [this, property](bool add, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { - // SinkTraceCtx(mLogCtx) << "Indexing " << mType + ".index." + property << value.toByteArray(); - if (add) { - Index(indexName(property), transaction).add(getByteArray(value), identifier); - } else { - Index(indexName(property), transaction).remove(getByteArray(value), identifier); - } - }; - mIndexer.insert(property, indexer); - mProperties << property; +static void update(TypeIndex::Action action, const QByteArray &indexName, const QByteArray &key, const QByteArray &value, Sink::Storage::DataStore::Transaction &transaction) +{ + Index index(indexName, transaction); + switch (action) { + case TypeIndex::Add: + index.add(key, value); + break; + case TypeIndex::Remove: + index.remove(key, value); + break; + } } -template <> -void TypeIndex::addProperty(const QByteArray &property) +void TypeIndex::addProperty(const QByteArray &property) { - auto indexer = [this, property](bool add, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { - if (add) { - Index(indexName(property), transaction).add(getByteArray(value), identifier); - } else { - Index(indexName(property), transaction).remove(getByteArray(value), identifier); - } - }; - mIndexer.insert(property, indexer); - mProperties << property; -} - -template <> -void TypeIndex::addProperty(const QByteArray &property) -{ - auto indexer = [this, property](bool add, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { - // SinkTraceCtx(mLogCtx) << "Indexing " << mType + ".index." + property << value.toByteArray(); - if (add) { - Index(indexName(property), transaction).add(getByteArray(value), identifier); - } else { - Index(indexName(property), transaction).remove(getByteArray(value), identifier); - } + auto indexer = [=](Action action, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { + update(action, indexName(property), getByteArray(value), identifier, transaction); }; mIndexer.insert(property, indexer); mProperties << property; } -template <> -void TypeIndex::addProperty(const QByteArray &property) -{ - auto indexer = [this, property](bool add, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { - //SinkTraceCtx(mLogCtx) << "Indexing " << mType + ".index." + property << getByteArray(value); - if (add) { - Index(indexName(property), transaction).add(getByteArray(value), identifier); - } else { - Index(indexName(property), transaction).remove(getByteArray(value), identifier); - } - }; - mIndexer.insert(property, indexer); - mProperties << property; -} - -template <> -void TypeIndex::addProperty(const QByteArray &property) -{ - addProperty(property); -} - template <> void TypeIndex::addSortedProperty(const QByteArray &property) { - auto indexer = [this, property](bool add, const QByteArray &identifier, const QVariant &value, + auto indexer = [=](Action action, const QByteArray &identifier, const QVariant &value, Sink::Storage::DataStore::Transaction &transaction) { - const auto sortableDate = toSortableByteArray(value); - if (add) { - Index(sortedIndexName(property), transaction).add(sortableDate, identifier); - } else { - Index(sortedIndexName(property), transaction).remove(sortableDate, identifier); - } + update(action, sortedIndexName(property), toSortableByteArray(value), identifier, transaction); }; mSortIndexer.insert(property, indexer); mSortedProperties << property; @@ -204,14 +156,10 @@ void TypeIndex::addSortedProperty(const QByteArray &property) template <> void TypeIndex::addPropertyWithSorting(const QByteArray &property, const QByteArray &sortProperty) { - auto indexer = [=](bool add, const QByteArray &identifier, const QVariant &value, const QVariant &sortValue, Sink::Storage::DataStore::Transaction &transaction) { + auto indexer = [=](Action action, const QByteArray &identifier, const QVariant &value, const QVariant &sortValue, Sink::Storage::DataStore::Transaction &transaction) { const auto date = sortValue.toDateTime(); const auto propertyValue = getByteArray(value); - if (add) { - Index(indexName(property, sortProperty), transaction).add(propertyValue + toSortableByteArray(date), identifier); - } else { - Index(indexName(property, sortProperty), transaction).remove(propertyValue + toSortableByteArray(date), identifier); - } + update(action, indexName(property, sortProperty), propertyValue + toSortableByteArray(date), identifier, transaction); }; mGroupedSortIndexer.insert(property + sortProperty, indexer); mGroupedSortedProperties.insert(property, sortProperty); @@ -227,7 +175,7 @@ template <> void TypeIndex::addSampledPeriodIndex( const QByteArray &beginProperty, const QByteArray &endProperty) { - auto indexer = [=](bool add, const QByteArray &identifier, const QVariant &begin, + auto indexer = [=](Action action, const QByteArray &identifier, const QVariant &begin, const QVariant &end, Sink::Storage::DataStore::Transaction &transaction) { SinkTraceCtx(mLogCtx) << "Adding entity to sampled period index"; const auto beginDate = begin.toDateTime(); @@ -244,12 +192,13 @@ void TypeIndex::addSampledPeriodIndex( Index index(sampledPeriodIndexName(beginProperty, endProperty), transaction); for (auto bucket = beginBucket; bucket <= endBucket; ++bucket) { QByteArray bucketKey = padNumber(bucket); - if (add) { - SinkTraceCtx(mLogCtx) << "Adding entity to bucket:" << bucketKey; - index.add(bucketKey, identifier); - } else { - SinkTraceCtx(mLogCtx) << "Removing entity from bucket:" << bucketKey; - index.remove(bucketKey, identifier); + switch (action) { + case TypeIndex::Add: + index.add(bucketKey, identifier); + break; + case TypeIndex::Remove: + index.remove(bucketKey, identifier); + break; } } }; @@ -258,37 +207,29 @@ void TypeIndex::addSampledPeriodIndex( mSampledPeriodIndexer.insert({ beginProperty, endProperty }, indexer); } -void TypeIndex::updateIndex(bool add, const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) +void TypeIndex::updateIndex(Action action, const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) { for (const auto &property : mProperties) { const auto value = entity.getProperty(property); auto indexer = mIndexer.value(property); - indexer(add, identifier, value, transaction); + indexer(action, identifier, value, transaction); } for (const auto &properties : mSampledPeriodProperties) { const auto beginValue = entity.getProperty(properties.first); const auto endValue = entity.getProperty(properties.second); auto indexer = mSampledPeriodIndexer.value(properties); - indexer(add, identifier, beginValue, endValue, transaction); + indexer(action, identifier, beginValue, endValue, transaction); } for (const auto &property : mSortedProperties) { const auto value = entity.getProperty(property); auto indexer = mSortIndexer.value(property); - indexer(add, identifier, value, transaction); + indexer(action, identifier, value, transaction); } for (auto it = mGroupedSortedProperties.constBegin(); it != mGroupedSortedProperties.constEnd(); it++) { const auto value = entity.getProperty(it.key()); const auto sortValue = entity.getProperty(it.value()); auto indexer = mGroupedSortIndexer.value(it.key() + it.value()); - indexer(add, identifier, value, sortValue, transaction); - } - for (const auto &indexer : mCustomIndexer) { - indexer->setup(this, &transaction, resourceInstanceId); - if (add) { - indexer->add(entity); - } else { - indexer->remove(entity); - } + indexer(action, identifier, value, sortValue, transaction); } } @@ -309,12 +250,30 @@ void TypeIndex::abortTransaction() void TypeIndex::add(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) { - updateIndex(true, identifier, entity, transaction, resourceInstanceId); + updateIndex(Add, identifier, entity, transaction, resourceInstanceId); + for (const auto &indexer : mCustomIndexer) { + indexer->setup(this, &transaction, resourceInstanceId); + indexer->add(entity); + } +} + +void TypeIndex::modify(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &oldEntity, const Sink::ApplicationDomain::ApplicationDomainType &newEntity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) +{ + updateIndex(Remove, identifier, oldEntity, transaction, resourceInstanceId); + updateIndex(Add, identifier, newEntity, transaction, resourceInstanceId); + for (const auto &indexer : mCustomIndexer) { + indexer->setup(this, &transaction, resourceInstanceId); + indexer->modify(oldEntity, newEntity); + } } void TypeIndex::remove(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId) { - updateIndex(false, identifier, entity, transaction, resourceInstanceId); + updateIndex(Remove, identifier, entity, transaction, resourceInstanceId); + for (const auto &indexer : mCustomIndexer) { + indexer->setup(this, &transaction, resourceInstanceId); + indexer->remove(entity); + } } static QVector indexLookup(Index &index, QueryBase::Comparator filter, @@ -540,11 +499,5 @@ QVector TypeIndex::secondaryLookup(const QByteArray &lef template <> QVector TypeIndex::secondaryLookup(const QByteArray &leftName, const QByteArray &rightName, const QVariant &value) { - QVector keys; - Index index(indexName(leftName + rightName), *mTransaction); - const auto lookupKey = getByteArray(value); - index.lookup( - lookupKey, [&](const QByteArray &value) { keys << QByteArray{value.constData(), value.size()}; }, [=](const Index::Error &error) { SinkWarning() << "Lookup error in secondary index: " << error.message << value << lookupKey; }); - - return keys; + return secondaryLookup(leftName, rightName, value); } diff --git a/common/typeindex.h b/common/typeindex.h index a8c0e10..4e5a555 100644 --- a/common/typeindex.h +++ b/common/typeindex.h @@ -36,8 +36,15 @@ class TypeIndex public: TypeIndex(const QByteArray &type, const Sink::Log::Context &); - template void addProperty(const QByteArray &property); + + //FIXME We currently simply serialize based on the QVariant we get and ignore the index type + template + void addProperty(const QByteArray &property) + { + addProperty(property); + } + template void addSortedProperty(const QByteArray &property); template @@ -83,6 +90,7 @@ public: } void add(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); + void modify(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &oldEntity, const Sink::ApplicationDomain::ApplicationDomainType &newEntity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); void remove(const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); QVector query(const Sink::QueryBase &query, QSet &appliedFilters, QByteArray &appliedSorting, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); @@ -118,10 +126,14 @@ public: void commitTransaction(); void abortTransaction(); + enum Action { + Add, + Remove + }; private: friend class Sink::Storage::EntityStore; - void updateIndex(bool add, const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); + void updateIndex(Action action, const QByteArray &identifier, const Sink::ApplicationDomain::ApplicationDomainType &entity, Sink::Storage::DataStore::Transaction &transaction, const QByteArray &resourceInstanceId); QByteArray indexName(const QByteArray &property, const QByteArray &sortProperty = QByteArray()) const; QByteArray sortedIndexName(const QByteArray &property) const; QByteArray sampledPeriodIndexName(const QByteArray &rangeBeginProperty, const QByteArray &rangeEndProperty) const; @@ -135,8 +147,8 @@ private: QSet> mSampledPeriodProperties; QList mCustomIndexer; Sink::Storage::DataStore::Transaction *mTransaction; - QHash> mIndexer; - QHash> mSortIndexer; - QHash> mGroupedSortIndexer; - QHash, std::function> mSampledPeriodIndexer; + QHash> mIndexer; + QHash> mSortIndexer; + QHash> mGroupedSortIndexer; + QHash, std::function> mSampledPeriodIndexer; }; diff --git a/tests/mailthreadtest.cpp b/tests/mailthreadtest.cpp index 8c325d8..d2962e5 100644 --- a/tests/mailthreadtest.cpp +++ b/tests/mailthreadtest.cpp @@ -29,6 +29,8 @@ #include "log.h" #include "test.h" #include "standardqueries.h" +#include "index.h" +#include "definitions.h" using namespace Sink; using namespace Sink::ApplicationDomain; @@ -249,3 +251,79 @@ void MailThreadTest::testRealWorldThread() QCOMPARE(mails.size(), 8); } } + +//Avoid accidentally merging or changing threads +void MailThreadTest::testNoParentsWithModifications() +{ + auto folder = Folder::create(mResourceInstanceIdentifier); + folder.setName("folder2"); + VERIFYEXEC(Store::create(folder)); + + auto createMail = [&] (const QString &subject) { + auto message1 = KMime::Message::Ptr::create(); + message1->subject(true)->fromUnicodeString(subject, "utf8"); + message1->messageID(true)->fromUnicodeString("<" + subject + "@foobar.com" + ">", "utf8"); + message1->date(true)->setDateTime(QDateTime::currentDateTimeUtc()); + message1->assemble(); + + auto mail = Mail::create(mResourceInstanceIdentifier); + mail.setMimeMessage(message1->encodedContent(true)); + mail.setFolder(folder); + return mail; + }; + + auto mail1 = createMail("1"); + VERIFYEXEC(Store::create(mail1)); + auto mail2 = createMail("2"); + VERIFYEXEC(Store::create(mail2)); + VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto query = Sink::StandardQueries::threadLeaders(folder); + query.resourceFilter(mResourceInstanceIdentifier); + query.request().request().request().request().request(); + + QSet threadIds; + { + auto mails = Store::read(query); + QCOMPARE(mails.size(), 2); + for (const auto &m : mails) { + threadIds << m.getProperty(Mail::ThreadId::name).toByteArray(); + } + } + + auto readIndex = [&] (const QString &indexName, const QByteArray &lookupKey) { + Index index(Sink::storageLocation(), mResourceInstanceIdentifier, indexName, Sink::Storage::DataStore::ReadOnly); + QByteArrayList keys; + index.lookup(lookupKey, + [&](const QByteArray &value) { keys << QByteArray{value.constData(), value.size()}; }, + [=](const Index::Error &error) { SinkWarning() << "Lookup error in secondary index: " << error.message; }, + false); + return keys; + }; + QCOMPARE(readIndex("mail.index.messageIdthreadId", "1@foobar.com").size(), 1); + QCOMPARE(readIndex("mail.index.messageIdthreadId", "2@foobar.com").size(), 1); + + //We try to modify both mails on purpose + auto checkMail = [&] (Mail mail1) { + Mail modification = mail1; + modification.setChangedProperties({}); + modification.setImportant(true); + VERIFYEXEC(Store::modify(modification)); + VERIFYEXEC(ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + QCOMPARE(readIndex("mail.index.messageIdthreadId", "1@foobar.com").size(), 1); + QCOMPARE(readIndex("mail.index.messageIdthreadId", "2@foobar.com").size(), 1); + + { + auto mails = Store::read(query); + QCOMPARE(mails.size(), 2); + QSet newThreadIds; + for (const auto &m : mails) { + newThreadIds << m.getProperty(Mail::ThreadId::name).toByteArray(); + } + QCOMPARE(threadIds, newThreadIds); + } + }; + checkMail(mail1); + checkMail(mail2); +} diff --git a/tests/mailthreadtest.h b/tests/mailthreadtest.h index 6b64d4e..9ae1b4c 100644 --- a/tests/mailthreadtest.h +++ b/tests/mailthreadtest.h @@ -54,6 +54,7 @@ private slots: void testListThreadLeader(); void testIndexInMixedOrder(); void testRealWorldThread(); + void testNoParentsWithModifications(); }; } -- cgit v1.2.3