diff --git a/autotests/knewstuffentrytest.cpp b/autotests/knewstuffentrytest.cpp --- a/autotests/knewstuffentrytest.cpp +++ b/autotests/knewstuffentrytest.cpp @@ -44,6 +44,7 @@ "https://testpreview" "http://testpayload" "" "" "installed" "" "" +"ghns_exclude=1" ""; const QString name = QStringLiteral("Name"); diff --git a/src/attica/atticaprovider.cpp b/src/attica/atticaprovider.cpp --- a/src/attica/atticaprovider.cpp +++ b/src/attica/atticaprovider.cpp @@ -18,6 +18,7 @@ #include "atticaprovider_p.h" #include "question.h" +#include "tagsfilterchecker.h" #include #include @@ -269,9 +270,23 @@ Content::List contents = listJob->itemList(); EntryInternal::List entries; + TagsFilterChecker checker(mCurrentRequest.tagFilter); + TagsFilterChecker downloadschecker(mCurrentRequest.downloadTagFilter); Q_FOREACH (const Content &content, contents) { - mCachedContent.insert(content.id(), content); - entries.append(entryFromAtticaContent(content)); + bool filterAcceptsDownloads = content.downloads() == 0 ? true : false; + foreach(const Attica::DownloadDescription & dli, content.downloadUrlDescriptions()) { + filterAcceptsDownloads = downloadschecker.filterAccepts(dli.tags()); + if(filterAcceptsDownloads) { + break; + } + } + if(filterAcceptsDownloads && checker.filterAccepts(content.tags())) { + mCachedContent.insert(content.id(), content); + entries.append(entryFromAtticaContent(content)); + } + else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << mCurrentRequest.tagFilter << "and download filter" << mCurrentRequest.downloadTagFilter; + } } qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size(); @@ -481,6 +496,7 @@ entry.setSummary(content.description()); entry.setShortSummary(content.summary()); entry.setChangelog(content.changelog()); + entry.setTags(content.tags()); entry.clearDownloadLinkInformation(); QList descs = content.downloadUrlDescriptions(); @@ -493,6 +509,7 @@ info.id = desc.id(); info.size = desc.size(); info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload; + info.tags = desc.tags(); entry.appendDownloadLinkInformation(info); } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -9,6 +9,7 @@ itemsmodel.cpp provider.cpp security.cpp + tagsfilterchecker.cpp xmlloader.cpp # A system by which queries can be passed to the user, and responses diff --git a/src/core/engine.h b/src/core/engine.h --- a/src/core/engine.h +++ b/src/core/engine.h @@ -179,6 +179,81 @@ void requestMoreData(); void requestData(int page, int pageSize); + /** + * Set a filter for results, which filters out all entries which do not match + * the filter, as applied to the tags for the entry. This filters only on the + * tags specified for the entry itself. To filter the downloadlinks, use + * setDownloadTagFilter(QStringList). + * + * @note The default filter if one is not set from your knsrc file will filter + * out entries marked as ghns_exclude=1. To retain this when setting a custom + * filter, add "ghns_exclude!=1" as one of the filters. + * + * @note Some tags provided by OCS do not supply a value (and are simply passsed + * as a key). These will be interpreted as having the value 1 for filtering + * purposes. An example of this might be ghns_exclude, which in reality will + * generally be passed through ocs as "ghns_exclude" rather than "ghns_exclude=1" + * + * == Examples of use == + * Value for tag "tagname" must be exactly "tagdata": + * tagname==tagdata + * + * Value for tag "tagname" must be different from "tagdata": + * tagname!=tagdata + * + * == KNSRC entry == + * A tag filter line in a .knsrc file, which is a comma semarated list of + * tag/value pairs, might look like: + * + * TagFilter=ghns_exclude!=1,data##mimetype==application/cbr+zip,data##mimetype==application/cbr+rar + * which would honour the exclusion and filter out anything that does not + * include a comic book archive in either zip or rar format in one or more + * of the download items. + * Notice in particular that there are two data##mimetype entries. Use this + * for when a tag may have multiple values. + * + * TagFilter=application##architecture==x86_64 + * which would not honour the exclusion, and would filter out all entries + * which do not mark themselves as having a 64bit application binary in at + * least one download item. + * + * The value does not current suppport wildcards. The list should be considered + * a binary AND operation (that is, all filter entries must match for the data + * entry to be included in the return data) + * + * @param filter The filter in the form of a list of strings + * @see setDownloadTagFilter(QStringList) + */ + void setTagFilter(QStringList filter); + /** + * Gets the current tag filter list + * @see setTagFilter(QStringList) + */ + QStringList tagFilter() const; + /** + * Sets a filter to be applied to the downloads for an entry. The logic is the + * same as used in setTagFilter(QStringList), but vitally, only one downloadlink + * is required to match the filter for the list to be valid. If you do not wish + * to show the others in your client, you must hide them yourself. + * + * For an entry to be accepted when a download tag filter is set, it must also + * be accepted by the entry filter (so, for example, while a list of downloads + * might be accepted, if the entry has ghns_exclude set, and the default entry + * filter is set, the entry will still be filtered out). + * + * In your knsrc file, set DownloadTagFilter to the filter you wish to apply, + * using the same logic as described for the entry tagfilter. + * + * @param filter The filter in the form of a list of strings + * @see setTagFilter(QStringList) + */ + void setDownloadTagFilter(QStringList filter); + /** + * Gets the current downloadlink tag filter list + * @see setDownloadTagFilter(QStringList) + */ + QStringList downloadTagFilter() const; + /** * Request for packages that are installed and need update * diff --git a/src/core/engine.cpp b/src/core/engine.cpp --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -61,6 +61,8 @@ public: QList categoriesMetadata; Attica::ProviderManager *m_atticaProviderManager = nullptr; + QStringList tagFilter; + QStringList downloadTagFilter; }; Engine::Engine(QObject *parent) @@ -126,6 +128,17 @@ qCDebug(KNEWSTUFFCORE) << "Categories: " << m_categories; m_providerFileUrl = group.readEntry("ProvidersUrl", QString()); + d->tagFilter = group.readEntry("TagFilter").split(QStringLiteral(",")); + if(d->tagFilter.length() == 1 && d->tagFilter.at(0).isEmpty()) { + d->tagFilter[0] = QStringLiteral("ghns_exclude!=1"); + } + m_currentRequest.tagFilter = d->tagFilter; + d->downloadTagFilter = group.readEntry("DownloadTagFilter").split(QStringLiteral(",")); + if(d->downloadTagFilter.length() == 1 && d->downloadTagFilter.at(0).isEmpty()) { + d->downloadTagFilter.clear(); + } + m_currentRequest.downloadTagFilter = d->downloadTagFilter; + const QString configFileName = QFileInfo(QDir::isAbsolutePath(configfile) ? configfile : QStandardPaths::locate(QStandardPaths::GenericConfigLocation, configfile)).baseName(); // let installation read install specific config if (!m_installation->readConfig(group)) { @@ -304,6 +317,8 @@ { emit signalResetView(); m_currentPage = -1; + m_currentRequest.tagFilter = tagFilter(); + m_currentRequest.downloadTagFilter = downloadTagFilter(); m_currentRequest.pageSize = m_pageSize; m_currentRequest.page = 0; m_numDataJobs = 0; @@ -374,6 +389,8 @@ m_searchTimer->stop(); m_currentRequest = KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, id); m_currentRequest.pageSize = m_pageSize; + m_currentRequest.tagFilter = tagFilter(); + m_currentRequest.downloadTagFilter = downloadTagFilter(); EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest); if (!cache.isEmpty()) { @@ -395,6 +412,28 @@ } } +void Engine::setTagFilter(QStringList filter) +{ + d->tagFilter = filter; + m_currentRequest.tagFilter = filter; +} + +QStringList Engine::tagFilter() const +{ + return d->tagFilter; +} + +void Engine::setDownloadTagFilter(QStringList filter) +{ + d->downloadTagFilter = filter; + m_currentRequest.downloadTagFilter = filter; +} + +QStringList Engine::downloadTagFilter() const +{ + return d->downloadTagFilter; +} + void Engine::slotSearchTimerExpired() { reloadEntries(); diff --git a/src/core/entryinternal.h b/src/core/entryinternal.h --- a/src/core/entryinternal.h +++ b/src/core/entryinternal.h @@ -89,6 +89,7 @@ int id; bool isDownloadtypeLink; quint64 size = 0; + QStringList tags; }; /** @@ -440,6 +441,16 @@ */ void setDonationLink(const QString &link); + /** + * The set of tags assigned specifically to this content item. This does not include + * tags for the download links. To get those, you must concatenate the lists yourself. + * @see downloadLinkInformationList() + * @see DownloadLinkInformation + * @see Engine::setTagFilter(QStringList) + */ + QStringList tags() const; + void setTags(const QStringList &tags); + /** The id of the provider this entry belongs to */ diff --git a/src/core/entryinternal.cpp b/src/core/entryinternal.cpp --- a/src/core/entryinternal.cpp +++ b/src/core/entryinternal.cpp @@ -79,6 +79,7 @@ QString mProviderId; QStringList mUnInstalledFiles; QString mDonationLink; + QStringList mTags; QString mChecksum; QString mSignature; @@ -156,6 +157,16 @@ d->mProviderId = id; } +QStringList KNSCore::EntryInternal::tags() const +{ + return d->mTags; +} + +void KNSCore::EntryInternal::setTags(const QStringList& tags) +{ + d->mTags = tags; +} + QString EntryInternal::category() const { return d->mCategory; @@ -481,7 +492,7 @@ bool KNSCore::EntryInternal::setEntryXML(QXmlStreamReader& reader) { if (reader.name() != QLatin1String("stuff")) { - qWarning() << "Parsing Entry from invalid XML"; + qWarning() << "Parsing Entry from invalid XML. Reader tag name was expected to be \"stuff\", but was found as:" << reader.name(); return false; } @@ -540,6 +551,8 @@ d->mInstalledFiles.append(reader.readElementText(QXmlStreamReader::SkipChildElements)); } else if (reader.name() == QLatin1String("id")) { d->mUniqueId = reader.readElementText(QXmlStreamReader::SkipChildElements); + } else if (reader.name() == QLatin1String("tags")) { + d->mTags = reader.readElementText(QXmlStreamReader::SkipChildElements).split(QChar(',')); } else if (reader.name() == QLatin1String("status")) { const auto statusText = readText(&reader); if (statusText == QLatin1String("installed")) { @@ -551,7 +564,7 @@ if (reader.tokenType() == QXmlStreamReader::Characters) readNextSkipComments(&reader); } - Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement); + Q_ASSERT_X(reader.tokenType() == QXmlStreamReader::EndElement, Q_FUNC_INFO, QString("token name was %1 and the type was %2").arg(reader.name()).arg(reader.tokenString()).toLocal8Bit().data()); } // Validation @@ -633,6 +646,8 @@ d->mInstalledFiles.append(e.text()); } else if (e.tagName() == QLatin1String("id")) { d->mUniqueId = e.text(); + } else if (e.tagName() == QLatin1String("tags")) { + d->mTags = e.text().split(QChar(',')); } else if (e.tagName() == QLatin1String("status")) { QString statusText = e.text(); if (statusText == QLatin1String("installed")) { @@ -724,6 +739,7 @@ e = addElement(doc, el, QStringLiteral("preview"), d->mPreviewUrl[PreviewSmall1]); e = addElement(doc, el, QStringLiteral("previewBig"), d->mPreviewUrl[PreviewBig1]); e = addElement(doc, el, QStringLiteral("payload"), d->mPayload); + e = addElement(doc, el, QStringLiteral("tags"), d->mTags.join(QChar(','))); if (d->mStatus == KNS3::Entry::Installed) { (void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("installed")); diff --git a/src/core/provider.h b/src/core/provider.h --- a/src/core/provider.h +++ b/src/core/provider.h @@ -72,13 +72,15 @@ struct SearchRequest { SortMode sortMode; Filter filter; + QStringList tagFilter; + QStringList downloadTagFilter; QString searchTerm; QStringList categories; int page; int pageSize; - SearchRequest(SortMode sortMode_ = Newest, Filter filter_ = None, const QString &searchTerm_ = QString(), const QStringList &categories_ = QStringList(), int page_ = -1, int pageSize_ = 20) - : sortMode(sortMode_), filter(filter_), searchTerm(searchTerm_), categories(categories_), page(page_), pageSize(pageSize_) + SearchRequest(SortMode sortMode_ = Newest, Filter filter_ = None, const QString &searchTerm_ = QString(), const QStringList &categories_ = QStringList(), int page_ = -1, int pageSize_ = 20, const QStringList &tagFilter_ = QStringList(), const QStringList &downloadTagFilter_ = QStringList()) + : sortMode(sortMode_), filter(filter_), tagFilter(tagFilter_), downloadTagFilter(downloadTagFilter_), searchTerm(searchTerm_), categories(categories_), page(page_), pageSize(pageSize_) {} QString hashForRequest() const; diff --git a/src/core/tagsfilterchecker.h b/src/core/tagsfilterchecker.h new file mode 100644 --- /dev/null +++ b/src/core/tagsfilterchecker.h @@ -0,0 +1,79 @@ +/* + Copyright (c) 2018 Dan Leinir Turthra Jensen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef KNSCORE_TAGSFILTERCHECKER_H +#define KNSCORE_TAGSFILTERCHECKER_H + +#include + +namespace KNSCore { + +/** + * @brief Apply simple filtering logic to a list of tags + * + * == Examples of specifying tag filters == + * Value for tag "tagname" must be exactly "tagdata": + * tagname==tagdata + * + * Value for tag "tagname" must be different from "tagdata": + * tagname!=tagdata + * + * == Tag filter list == + * A tag filter list is a string list of filters as shown above, and a combination + * of which might look like: + * + * - ghns_exclude!=1 + * - data##mimetype==application/cbr+zip + * - data##mimetype==application/cbr+rar + * + * which would filter out anything which has ghns_exclude set to 1, and + * anything where the value of data##mimetype does not equal either + * "application/cbr+zip" or "application/cbr+rar". + * Notice in particular the two data##mimetype entries. Use this + * for when a tag may have multiple values. + * + * The value does not current suppport wildcards. The list should be considered + * a binary AND operation (that is, all filter entries must match for the data + * entry to be included in the return data) + */ +class TagsFilterChecker { +public: + /** + * Constructs an instance of the tags filter checker, prepopulated + * with the list of tag filters in the tagFilter parameter. + * + * @param tagFilter The list of tag filters + */ + TagsFilterChecker(const QStringList& tagFilter); + ~TagsFilterChecker(); + + /** + * Check whether the filter list accepts the passed list of tags + * + * @param tags A list of tags in the form of key=value strings + * @return True if the filter accepts the list, false if not + */ + bool filterAccepts(const QStringList& tags); + +private: + class Private; + Private *d; +}; + +} + +#endif//KNSCORE_TAGSFILTERCHECKER_H diff --git a/src/core/tagsfilterchecker.cpp b/src/core/tagsfilterchecker.cpp new file mode 100644 --- /dev/null +++ b/src/core/tagsfilterchecker.cpp @@ -0,0 +1,179 @@ +/* + Copyright (c) 2018 Dan Leinir Turthra Jensen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "tagsfilterchecker.h" + +#include + +#include + +namespace KNSCore +{ + +class TagsFilterChecker::Private +{ +public: + Private() {} + ~Private() + { + qDeleteAll(validators.values()); + } + class Validator; + // If people start using a LOT of validators (>20ish), we can always change it, but + // for now it seems reasonable that QMap is better than QHash here... + QMap validators; + + class Validator + { + public: + Validator(const QString& tag, const QString value) { + m_tag = tag; + m_acceptedValues << value; + } + virtual ~Validator() {}; + virtual bool filterAccepts(const QString& tag, const QString& value) = 0; + protected: + friend class TagsFilterChecker::Private; + QString m_tag; + QStringList m_acceptedValues; + }; + + // Will only accept entries which have one of the accepted values set for the tag key + class EqualityValidator : public Validator + { + public: + EqualityValidator(const QString& tag, const QString value) + : Validator(tag, value) + {} + ~EqualityValidator() override {} + bool filterAccepts(const QString& tag, const QString& value) override + { + bool result = true; + if(tag == m_tag && !m_acceptedValues.contains(value)) { + result = false; + } + return result; + } + }; + + // Will only accept entries which have none of the values set for the tag key + class InequalityValidator : public Validator + { + public: + InequalityValidator(const QString& tag, const QString value) + : Validator(tag, value) + {} + ~InequalityValidator() override {} + bool filterAccepts(const QString& tag, const QString& value) override + { + bool result = true; + if(tag == m_tag && m_acceptedValues.contains(value)) { + result = false; + } + return result; + } + }; + + void addValidator(const QString& filter) + { + int pos = 0; + if((pos = filter.indexOf(QStringLiteral("=="))) > -1) + { + QString tag = filter.left(pos); + QString value = filter.mid(tag.length() + 2); + Validator* val = validators.value(tag, nullptr); + if(val) + { + val->m_acceptedValues << value; + } + else + { + val = new EqualityValidator(tag, value); + validators[tag] = val; + } + qCDebug(KNEWSTUFFCORE) << "Created EqualityValidator for tag" << tag << "with value" << value; + } + else if((pos = filter.indexOf(QStringLiteral("!="))) > -1) + { + QString tag = filter.left(pos); + QString value = filter.mid(tag.length() + 2); + Validator* val = validators.value(tag, nullptr); + if(val) + { + val->m_acceptedValues << value; + } + else + { + val = new InequalityValidator(tag, value); + validators[tag] = val; + } + qCDebug(KNEWSTUFFCORE) << "Created InequalityValidator for tag" << tag << "with value" << value; + } + else + { + qCDebug(KNEWSTUFFCORE) << "Critical error attempting to create tag filter validators. The filter is defined as" << filter << "which is not in the accepted formats key==value or key!=value"; + } + } +}; + +TagsFilterChecker::TagsFilterChecker(const QStringList& tagFilter) + : d(new TagsFilterChecker::Private) +{ + for(const QString& filter : tagFilter) { + d->addValidator(filter); + } +} + +TagsFilterChecker::~TagsFilterChecker() +{ + delete d; +} + +bool TagsFilterChecker::filterAccepts(const QStringList& tags) +{ + // if any tag in the content matches any of the tag filters, skip this entry + qCDebug(KNEWSTUFFCORE) << "Checking tags list" << tags << "against validators with keys" << d->validators.keys(); + for(const QString &tag : tags) { + if(tag.length() == 0) { + // This happens when you do a split on an empty string (not an empty list, a list with one empty element... because reasons). + // Also handy for other things, i guess, though, so let's just catch it here. + continue; + } + QStringList current = tag.split(QStringLiteral("=")); + if(current.length() > 2) { + qCDebug(KNEWSTUFFCORE) << "Critical error attempting to filter tags. Entry has tag defined as" << tag << "which is not in the format \"key=value\" or \"key\"."; + return false; + } + else if(current.length() == 1) { + // If the tag is defined simply as a key, we give it the value "1", just to make our filtering work simpler + current << QStringLiteral("1"); + } + QMap::const_iterator i = d->validators.constBegin(); + while(i != d->validators.constEnd()) { + if(!i.value()->filterAccepts(current.at(0), current.at(1))) { + return false; + } + ++i; + } + } + // If we have arrived here, nothing has filtered the entry + // out (by being either incorrectly tagged or a filter rejecting + // it), and consequently it is an acceptable entry. + return true; +} + +} diff --git a/src/staticxml/staticxmlprovider.cpp b/src/staticxml/staticxmlprovider.cpp --- a/src/staticxml/staticxmlprovider.cpp +++ b/src/staticxml/staticxmlprovider.cpp @@ -27,6 +27,7 @@ #include #include +#include namespace KNSCore @@ -200,6 +201,8 @@ const Provider::Filter filter = loader->property("filter").value(); const QString searchTerm = loader->property("searchTerm").toString(); + TagsFilterChecker checker(mCurrentRequest.tagFilter); + TagsFilterChecker downloadschecker(mCurrentRequest.downloadTagFilter); element = doc.documentElement(); QDomElement n; for (n = element.firstChildElement(); !n.isNull(); n = n.nextSiblingElement()) { @@ -225,28 +228,41 @@ } cacheEntry = entry; } - mCachedEntries.append(entry); - - if (searchIncludesEntry(entry)) { - switch(filter) { - case Installed: - //This is dealth with in loadEntries separately - Q_UNREACHABLE(); - case Updates: - if (entry.status() == KNS3::Entry::Updateable) { - entries << entry; - } - break; - case ExactEntryId: - if (entry.uniqueId() == searchTerm) { + + bool filterAcceptsDownloads = entry.downloadCount() == 0 ? true : false; + foreach(const KNSCore::EntryInternal::DownloadLinkInformation& dli, entry.downloadLinkInformationList()) { + filterAcceptsDownloads = downloadschecker.filterAccepts(dli.tags); + if(filterAcceptsDownloads) { + break; + } + } + if(filterAcceptsDownloads && checker.filterAccepts(entry.tags())) { + mCachedEntries.append(entry); + + if (searchIncludesEntry(entry)) { + switch(filter) { + case Installed: + //This is dealth with in loadEntries separately + Q_UNREACHABLE(); + case Updates: + if (entry.status() == KNS3::Entry::Updateable) { + entries << entry; + } + break; + case ExactEntryId: + if (entry.uniqueId() == searchTerm) { + entries << entry; + } + break; + case None: entries << entry; - } - break; - case None: - entries << entry; - break; + break; + } } } + else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << entry.name() << "on entry filter" << mCurrentRequest.tagFilter << "and download filter" << mCurrentRequest.downloadTagFilter; + } } emit loadingFinished(mCurrentRequest, entries); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,25 +1,27 @@ include(ECMMarkAsTest) -find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets) # Widgets for KMoreTools +find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets Gui Quick) # Widgets for KMoreTools and Qick for the interactive KNS test + +configure_file(khotnewstuff_test.knsrc.in khotnewstuff_test.knsrc @ONLY) macro(knewstuff_executable_tests) foreach(_testname ${ARGN}) - add_executable(${_testname} ${_testname}.cpp ../src/knewstuff_debug.cpp) - target_link_libraries(${_testname} KF5::NewStuffCore KF5::NewStuff KF5::I18n Qt5::Xml Qt5::Test) + add_executable(${_testname} ${_testname}.cpp ../src/knewstuff_debug.cpp ../src/core/knewstuffcore_debug.cpp ../src/core/tagsfilterchecker.cpp ../src/staticxml/staticxmlprovider.cpp) + target_link_libraries(${_testname} KF5::NewStuffCore KF5::NewStuff KF5::I18n Qt5::Xml Qt5::Test Qt5::Quick Qt5::Gui) target_compile_definitions(${_testname} PRIVATE - KNSSRCDIR="\\"${CMAKE_CURRENT_SOURCE_DIR}/\\"" - KNSBUILDDIR="\\"${CMAKE_CURRENT_BINARY_DIR}\\"") + KNSSRCDIR="${CMAKE_CURRENT_SOURCE_DIR}/" + KNSBUILDDIR="${CMAKE_CURRENT_BINARY_DIR}") endforeach() endmacro() knewstuff_executable_tests( khotnewstuff khotnewstuff_upload + khotnewstuff_test ) # FIXME: port to new API #knewstuff_executable_tests( -# knewstuff2_test # knewstuff2_download # knewstuff2_standard # knewstuff2_cache diff --git a/tests/khotnewstuff_test-ui/main.qml b/tests/khotnewstuff_test-ui/main.qml new file mode 100644 --- /dev/null +++ b/tests/khotnewstuff_test-ui/main.qml @@ -0,0 +1,67 @@ +import QtQuick 2.7 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root; + + globalDrawer: Kirigami.GlobalDrawer { + title: "KNewStuff Test" + titleIcon: "applications-development" + drawerOpen: true; + modal: false; + + actions: [ + Kirigami.Action { + text: "Run Engine test" + onTriggered: testObject.engineTest(); + iconName: "run-build" + }, + Kirigami.Action { + text: "Test entry download as well" + onTriggered: testObject.testAll = !testObject.testAll + iconName: typeof(testObject) !== "undefined" ? (testObject.testAll ? "checkmark" : "") : "" + }, + Kirigami.Action {}, + Kirigami.Action { + text: "Run Entry test" + onTriggered: testObject.entryTest(); + iconName: "run-build" + }, + Kirigami.Action { + text: "Run Provider test" + onTriggered: testObject.providerTest(); + iconName: "run-build" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Welcome" + ListView { + id: messageView; + model: testObject.messages(); + onCountChanged: { + messageView.currentIndex = messageView.count - 1; + } + delegate: Kirigami.BasicListItem { + id: listItem + + reserveSpaceForIcon: true + label: model.display + icon: model.whatsThis + + Accessible.role: Accessible.MenuItem + onClicked: {} + highlighted: focus && ListView.isCurrentItem + } + } + } + } +} diff --git a/tests/knewstuff2_test.h b/tests/khotnewstuff_test.h rename from tests/knewstuff2_test.h rename to tests/khotnewstuff_test.h --- a/tests/knewstuff2_test.h +++ b/tests/khotnewstuff_test.h @@ -19,39 +19,46 @@ #ifndef KNEWSTUFF2_TEST_TEST_H #define KNEWSTUFF2_TEST_TEST_H -#include -#include +#include +#include #include +#include -namespace KNS +namespace KNSCore { -class CoreEngine; +class Engine; } class KNewStuff2Test : public QObject { Q_OBJECT + Q_PROPERTY(bool testAll READ testAll WRITE setTestAll NOTIFY testAllChanged) public: - KNewStuff2Test(); + KNewStuff2Test(const QString& configFile); + void setTestAll(bool testall); - void entryTest(); - void providerTest(); - void engineTest(); + bool testAll() const; + Q_SIGNAL void testAllChanged(); + + Q_INVOKABLE void entryTest(); + Q_INVOKABLE void providerTest(); + Q_INVOKABLE void engineTest(); + + Q_INVOKABLE QObject* messages(); + void addMessage(const QString& message, const QString& iconName = QStringLiteral()); + public Q_SLOTS: - void slotProviderLoaded(KNS::Provider *provider); - void slotProvidersFailed(); - void slotEntryLoaded(KNS::Entry *entry, const KNS::Feed *feed, const KNS::Provider *provider); - void slotEntriesFailed(); - void slotEntriesFinished(); - void slotPayloadLoaded(QUrl payload); - void slotPayloadFailed(); + void slotProvidersLoaded(); + void slotEngineError(const QString& error); + void slotEntriesLoaded(const KNSCore::EntryInternal::List &entries); void slotInstallationFinished(); - void slotInstallationFailed(); + private: - void quitTest(); - KNS::CoreEngine *m_engine; + KNSCore::Engine *m_engine; bool m_testall; + QString m_configFile; + QStandardItemModel* m_messages; }; #endif diff --git a/tests/khotnewstuff_test.cpp b/tests/khotnewstuff_test.cpp new file mode 100644 --- /dev/null +++ b/tests/khotnewstuff_test.cpp @@ -0,0 +1,245 @@ +/* + This file is part of KNewStuff2. + Copyright (c) 2007 Josef Spillner + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "khotnewstuff_test.h" + +#include +#include +#include +#include "../src/staticxml/staticxmlprovider_p.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include // for exit() +#include // for stdout + +KNewStuff2Test::KNewStuff2Test(const QString& configFile) + : QObject() +{ + m_messages = new QStandardItemModel(this); + m_configFile = configFile; + m_engine = NULL; + m_testall = false; +} + +void KNewStuff2Test::setTestAll(bool testall) +{ + m_testall = testall; + emit testAllChanged(); +} + +bool KNewStuff2Test::testAll() const +{ + return m_testall; +} + +void KNewStuff2Test::entryTest() +{ + addMessage(QString::fromLocal8Bit("-- test kns2 entry class"), QStringLiteral("msg_info")); + + QFile f(QString::fromLatin1("%1/testdata/entry.xml").arg(QStringLiteral(KNSSRCDIR))); + if (!f.open(QIODevice::ReadOnly)) { + addMessage(QString::fromLocal8Bit("Error loading entry file: %1").arg(f.fileName()), QStringLiteral("msg_error")); + return; + } + + QXmlStreamReader reader(&f); + KNSCore::EntryInternal e; + reader.readNextStartElement(); // Skip the first (the external OCS container) + bool xmlResult = reader.readNextStartElement() && e.setEntryXML(reader); + e.setProviderId(QStringLiteral("test-provider")); + + f.close(); + if(!xmlResult) { + addMessage(QString::fromLocal8Bit("Error parsing entry file."), QStringLiteral("msg_error")); + return; + } + + addMessage(QString::fromLocal8Bit("-- entry->xml test result: %1").arg(e.isValid()), e.isValid() ? QStringLiteral("msg_info") : QStringLiteral("msg_error")); + if (!e.isValid()) { + return; + } else { + QTextStream out(stdout); + out << e.entryXML(); + } +} + +void KNewStuff2Test::providerTest() +{ + addMessage(QString::fromLocal8Bit("-- test kns2 provider class"), QStringLiteral("msg_info")); + + QDomDocument doc; + QFile f(QString::fromLatin1("%1/testdata/provider.xml").arg(QStringLiteral(KNSSRCDIR))); + if (!f.open(QIODevice::ReadOnly)) { + addMessage(QString::fromLocal8Bit("Error loading provider file: %1").arg(f.fileName()), QStringLiteral("msg_error")); + return; + } + if (!doc.setContent(&f)) { + addMessage(QString::fromLocal8Bit("Error parsing provider file: %1").arg(f.fileName()), QStringLiteral("msg_error")); + f.close(); + return; + } + f.close(); + + KNSCore::StaticXmlProvider p; + p.setProviderXML(doc.documentElement()); + + addMessage(QString::fromLocal8Bit("-- xml->provider test result: %1").arg(p.isInitialized()), p.isInitialized()? QStringLiteral("msg_info") : QStringLiteral("msg_error")); + +// QDomElement pxml = p.providerXML(); + + // qDebug() << "-- provider->xml test result: " << ph.isValid(); + +// if (!p.isValid()) { +// quitTest(); +// } else { +// QTextStream out(stdout); +// out << pxml; +// } +} + +void KNewStuff2Test::engineTest() +{ + addMessage(QString::fromLocal8Bit("-- test kns2 engine"), QStringLiteral("msg_info")); + + m_engine = new KNSCore::Engine(this); + + connect(m_engine, + &KNSCore::Engine::signalError, + this, &KNewStuff2Test::slotEngineError); + connect(m_engine, + &KNSCore::Engine::signalProvidersLoaded, + this, &KNewStuff2Test::slotProvidersLoaded); + connect(m_engine, + &KNSCore::Engine::signalEntriesLoaded, + this, &KNewStuff2Test::slotEntriesLoaded); + connect(m_engine, + &KNSCore::Engine::signalEntryChanged, + this, &KNewStuff2Test::slotInstallationFinished); + + bool ret = m_engine->init(m_configFile); + + addMessage(QString::fromLocal8Bit("-- engine test result: %1").arg(ret), ret ? QStringLiteral("msg_info") : QStringLiteral("msg_error")); + + if (!ret) { + addMessage(QString::fromLocal8Bit("ACHTUNG: you probably need to 'make install' the knsrc file first. Although this is not required anymore, so something went really wrong."), QStringLiteral("msg_warning")); + } + addMessage(QString::fromLocal8Bit("-- initial engine test completed"), QStringLiteral("msg_info")); +} + +void KNewStuff2Test::slotProvidersLoaded() +{ + addMessage(QString::fromLocal8Bit("SLOT: slotProvidersLoaded"), QStringLiteral("msg_info")); +// qDebug() << "-- provider: " << provider->name().representation(); + + m_engine->reloadEntries(); +} + +void KNewStuff2Test::slotEntriesLoaded(const KNSCore::EntryInternal::List &entries) +{ + addMessage(QString::fromLocal8Bit("SLOT: slotEntriesLoaded. Number of entries %1").arg(entries.count()), QStringLiteral("msg_info")); + + if (m_testall) { + addMessage(QString::fromLocal8Bit("-- now, download the entries' previews and payload files"), QStringLiteral("msg_info")); + + Q_FOREACH(const KNSCore::EntryInternal& entry, entries) { + addMessage(QString::fromLocal8Bit("-- entry: %1").arg(entry.name()), QStringLiteral("msg_info")); + if (!entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1).isEmpty()) { + m_engine->loadPreview(entry, KNSCore::EntryInternal::PreviewSmall1); + } + if (!entry.payload().isEmpty()) { + m_engine->install(entry); + } + } + } +} + +void KNewStuff2Test::slotInstallationFinished() +{ + addMessage(QString::fromLocal8Bit("SLOT: slotInstallationFinished")); +} + +void KNewStuff2Test::slotEngineError(const QString& error) +{ + addMessage(QString::fromLocal8Bit("SLOT: slotEngineError %1").arg(error), QStringLiteral("msg_error")); +} + +QObject * KNewStuff2Test::messages() +{ + return m_messages; +} + +void KNewStuff2Test::addMessage(const QString& message, const QString& iconName) +{ + QStandardItem* item = new QStandardItem(message); + item->setData(iconName, Qt::WhatsThisRole); + m_messages->appendRow(item); +} + +KNewStuff2Test *test = nullptr; +static const QtMessageHandler QT_DEFAULT_MESSAGE_HANDLER = qInstallMessageHandler(0); +void debugOutputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if(test) { + test->addMessage(msg, QStringLiteral("msg_info")); + } + // Call the default handler. + (*QT_DEFAULT_MESSAGE_HANDLER)(type, context, msg); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + QCommandLineParser* parser = new QCommandLineParser; + parser->addHelpOption(); + parser->addOption(QCommandLineOption(QStringLiteral("testall"), i18n("Downloads all previews and payloads"))); + parser->addPositionalArgument(QStringLiteral("knsrcfile"), i18n("The KNSRC file you want to use for testing. If none is passed, we will use khotnewstuff_test.knsrc, which must be installed.")); + parser->process(app); + + if(parser->positionalArguments().count() > 0) { + test = new KNewStuff2Test(parser->positionalArguments().first()); + } + else { + test = new KNewStuff2Test(QString::fromLatin1("%1/khotnewstuff_test.knsrc").arg(QStringLiteral(KNSBUILDDIR))); + } + test->setTestAll(parser->isSet(QStringLiteral("testall"))); + + QQmlApplicationEngine* appengine = new QQmlApplicationEngine(); + appengine->rootContext()->setContextProperty(QStringLiteral("testObject"), test); + appengine->load(QUrl::fromLocalFile(QString::fromLatin1("%1/khotnewstuff_test-ui/main.qml").arg(QStringLiteral(KNSSRCDIR)))); + + // Don't really want to add messages until the tester + // begins to actually request stuff in the UI, + // so let's just install it here + qInstallMessageHandler(debugOutputHandler); + + return app.exec(); +} diff --git a/tests/knewstuff2_test.knsrc b/tests/khotnewstuff_test.knsrc.in rename from tests/knewstuff2_test.knsrc rename to tests/khotnewstuff_test.knsrc.in --- a/tests/knewstuff2_test.knsrc +++ b/tests/khotnewstuff_test.knsrc.in @@ -1,6 +1,7 @@ [KNewStuff2] #ProvidersUrl=http://edu.kde.org/kalzium/molecules.xml -ProvidersUrl=http://new.kstuff.org/provider-kalzium.xml +#ProvidersUrl=http://new.kstuff.org/provider-kalzium.xml +ProvidersUrl=file://@CMAKE_CURRENT_SOURCE_DIR@/testdata/provider.xml LocalRegistryDir=/tmp/knewstuff2.metafiles TargetDir=knewstuff2_test diff --git a/tests/knewstuff2_test.cpp b/tests/knewstuff2_test.cpp deleted file mode 100644 --- a/tests/knewstuff2_test.cpp +++ /dev/null @@ -1,278 +0,0 @@ -/* - This file is part of KNewStuff2. - Copyright (c) 2007 Josef Spillner - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library. If not, see . -*/ - -#include "knewstuff2_test.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include // for exit() -#include // for stdout - -KNewStuff2Test::KNewStuff2Test() - : QObject() -{ - m_engine = NULL; - m_testall = false; -} - -void KNewStuff2Test::setTestAll(bool testall) -{ - m_testall = testall; -} - -void KNewStuff2Test::entryTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 entry class"; - - QDomDocument doc; - QFile f(QString("%1/testdata/entry.xml").arg(KNSSRCDIR)); - if (!f.open(QIODevice::ReadOnly)) { - // qCDebug(KNEWSTUFF) << "Error loading entry file."; - quitTest(); - } - if (!doc.setContent(&f)) { - // qCDebug(KNEWSTUFF) << "Error parsing entry file."; - f.close(); - quitTest(); - } - f.close(); - - KNS::EntryHandler eh(doc.documentElement()); - KNS::Entry e = eh.entry(); - - // qCDebug(KNEWSTUFF) << "-- xml->entry test result: " << eh.isValid(); - - KNS::EntryHandler eh2(e); - QDomElement exml = eh2.entryXML(); - - // qCDebug(KNEWSTUFF) << "-- entry->xml test result: " << eh.isValid(); - - if (!eh.isValid()) { - quitTest(); - } else { - QTextStream out(stdout); - out << exml; - } -} - -void KNewStuff2Test::providerTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 provider class"; - - QDomDocument doc; - QFile f(QString("%1/testdata/provider.xml").arg(KNSSRCDIR)); - if (!f.open(QIODevice::ReadOnly)) { - // qCDebug(KNEWSTUFF) << "Error loading provider file."; - quitTest(); - } - if (!doc.setContent(&f)) { - // qCDebug(KNEWSTUFF) << "Error parsing provider file."; - f.close(); - quitTest(); - } - f.close(); - - KNS::ProviderHandler ph(doc.documentElement()); - KNS::Provider p = ph.provider(); - - // qCDebug(KNEWSTUFF) << "-- xml->provider test result: " << ph.isValid(); - - KNS::ProviderHandler ph2(p); - QDomElement pxml = ph2.providerXML(); - - // qCDebug(KNEWSTUFF) << "-- provider->xml test result: " << ph.isValid(); - - if (!ph.isValid()) { - quitTest(); - } else { - QTextStream out(stdout); - out << pxml; - } -} - -void KNewStuff2Test::engineTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 engine"; - - m_engine = new KNS::CoreEngine(NULL); - bool ret = m_engine->init("knewstuff2_test.knsrc"); - - // qCDebug(KNEWSTUFF) << "-- engine test result: " << ret; - - if (ret) { - connect(m_engine, - &KNS::CoreEngine::signalProviderLoaded, - this, &KNewStuff2Test::slotProviderLoaded); - connect(m_engine, - &KNS::CoreEngine::signalProvidersFailed, - this, &KNewStuff2Test::slotProvidersFailed); - connect(m_engine, - &KNS::CoreEngine::signalEntryLoaded, - this, &KNewStuff2Test::slotEntryLoaded); - connect(m_engine, - &KNS::CoreEngine::signalEntriesFinished, - this, &KNewStuff2Test::slotEntriesFinished); - connect(m_engine, - &KNS::CoreEngine::signalEntriesFailed, - this, &KNewStuff2Test::slotEntriesFailed); - connect(m_engine, - &KNS::CoreEngine::signalPayloadLoaded, - this, &KNewStuff2Test::slotPayloadLoaded); - connect(m_engine, - &KNS::CoreEngine::signalPayloadFailed, - this, &KNewStuff2Test::slotPayloadFailed); - connect(m_engine, - &KNS::CoreEngine::signalInstallationFinished, - this, &KNewStuff2Test::slotInstallationFinished); - connect(m_engine, - &KNS::CoreEngine::signalInstallationFailed, - this, &KNewStuff2Test::slotInstallationFailed); - - m_engine->start(); - } else { - qWarning() << "ACHTUNG: you probably need to 'make install' the knsrc file first."; - qWarning() << "Although this is not required anymore, so something went really wrong."; - quitTest(); - } -} - -void KNewStuff2Test::slotProviderLoaded(KNS::Provider *provider) -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotProviderLoaded"; - // qCDebug(KNEWSTUFF) << "-- provider: " << provider->name().representation(); - - m_engine->loadEntries(provider); -} - -void KNewStuff2Test::slotEntryLoaded(KNS::Entry *entry, const KNS::Feed *feed, const KNS::Provider *provider) -{ - Q_UNUSED(feed); - Q_UNUSED(provider); - - // qCDebug(KNEWSTUFF) << "SLOT: slotEntryLoaded"; - // qCDebug(KNEWSTUFF) << "-- entry: " << entry->name().representation(); - - if (m_testall) { - // qCDebug(KNEWSTUFF) << "-- now, download the entry's preview and payload file"; - - if (!entry->preview().isEmpty()) { - m_engine->downloadPreview(entry); - } - if (!entry->payload().isEmpty()) { - m_engine->downloadPayload(entry); - } - } -} - -void KNewStuff2Test::slotEntriesFinished() -{ - // Wait for installation if requested - if (!m_testall) { - quitTest(); - } -} - -void KNewStuff2Test::slotPayloadLoaded(QUrl payload) -{ - // qCDebug(KNEWSTUFF) << "-- entry downloaded successfully"; - // qCDebug(KNEWSTUFF) << "-- downloaded to " << payload.prettyUrl(); - - // qCDebug(KNEWSTUFF) << "-- run installation"; - - bool ret = m_engine->install(payload.path()); - - // qCDebug(KNEWSTUFF) << "-- installation result: " << ret; - // qCDebug(KNEWSTUFF) << "-- now, wait for installation to finish..."; -} - -void KNewStuff2Test::slotPayloadFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotPayloadFailed"; - quitTest(); -} - -void KNewStuff2Test::slotProvidersFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotProvidersFailed"; - quitTest(); -} - -void KNewStuff2Test::slotEntriesFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotEntriesFailed"; - quitTest(); -} - -void KNewStuff2Test::slotInstallationFinished() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotInstallationFinished"; - // qCDebug(KNEWSTUFF) << "-- OK, finish test"; - quitTest(); -} - -void KNewStuff2Test::slotInstallationFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotInstallationFailed"; - quitTest(); -} - -void KNewStuff2Test::quitTest() -{ - // qCDebug(KNEWSTUFF) << "-- quitting now..."; - if (1 == 0) { - // this would be the soft way out... - delete m_engine; - deleteLater(); - qApp->quit(); - } else { - exit(1); - } -} - -int main(int argc, char **argv) -{ - //options.add("testall", qi18n("Downloads all previews and payloads")); - - QApplication app(argc, argv); - - // Take source directory into account - // qCDebug(KNEWSTUFF) << "-- adding source directory " << KNSSRCDIR; - // qCDebug(KNEWSTUFF) << "-- adding build directory " << KNSBUILDDIR; - KGlobal::dirs()->addResourceDir("config", KNSSRCDIR); - KGlobal::dirs()->addResourceDir("config", KNSBUILDDIR); - - KNewStuff2Test *test = new KNewStuff2Test(); - if (app.arguments().contains("--testall")) { - test->setTestAll(true); - test->entryTest(); - test->providerTest(); - } - test->engineTest(); - - return app.exec(); -} - diff --git a/tests/testdata/entry.xml b/tests/testdata/entry.xml --- a/tests/testdata/entry.xml +++ b/tests/testdata/entry.xml @@ -1,14 +1,55 @@ - - Some Name - Anonymous Guy - GPL - 1.0 - 2005-06-17 - This is what it is all about. - http://some.http.server/preview.png - http://some.http.server/coolstuff.tar.gz - 10 - 0 - - + + + Entry 1 (ghns excluded) + Anonymous Guy + GPL + 1.0 + 2005-06-17 + This is what it is all about. + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + ghns_exclude=1 + + + Entry 2 (ghns included) + Anonymous Guy + GPL + 2.1git2 + 2018-06-05 + A short description in English (not ghns excluded). + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + + + + Entry 3 (ghns excluded) + Anonymous Guy + GPL + 1.0 + 2005-06-17 + This is what it is all about. + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + ghns_exclude=1 + + + Entry 4 (ghns included) + Anonymous Guy + GPL + 2.1git2 + 2018-06-05 + A short description in English (not ghns excluded). + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + + + diff --git a/tests/testdata/provider.xml b/tests/testdata/provider.xml --- a/tests/testdata/provider.xml +++ b/tests/testdata/provider.xml @@ -1,12 +1,22 @@ - + + Some cool stuff Viele neue Dinge +