Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ #include <QtTest/QtTest>
++
++ #include <QtCore/QUrl>
++ #include <QtCore/QDataStream>
++ #include <QtCore/QFileInfo>
++ #include <QtCore/QScopeGuard>
++ #include <QtCore/QTemporaryDir>
++ #include <QtSql/QSqlDatabase>
++ #include <QtSql/QSqlQuery>
++
++ #include <QtHelp/QHelpContentItem>
++ #include <QtHelp/QHelpEngineCore>
++
++ using namespace Qt::StringLiterals;
++
++ class tst_QHelpEngineCore : public QObject
++ {
++ Q_OBJECT
++
++ private slots:
++ void init();
++
++ void setupData();
++ void setupDataMissingCollectionFile();
++ void setupDataEmptyCollectionFile();
++ void setupDataReadOnlyLocation();
++ void collectionFile();
++ void setCollectionFile();
++ void copyCollectionFile();
++ void collectionSchemaVersion();
++
++ void namespaceName();
++ void registeredDocumentations();
++ void registerDocumentation();
++ void registerDocumentationFailure();
++ void unregisterDocumentation();
++ void unregisterDocumentationWithoutFolder();
++ void documentationFileName();
++
++ void customFilters();
++ void removeCustomFilter();
++ void removeCustomFilterFailure();
++ void addCustomFilter();
++ void filterAttributes();
++ void currentFilter();
++ void setCurrentFilter();
++
++ void filterAttributeSets();
++ void files();
++ void fileData();
++
++ void customValue();
++ void setCustomValue();
++ void removeCustomValue();
++
++ void setAutoSaveFilter();
++
++ void metaData();
++
++ void requestContentDepths_data();
++ void requestContentDepths();
++
++ private:
++ QString m_path;
++ QString m_colFile;
++ };
++
++ void tst_QHelpEngineCore::init()
++ {
++ // defined in profile
++ m_path = QLatin1String(SRCDIR);
++
++ m_path = QFileInfo(m_path).absoluteFilePath();
++
++ m_colFile = m_path + QLatin1String("/data/col.qhc");
++ if (QFile::exists(m_colFile))
++ QDir::current().remove(m_colFile);
++ if (!QFile::copy(m_path + "/data/collection.qhc", m_colFile))
++ QFAIL("Cannot copy file!");
++ QFile f(m_colFile);
++ f.setPermissions(QFile::WriteUser|QFile::ReadUser);
++ }
++
++ void tst_QHelpEngineCore::setupData()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ }
++
++ void tst_QHelpEngineCore::setupDataMissingCollectionFile()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++ const QString colFile = dir.filePath(QLatin1String("missing.qhc"));
++
++ QHelpEngineCore help(colFile, nullptr);
++ QVERIFY(help.isReadOnly());
++ QVERIFY(!help.setupData());
++ QVERIFY(!help.error().isEmpty());
++ // A read-only engine must not create the collection file.
++ QVERIFY(!QFile::exists(colFile));
++ }
++
++ void tst_QHelpEngineCore::setupDataEmptyCollectionFile()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++ const QString colFile = dir.filePath(QLatin1String("empty.qhc"));
++ {
++ QFile file(colFile);
++ QVERIFY(file.open(QIODevice::WriteOnly));
++ }
++
++ QHelpEngineCore help(colFile, nullptr);
++ QVERIFY(help.isReadOnly());
++ QVERIFY(!help.setupData());
++ QVERIFY(!help.error().isEmpty());
++ }
++
++ void tst_QHelpEngineCore::setupDataReadOnlyLocation()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++ const QString colFile = dir.filePath(QLatin1String("collection.qhc"));
++ QVERIFY(QFile::copy(m_path + "/data/collection.qhc", colFile));
++
++ // Mimic a documentation set installed into a read-only location: neither the
++ // collection file nor the directory holding it may be written to (QTBUG-72174).
++ // Note that data/collection.qhc is in the old format, without the index tables,
++ // so the engine must not attempt to add them here.
++ QFile::setPermissions(colFile, QFile::ReadOwner);
++ QFile::setPermissions(dir.path(), QFile::ReadOwner | QFile::ExeOwner);
++ // Let QTemporaryDir remove its contents again when going out of scope.
++ const auto permissionGuard = qScopeGuard([&dir, &colFile] {
++ QFile::setPermissions(dir.path(),
++ QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
++ QFile::setPermissions(colFile, QFile::ReadOwner | QFile::WriteOwner);
++ });
++
++ // Not every platform and file system enforces the permissions set above, and
++ // the superuser bypasses them altogether. Check for what the test relies on
++ // instead of guessing from the platform or from the user id.
++ QFile probe(dir.filePath(QLatin1String("probe")));
++ if (probe.open(QIODevice::WriteOnly)) {
++ probe.close();
++ probe.remove();
++ QSKIP("The read-only permissions are not enforced in this environment.");
++ }
++
++ const qint64 sizeBefore = QFileInfo(colFile).size();
++ const QStringList entriesBefore = QDir(dir.path()).entryList(QDir::Files, QDir::Name);
++
++ QHelpEngineCore help(colFile, nullptr);
++ QVERIFY(help.isReadOnly());
++ QVERIFY(help.setupData());
++ QVERIFY(help.error().isEmpty());
++ QVERIFY(!help.registeredDocumentations().isEmpty());
++
++ // The collection file must be left untouched, without any journal file next to it.
++ QCOMPARE(QFileInfo(colFile).size(), sizeBefore);
++ QCOMPARE(QDir(dir.path()).entryList(QDir::Files, QDir::Name), entriesBefore);
++ }
++
++ void tst_QHelpEngineCore::collectionFile()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.collectionFile(), QFileInfo(m_colFile).absoluteFilePath());
++ }
++
++ void tst_QHelpEngineCore::setCollectionFile()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.collectionFile(), QFileInfo(m_colFile).absoluteFilePath());
++
++ QString col1File = m_path + QLatin1String("/data/collection1.qhc");
++ help.setCollectionFile(col1File);
++ QCOMPARE(help.collectionFile(), QFileInfo(col1File).absoluteFilePath());
++
++ QStringList docs = help.registeredDocumentations();
++ QCOMPARE(docs.size(), 1);
++ QCOMPARE(docs.first(), QLatin1String("trolltech.com.1.0.0.test"));
++ }
++
++ void tst_QHelpEngineCore::copyCollectionFile()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.collectionFile(), QFileInfo(m_colFile).absoluteFilePath());
++
++ QString copiedFile = m_path + QLatin1String("/collectionCopy.qhc");
++ if (QFile::exists(copiedFile))
++ QDir::current().remove(copiedFile);
++
++ QCOMPARE(help.copyCollectionFile(copiedFile), true);
++
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "testdb");
++ db.setDatabaseName(copiedFile);
++ if (!db.open()) {
++ QSqlDatabase::removeDatabase("testdb");
++ QFAIL("Created database seems to be corrupt!");
++ }
++ QSqlQuery *m_query = new QSqlQuery(db);
++
++ m_query->exec("SELECT Key, Value FROM SettingsTable");
++ if (m_query->next()) {
++ QCOMPARE(m_query->value(0).toString(), QString("CurrentFilter"));
++ QCOMPARE(m_query->value(1).toString(), QString("unfiltered"));
++ } else {
++ QFAIL("Settingstable is corrupt!");
++ }
++
++ m_query->exec("SELECT NameId, FilterAttributeId FROM FilterTable");
++ int i = 0;
++ while (m_query->next()) {
++ if (i == 3) {
++ QCOMPARE(m_query->value(0).toInt(), 2);
++ QCOMPARE(m_query->value(1).toInt(), 6);
++ }
++ ++i;
++ }
++ QCOMPARE(i, 7);
++ m_query->clear();
++
++ m_query->exec("SELECT Name, FilePath FROM NamespaceTable");
++ i = 0;
++ while (m_query->next()) {
++ if (i == 0) {
++ QCOMPARE(m_query->value(0).toString(), QString("trolltech.com.3-3-8.qmake"));
++ QCOMPARE(m_query->value(1).toString(), QString("data/qmake-3.3.8.qch"));
++ }
++ ++i;
++ }
++ QCOMPARE(i, 3);
++
++ m_query->clear();
++ delete m_query;
++ }
++ QSqlDatabase::removeDatabase("testdb");
++ }
++
++ void tst_QHelpEngineCore::collectionSchemaVersion()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++
++ const auto execQuery = [](const QString &colFile, const QString &statement) {
++ const QString connectionName = "collectionSchemaVersion";
++ int result = -1;
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
++ db.setDatabaseName(colFile);
++ if (db.open()) {
++ QSqlQuery query(db);
++ if (query.exec(statement))
++ result = query.next() ? query.value(0).toInt() : 0;
++ }
++ }
++ QSqlDatabase::removeDatabase(connectionName);
++ return result;
++ };
++
++ // A new collection file gets the current version.
++ const QString newColFile = dir.filePath("new.qhc");
++ {
++ QHelpEngineCore c(newColFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++ QVERIFY(c.registerDocumentation(m_path + "/data/qmake-3.3.8.qch"));
++ }
++ QCOMPARE(execQuery(newColFile, "PRAGMA user_version"), 1);
++
++ // A collection file without a version is upgraded.
++ QCOMPARE(execQuery(m_colFile, "PRAGMA user_version"), 0);
++ {
++ QHelpEngineCore c(m_colFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++ QCOMPARE(c.registeredDocumentations().size(), 3);
++
++ // A copy gets the current version.
++ const QString copiedColFile = dir.filePath("copy.qhc");
++ QVERIFY(c.copyCollectionFile(copiedColFile));
++ QCOMPARE(execQuery(copiedColFile, "PRAGMA user_version"), 1);
++ }
++ QCOMPARE(execQuery(m_colFile, "PRAGMA user_version"), 1);
++
++ // A collection file of a newer version is refused.
++ const QString newerColFile = dir.filePath("newer.qhc");
++ QVERIFY(QFile::copy(newColFile, newerColFile));
++ QCOMPARE(execQuery(newerColFile, "PRAGMA user_version = 2"), 0);
++ for (const bool readOnly : {false, true}) {
++ QHelpEngineCore c(newerColFile);
++ c.setReadOnly(readOnly);
++ QCOMPARE(c.setupData(), false);
++ QVERIFY(!c.error().isEmpty());
++ }
++
++ // Missing tables of a collection file of the current version are recreated.
++ QCOMPARE(execQuery(newColFile, "DROP TABLE VersionFilter"), 0);
++ {
++ QHelpEngineCore c(newColFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++ QCOMPARE(c.registeredDocumentations().size(), 1);
++ }
++ QCOMPARE(execQuery(newColFile, "SELECT COUNT(*) FROM sqlite_master "
++ "WHERE type = 'table' AND name = 'VersionFilter'"), 1);
++ QVERIFY(execQuery(newColFile, "SELECT COUNT(*) FROM FileNameTable") > 0);
++ QCOMPARE(execQuery(newColFile, "PRAGMA user_version"), 1);
++ }
++
++ void tst_QHelpEngineCore::namespaceName()
++ {
++ QCOMPARE(QHelpEngineCore::namespaceName(m_path + "/data/qmake-3.3.8.qch"),
++ QString("trolltech.com.3-3-8.qmake"));
++ QCOMPARE(QHelpEngineCore::namespaceName(m_path + "/data/linguist-3.3.8.qch"),
++ QString("trolltech.com.3-3-8.linguist"));
++ }
++
++ void tst_QHelpEngineCore::registeredDocumentations()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ const QStringList docs = help.registeredDocumentations();
++ QCOMPARE(docs.size(), 3);
++ QStringList lst;
++ lst << "trolltech.com.3-3-8.qmake" << "trolltech.com.4-3-0.qmake"
++ << "trolltech.com.1.0.0.test";
++ for (const QString &s : docs)
++ lst.removeAll(s);
++ QCOMPARE(lst.isEmpty(), true);
++ }
++
++ void tst_QHelpEngineCore::registerDocumentation()
++ {
++ if (QFile::exists(m_colFile))
++ QDir::current().remove(m_colFile);
++ {
++ QHelpEngineCore c(m_colFile);
++ c.setReadOnly(false);
++ QCOMPARE(c.setupData(), true);
++ c.registerDocumentation(m_path + "/data/qmake-3.3.8.qch");
++ QCOMPARE(c.registeredDocumentations().size(), 1);
++ c.registerDocumentation(m_path + "/data/qmake-3.3.8.qch");
++ QCOMPARE(c.registeredDocumentations().size(), 1);
++ c.registerDocumentation(m_path + "/data/linguist-3.3.8.qch");
++ QCOMPARE(c.registeredDocumentations().size(), 2);
++ }
++
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "testdb");
++ db.setDatabaseName(m_colFile);
++ if (!db.open()) {
++ QSqlDatabase::removeDatabase("testdb");
++ QFAIL("Created database seems to be corrupt!");
++ }
++ QSqlQuery query(db);
++ query.exec("SELECT FilePath FROM NamespaceTable WHERE "
++ "Name=\'trolltech.com.3-3-8.linguist\'");
++ if (query.next())
++ QCOMPARE(query.value(0).toString(),
++ QString("linguist-3.3.8.qch"));
++ else
++ QFAIL("Query error!");
++ }
++ QSqlDatabase::removeDatabase("testdb");
++ }
++
++ void tst_QHelpEngineCore::registerDocumentationFailure()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++ const QString colFile = dir.filePath("register.qhc");
++
++ QHelpEngineCore c(colFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++
++ const auto execQuery = [&colFile](const QString &statement) {
++ const QString connectionName = "registerDocumentationFailure";
++ int result = -1;
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
++ db.setDatabaseName(colFile);
++ if (db.open()) {
++ QSqlQuery query(db);
++ if (query.exec(statement))
++ result = query.next() ? query.value(0).toInt() : 0;
++ }
++ }
++ QSqlDatabase::removeDatabase(connectionName);
++ return result;
++ };
++ // Makes the registration fail after the namespace was added.
++ QCOMPARE(execQuery("DROP TABLE TimeStampTable"), 0);
++
++ QCOMPARE(c.registerDocumentation(m_path + "/data/qmake-3.3.8.qch"), false);
++ QVERIFY(c.registeredDocumentations().isEmpty());
++ QCOMPARE(execQuery("SELECT COUNT(*) FROM NamespaceTable"), 0);
++ QCOMPARE(execQuery("SELECT COUNT(*) FROM FolderTable"), 0);
++ QCOMPARE(execQuery("SELECT COUNT(*) FROM FileNameTable"), 0);
++ }
++
++ void tst_QHelpEngineCore::unregisterDocumentation()
++ {
++ QHelpEngineCore c(m_colFile);
++ c.setReadOnly(false);
++ QCOMPARE(c.setupData(), true);
++ QCOMPARE(c.registeredDocumentations().size(), 3);
++ c.unregisterDocumentation("trolltech.com.3-3-8.qmake");
++ QCOMPARE(c.registeredDocumentations().size(), 2);
++ QCOMPARE(c.unregisterDocumentation("noexisting"), false);
++ }
++
++ void tst_QHelpEngineCore::unregisterDocumentationWithoutFolder()
++ {
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++ const QString colFile = dir.filePath("unregister.qhc");
++ const QString ns = "trolltech.com.3-3-8.qmake";
++ const QString orphanNs = "orphan.namespace";
++
++ {
++ QHelpEngineCore c(colFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++ QVERIFY(c.registerDocumentation(m_path + "/data/qmake-3.3.8.qch"));
++ }
++
++ const auto execQuery = [&colFile](const QString &statement) {
++ const QString connectionName = "unregisterDocumentationWithoutFolder";
++ int result = -1;
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
++ db.setDatabaseName(colFile);
++ if (db.open()) {
++ QSqlQuery query(db);
++ if (query.exec(statement))
++ result = query.next() ? query.value(0).toInt() : 0;
++ }
++ }
++ QSqlDatabase::removeDatabase(connectionName);
++ return result;
++ };
++ // A namespace left behind by a registration that failed before adding its folder.
++ QCOMPARE(execQuery("INSERT INTO NamespaceTable VALUES(NULL, '%1', 'orphan.qch')"_L1
++ .arg(orphanNs)), 0);
++ const int fileCount = execQuery("SELECT COUNT(*) FROM FileNameTable");
++ QVERIFY(fileCount > 0);
++
++ QHelpEngineCore c(colFile);
++ c.setReadOnly(false);
++ QVERIFY(c.setupData());
++ QCOMPARE(c.unregisterDocumentation(orphanNs), false);
++ QCOMPARE(execQuery("SELECT COUNT(*) FROM NamespaceTable"), 2);
++ QCOMPARE(execQuery("SELECT COUNT(*) FROM FileNameTable"), fileCount);
++ QCOMPARE(c.registeredDocumentations(), QStringList(ns));
++ }
++
++ void tst_QHelpEngineCore::documentationFileName()
++ {
++ QHelpEngineCore c(m_colFile);
++ QCOMPARE(c.setupData(), true);
++ QCOMPARE(c.documentationFileName(QLatin1String("trolltech.com.3-3-8.qmake")),
++ QString(m_path + "/data/qmake-3.3.8.qch"));
++ QCOMPARE(c.documentationFileName(QLatin1String("trolltech.com.1.0.0.test")),
++ QString(m_path + "/data/test.qch"));
++ QCOMPARE(c.documentationFileName(QLatin1String("trolltech.com.empty")),
++ QString());
++ }
++
++ void tst_QHelpEngineCore::customFilters()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ const QStringList custom = help.customFilters();
++ QCOMPARE(custom.size(), 4);
++ QStringList lst;
++ lst << "qmake Manual" << "Custom Filter 1"
++ << "Custom Filter 2" << "unfiltered";
++ for (const QString &s : custom)
++ lst.removeAll(s);
++ QCOMPARE(lst.size(), 0);
++ }
++
++ void tst_QHelpEngineCore::removeCustomFilter()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ help.removeCustomFilter("Custom Filter 1");
++ QStringList custom = help.customFilters();
++ QCOMPARE(custom.size(), 3);
++ QCOMPARE((bool)custom.contains("Custom Filter 1"), false);
++ }
++
++ void tst_QHelpEngineCore::removeCustomFilterFailure()
++ {
++ const auto execQuery = [this](const QString &statement) {
++ const QString connectionName = "removeCustomFilterFailure";
++ int result = -1;
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
++ db.setDatabaseName(m_colFile);
++ if (db.open()) {
++ QSqlQuery query(db);
++ if (query.exec(statement))
++ result = query.next() ? query.value(0).toInt() : 0;
++ }
++ }
++ QSqlDatabase::removeDatabase(connectionName);
++ return result;
++ };
++
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ // Makes removing the filter attributes of the filter fail.
++ QCOMPARE(execQuery("DROP TABLE FilterTable"), 0);
++
++ QCOMPARE(help.removeCustomFilter("Custom Filter 1"), false);
++ QVERIFY(help.customFilters().contains("Custom Filter 1"));
++ }
++
++ void tst_QHelpEngineCore::addCustomFilter()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ help.addCustomFilter("Qt Tools", QStringList() << "tools" << "qt");
++ QStringList custom = help.customFilters();
++ QCOMPARE(custom.size(), 5);
++ QCOMPARE((bool)custom.contains("Qt Tools"), true);
++ }
++
++ void tst_QHelpEngineCore::filterAttributes()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ const QStringList atts = help.filterAttributes("qmake Manual");
++ QCOMPARE(atts.size(), 3);
++ QStringList lst;
++ lst << "qmake" << "tools" << "qt";
++ for (const QString &s : atts)
++ lst.removeAll(s);
++ QCOMPARE(lst.size(), 0);
++ }
++
++ void tst_QHelpEngineCore::currentFilter()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ QCOMPARE(help.currentFilter(), QString("unfiltered"));
++ }
++
++ void tst_QHelpEngineCore::setCurrentFilter()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ QCOMPARE(help.currentFilter(), QString("unfiltered"));
++ help.setCurrentFilter("qmake Manual");
++ QCOMPARE(help.currentFilter(), QString("qmake Manual"));
++ QCOMPARE(help.customValue("CurrentFilter").toString(),
++ QString("qmake Manual"));
++ }
++
++ void tst_QHelpEngineCore::filterAttributeSets()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ help.setReadOnly(false);
++ QCOMPARE(help.setupData(), true);
++ QList<QStringList> lst = help.filterAttributeSets("trolltech.com.1.0.0.test");
++ QCOMPARE(lst.size(), 2);
++ QCOMPARE(lst.first().size(), 2);
++ QCOMPARE((bool)lst.first().contains("filter1"), true);
++ QCOMPARE((bool)lst.last().contains("filter2"), true);
++ }
++
++ void tst_QHelpEngineCore::files()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ help.setReadOnly(false);
++ QCOMPARE(help.setupData(), true);
++ QList<QUrl> lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList());
++ QCOMPARE(lst.size(), 16);
++ lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList(), "png");
++ QCOMPARE(lst.size(), 2);
++ lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList() << "qt", "html");
++ QCOMPARE(lst.size(), 13);
++ lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList() << "qt" << "qmake", "html");
++ QCOMPARE(lst.size(), 13);
++ lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList() << "qt" << "qmake" << "bla", "html");
++ QCOMPARE(lst.size(), 0);
++ lst = help.files("trolltech.com.4-3-0.qmake",
++ QStringList() << "qt" << "qmake", "foo");
++
++ // print 'lst' if test fails:
++ auto printRemainder = qScopeGuard([&]{ for (const QUrl &url : lst) qDebug() << url; });
++
++ QCOMPARE(lst.size(), 0);
++
++ printRemainder.dismiss();
++ }
++
++ void tst_QHelpEngineCore::fileData()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ help.setReadOnly(false);
++ QCOMPARE(help.setupData(), true);
++ QByteArray ba = help.fileData(QUrl("NotExisting"));
++ QCOMPARE(ba.size(), 0);
++ ba = help.fileData(QUrl("qthelp://trolltech.com.1.0.0.test/testFolder/test.html"));
++ QTextStream s(ba, QIODevice::ReadOnly|QIODevice::Text);
++ QFile f(m_path + "/data/test.html");
++ if (!f.open(QIODevice::ReadOnly|QIODevice::Text))
++ QFAIL("Cannot open original file!");
++ QTextStream ts(&f);
++ QCOMPARE(s.readAll(), ts.readAll());
++ }
++
++ void tst_QHelpEngineCore::customValue()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ QCOMPARE(help.customValue("CurrentFilter").toString(),
++ QString("unfiltered"));
++ }
++
++ void tst_QHelpEngineCore::setCustomValue()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ QCOMPARE(help.setCustomValue("Test", 3), true);
++ QCOMPARE(help.customValue("Test").toInt(), 3);
++ QCOMPARE(help.removeCustomValue("Test"), true);
++ QCOMPARE(help.customValue("Test"), QVariant());
++ }
++
++ void tst_QHelpEngineCore::removeCustomValue()
++ {
++ setCustomValue();
++ }
++
++ void tst_QHelpEngineCore::setAutoSaveFilter()
++ {
++ QHelpEngineCore help(m_colFile, 0);
++ QCOMPARE(help.setupData(), true);
++ QCOMPARE(help.currentFilter(), QString("unfiltered"));
++
++ help.setAutoSaveFilter(false);
++ help.setCurrentFilter("qmake Manual");
++ QCOMPARE(help.currentFilter(), QString("qmake Manual"));
++ QCOMPARE(help.customValue("CurrentFilter").toString(),
++ QString("unfiltered"));
++ }
++
++ void tst_QHelpEngineCore::metaData()
++ {
++ QCOMPARE(QHelpEngineCore::metaData(m_path + "/data/test.qch", "author").toString(),
++ QString("Digia Plc and/or its subsidiary(-ies)"));
++ QCOMPARE(QHelpEngineCore::metaData(m_path + "/data/test.qch", "notExisting").isValid(),
++ false);
++ }
++
++ void tst_QHelpEngineCore::requestContentDepths_data()
++ {
++ QTest::addColumn<QList<int>>("depths");
++ QTest::addColumn<QString>("expectedTree");
++
++ // Items are titled A, B, C, ... in order. The tree is written as title(children).
++ QTest::newRow("valid") << QList<int>{0, 1, 2, 1, 0} << "A(B(C),D),E";
++ QTest::newRow("gap") << QList<int>{0, 2, 2, 1} << "A(B,C,D)";
++ QTest::newRow("first-deep") << QList<int>{2, 0} << "A,B";
++ QTest::newRow("first-deep-children") << QList<int>{2, 3, 1} << "A(B),C";
++ QTest::newRow("negative") << QList<int>{0, 1, -5, 1} << "A(B),C(D)";
++ QTest::newRow("gap-back-into-gap") << QList<int>{0, 3, 2} << "A(B,C)";
++ QTest::newRow("gap-then-deeper") << QList<int>{0, 3, 4, 1, 2} << "A(B(C),D(E))";
++ QTest::newRow("gap-inside") << QList<int>{0, 1, 3, 2} << "A(B(C,D))";
++ QTest::newRow("gap-inside2") << QList<int>{0, 1, 3, 1} << "A(B(C),D)";
++ QTest::newRow("huge") << QList<int>{0, std::numeric_limits<int>::max(), 1} << "A(B,C)";
++ QTest::newRow("huge-nested") << QList<int>{0, 1, std::numeric_limits<int>::max(), 2}
++ << "A(B(C,D))";
++ QTest::newRow("huge-first") << QList<int>{std::numeric_limits<int>::max(), 1} << "A,B";
++ }
++
++ static QString childrenToString(const QHelpContentItem *item)
++ {
++ QStringList children;
++ for (int i = 0; i < item->childCount(); ++i) {
++ const QHelpContentItem *child = item->child(i);
++ QString str = child->title();
++ if (child->childCount())
++ str += u'(' + childrenToString(child) + u')';
++ children.append(str);
++ }
++ return children.join(u',');
++ }
++
++ void tst_QHelpEngineCore::requestContentDepths()
++ {
++ QFETCH(QList<int>, depths);
++ QFETCH(QString, expectedTree);
++
++ QTemporaryDir dir;
++ QVERIFY(dir.isValid());
++
++ const QString qchFile = dir.filePath("test.qch");
++ QVERIFY(QFile::copy(m_path + "/data/test.qch", qchFile));
++ QVERIFY(QFile::setPermissions(qchFile, QFile::WriteUser | QFile::ReadUser));
++
++ QByteArray contents;
++ {
++ QDataStream s(&contents, QIODevice::WriteOnly);
++ for (int i = 0; i < depths.size(); ++i) {
++ const QString title(QChar(u'A' + i));
++ s << depths.at(i) << QString(title + ".html") << title;
++ }
++ }
++
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "depthsdb");
++ const auto cleanup = qScopeGuard([&db] {
++ db.close();
++ db = {};
++ QSqlDatabase::removeDatabase("depthsdb");
++ });
++ db.setDatabaseName(qchFile);
++ QVERIFY(db.open());
++ QSqlQuery query(db);
++ QVERIFY(query.exec("DELETE FROM ContentsTable WHERE Id != 1"));
++ QVERIFY(query.prepare("UPDATE ContentsTable SET Data = ? WHERE Id = 1"));
++ query.addBindValue(contents);
++ QVERIFY(query.exec());
++ }
++
++ QHelpEngineCore help(dir.filePath("collection.qhc"));
++ help.setReadOnly(false);
++ help.setUsesFilterEngine(true);
++ QVERIFY(help.setupData());
++ QVERIFY(help.registerDocumentation(qchFile));
++
++ const std::shared_ptr<QHelpContentItem> root = help.requestContent({}).result();
++ QVERIFY(root);
++ QCOMPARE(childrenToString(root.get()), expectedTree);
++ }
++
++ QTEST_MAIN(tst_QHelpEngineCore)
++ #include "tst_qhelpenginecore.moc"