diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,13 @@ include(CheckFunctionExists) include(FeatureSummary) +kde_enable_exceptions() + find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test DBus Concurrent) -find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n CoreAddons Service ConfigWidgets JobWidgets KIO Crash Completion XmlRpcClient WidgetsAddons Wallet Notifications IdleTime) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n CoreAddons Service + ConfigWidgets JobWidgets KIO Crash Completion WidgetsAddons Wallet + Notifications IdleTime) if(APPLE) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS WindowSystem) endif() diff --git a/drkonqi.categories b/drkonqi.categories --- a/drkonqi.categories +++ b/drkonqi.categories @@ -1,2 +1,3 @@ org.kde.drkonqi drkonqi IDENTIFIER [DRKONQI_LOG] org.kde.drkonqi.parser drkonqi parser IDENTIFIER [DRKONQI_PARSER_LOG] +org.kde.drkonqi.bugzilla drkonqi bugzilla IDENTIFIER [BUGZILLA_LOG] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,7 @@ configure_file (config-drkonqi.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-drkonqi.h ) +add_subdirectory( bugzillaintegration/libbugzilla ) add_subdirectory( data ) add_subdirectory( parser ) if ( WIN32 ) @@ -62,7 +63,7 @@ bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp bugzillaintegration/reportinterface.cpp bugzillaintegration/productmapping.cpp - bugzillaintegration/parsebugbacktraces.cpp # Requires kxmlrpcclient + bugzillaintegration/parsebugbacktraces.cpp bugzillaintegration/duplicatefinderjob.cpp ) ki18n_wrap_ui(drkonqi_SRCS @@ -81,7 +82,6 @@ ecm_qt_declare_logging_category(drkonqi_SRCS HEADER drkonqi_debug.h IDENTIFIER DRKONQI_LOG CATEGORY_NAME org.kde.drkonqi) - add_executable(drkonqi ${drkonqi_SRCS}) ecm_mark_nongui_executable(drkonqi) @@ -96,14 +96,14 @@ KF5::Completion Qt5::DBus - KF5::XmlRpcClient KF5::WidgetsAddons KF5::Wallet KF5::Notifications # for status notifier KF5::IdleTime # hide status notifier only if user saw it drkonqi_backtrace_parser + qbugzilla ) if (${Qt5X11Extras_FOUND}) target_link_libraries(drkonqi @@ -124,7 +124,6 @@ configure_file(org.kde.drkonqi.desktop.cmake ${CMAKE_BINARY_DIR}/src/org.kde.drkonqi.desktop) install(PROGRAMS ${CMAKE_BINARY_DIR}/src/org.kde.drkonqi.desktop DESTINATION ${KDE_INSTALL_APPDIR}) - -# Only go into tests once we have a drkonqi target so the tests can reference -# it. add_subdirectory( tests ) + +add_subdirectory(bugzillaintegration/libbugzilla/autotests) diff --git a/src/bugzillaintegration/bugzillalib.h b/src/bugzillaintegration/bugzillalib.h --- a/src/bugzillaintegration/bugzillalib.h +++ b/src/bugzillaintegration/bugzillalib.h @@ -2,6 +2,7 @@ * bugzillalib.h * Copyright 2009, 2011 Dario Andres Rodriguez * Copyright 2012 George Kiagiadakis +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -22,335 +23,15 @@ #define BUGZILLALIB__H #include -#include -#include -#include +#include -#include +#include "libbugzilla/clients/bugclient.h" +#include "libbugzilla/clients/productclient.h" +#include "libbugzilla/clients/attachmentclient.h" -namespace KIO { class Job; } +namespace KIO { class KJob; -class QString; -class QByteArray; - -//Typedefs for Bug Report Listing -typedef QMap BugMap; //Report basic fields map -typedef QList BugMapList; //List of reports - -//Main bug report data, full fields + comments -class BugReport -{ -public: - enum Status { - UnknownStatus, - Unconfirmed, - New, - Assigned, - Reopened, - Resolved, - NeedsInfo, - Verified, - Closed - }; - - enum Resolution { - UnknownResolution, - NotResolved, - Fixed, - Invalid, - WontFix, - Later, - Remind, - Duplicate, - WorksForMe, - Moved, - Upstream, - Downstream, - WaitingForInfo, - Backtrace, - Unmaintained - }; - - BugReport() - : m_isValid(false), - m_status(UnknownStatus), - m_resolution(UnknownResolution) - {} - - void setBugNumber(const QString & value) { - setData(QStringLiteral("bug_id"), value); - } - QString bugNumber() const { - return getData(QStringLiteral("bug_id")); - } - int bugNumberAsInt() const { - return getData(QStringLiteral("bug_id")).toInt(); - } - - void setShortDescription(const QString & value) { - setData(QStringLiteral("short_desc"), value); - } - QString shortDescription() const { - return getData(QStringLiteral("short_desc")); - } - - void setProduct(const QString & value) { - setData(QStringLiteral("product"), value); - } - QString product() const { - return getData(QStringLiteral("product")); - } - - void setComponent(const QString & value) { - setData(QStringLiteral("component"), value); - } - QString component() const { - return getData(QStringLiteral("component")); - } - - void setVersion(const QString & value) { - setData(QStringLiteral("version"), value); - } - QString version() const { - return getData(QStringLiteral("version")); - } - - void setOperatingSystem(const QString & value) { - setData(QStringLiteral("op_sys"), value); - } - QString operatingSystem() const { - return getData(QStringLiteral("op_sys")); - } - - void setPlatform(const QString & value) { - setData(QStringLiteral("rep_platform"), value); - } - QString platform() const { - return getData(QStringLiteral("rep_platform")); - } - - void setBugStatus(const QString &status); - QString bugStatus() const { - return getData(QStringLiteral("bug_status")); - } - - void setResolution(const QString &resolution); - QString resolution() const { - return getData(QStringLiteral("resolution")); - } - - Status statusValue() const { - return m_status; - } - - Resolution resolutionValue() const { - return m_resolution; - } - - void setPriority(const QString & value) { - setData(QStringLiteral("priority"), value); - } - QString priority() const { - return getData(QStringLiteral("priority")); - } - - void setBugSeverity(const QString & value) { - setData(QStringLiteral("bug_severity"), value); - } - QString bugSeverity() const { - return getData(QStringLiteral("bug_severity")); - } - - void setKeywords(const QStringList & keywords) { - setData(QStringLiteral("keywords"), keywords.join(QStringLiteral(","))); - } - QStringList keywords() const { - return getData(QStringLiteral("keywords")).split(QLatin1Char(',')); - } - - void setDescription(const QString & desc) { - m_commentList.insert(0, desc); - } - QString description() const { - return m_commentList.at(0); - } - - void setComments(const QStringList & comm) { - m_commentList.append(comm); - } - QStringList comments() const { - return m_commentList.mid(1); - } - - void setMarkedAsDuplicateOf(const QString & dupID) { - setData(QStringLiteral("dup_id"), dupID); - } - QString markedAsDuplicateOf() const { - return getData(QStringLiteral("dup_id")); - } - - void setVersionFixedIn(const QString & dupID) { - setData(QStringLiteral("cf_versionfixedin"), dupID); - } - QString versionFixedIn() const { - return getData(QStringLiteral("cf_versionfixedin")); - } - - void setValid(bool valid) { - m_isValid = valid; - } - bool isValid() const { - return m_isValid; - } - - /** - * @return true if the bug report is still open - * @note false does not mean, that the bug report is closed, - * as the status could be unknown - */ - bool isOpen() const { - return isOpen(m_status); - } - - static bool isOpen(Status status) { - return (status == Unconfirmed || status == New || status == Assigned || status == Reopened); - } - - /** - * @return true if the bug report is closed - * @note false does not mean, that the bug report is still open, - * as the status could be unknown - */ - bool isClosed() const { - return isClosed(m_status); - } - - static bool isClosed(Status status) { - return (status == Resolved || status == NeedsInfo || status == Verified || status == Closed); - } - - static Status parseStatus(const QString &text); - static Resolution parseResolution(const QString &text); - -private: - void setData(const QString & key, const QString & val) { - m_dataMap.insert(key, val); - } - QString getData(const QString & key) const { - return m_dataMap.value(key); - } - - bool m_isValid; - Status m_status; - Resolution m_resolution; - - BugMap m_dataMap; - QStringList m_commentList; -}; - -//XML parser that creates a BugReport object -class BugReportXMLParser -{ -public: - explicit BugReportXMLParser(const QByteArray &); - - BugReport parse(); - - bool isValid() const { - return m_valid; - } - -private: - QString getSimpleValue(const QString &); - - bool m_valid; - QDomDocument m_xml; -}; - -class BugListCSVParser -{ -public: - explicit BugListCSVParser(const QByteArray&); - - bool isValid() const { - return m_isValid; - } - - BugMapList parse(); - -private: - bool m_isValid; - QByteArray m_data; -}; - -class Component -{ -public: - Component(const QString& name, bool active): m_name(name), m_active(active) {} - - QString name() const { return m_name; } - bool active() const { return m_active; } - -private: - QString m_name; - bool m_active; -}; - -class Version -{ -public: - - Version(const QString& name, bool active): m_name(name), m_active(active) {} - - QString name() const { return m_name; } - bool active() const { return m_active; } - -private: - QString m_name; - bool m_active; -}; - - -class Product -{ -public: - - Product(const QString& name, bool active): m_name(name), m_active(active) {} - - bool isActive() const { return m_active; } - - void addComponent(const Component& component) { - m_allComponents.append(component.name()); - } - - void addVersion(const Version& version) { - m_allVersions.append(version.name()); - - if (version.active()) { - m_activeVersions.append(version.name()); - } else { - m_inactiveVersions.append(version.name()); - } - } - - QStringList components() const { return m_allComponents; } - - QStringList allVersions() const { return m_allVersions; } - QStringList activeVersions() const { return m_activeVersions; } - QStringList inactiveVersions() const { return m_inactiveVersions; } - -private: - - QString m_name; - bool m_active; - - QStringList m_allComponents; - - QStringList m_allVersions; - QStringList m_activeVersions; - QStringList m_inactiveVersions; - -}; +} class BugzillaManager : public QObject { @@ -362,83 +43,63 @@ explicit BugzillaManager(const QString &bugTrackerUrl, QObject *parent = nullptr); /* Login methods */ - void tryLogin(const QString&, const QString&); + void tryLogin(const QString &username, const QString &password); bool getLogged() const; QString getUsername() const; /* Bugzilla Action methods */ - void fetchBugReport(int, QObject * jobOwner = nullptr); - - void searchBugs(const QStringList & products, const QString & severity, - const QString & date_start, const QString & date_end , QString comment); - - void sendReport(const BugReport & report); - - void attachTextToReport(const QString & text, const QString & filename, - const QString & description, int bugId, const QString & comment); - + void fetchBugReport(int, QObject *jobOwner = nullptr); + void searchBugs(const QStringList &products, const QString &severity, const QString &comment, int offset); + void sendReport(const Bugzilla::NewBug &bug); + void attachTextToReport(const QString &text, const QString &filename, + const QString &description, int bugId, + const QString &comment); void addMeToCC(int bugId); - void fetchProductInfo(const QString &); /* Misc methods */ QString urlForBug(int bug_number) const; - void stopCurrentSearch(); - /* Codes for security methods used by Bugzilla in various versions. */ - enum SecurityMethod {UseCookies, UseTokens, UsePasswords}; - SecurityMethod securityMethod() { return m_security; } + void fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner); private Q_SLOTS: - /* Slots to handle KJob::finished */ - void fetchBugJobFinished(KJob*); - void searchBugsJobFinished(KJob*); - void fetchProductInfoFinished(const QVariantMap&); - void lookupVersion(); - void callMessage(const QList & result, const QVariant & id); - void callFault(int errorCode, const QString & errorString, const QVariant &id); - Q_SIGNALS: /* Bugzilla actions finished successfully */ - void loginFinished(bool); - void bugReportFetched(BugReport, QObject *); - void searchFinished(const BugMapList &); - void reportSent(int); - void attachToReportSent(int); - void addMeToCCFinished(int); - void productInfoFetched(Product); + void loginFinished(bool logged); + void bugReportFetched(Bugzilla::Bug::Ptr bug, QObject *jobOwner); + void commentsFetched(QList comments, QObject *jobOwner); + void searchFinished(const QList &bug); + void reportSent(int bugId); + void attachToReportSent(int bugId); + void addMeToCCFinished(int bugId); + void productInfoFetched(const Bugzilla::Product::Ptr &product); void bugzillaVersionFound(); /* Bugzilla actions had errors */ - void loginError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); - void bugReportError(const QString &, QObject *); - void searchError(const QString &); - void sendReportError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); + void loginError(const QString &errorMsg, const QString & xtendedErrorMsg = QString()); + void bugReportError(const QString &errorMsg, QObject *jobOwner); + void commentsError(const QString &errorMsg, QObject *jobOwner); + void searchError(const QString &errorMsg); + void sendReportError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); void sendReportErrorInvalidValues(); //To use default values - void attachToReportError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); - void addMeToCCError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); + void attachToReportError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); + void addMeToCCError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); void productInfoError(); private: - QString m_bugTrackerUrl; - QString m_username; - QString m_token; - QString m_password; - bool m_logged; - SecurityMethod m_security; + QString m_bugTrackerUrl; + QString m_username; + QString m_token; + QString m_password; + bool m_logged = false; - KIO::Job * m_searchJob = nullptr; - KXmlRpc::Client *m_xmlRpcClient = nullptr; + KJob *m_searchJob = nullptr; - enum SecurityStatus {SecurityDisabled, SecurityEnabled}; - void callBugzilla(const char* method, const char* id, - QMap& args, - SecurityStatus security); - void setFeaturesForVersion(const QString& version); + void setFeaturesForVersion(const QString &version); }; #endif diff --git a/src/bugzillaintegration/bugzillalib.cpp b/src/bugzillaintegration/bugzillalib.cpp --- a/src/bugzillaintegration/bugzillalib.cpp +++ b/src/bugzillaintegration/bugzillalib.cpp @@ -2,6 +2,7 @@ * bugzillalib.cpp * Copyright 2009, 2011 Dario Andres Rodriguez * Copyright 2012 George Kiagiadakis +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -20,60 +21,103 @@ #include "bugzillalib.h" -#include -#include -#include -#include +#include -#include -#include -#include -#include - -#include -#include +#include "libbugzilla/clients/commentclient.h" +#include "libbugzilla/connection.h" +#include "libbugzilla/bugzilla.h" #include "drkonqi_debug.h" -#define MAKE_BUGZILLA_VERSION(a,b,c) (((a) << 16) | ((b) << 8) | (c)) +static const char showBugUrl[] = "show_bug.cgi?id=%1"; -static const char columns[] = "bug_severity,priority,bug_status,product,short_desc,resolution"; +// Extra filter rigging. We don't want to leak secrets via qdebug, so install +// a message handler which does nothing more than replace secrets in debug +// messages with placeholders. +// This is used as a global static (since message handlers are meant to be +// static) and is slightly synchronizing across threads WRT the filter hash. +struct QMessageFilterContainer { + QMessageFilterContainer(); + void insert(const QString &needle, const QString &replace); + void clear(); -//Bugzilla URLs -static const char searchUrl[] = - "buglist.cgi?query_format=advanced&order=Importance&ctype=csv" - "&product=%1" - "&longdesc_type=allwordssubstr&longdesc=%2" - "&chfieldfrom=%3&chfieldto=%4&chfield=[Bug+creation]" - "&bug_severity=%5" - "&columnlist=%6"; -// short_desc, product, long_desc(possible backtraces lines), searchFrom, searchTo, severity, columnList -static const char showBugUrl[] = "show_bug.cgi?id=%1"; -static const char fetchBugUrl[] = "show_bug.cgi?id=%1&ctype=xml"; + QString filter(const QString &msg); + + // Message handler is called across threads. Syncronize for good meassure. + QReadWriteLock lock; + QtMessageHandler handler; + +private: + QHash filters; +}; + +Q_GLOBAL_STATIC(QMessageFilterContainer, s_messageFilter) + +QMessageFilterContainer::QMessageFilterContainer() +{ + handler = + qInstallMessageHandler([](QtMsgType type, + const QMessageLogContext &context, + const QString &msg) { + s_messageFilter->handler(type, context, s_messageFilter->filter(msg)); +}); +} + +void QMessageFilterContainer::insert(const QString &needle, const QString &replace) +{ + if (needle.isEmpty()) { + return; + } -static inline Component buildComponent(const QVariantMap& map); -static inline Version buildVersion(const QVariantMap& map); -static inline Product buildProduct(const QVariantMap& map); + QWriteLocker locker(&lock); + filters[needle] = replace; +} -//BEGIN BugzillaManager +QString QMessageFilterContainer::filter(const QString &msg) +{ + QReadLocker locker(&lock); + QString filteredMsg = msg; + for (auto it = filters.constBegin(); it != filters.constEnd(); ++it) { + filteredMsg.replace(it.key(), it.value()); + } + return filteredMsg; +} + +void QMessageFilterContainer::clear() +{ + QWriteLocker locker(&lock); + filters.clear(); +} BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent) : QObject(parent) , m_bugTrackerUrl(bugTrackerUrl) , m_logged(false) , m_searchJob(nullptr) { - m_xmlRpcClient = new KXmlRpc::Client(QUrl(m_bugTrackerUrl + QStringLiteral("xmlrpc.cgi")), this); - m_xmlRpcClient->setUserAgent(QStringLiteral("DrKonqi")); + Q_ASSERT(bugTrackerUrl.endsWith(QLatin1Char('/'))); + Bugzilla::setConnection(new Bugzilla::HTTPConnection(QUrl(m_bugTrackerUrl + QStringLiteral("rest")))); // Allow constructors for ReportInterface and assistant dialogs to finish. - // We do not want them to be racing the remote Bugzilla database. - QMetaObject::invokeMethod (this, &BugzillaManager::lookupVersion, Qt::QueuedConnection); + // Otherwise we may have a race on our hand if the lookup finishes before + // the constructors. + // I am not sure why this is so weirdly done TBH. Might deserve some looking + // into. + QMetaObject::invokeMethod(this, &BugzillaManager::lookupVersion, Qt::QueuedConnection); } -// BEGIN Checks of Bugzilla software versions. void BugzillaManager::lookupVersion() { - QMap args; - callBugzilla("Bugzilla.version", "version", args, SecurityDisabled); + KJob *job = Bugzilla::version(); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + QString version = Bugzilla::version(job); + setFeaturesForVersion(version); + emit bugzillaVersionFound(); + } catch (Bugzilla::Exception &e) { + // Version detection problems simply mean we'll not mark the version + // found and the UI will not allow reporting. + qCWarning(DRKONQI_LOG) << e.whatString(); + } + }); } void BugzillaManager::setFeaturesForVersion(const QString& version) @@ -101,72 +145,37 @@ if (digits.count() > nVersionParts) { qCWarning(DRKONQI_LOG) << QStringLiteral("Current Bugzilla version %1 has more than %2 parts. Check that this is not a problem.").arg(version).arg(nVersionParts); } - int currentVersion = MAKE_BUGZILLA_VERSION(digits.at(0).toUInt(), - digits.at(1).toUInt(), digits.at(2).toUInt()); - - // Set the code(s) for historical versions of Bugzilla - before any change. - m_security = UseCookies; // Used to have cookies for update-security. - - if (currentVersion >= MAKE_BUGZILLA_VERSION(4, 4, 3)) { - // Security method changes from cookies to tokens in Bugzilla 4.4.3. - // BUT, tokens fail when kio_http sends any cookies found in KCookieJar, - // so go directly to passwords-only security (supported since Bugzilla - // 3.6 and will be enforced in Bugzilla 4.5.x). - m_security = UsePasswords; - } - qCDebug(DRKONQI_LOG) << "VERSION" << version << "SECURITY" << m_security; + qCDebug(DRKONQI_LOG) << "VERSION" << version; } -// END Checks of Bugzilla software versions. -// BEGIN Generic remote-procedure (RPC) call to Bugzilla -void BugzillaManager::callBugzilla(const char* method, const char* id, - QMap& args, - SecurityStatus security) -{ - if (security == SecurityEnabled) { - switch (m_security) { - case UseTokens: - qCDebug(DRKONQI_LOG) << method << id << "using token"; - args.insert(QLatin1String("Bugzilla_token"), m_token); - break; - case UsePasswords: - qCDebug(DRKONQI_LOG) << method << id << "using username" << m_username; - args.insert(QLatin1String("Bugzilla_login"), m_username); - args.insert(QLatin1String("Bugzilla_password"), m_password); - break; - case UseCookies: - qCDebug(DRKONQI_LOG) << method << id << "using cookies"; - // Some KDE process other than Dr Konqi should provide cookies. - break; - } - } - - m_xmlRpcClient->call(QLatin1String(method), args, - this, SLOT(callMessage(QList,QVariant)), - this, SLOT(callFault(int,QString,QVariant)), - QLatin1String(id)); -} -// END Generic call to Bugzilla - -//BEGIN Login methods -void BugzillaManager::tryLogin(const QString& username, const QString& password) +void BugzillaManager::tryLogin(const QString &username, const QString &password) { m_username = username; - if (m_security == UsePasswords) { - m_password = password; - } + m_password = password; m_logged = false; - QMap args; - args.insert(QLatin1String("login"), username); - args.insert(QLatin1String("password"), password); - if (m_security == UseCookies) { - // Removed in Bugzilla 4.4.3 software, which no longer issues cookies. - args.insert(QLatin1String("remember"), false); - } - - callBugzilla("User.login", "login", args, SecurityDisabled); + s_messageFilter->clear(); + s_messageFilter->insert(password, QStringLiteral("PASSWORD")); + KJob *job = Bugzilla::login(username, password); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + auto details = Bugzilla::login(job); + m_token = details.token; + if (m_token.isEmpty()) { + throw Bugzilla::RuntimeException(QStringLiteral("Did not receive a token")); + } + s_messageFilter->insert(m_token, QStringLiteral("TOKEN")); + Bugzilla::connection().setToken(m_token); + m_logged = true; + emit loginFinished(true); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + // Version detection problems simply mean we'll not mark the version + // found and the UI will not allow reporting. + emit loginError(e.whatString()); + } + }); } bool BugzillaManager::getLogged() const @@ -178,114 +187,157 @@ { return m_username; } -//END Login methods -//BEGIN Bugzilla Action methods -void BugzillaManager::fetchBugReport(int bugnumber, QObject * jobOwner) +void BugzillaManager::fetchBugReport(int bugnumber, QObject *jobOwner) { - QUrl url(m_bugTrackerUrl + QString::fromLatin1(fetchBugUrl).arg(bugnumber)); + Bugzilla::BugSearch search; + search.id = bugnumber; - if (!jobOwner) { - jobOwner = this; - } - - KIO::Job * fetchBugJob = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); - fetchBugJob->setParent(jobOwner); - connect(fetchBugJob, &KIO::Job::finished, this, &BugzillaManager::fetchBugJobFinished); + Bugzilla::BugClient client; + auto job = m_searchJob = client.search(search); + connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { + try { + auto list = client.search(job); + if (list.size() != 1) { + throw Bugzilla::RuntimeException(QStringLiteral("Unexpected bug amount returned: %1").arg(list.size())); + } + auto bug = list.at(0); + m_searchJob = nullptr; + emit bugReportFetched(bug, jobOwner); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit bugReportError(e.whatString(), jobOwner); + } + }); } - -void BugzillaManager::searchBugs(const QStringList & products, - const QString & severity, const QString & date_start, - const QString & date_end, QString comment) +void BugzillaManager::fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner) { - QString product; - if (products.size() > 0) { - if (products.size() == 1) { - product = products.at(0); - } else { - Q_FOREACH(const QString & p, products) { - product += p + QStringLiteral("&product="); - } - product = product.mid(0,product.size()-9); + Bugzilla::CommentClient client; + auto job = client.getFromBug(bug->id()); + connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { + try { + auto comments = client.getFromBug(job); + emit commentsFetched(comments, jobOwner); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit commentsError(e.whatString(), jobOwner); } - } - - QString url = QString(m_bugTrackerUrl) + - QString::fromLatin1(searchUrl).arg(product, comment.replace(QLatin1Char(' ') , QLatin1Char('+')), date_start, - date_end, severity, QString::fromLatin1(columns)); + }); +} + +// TODO: This would kinda benefit from an actual pagination class, +// currently this implicitly relies on the caller to handle offests correctly. +// Fortunately we only have one caller so it makes no difference. +void BugzillaManager::searchBugs(const QStringList &products, + const QString &severity, + const QString &comment, + int offset) +{ + Bugzilla::BugSearch search; + search.products = products; + search.severity = severity; + search.longdesc = comment; + // Order descedingly by bug_id. This allows us to offset through the results + // from newest to oldest. + // The UI will later order our data anyway, so the order at which we receive + // the data is not important for the UI (outside the fact that we want + // to step through pages of data) + search.order << QStringLiteral("bug_id DESC"); + search.limit = 25; + search.offset = offset; stopCurrentSearch(); - m_searchJob = KIO::storedGet(QUrl(url) , KIO::Reload, KIO::HideProgressInfo); - connect(m_searchJob, &KIO::Job::finished, this, &BugzillaManager::searchBugsJobFinished); + Bugzilla::BugClient client; + auto job = m_searchJob = Bugzilla::BugClient().search(search); + connect(job, &KJob::finished, this, [this, &client](KJob *job) { + try { + auto list = client.search(job); + m_searchJob = nullptr; + emit searchFinished(list); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit searchError(e.whatString()); + } + }); } -void BugzillaManager::sendReport(const BugReport & report) +void BugzillaManager::sendReport(const Bugzilla::NewBug &bug) { - QMap args; - args.insert(QLatin1String("product"), report.product()); - args.insert(QLatin1String("component"), report.component()); - args.insert(QLatin1String("version"), report.version()); - args.insert(QLatin1String("summary"), report.shortDescription()); - args.insert(QLatin1String("description"), report.description()); - args.insert(QLatin1String("op_sys"), report.operatingSystem()); - args.insert(QLatin1String("platform"), report.platform()); - args.insert(QLatin1String("keywords"), report.keywords()); - args.insert(QLatin1String("priority"), report.priority()); - args.insert(QLatin1String("severity"), report.bugSeverity()); - - callBugzilla("Bug.create", "Bug.create", args, SecurityEnabled); + auto job = Bugzilla::BugClient().create(bug); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + int id = Bugzilla::BugClient().create(job); + Q_ASSERT(id > 0); + emit reportSent(id); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit sendReportError(e.whatString()); + } + }); } void BugzillaManager::attachTextToReport(const QString & text, const QString & filename, const QString & summary, int bugId, const QString & comment) { - QMap args; - args.insert(QLatin1String("ids"), QVariantList() << bugId); - args.insert(QLatin1String("file_name"), filename); - args.insert(QLatin1String("summary"), summary); - args.insert(QLatin1String("comment"), comment); - args.insert(QLatin1String("content_type"), QLatin1String("text/plain")); - - //data needs to be a QByteArray so that it is encoded in base64 (query.cpp:246) - args.insert(QLatin1String("data"), text.toUtf8()); - - callBugzilla("Bug.add_attachment", "Bug.add_attachment", args, - SecurityEnabled); + Bugzilla::NewAttachment attachment; + attachment.ids = QList { bugId }; + attachment.data = text; + attachment.file_name = filename; + attachment.summary = summary; + attachment.comment = comment; + attachment.content_type = QLatin1Literal("text/plain"); + + auto job = Bugzilla::AttachmentClient().createAttachment(bugId, attachment); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + QList ids = Bugzilla::AttachmentClient().createAttachment(job); + Q_ASSERT(ids.size() == 1); + emit attachToReportSent(ids.at(0)); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit attachToReportError(e.whatString()); + } + }); } void BugzillaManager::addMeToCC(int bugId) { - QMap args; - args.insert(QLatin1String("ids"), QVariantList() << bugId); - - QMap ccChanges; - ccChanges.insert(QLatin1String("add"), QVariantList() << m_username); - args.insert(QLatin1String("cc"), ccChanges); - - callBugzilla("Bug.update", "Bug.update.cc", args, SecurityEnabled); -} - -void BugzillaManager::fetchProductInfo(const QString & product) -{ - QMap args; - - args.insert(QStringLiteral("names"), (QStringList() << product) ) ; - - QStringList includeFields; - // currently we only need these informations - includeFields << QStringLiteral("name") << QStringLiteral("is_active") << QStringLiteral("components") << QStringLiteral("versions"); - - args.insert(QStringLiteral("include_fields"), includeFields) ; - - callBugzilla("Product.get", "Product.get.versions", args, SecurityDisabled); + Bugzilla::BugUpdate update; + Q_ASSERT(!m_username.isEmpty()); + update.cc->add << m_username; + + auto job = Bugzilla::BugClient().update(bugId, update); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + const auto bugId = Bugzilla::BugClient().update(job); + Q_ASSERT(bugId != 0); + emit addMeToCCFinished(bugId); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit addMeToCCError(e.whatString()); + } + }); +} + +void BugzillaManager::fetchProductInfo(const QString &product) +{ + auto job = Bugzilla::ProductClient().get(product); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + auto ptr = Bugzilla::ProductClient().get(job); + Q_ASSERT(ptr); + productInfoFetched(ptr); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + // This doesn't have a string because it is actually not used for + // anything... + emit productInfoError(); + } + }); } - -//END Bugzilla Action methods - -//BEGIN Misc methods QString BugzillaManager::urlForBug(int bug_number) const { return QString(m_bugTrackerUrl) + QString::fromLatin1(showBugUrl).arg(bug_number); @@ -299,387 +351,3 @@ m_searchJob = nullptr; } } -//END Misc methods - -//BEGIN Slots to handle KJob::finished - -void BugzillaManager::fetchBugJobFinished(KJob* job) -{ - if (!job->error()) { - KIO::StoredTransferJob * fetchBugJob = static_cast(job); - - BugReportXMLParser * parser = new BugReportXMLParser(fetchBugJob->data()); - BugReport report = parser->parse(); - - if (parser->isValid()) { - emit bugReportFetched(report, job->parent()); - } else { - emit bugReportError(i18nc("@info","Invalid report information (malformed data). This " - "could mean that the bug report does not exist, or the " - "bug tracking site is experiencing a problem."), job->parent()); - } - - delete parser; - } else { - emit bugReportError(job->errorString(), job->parent()); - } -} - -void BugzillaManager::searchBugsJobFinished(KJob * job) -{ - if (!job->error()) { - KIO::StoredTransferJob * searchBugsJob = static_cast(job); - - BugListCSVParser * parser = new BugListCSVParser(searchBugsJob->data()); - BugMapList list = parser->parse(); - - if (parser->isValid()) { - emit searchFinished(list); - } else { - emit searchError(i18nc("@info","Invalid bug list: corrupted data")); - } - - delete parser; - } else { - emit searchError(job->errorString()); - } - - m_searchJob = nullptr; -} - -static inline Component buildComponent(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - return Component(name, active); -} - -static inline Version buildVersion(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - return Version(name, active); -} - -static inline Product buildProduct(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - Product product(name, active); - - QVariantList components = map.value(QStringLiteral("components")).toList(); - foreach (const QVariant& c, components) { - Component component = buildComponent(c.toMap()); - product.addComponent(component); - - } - - QVariantList versions = map.value(QStringLiteral("versions")).toList(); - foreach (const QVariant& v, versions) { - Version version = buildVersion(v.toMap()); - product.addVersion(version); - } - - return product; -} - -void BugzillaManager::fetchProductInfoFinished(const QVariantMap & map) -{ - QList products; - - QVariantList plist = map.value(QStringLiteral("products")).toList(); - foreach (const QVariant& p, plist) { - Product product = buildProduct(p.toMap()); - products.append(product); - } - - if ( products.size() > 0 ) { - emit productInfoFetched(products.at(0)); - } else { - emit productInfoError(); - } -} - -//END Slots to handle KJob::finished - -void BugzillaManager::callMessage(const QList & result, const QVariant & id) -{ - qCDebug(DRKONQI_LOG) << id << result; - - if (id.toString() == QLatin1String("login")) { - if ((m_security == UseTokens) && (result.count() > 0)) { - QVariantMap map = result.at(0).toMap(); - m_token = map.value(QLatin1String("token")).toString(); - } - m_logged = true; - Q_EMIT loginFinished(true); - } else if (id.toString() == QLatin1String("Product.get.versions")) { - QVariantMap map = result.at(0).toMap(); - fetchProductInfoFinished(map); - } else if (id.toString() == QLatin1String("Bug.create")) { - QVariantMap map = result.at(0).toMap(); - int bug_id = map.value(QLatin1String("id")).toInt(); - Q_ASSERT(bug_id != 0); - Q_EMIT reportSent(bug_id); - } else if (id.toString() == QLatin1String("Bug.add_attachment")) { - QVariantMap map = result.at(0).toMap(); - if (map.contains(QLatin1String("attachments"))){ // for bugzilla 4.2 - map = map.value(QLatin1String("attachments")).toMap(); - map = map.constBegin()->toMap(); - const int attachment_id = map.value(QLatin1String("id")).toInt(); - Q_EMIT attachToReportSent(attachment_id); - } else if (map.contains(QLatin1String("ids"))) { // for bugzilla 4.4 - const int attachment_id = map.value(QLatin1String("ids")).toList().at(0).toInt(); - Q_EMIT attachToReportSent(attachment_id); - } - } else if (id.toString() == QLatin1String("Bug.update.cc")) { - QVariantMap map = result.at(0).toMap().value(QLatin1String("bugs")).toList().at(0).toMap(); - int bug_id = map.value(QLatin1String("id")).toInt(); - Q_ASSERT(bug_id != 0); - Q_EMIT addMeToCCFinished(bug_id); - } else if (id.toString() == QLatin1String("version")) { - QVariantMap map = result.at(0).toMap(); - QString bugzillaVersion = map.value(QLatin1String("version")).toString(); - setFeaturesForVersion(bugzillaVersion); - Q_EMIT bugzillaVersionFound(); - } -} - -void BugzillaManager::callFault(int errorCode, const QString & errorString, const QVariant & id) -{ - qCDebug(DRKONQI_LOG) << id << errorCode << errorString; - - QString genericError = i18nc("@info", "Received unexpected error code %1 from bugzilla. " - "Error message was: %2", errorCode, errorString); - - if (id.toString() == QLatin1String("login")) { - switch(errorCode) { - case 300: //invalid username or password - Q_EMIT loginFinished(false); //TODO replace with loginError - break; - default: - Q_EMIT loginError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.create")) { - switch (errorCode) { - case 51: //invalid object (one example is invalid platform value) - case 105: //invalid component - case 106: //invalid product - Q_EMIT sendReportErrorInvalidValues(); - break; - default: - Q_EMIT sendReportError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.add_attachment")) { - switch (errorCode) { - default: - Q_EMIT attachToReportError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.update.cc")) { - switch (errorCode) { - default: - Q_EMIT addMeToCCError(genericError); - break; - } - } -} - -//END BugzillaManager - -//BEGIN BugzillaCSVParser - -BugListCSVParser::BugListCSVParser(const QByteArray& data) -{ - m_data = data; - m_isValid = false; -} - -BugMapList BugListCSVParser::parse() -{ - BugMapList list; - - if (!m_data.isEmpty()) { - //Parse buglist CSV - QTextStream ts(&m_data); - QString headersLine = ts.readLine().remove(QLatin1Char('\"')) ; //Discard headers - QString expectedHeadersLine = QString::fromLatin1(columns); - - if (headersLine == (QStringLiteral("bug_id,") + expectedHeadersLine)) { - QStringList headers = expectedHeadersLine.split(QLatin1Char(','), QString::KeepEmptyParts); - int headersCount = headers.count(); - - while (!ts.atEnd()) { - BugMap bug; //bug report data map - - QString line = ts.readLine(); - - //Get bug_id (always at first column) - int bug_id_index = line.indexOf(QLatin1Char(',')); - QString bug_id = line.left(bug_id_index); - bug.insert(QStringLiteral("bug_id"), bug_id); - - line = line.mid(bug_id_index + 2); - - QStringList fields = line.split(QStringLiteral(",\"")); - - for (int i = 0; i < headersCount && i < fields.count(); i++) { - QString field = fields.at(i); - field = field.left(field.size() - 1) ; //Remove trailing " - bug.insert(headers.at(i), field); - } - - list.append(bug); - } - - m_isValid = true; - } - } - - return list; -} - -//END BugzillaCSVParser - -//BEGIN BugzillaXMLParser - -BugReportXMLParser::BugReportXMLParser(const QByteArray & data) -{ - m_valid = m_xml.setContent(data, true); -} - -BugReport BugReportXMLParser::parse() -{ - BugReport report; //creates an invalid and empty report object - - if (m_valid) { - //Check bug notfound - QDomNodeList bug_number = m_xml.elementsByTagName(QStringLiteral("bug")); - QDomNode d = bug_number.at(0); - QDomNamedNodeMap a = d.attributes(); - QDomNode d2 = a.namedItem(QStringLiteral("error")); - m_valid = d2.isNull(); - - if (m_valid) { - report.setValid(true); - - //Get basic fields - report.setBugNumber(getSimpleValue(QStringLiteral("bug_id"))); - report.setShortDescription(getSimpleValue(QStringLiteral("short_desc"))); - report.setProduct(getSimpleValue(QStringLiteral("product"))); - report.setComponent(getSimpleValue(QStringLiteral("component"))); - report.setVersion(getSimpleValue(QStringLiteral("version"))); - report.setOperatingSystem(getSimpleValue(QStringLiteral("op_sys"))); - report.setBugStatus(getSimpleValue(QStringLiteral("bug_status"))); - report.setResolution(getSimpleValue(QStringLiteral("resolution"))); - report.setPriority(getSimpleValue(QStringLiteral("priority"))); - report.setBugSeverity(getSimpleValue(QStringLiteral("bug_severity"))); - report.setMarkedAsDuplicateOf(getSimpleValue(QStringLiteral("dup_id"))); - report.setVersionFixedIn(getSimpleValue(QStringLiteral("cf_versionfixedin"))); - - //Parse full content + comments - QStringList m_commentList; - QDomNodeList comments = m_xml.elementsByTagName(QStringLiteral("long_desc")); - for (int i = 0; i < comments.count(); i++) { - QDomElement element = comments.at(i).firstChildElement(QStringLiteral("thetext")); - m_commentList << element.text(); - } - - report.setComments(m_commentList); - - } //isValid - } //isValid - - return report; -} - -QString BugReportXMLParser::getSimpleValue(const QString & name) //Extract an unique tag from XML -{ - QString ret; - - QDomNodeList bug_number = m_xml.elementsByTagName(name); - if (bug_number.count() == 1) { - QDomNode node = bug_number.at(0); - ret = node.toElement().text(); - } - return ret; -} - -//END BugzillaXMLParser - -void BugReport::setBugStatus(const QString &stat) -{ - setData(QStringLiteral("bug_status"), stat); - - m_status = parseStatus(stat); -} - -void BugReport::setResolution(const QString &res) -{ - setData(QStringLiteral("resolution"), res); - - m_resolution = parseResolution(res); -} - -BugReport::Status BugReport::parseStatus(const QString &stat) -{ - if (stat == QLatin1String("UNCONFIRMED")) { - return Unconfirmed; - } else if (stat == QLatin1String("CONFIRMED")) { - return New; - } else if (stat == QLatin1String("ASSIGNED")) { - return Assigned; - } else if (stat == QLatin1String("REOPENED")) { - return Reopened; - } else if (stat == QLatin1String("RESOLVED")) { - return Resolved; - } else if (stat == QLatin1String("NEEDSINFO")) { - return NeedsInfo; - } else if (stat == QLatin1String("VERIFIED")) { - return Verified; - } else if (stat == QLatin1String("CLOSED")) { - return Closed; - } else { - return UnknownStatus; - } -} - -BugReport::Resolution BugReport::parseResolution(const QString &res) -{ - if (res.isEmpty()) { - return NotResolved; - } else if (res == QLatin1String("FIXED")) { - return Fixed; - } else if (res == QLatin1String("INVALID")) { - return Invalid; - } else if (res == QLatin1String("WONTFIX")) { - return WontFix; - } else if (res == QLatin1String("LATER")) { - return Later; - } else if (res == QLatin1String("REMIND")) { - return Remind; - } else if (res == QLatin1String("DUPLICATE")) { - return Duplicate; - } else if (res == QLatin1String("WORKSFORME")) { - return WorksForMe; - } else if (res == QLatin1String("MOVED")) { - return Moved; - } else if (res == QLatin1String("UPSTREAM")) { - return Upstream; - } else if (res == QLatin1String("DOWNSTREAM")) { - return Downstream; - } else if (res == QLatin1String("WAITINGFORINFO")) { - return WaitingForInfo; - } else if (res == QLatin1String("BACKTRACE")) { - return Backtrace; - } else if (res == QLatin1String("UNMAINTAINED")) { - return Unmaintained; - } else { - return UnknownResolution; - } -} diff --git a/src/bugzillaintegration/duplicatefinderjob.h b/src/bugzillaintegration/duplicatefinderjob.h --- a/src/bugzillaintegration/duplicatefinderjob.h +++ b/src/bugzillaintegration/duplicatefinderjob.h @@ -1,6 +1,7 @@ /******************************************************************* * duplicatefinderjob.h * Copyright 2011 Matthias Fuchs +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -37,13 +38,6 @@ public: struct Result { - Result() - : duplicate(0), - parentDuplicate(0), - status(BugReport::UnknownStatus), - resolution(BugReport::UnknownResolution) - {} - /** * First duplicate that was found, it might be that * this one is a duplicate itself, though this is still @@ -54,21 +48,20 @@ * @note 0 means that there is no duplicate * @see parrentDuplicate */ - int duplicate; + int duplicate = 0; /** * This always points to the parent bug, i.e. * the bug that has no duplicates itself. * If this is 0 it means that there are no duplicates */ - int parentDuplicate; - - BugReport::Status status; + int parentDuplicate = 0; - BugReport::Resolution resolution; + Bugzilla::Bug::Status status = Bugzilla::Bug::Status::Unknown; + Bugzilla::Bug::Resolution resolution = Bugzilla::Bug::Resolution::Unknown; }; - DuplicateFinderJob(const QList &bugIds, BugzillaManager *manager, QObject *parent = nullptr); + DuplicateFinderJob(const QList &bugs, BugzillaManager *manager, QObject *parent = nullptr); ~DuplicateFinderJob() override; void start() override; @@ -80,16 +73,21 @@ Result result() const; private Q_SLOTS: - void slotBugReportFetched(const BugReport &bug, QObject *owner); - void slotBugReportError(const QString &message, QObject *owner); + void slotBugReportFetched(const Bugzilla::Bug::Ptr &bug, QObject *owner); + void slotCommentsFetched(const QList &comments, QObject *owner); + + void slotError(const QString &message, QObject *owner); private: void analyzeNextBug(); - void fetchBug(const QString &bugId); + void fetchBug(int bugId); private: BugzillaManager *m_manager = nullptr; - QList m_bugIds; Result m_result; + + Bugzilla::Bug::Ptr m_bug = nullptr; + + QList m_bugs; }; #endif diff --git a/src/bugzillaintegration/duplicatefinderjob.cpp b/src/bugzillaintegration/duplicatefinderjob.cpp --- a/src/bugzillaintegration/duplicatefinderjob.cpp +++ b/src/bugzillaintegration/duplicatefinderjob.cpp @@ -1,6 +1,7 @@ /******************************************************************* * duplicatefinderjob.cpp * Copyright 2011 Matthias Fuchs +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -27,14 +28,21 @@ #include "drkonqi.h" #include "parsebugbacktraces.h" -DuplicateFinderJob::DuplicateFinderJob(const QList &bugIds, BugzillaManager *manager, QObject *parent) +DuplicateFinderJob::DuplicateFinderJob(const QList &bugs, BugzillaManager *manager, QObject *parent) : KJob(parent), m_manager(manager), - m_bugIds(bugIds) + m_bugs(bugs) { - qCDebug(DRKONQI_LOG) << "Possible duplicates:" << m_bugIds; - connect(m_manager, &BugzillaManager::bugReportFetched, this, &DuplicateFinderJob::slotBugReportFetched); - connect(m_manager, &BugzillaManager::bugReportError, this, &DuplicateFinderJob::slotBugReportError); + qCDebug(DRKONQI_LOG) << "Possible duplicates:" << m_bugs.size(); + connect(m_manager, &BugzillaManager::bugReportFetched, + this, &DuplicateFinderJob::slotBugReportFetched); + connect(m_manager, &BugzillaManager::bugReportError, + this, &DuplicateFinderJob::slotError); + + connect(m_manager, &BugzillaManager::commentsFetched, + this, &DuplicateFinderJob::slotCommentsFetched); + connect(m_manager, &BugzillaManager::commentsError, + this, &DuplicateFinderJob::slotError); } DuplicateFinderJob::~DuplicateFinderJob() @@ -53,78 +61,100 @@ void DuplicateFinderJob::analyzeNextBug() { - if (m_bugIds.isEmpty()) { + if (m_bugs.isEmpty()) { emitResult(); return; } - const int bugId = m_bugIds.takeFirst(); - qCDebug(DRKONQI_LOG) << "Fetching:" << bugId; - m_manager->fetchBugReport(bugId, this); + m_bug = m_bugs.takeFirst(); + qCDebug(DRKONQI_LOG) << "Fetching:" << m_bug->id(); + m_manager->fetchComments(m_bug, this); } -void DuplicateFinderJob::fetchBug(const QString &bugId) +void DuplicateFinderJob::fetchBug(int bugId) { - bool ok; - const int num = bugId.toInt(&ok); - if (ok) { + if (bugId > 0) { qCDebug(DRKONQI_LOG) << "Fetching:" << bugId; - m_manager->fetchBugReport(num, this); + m_manager->fetchBugReport(bugId, this); } else { qCDebug(DRKONQI_LOG) << "Bug id not valid:" << bugId; analyzeNextBug(); } } -void DuplicateFinderJob::slotBugReportFetched(const BugReport &bug, QObject *owner) +void DuplicateFinderJob::slotBugReportFetched(const Bugzilla::Bug::Ptr &bug, QObject *owner) { if (this != owner) { return; } - ParseBugBacktraces parse(bug, this); + m_bug = bug; + qCDebug(DRKONQI_LOG) << "Fetching:" << m_bug->id(); + m_manager->fetchComments(m_bug, this); +} + +void DuplicateFinderJob::slotCommentsFetched(const QList &comments, QObject *owner) +{ + if (this != owner) { + return; + } + + // NOTE: we do not hold the comments in our bug object, once they go out + // of scope they are gone again. We have no use for keeping them in memory + // a user might look at 3 out of 20 bugs, and for those we can simply + // request the comments again instead of holding the potentially very large + // comments in memory. + + ParseBugBacktraces parse(comments, this); parse.parse(); BacktraceGenerator *btGenerator = DrKonqi::debuggerManager()->backtraceGenerator(); const ParseBugBacktraces::DuplicateRating rating = parse.findDuplicate(btGenerator->parser()->parsedBacktraceLines()); qCDebug(DRKONQI_LOG) << "Duplicate rating:" << rating; //TODO handle more cases here if (rating != ParseBugBacktraces::PerfectDuplicate) { - qCDebug(DRKONQI_LOG) << "Bug" << bug.bugNumber() << "most likely not a duplicate:" << rating; + qCDebug(DRKONQI_LOG) << "Bug" << m_bug->id() << "most likely not a duplicate:" << rating; analyzeNextBug(); return; } + bool unknownStatus = (m_bug->status() == Bugzilla::Bug::Status::Unknown); + bool unknownResolution = (m_bug->resolution() == Bugzilla::Bug::Resolution::Unknown); + //The Bug is a duplicate, now find out the status and resolution of the existing report - if (bug.resolutionValue() == BugReport::Duplicate) { + if (m_bug->resolution() == Bugzilla::Bug::Resolution::DUPLICATE) { qCDebug(DRKONQI_LOG) << "Found duplicate is a duplicate itself."; if (!m_result.duplicate) { - m_result.duplicate = bug.bugNumberAsInt(); + m_result.duplicate = m_bug->id(); } - fetchBug(bug.markedAsDuplicateOf()); - } else if ((bug.statusValue() == BugReport::UnknownStatus) || (bug.resolutionValue() == BugReport::UnknownResolution)) { + fetchBug(m_bug->dupe_of()); + } else if (unknownStatus || unknownResolution) { + // A resolution is unknown when the bug is unresolved. + // Status generally is never unknown. qCDebug(DRKONQI_LOG) << "Either the status or the resolution is unknown."; - qCDebug(DRKONQI_LOG) << "Status \"" << bug.bugStatus() << "\" known:" << (bug.statusValue() != BugReport::UnknownStatus); - qCDebug(DRKONQI_LOG) << "Resolution \"" << bug.resolution() << "\" known:" << (bug.resolutionValue() != BugReport::UnknownResolution); + qCDebug(DRKONQI_LOG) << "Status \"" << m_bug->status() << "\" known:" << !unknownStatus; + qCDebug(DRKONQI_LOG) << "Resolution \"" << m_bug->resolution() << "\" known:" << !unknownResolution; analyzeNextBug(); } else { if (!m_result.duplicate) { - m_result.duplicate = bug.bugNumberAsInt(); + m_result.duplicate = m_bug->id(); } - m_result.parentDuplicate = bug.bugNumberAsInt(); - m_result.status = bug.statusValue(); - m_result.resolution = bug.resolutionValue(); - qCDebug(DRKONQI_LOG) << "Found duplicate information (id/status/resolution):" << bug.bugNumber() << bug.bugStatus() << bug.resolution(); + m_result.parentDuplicate = m_bug->id(); + m_result.status = m_bug->status(); + m_result.resolution = m_bug->resolution(); + qCDebug(DRKONQI_LOG) << "Found duplicate information (id/status/resolution):" + << m_bug->id() << m_bug->status() << m_bug->resolution(); emitResult(); } } -void DuplicateFinderJob::slotBugReportError(const QString &message, QObject *owner) +void DuplicateFinderJob::slotError(const QString &message, QObject *owner) { if (this != owner) { return; } qCDebug(DRKONQI_LOG) << "Error fetching bug:" << message; analyzeNextBug(); } + diff --git a/src/bugzillaintegration/libbugzilla/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/CMakeLists.txt @@ -0,0 +1,39 @@ +set(lib_SRCS + apijob.cpp + bugzilla.cpp + connection.cpp + exceptions.cpp + + clients/attachmentclient.cpp + clients/bugclient.cpp + clients/commentclient.cpp + clients/clientbase.cpp + clients/productclient.cpp + + clients/commands/bugsearch.cpp + clients/commands/bugupdate.cpp + clients/commands/jsoncommand.cpp + clients/commands/newattachment.cpp + clients/commands/newbug.cpp + clients/commands/querycommand.cpp + + models/bug.cpp + models/comment.cpp + models/logindetails.cpp + models/product.cpp +) + +ecm_qt_declare_logging_category(lib_SRCS + HEADER bugzilla_debug.h + IDENTIFIER BUGZILLA_LOG + CATEGORY_NAME org.kde.drkonqi.bugzilla + DEFAULT_SEVERITY Warning) + +add_library(qbugzilla STATIC ${lib_SRCS}) +target_link_libraries(qbugzilla + PUBLIC + Qt5::Core + Qt5::Network + KF5::CoreAddons + KF5::KIOCore +) diff --git a/src/bugzillaintegration/libbugzilla/apijob.h b/src/bugzillaintegration/libbugzilla/apijob.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apijob.h @@ -0,0 +1,90 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 APIJOB_H +#define APIJOB_H + +#include // Not used here but included so clients don't have to +#include +#include + +#include + +namespace KIO { +class TransferJob; +} + +namespace Bugzilla { + +class APIJob : public KJob +{ + Q_OBJECT +public: + using KJob::KJob; + + /** + * @throws ProtocolException on unexpected HTTP statuses or KIO errors + * @throws APIException when the API returned an error object + * @return json document returned by api request + */ + QJsonDocument document() const; + + /** + * This is a convenience function on top of document() and may throw all + * the same exceptions. + * @return json object of document (may be in valid if the doc has none) + */ + QJsonObject object() const; + + void setAutoStart(bool start); + +public: + // Should be protected but since we call it for testing I don't care. + virtual QByteArray data() const = 0; + +private: + virtual void start() override {} + virtual void connectNotify(const QMetaMethod &signal) override; + + bool m_autostart = true; +}; + +class TransferAPIJob : public APIJob +{ + Q_OBJECT + friend class HTTPConnection; // constructs us, ctor is private though +public: + virtual QByteArray data() const override { return m_data; } + +private: + explicit TransferAPIJob(KIO::TransferJob *transferJob, QObject *parent = nullptr); + + void setPutData(const QByteArray &data); + void addMetaData(const QString &key, const QString &value); + + KIO::TransferJob *m_transferJob = nullptr; + QByteArray m_data; + QByteArray m_putData; + QList m_dataSegments; +}; + +} // namespace Bugzilla + +#endif // APIJOB_H diff --git a/src/bugzillaintegration/libbugzilla/apijob.cpp b/src/bugzillaintegration/libbugzilla/apijob.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apijob.cpp @@ -0,0 +1,129 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "apijob.h" + +#include +#include + +#include + +#include "bugzilla_debug.h" +#include "exceptions.h" + +namespace Bugzilla { + +TransferAPIJob::TransferAPIJob(KIO::TransferJob *transferJob, QObject *parent) + : APIJob(parent) + , m_transferJob(transferJob) +{ + // Required for every request type. + addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); + addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); + addMetaData(QStringLiteral("UserAgent"), QStringLiteral("DrKonqi")); + + connect(m_transferJob, &KIO::TransferJob::data, + this, [this](KIO::Job *, const QByteArray &data) { + m_data += data; + }); + + connect(m_transferJob, &KIO::TransferJob::finished, + this, [this](KJob *job) { + // Set errors, they are read by document() when the consumer reads + // the data and possibly raised as exception. + setError(job->error()); + setErrorText(job->errorText()); + + emitResult(); + }); +} + +void TransferAPIJob::addMetaData(const QString &key, const QString &value) +{ + m_transferJob->addMetaData(key, value); +} + +void TransferAPIJob::setPutData(const QByteArray &data) +{ + m_putData = data; + + // This is rally awkward, does it need to be this way? Why can't we just + // push the entire array in? + + // dataReq says we shouldn't send data >1mb, so segment the incoming data + // accordingly and generate QBAs wrapping the raw data (zero-copy). + int segmentSize = 1024 * 1024; // 1 mb per segment maximum + int segments = qMax(data.size() / segmentSize, 1); + m_dataSegments.reserve(segments); + for (int i = 0; i < segments; ++i) { + int offset = i * segmentSize; + const char *buf = data.constData() + offset; + int segmentLength = qMin(offset + segmentSize, data.size()); + m_dataSegments.append(QByteArray::fromRawData(buf, segmentLength)); + } + + // TODO: throw away, only here to make sure I don't mess up the + // segmentation. + int allLengths = 0; + for (const auto &a : qAsConst(m_dataSegments)) { + allLengths += a.size(); + } + Q_ASSERT(allLengths == data.size()); + + connect(m_transferJob, &KIO::TransferJob::dataReq, + this, [this](KIO::Job *, QByteArray &dataForSending) { + if (m_dataSegments.isEmpty()) { + return; + } + dataForSending = m_dataSegments.takeFirst(); + }); +} + +QJsonDocument APIJob::document() const +{ + ProtocolException::maybeThrow(this); + Q_ASSERT(error() == KJob::NoError); + + auto document = QJsonDocument::fromJson(data()); + APIException::maybeThrow(document); + return document; +} + +QJsonObject APIJob::object() const +{ + return document().object(); +} + +void APIJob::setAutoStart(bool start) +{ + m_autostart = start; +} + +void APIJob::connectNotify(const QMetaMethod &signal) +{ + if (m_autostart && signal == QMetaMethod::fromSignal(&KJob::finished)) { + qCDebug(BUGZILLA_LOG) << "auto starting"; + start(); + } + KJob::connectNotify(signal); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt @@ -0,0 +1,27 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +include(ECMAddTests) +include(GenerateExportHeader) + +find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG REQUIRED) + +# Include src so we have access to config-kcrash.h +include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +ecm_add_tests( + attachmenttest.cpp + bugtest.cpp + bugzillatest.cpp + commenttest.cpp + connectiontest.cpp + producttest.cpp + LINK_LIBRARIES + Qt5::Core + Qt5::Test + Qt5::Network + + qbugzilla +) + +ecm_mark_nongui_executable(bugzillatest) diff --git a/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp @@ -0,0 +1,132 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 + +#include + +#include "../clients/attachmentclient.h" + +namespace Bugzilla +{ + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &data, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug/1/attachment") { + QJsonParseError error; + QJsonDocument::fromJson(data, &error); + Q_ASSERT(error.error == QJsonParseError::NoError); + return new JobDouble { QFINDTESTDATA("data/attachment.new.json") }; + } + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +class AttachmentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testCreate() + { + Bugzilla::AttachmentClient c; + NewAttachment attachment; + attachment.ids = { 1 }; + attachment.data = "123"; + attachment.file_name = "filename123"; + attachment.summary = "summary123"; +// attachment.content_type = "123"; + attachment.comment = "comment123"; + attachment.is_patch = true; + attachment.is_private = false; + auto job = c.createAttachment(1, attachment); + job->start(); + QList ids = c.createAttachment(job); + QCOMPARE(ids, QList({1234})); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::AttachmentTest) + +#include "attachmenttest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp b/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp @@ -0,0 +1,239 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 + +#include + +#include "../clients/bugclient.h" +#include "../clients/productclient.h" + +namespace Bugzilla +{ + +static void compareNewBugHash(const QVariantHash &hash, bool *ok) +{ + *ok = false; + QCOMPARE(hash["product"], "aproduct"); + QCOMPARE(hash["component"], "acomp"); + QCOMPARE(hash["summary"], "asummary"); + QCOMPARE(hash["version"], "aversion"); + QCOMPARE(hash["description"], "adescription"); + QCOMPARE(hash["op_sys"], "asys"); + QCOMPARE(hash["platform"], "aplatform"); + QCOMPARE(hash["priority"], "apriority"); + QCOMPARE(hash["severity"], "aseverity"); + QCOMPARE(hash["keywords"], QStringList({ "aword", "anotherword" })); + *ok = true; +} + +static void compareUpdateBugHash(const QVariantHash &hash, bool *ok) +{ + *ok = false; + QCOMPARE(hash["cc"].toHash()["add"].toStringList(), QStringList({ "me@host.com" })); + QCOMPARE(hash["cc"].toHash()["remove"].toStringList(), QStringList({ "you@host.com" })); + *ok = true; +} + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug" && query.toString() == "product=dragonplayer") { + return new JobDouble { QFINDTESTDATA("data/bugs.dragonplayer.json") }; + } + if (path == "/bug" && query.toString() == "product=dragonplayer2") { + return new JobDouble { QFINDTESTDATA("data/bugs.unresolved.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &data, + const QUrlQuery &query = QUrlQuery()) const override + { + qDebug() << path << query.toString(); + if (path == "/bug" && query.isEmpty()) { + QJsonParseError e; + auto doc = QJsonDocument::fromJson(data, &e); + Q_ASSERT(e.error == QJsonParseError::NoError); + auto hash = doc.object().toVariantHash(); + bool ok; + compareNewBugHash(hash, &ok); + Q_ASSERT(ok); + + return new JobDouble { QFINDTESTDATA("data/bugs.new.json") }; + } + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &data, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug/54321" && query.isEmpty()) { + QJsonParseError e; + auto doc = QJsonDocument::fromJson(data, &e); + Q_ASSERT(e.error == QJsonParseError::NoError); + auto hash = doc.object().toVariantHash(); + bool ok; + compareUpdateBugHash(hash, &ok); + Q_ASSERT(ok); + + return new JobDouble { QFINDTESTDATA("data/bugs.update.json") }; + } + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +class BugTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testSearch() + { + Bugzilla::BugSearch search; + search.products = QStringList { "dragonplayer" }; + auto job = Bugzilla::BugClient().search(search); + job->start(); + QList bugs = Bugzilla::BugClient().search(job); + QCOMPARE(bugs.size(), 2); + Bug::Ptr bug; + for (auto it = bugs.begin(); it != bugs.end(); ++it) { + if ((*it)->id() == 156514) { + bug = *it; + } + } + + QCOMPARE(bug.isNull(), false); + QCOMPARE(bug->id(), 156514); + QCOMPARE(bug->product(), "dragonplayer"); + QCOMPARE(bug->component(), "general"); + QCOMPARE(bug->summary(), "Supported filetypes not shown in Play File.. Dialog"); + QCOMPARE(bug->version(), "unspecified"); + QCOMPARE(bug->op_sys(), "Linux"); + QCOMPARE(bug->priority(), "NOR"); + QCOMPARE(bug->severity(), "normal"); + QCOMPARE(bug->status(), Bug::Status::RESOLVED); + QCOMPARE(bug->resolution(), Bug::Resolution::FIXED); + QCOMPARE(bug->dupe_of(), -1); + QCOMPARE(bug->is_open(), false); + QCOMPARE(bug->customField("cf_versionfixedin"), "5.0"); + } + + void testSearchUnresolved() + { + Bugzilla::BugSearch search; + search.products = QStringList { "dragonplayer2" }; + auto job = Bugzilla::BugClient().search(search); + job->start(); + QList bugs = Bugzilla::BugClient().search(job); + QCOMPARE(bugs.size(), 1); + // resolution:"" maps to NONE + QCOMPARE(bugs.at(0)->resolution(), Bug::Resolution::NONE); + // None of the above should fail assertions or exception tests. + } + + void testNewBug() + { + Bugzilla::NewBug bug; + + bug.product = "aproduct"; + bug.component = "acomp"; + bug.summary = "asummary"; + bug.version = "aversion"; + bug.description = "adescription"; + bug.op_sys = "asys"; + bug.platform = "aplatform"; + bug.priority = "apriority"; + bug.severity = "aseverity"; + bug.keywords = QStringList { "aword", "anotherword" }; + + auto job = Bugzilla::BugClient().create(bug); + job->start(); + qint64 id = Bugzilla::BugClient().create(job); + QCOMPARE(id, 12345); + } + + void testUpdateBug() + { + Bugzilla::BugUpdate bug; + bug.cc->add << "me@host.com"; + bug.cc->remove << "you@host.com"; + + auto job = Bugzilla::BugClient().update(54321, bug); + job->start(); + qint64 id = Bugzilla::BugClient().update(job); + QCOMPARE(id, 54321); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::BugTest) + +#include "bugtest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp b/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp @@ -0,0 +1,128 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 + +#include + +//#include "../apijob.h" +#include "../bugzilla.h" + +namespace Bugzilla { + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/version") { + return new JobDouble { QFINDTESTDATA("data/bugzilla.version.json") }; + } else if (path == "/login" && query.toString() == "login=auser&password=apass&restrict_login=true") { + return new JobDouble { QFINDTESTDATA("data/bugzilla.login.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +} // namespace Bugzilla + +class BugzillaTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testVersion() + { + KJob *job = Bugzilla::version(); + job->start(); + QCOMPARE(Bugzilla::version(job), "5.0.0"); + } + + void testLogin() + { + KJob *job = Bugzilla::login("auser", "apass"); + job->start(); + auto details = Bugzilla::login(job); + QCOMPARE(details.id, 52960); + QCOMPARE(details.token, "52960-aaaaaaaaaa"); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +QTEST_GUILESS_MAIN(BugzillaTest) + +#include "bugzillatest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp @@ -0,0 +1,144 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 + +#include + +#include "../clients/commentclient.h" + +namespace Bugzilla +{ + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug/407363/comment" && query.toString().isEmpty()) { + return new JobDouble { QFINDTESTDATA("data/comments.json") }; + } + if (path == "/bug/1/comment" && query.toString().isEmpty()) { + return new JobDouble { QFINDTESTDATA("data/error.nobug.invalid.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +class CommentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testSearch() + { + Bugzilla::CommentClient c; + auto job = c.getFromBug(407363); + job->start(); + auto comments = c.getFromBug(job); + QCOMPARE(comments.size(), 3); + + Comment::Ptr uno = comments[0]; + QCOMPARE(uno->bug_id(), 407363); + QCOMPARE(uno->text(), "uno"); + + Comment::Ptr tre = comments[2]; + QCOMPARE(tre->bug_id(), 407363); + QCOMPARE(tre->text(), "tre"); + } + + void testSearchNoBugInvalid() + { + // Our bugzilla has a bug where errors do not have error:true! + // Make sure we correctly handle objects that are errors but do not + // necessarily have error:true. + // This is particularly relevant for comments because we make + // expectations about bugs being valid/invalid/throwing. + Bugzilla::CommentClient c; + auto job = c.getFromBug(1); + job->start(); + QVERIFY_EXCEPTION_THROWN(c.getFromBug(job), Bugzilla::APIException); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::CommentTest) + +#include "commenttest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp b/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp @@ -0,0 +1,189 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 +#include +#include +#include +#include +#include +#include +#include + +#include "../connection.h" + +namespace Bugzilla +{ + +class ConnectionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + } + + void testGet() + { + qDebug() << Q_FUNC_INFO; + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QTcpServer t; + QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true); + connect(&t, &QTcpServer::newConnection, + &t, [&t]() { + QTcpSocket *socket = t.nextPendingConnection(); + socket->waitForReadyRead(); + QString httpBlob = socket->readAll(); + qDebug() << httpBlob; + // The query is important to see if this actually gets properly + // passed along! + if (httpBlob.startsWith("GET /hi?informal=yes")) { + QFile file(QFINDTESTDATA("data/hi.http")); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + socket->write(file.readAll()); + socket->waitForBytesWritten(); + socket->disconnect(); + socket->close(); + return; + } + qDebug() << httpBlob; + Q_ASSERT_X(false, "server", "Unexpected request"); + }); + + QUrl root("http://localhost"); + root.setPort(t.serverPort()); + HTTPConnection c(root); + QUrlQuery query; + query.addQueryItem("informal", "yes"); + auto job = c.get("/hi", query); + job->exec(); + QCOMPARE(job->data(), "Hello!\n"); + } + + void testGetJsonError() + { + qDebug() << Q_FUNC_INFO; + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QTcpServer t; + QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true); + connect(&t, &QTcpServer::newConnection, + &t, [&t]() { + QTcpSocket *socket = t.nextPendingConnection(); + socket->waitForReadyRead(); + QString httpBlob = socket->readAll(); + qDebug() << httpBlob; + QFile file(QFINDTESTDATA("data/error.http")); + QVERIFY(file.open(QFile::ReadOnly | QFile::Text)); + socket->write(file.readAll()); + socket->waitForBytesWritten(); + socket->disconnect(); + socket->close(); + return; + }); + + QUrl root("http://localhost"); + root.setPort(t.serverPort()); + HTTPConnection c(root); + auto job = c.get("/hi"); + job->exec(); + QVERIFY_EXCEPTION_THROWN(job->document(), Bugzilla::APIException); + } + + void testPut() + { + qDebug() << Q_FUNC_INFO; + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QThread thread; + QTcpServer server; + server.moveToThread(&thread); + + QString readBlob; // lambda member essentially + + connect(&server, &QTcpServer::newConnection, + &server, [&server, &readBlob]() { + QCOMPARE(server.thread(), QThread::currentThread()); + QTcpSocket *socket = server.nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, + socket, [&readBlob, socket] { + readBlob += socket->readAll(); + readBlob.replace("\r\n", "\n"); + auto parts = readBlob.split("\n"); + if (parts.contains("PUT /put HTTP/1.1") && + parts.contains("Content-Length: 12") && + parts.contains("hello there!")) { + QFile file(QFINDTESTDATA("data/put.http")); + QVERIFY(file.open(QFile::ReadOnly | QFile::Text)); + QByteArray ret = file.readAll(); + ret.replace("\n", "\r\n"); + qDebug() << ret; + socket->write(ret); + socket->waitForBytesWritten(); + socket->disconnect(); + socket->close(); + qDebug() << "socket closed"; + } + }); + }); + + thread.start(); + + QMutex portMutex; + QWaitCondition portCondition; + quint16 port; + portMutex.lock(); + QTimer::singleShot(0, &server, [&server, &portMutex, &portCondition, &port]() { + Q_ASSERT(server.listen(QHostAddress::LocalHost, 0)); + QMutexLocker locker(&portMutex); + port = server.serverPort(); + portCondition.wakeAll(); + }); + portCondition.wait(&portMutex); + portMutex.unlock(); + + QUrl root("http://localhost"); + root.setPort(server.serverPort()); + HTTPConnection c(root); + APIJob *job = c.put("/put", "hello there!"); + KJob *kjob = job; + QSignalSpy spy(job, &KJob::finished); + kjob->start(); + // Because of how the request handling works the server may never return + // anything, so wait for the reply, if it doesn't arrive something went + // wrong with the server-side handling and the test cannot complete. + QVERIFY(spy.wait()); + + thread.quit(); + thread.wait(); + thread.terminate(); + + QCOMPARE(job->error(), KJob::NoError); + QCOMPARE(job->data(), "General Kenobi!\r\n"); + } +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ConnectionTest) + +#include "connectiontest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json b/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json @@ -0,0 +1,3 @@ +{ + "ids": [1234] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json @@ -0,0 +1,115 @@ +{ + "bugs" : [ + { + "alias" : [], + "assigned_to" : "dragon-bugs@dragonplayer.org", + "assigned_to_detail" : { + "email" : "dragon-bugs@dragonplayer.org", + "id" : 94747, + "name" : "dragon-bugs@dragonplayer.org", + "real_name" : "Dragon Player Mailing List" + }, + "blocks" : [], + "cc" : [ + "kde@davidedmundson.co.uk" + ], + "cc_detail" : [ + { + "email" : "kde@davidedmundson.co.uk", + "id" : 94748, + "name" : "kde@davidedmundson.co.uk", + "real_name" : "David Edmundson" + } + ], + "cf_commitlink" : "", + "cf_versionfixedin" : "5.0", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-01-24T00:57:41Z", + "creator" : "cduquette@gmail.com", + "creator_detail" : { + "email" : "cduquette@gmail.com", + "id" : 64830, + "name" : "cduquette@gmail.com", + "real_name" : "Craig Duquette" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 156514, + "is_cc_accessible" : true, + "is_confirmed" : false, + "is_creator_accessible" : true, + "is_open" : false, + "keywords" : [], + "last_change_time" : "2008-02-13T02:55:08Z", + "op_sys" : "Linux", + "platform" : "Compiled Sources", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "FIXED", + "see_also" : [], + "severity" : "normal", + "status" : "RESOLVED", + "summary" : "Supported filetypes not shown in Play File.. Dialog", + "target_milestone" : "---", + "url" : "", + "version" : "unspecified", + "whiteboard" : "" + }, + { + "alias" : [], + "assigned_to" : "dragon-bugs@dragonplayer.org", + "assigned_to_detail" : { + "email" : "dragon-bugs@dragonplayer.org", + "id" : 94747, + "name" : "dragon-bugs@dragonplayer.org", + "real_name" : "Dragon Player Mailing List" + }, + "blocks" : [], + "cc" : [], + "cc_detail" : [], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-01-29T15:31:36Z", + "creator" : "hansmbakker@gmail.com", + "creator_detail" : { + "email" : "hansmbakker@gmail.com", + "id" : 60871, + "name" : "hansmbakker@gmail.com", + "real_name" : "Hans Bakker" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 156917, + "is_cc_accessible" : true, + "is_confirmed" : false, + "is_creator_accessible" : true, + "is_open" : false, + "keywords" : [], + "last_change_time" : "2008-02-13T02:55:08Z", + "op_sys" : "Linux", + "platform" : "Compiled Sources", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "WONTFIX", + "see_also" : [], + "severity" : "wishlist", + "status" : "RESOLVED", + "summary" : "Changing settings not possible when not playing", + "target_milestone" : "---", + "url" : "", + "version" : "unspecified", + "whiteboard" : "" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.new.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.new.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.new.json @@ -0,0 +1,3 @@ +{ + "id": 12345 +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.unresolved.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.unresolved.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.unresolved.json @@ -0,0 +1,55 @@ +{ + "bugs" : [ + { + "alias" : [], + "assigned_to" : "dragon-bugs@dragonplayer.org", + "assigned_to_detail" : { + "email" : "dragon-bugs@dragonplayer.org", + "id" : 94747, + "name" : "dragon-bugs@dragonplayer.org", + "real_name" : "Dragon Player Mailing List" + }, + "blocks" : [], + "cc" : [], + "cc_detail" : [], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-06-16T21:29:47Z", + "creator" : "flabbergasted@gmx.de", + "creator_detail" : { + "email" : "flabbergasted@gmx.de", + "id" : 80677, + "name" : "flabbergasted@gmx.de", + "real_name" : "Tobias" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 164250, + "is_cc_accessible" : null, + "is_confirmed" : null, + "is_creator_accessible" : null, + "is_open" : null, + "keywords" : [], + "last_change_time" : "2008-06-17T18:36:52Z", + "op_sys" : "Linux", + "platform" : "Ubuntu Packages", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "", + "see_also" : [], + "severity" : "wishlist", + "status" : "UNCONFIRMED", + "summary" : "Save position of DVDs", + "target_milestone" : "---", + "url" : "", + "version" : "2.0.x", + "whiteboard" : "" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json @@ -0,0 +1,10 @@ +{ + "bugs": [ + { + "id": 54321, + "changes": {}, + "alias": [], + "last_change_time": "2019-06-27T13:40:31Z" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json @@ -0,0 +1,4 @@ +{ + "id" : 52960, + "token" : "52960-aaaaaaaaaa" +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json @@ -0,0 +1,3 @@ +{ + "version": "5.0.0" +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/comments.json b/src/bugzillaintegration/libbugzilla/autotests/data/comments.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/comments.json @@ -0,0 +1,45 @@ +{ + "bugs" : { + "407363" : { + "comments" : [ + { + "attachment_id" : null, + "bug_id" : 407363, + "count" : 0, + "creation_time" : "2019-05-09T13:49:28Z", + "creator" : "sitter@kde.org", + "id" : 1855155, + "is_private" : null, + "tags" : [], + "text" : "uno", + "time" : "2019-05-09T13:49:28Z" + }, + { + "attachment_id" : 119931, + "bug_id" : 407363, + "count" : 1, + "creation_time" : "2019-05-09T13:50:10Z", + "creator" : "sitter@kde.org", + "id" : 1855156, + "is_private" : null, + "tags" : [], + "text" : "due", + "time" : "2019-05-09T13:50:10Z" + }, + { + "attachment_id" : 119932, + "bug_id" : 407363, + "count" : 2, + "creation_time" : "2019-05-09T13:50:28Z", + "creator" : "sitter@kde.org", + "id" : 1855157, + "is_private" : null, + "tags" : [], + "text" : "tre", + "time" : "2019-05-09T13:50:28Z" + } + ] + } + }, + "comments" : {} +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/error.http b/src/bugzillaintegration/libbugzilla/autotests/data/error.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/error.http @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Date: Tue, 05 Mar 2019 13:42:49 GMT +Server: Apache/2.4.29 (Ubuntu) + +{"error":true,"documentation":"https://bugzilla.readthedocs.org/en/5.0/api/","message":"You must enter a summary for this bug.","code":107} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/error.nobug.invalid.json b/src/bugzillaintegration/libbugzilla/autotests/data/error.nobug.invalid.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/error.nobug.invalid.json @@ -0,0 +1,6 @@ +{ + "code" : 101, + "documentation" : "https://bugzilla.readthedocs.org/en/5.0/api/", + "error" : null, + "message" : "Bug #1 does not exist." +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/hi.http b/src/bugzillaintegration/libbugzilla/autotests/data/hi.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/hi.http @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Date: Tue, 05 Mar 2019 13:42:49 GMT +Server: Apache/2.4.29 (Ubuntu) + +Hello! diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json b/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json @@ -0,0 +1,194 @@ +{ + "products" : [ + { + "classification" : "Unclassified", + "components" : [ + { + "default_assigned_to" : "sitter@kde.org", + "default_qa_contact" : "", + "description" : "all bugs not for other components", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : false, + "is_requesteeble" : true, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : false, + "is_requesteeble" : true, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1200, + "is_active" : true, + "name" : "general", + "sort_key" : 0 + } + ], + "default_milestone" : "---", + "description" : "Simple Video and DVD Player [part of the kdemultimedia module]", + "has_unconfirmed" : true, + "id" : 393, + "is_active" : true, + "milestones" : [ + { + "id" : 393, + "is_active" : true, + "name" : "---", + "sort_key" : 0 + }, + { + "id" : 664, + "is_active" : true, + "name" : "2.1", + "sort_key" : 0 + }, + { + "id" : 665, + "is_active" : true, + "name" : "2.2", + "sort_key" : 0 + }, + { + "id" : 666, + "is_active" : true, + "name" : "3.0", + "sort_key" : 0 + } + ], + "name" : "dragonplayer", + "versions" : [ + { + "id" : 4408, + "is_active" : false, + "name" : "2.0", + "sort_key" : 0 + }, + { + "id" : 3240, + "is_active" : false, + "name" : "2.0-beta1", + "sort_key" : 0 + }, + { + "id" : 5106, + "is_active" : false, + "name" : "2.0-git", + "sort_key" : 0 + }, + { + "id" : 3271, + "is_active" : false, + "name" : "2.0.x", + "sort_key" : 0 + }, + { + "id" : 14902, + "is_active" : true, + "name" : "17.04", + "sort_key" : 0 + }, + { + "id" : 15110, + "is_active" : true, + "name" : "17.08", + "sort_key" : 0 + }, + { + "id" : 16546, + "is_active" : true, + "name" : "17.12", + "sort_key" : 0 + }, + { + "id" : 16778, + "is_active" : true, + "name" : "18.04", + "sort_key" : 0 + }, + { + "id" : 17542, + "is_active" : true, + "name" : "18.08", + "sort_key" : 0 + }, + { + "id" : 18356, + "is_active" : true, + "name" : "18.12", + "sort_key" : 0 + }, + { + "id" : 3272, + "is_active" : false, + "name" : "SVN", + "sort_key" : 0 + }, + { + "id" : 3239, + "is_active" : true, + "name" : "unspecified", + "sort_key" : 0 + } + ] + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/put.http b/src/bugzillaintegration/libbugzilla/autotests/data/put.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/put.http @@ -0,0 +1,5 @@ +HTTP/1.1 201 Created +Date: Thu, 07 Mar 2019 12:11:29 GMT +Server: Apache/2.4.29 (Ubuntu) + +General Kenobi! diff --git a/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp @@ -0,0 +1,133 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 +#include + +#include + +namespace Bugzilla +{ + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/product/dragonplayer") { + return new JobDouble { QFINDTESTDATA("data/product.dragonplayer.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + qDebug() << path << query.toString(); + Q_UNREACHABLE(); + return nullptr; + } + + virtual APIJob *put(const QString &, + const QByteArray &, + const QUrlQuery & = QUrlQuery()) const override + { + Q_UNREACHABLE(); + } +}; + +class ProductTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testProduct() + { + KJob *job = Bugzilla::ProductClient().get("dragonplayer"); + Q_ASSERT(job); + job->start(); + Product::Ptr product = Bugzilla::ProductClient().get(job); + + QCOMPARE(product->isActive(), true); + QCOMPARE(product->componentNames(), QStringList({"general"})); + QCOMPARE(product->allVersions(), + QStringList({"2.0", "2.0-beta1", "2.0-git", "2.0.x", "17.04", + "17.08", "17.12", "18.04", "18.08", "18.12", + "SVN", "unspecified"})); + + QCOMPARE(product->versions().size(), 12); + auto version = product->versions()[0]; + QCOMPARE(version->id(), 4408); + QCOMPARE(version->name(), "2.0"); + QCOMPARE(version->isActive(), false); + + QCOMPARE(product->components().size(), 1); + auto component = product->components()[0]; + QCOMPARE(component->id(), 1200); + QCOMPARE(component->name(), "general"); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ProductTest) + +#include "producttest.moc" diff --git a/src/bugzillaintegration/libbugzilla/bugzilla.h b/src/bugzillaintegration/libbugzilla/bugzilla.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bugzilla.h @@ -0,0 +1,39 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 BUGZILLA_H +#define BUGZILLA_H + +#include "connection.h" +#include "models/logindetails.h" + +namespace Bugzilla +{ + QString version(KJob *kjob); + APIJob *version(const Connection &connection = Bugzilla::connection()); + + // https://bugzilla.readthedocs.io/en/5.0/api/core/v1/user.html#login + LoginDetails login(KJob *kjob); + APIJob *login(const QString &username, + const QString &password, + const Connection &connection = Bugzilla::connection()); +} // namespace Bugzilla + +#endif // BUGZILLA_H diff --git a/src/bugzillaintegration/libbugzilla/bugzilla.cpp b/src/bugzillaintegration/libbugzilla/bugzilla.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bugzilla.cpp @@ -0,0 +1,56 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "bugzilla.h" + +namespace Bugzilla { + +QString version(KJob *kjob) +{ + const APIJob *job = qobject_cast(kjob); + const QString version = job->object().value(QLatin1String("version")).toString(); + return version; +} + +APIJob *version(const Connection &connection) +{ + return connection.get(QStringLiteral("/version")); +} + +LoginDetails login(KJob *kjob) +{ + const APIJob *job = qobject_cast(kjob); + const auto obj = job->object(); + const QString token = obj.value(QLatin1String("token")).toString(); + const int id = obj.value(QLatin1String("id")).toInt(-1); + return LoginDetails { id, token }; +} + +APIJob *login(const QString &username, const QString &password, const Connection &connection) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("login"), username); + query.addQueryItem(QStringLiteral("password"), password); + query.addQueryItem(QStringLiteral("restrict_login"), QStringLiteral("true")); + return connection.get(QStringLiteral("/login"), query); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h @@ -0,0 +1,41 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 ATTACHMENTCLIENT_H +#define ATTACHMENTCLIENT_H + +#include "clientbase.h" +#include "commands/newattachment.h" + +namespace Bugzilla { + +class AttachmentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + /// Attach to a bug. @returns list of bugs that were attached to. + QList createAttachment(KJob *kjob); + KJob *createAttachment(int bugId, const NewAttachment &attachment); +}; + +} // namespace Bugzilla + +#endif // ATTACHMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp @@ -0,0 +1,52 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "attachmentclient.h" + +#include + +namespace Bugzilla { + +QList AttachmentClient::createAttachment(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("ids")).toArray(); + // It's unclear if this can happen. When the ids would be empty there was + // an error, and when there was an error the API should have sent an error. + Q_ASSERT(ary.size() > 0); + + QList list; + for (auto ids : ary) { + bool ok = false; + list.append(ids.toVariant().toInt(&ok)); + Q_ASSERT(ok); + } + + return list; +} + +KJob *AttachmentClient::createAttachment(int bugId, const NewAttachment &attachment) +{ + return m_connection.post(QStringLiteral("/bug/%1/attachment").arg(bugId), + attachment.toJson()); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/bugclient.h b/src/bugzillaintegration/libbugzilla/clients/bugclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/bugclient.h @@ -0,0 +1,50 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 BUGCLIENT_H +#define BUGCLIENT_H + +#include "clientbase.h" +#include "commands/bugsearch.h" +#include "commands/bugupdate.h" +#include "commands/newbug.h" + +#include + +namespace Bugzilla { + +class BugClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList search(KJob *kjob); + KJob *search(const BugSearch &search); + + qint64 create(KJob *kjob); + KJob *create(const NewBug &bug); + + qint64 update(KJob *kjob); + KJob *update(qint64 bugId, BugUpdate &bug); +}; + +} // namespace Bugzilla + +#endif // BUGCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp @@ -0,0 +1,82 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "bugclient.h" + +#include +#include + +namespace Bugzilla { + +QList BugClient::search(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("bugs")).toArray(); + + QList list; + for (auto bug : ary) { + list.append(Bug::Ptr(new Bug(bug.toObject().toVariantHash()))); + } + + return list; +} + +KJob *BugClient::search(const BugSearch &search) +{ + return m_connection.get(QStringLiteral("/bug"), search.toQuery()); +} + +qint64 BugClient::create(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + qint64 ret = job->object().value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(ret != -1); + return ret; +} + +KJob *BugClient::create(const NewBug &bug) +{ + return m_connection.post(QStringLiteral("/bug"), + bug.toJson()); +} + +qint64 BugClient::update(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("bugs")).toArray(); + // It's unclear if this can happen. When the ids would be empty there was + // an error, and when there was an error the API should have sent an error. + Q_ASSERT(ary.size() == 1); + + int value = ary.at(0).toObject().value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(value != -1); + + return value; +} + +KJob *BugClient::update(qint64 bugId, BugUpdate &bug) +{ + return m_connection.put(QStringLiteral("/bug/%1").arg(bugId), bug.toJson()); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/clientbase.h b/src/bugzillaintegration/libbugzilla/clients/clientbase.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/clientbase.h @@ -0,0 +1,39 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 CLIENTBASE_H +#define CLIENTBASE_H + +#include "connection.h" + +namespace Bugzilla { + +class ClientBase +{ +public: + explicit ClientBase(const Connection &connection = Bugzilla::connection()); + +protected: + const Connection &m_connection; +}; + +} // namespace Bugzilla + +#endif // CLIENTBASE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp b/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp @@ -0,0 +1,30 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "clientbase.h" + +namespace Bugzilla { + +ClientBase::ClientBase(const Connection &connection) + : m_connection(connection) +{ +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h @@ -0,0 +1,45 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 BUGSEARCH_H +#define BUGSEARCH_H + +#include "querycommand.h" + +namespace Bugzilla { + +class BugSearch : public QueryCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QStringList, products); + BUGZILLA_MEMBER_PROPERTY(QString, severity); + BUGZILLA_MEMBER_PROPERTY(QString, creationTime); + BUGZILLA_MEMBER_PROPERTY(qint64, id) = -1; + BUGZILLA_MEMBER_PROPERTY(qint64, limit) = -1; + BUGZILLA_MEMBER_PROPERTY(qint64, offset) = -1; + BUGZILLA_MEMBER_PROPERTY(QString, longdesc); + BUGZILLA_MEMBER_PROPERTY(QStringList, order); +public: + virtual QUrlQuery toQuery() const override; +}; + +} // namespace Bugzilla + +#endif // BUGSEARCH_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp @@ -0,0 +1,47 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "bugsearch.h" + +#include + +namespace Bugzilla { + +QUrlQuery BugSearch::toQuery() const +{ + QUrlQuery query; + QSet seen; + + for (const QString &product : products) { + query.addQueryItem(QStringLiteral("product"), product); + } + seen << QStringLiteral("products"); + + if (!order.isEmpty()) { + query.addQueryItem(QStringLiteral("order"), order.join(QLatin1Char(','))); + } + seen << QStringLiteral("order"); + + expandQuery(query, seen); + + return query; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h @@ -0,0 +1,45 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 BUGUPDATE_H +#define BUGUPDATE_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class BugUpdateCC : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QStringList, add); + BUGZILLA_MEMBER_PROPERTY(QStringList, remove); +public: + using JsonCommand::JsonCommand; +}; + +class BugUpdate : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(BugUpdateCC *, cc) = new BugUpdateCC(this); +}; + +} // namespace Bugzilla + +#endif // BUGUPDATE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp @@ -0,0 +1,24 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "bugupdate.h" + +namespace Bugzilla { +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h b/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h @@ -0,0 +1,30 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 COMMANDBASE_H +#define COMMANDBASE_H + +#define BUGZILLA_MEMBER_PROPERTY(type, member) \ + private: \ + Q_PROPERTY(type member MEMBER member) \ + public: \ + type member + +#endif // COMMANDBASE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h @@ -0,0 +1,44 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 JSONCOMMAND_H +#define JSONCOMMAND_H + +#include + +#include "commandbase.h" + +namespace Bugzilla { + +class JsonCommand : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual QByteArray toJson() const; + virtual QVariantHash toVariantHash() const; +}; + +} // namespace Bugzilla + +Q_DECLARE_METATYPE(Bugzilla::JsonCommand *) + +#endif // JSONCOMMAND_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp @@ -0,0 +1,69 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "jsoncommand.h" + +#include +#include +#include +#include + +namespace Bugzilla { + +QByteArray JsonCommand::toJson() const +{ + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantHash(toVariantHash())); + return doc.toJson(); +} + +QVariantHash JsonCommand::toVariantHash() const +{ + QVariantHash hash; + + const auto propertyCount = metaObject()->propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + const auto property = metaObject()->property(i); + const auto name = QString::fromLatin1(property.name()); + const auto value = property.read(this); + + if (name == QStringLiteral("objectName")) { // Builtin property. + continue; + } + + if (value.isNull()) { + continue; + } + + // If this is a nested representation, serialize it and glue it in. + if (value.canConvert()) { + JsonCommand *repValue = value.value(); + hash.insert(name, repValue->toVariantHash()); + continue; + } + + hash.insert(name, value); + } + + return hash; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h @@ -0,0 +1,48 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 NEWATTACHMENT_H +#define NEWATTACHMENT_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class NewAttachment : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QList, ids); + BUGZILLA_MEMBER_PROPERTY(QString, data); + BUGZILLA_MEMBER_PROPERTY(QString, file_name); + BUGZILLA_MEMBER_PROPERTY(QString, summary); + BUGZILLA_MEMBER_PROPERTY(QString, content_type); + BUGZILLA_MEMBER_PROPERTY(QString, comment); + BUGZILLA_MEMBER_PROPERTY(bool, is_patch); + BUGZILLA_MEMBER_PROPERTY(bool, is_private); + + // flags property is not supported at this time + +public: + virtual QVariantHash toVariantHash() const override; +}; + +} // namespace Bugzilla + +#endif // NEWATTACHMENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp @@ -0,0 +1,42 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "newattachment.h" + +#include +#include + +namespace Bugzilla { + +QVariantHash NewAttachment::toVariantHash() const +{ + auto hash = JsonCommand::toVariantHash(); + + QVariantList idsVariant; + for (int id : ids) { + idsVariant << QVariant::fromValue(id); + } + hash[QStringLiteral("ids")] = idsVariant; + hash[QStringLiteral("data")] = data.toUtf8().toBase64(); + + return hash; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h @@ -0,0 +1,48 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 NEWBUG_H +#define NEWBUG_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class NewBug : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QString, product); + BUGZILLA_MEMBER_PROPERTY(QString, component); + BUGZILLA_MEMBER_PROPERTY(QString, summary); // aka shortdesc + BUGZILLA_MEMBER_PROPERTY(QString, version); + BUGZILLA_MEMBER_PROPERTY(QString, description); + BUGZILLA_MEMBER_PROPERTY(QString, op_sys); + BUGZILLA_MEMBER_PROPERTY(QString, platform); + BUGZILLA_MEMBER_PROPERTY(QString, priority); + BUGZILLA_MEMBER_PROPERTY(QString, severity); + BUGZILLA_MEMBER_PROPERTY(QStringList, keywords); // not documented but also supported +public: + using JsonCommand::JsonCommand; + NewBug(const NewBug &other); +}; + +} // namespace Bugzilla + +#endif // NEWBUG_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp @@ -0,0 +1,37 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "newbug.h" + +#include + +namespace Bugzilla { + +NewBug::NewBug(const NewBug &other) + : JsonCommand(other.parent()) +{ + const auto propertyCount = staticMetaObject.propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + const auto property = staticMetaObject.property(i); + property.write(this, property.read(&other)); + } +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h @@ -0,0 +1,44 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 QUERYCOMMAND_H +#define QUERYCOMMAND_H + +#include +#include +#include +#include + +#include "commandbase.h" + +namespace Bugzilla { + +class QueryCommand : public QObject +{ +public: + using QObject::QObject; + + virtual QUrlQuery toQuery() const; + QUrlQuery expandQuery(QUrlQuery &query, const QSet &seen) const; +}; + +} // namespace Bugzilla + +#endif // QUERYCOMMAND_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp @@ -0,0 +1,72 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "querycommand.h" + +#include +#include +#include +#include + +namespace Bugzilla { + +QUrlQuery QueryCommand::toQuery() const +{ + QUrlQuery query; + return expandQuery(query, QSet()); +} + +QUrlQuery QueryCommand::expandQuery(QUrlQuery &query, const QSet &seen) const +{ + const auto propertyCount = metaObject()->propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + const auto property = metaObject()->property(i); + const auto name = QString::fromLatin1(property.name()); + const auto value = property.read(this); + + if (query.hasQueryItem(name) || seen.contains(name) || name == QLatin1Literal("objectName")) { + // The element was manually set or builtin property. + continue; + } + + if (value.toLongLong() < 0) { + // Invalid value => member was not set! + // This does generally also work for all integers, ulonglong of + // course being the only one that can cause trouble. + continue; + } + + // Lists must be serialized manually. They could have a number of representations. + Q_ASSERT_X(value.type() != QVariant::StringList, Q_FUNC_INFO, + qPrintable(QStringLiteral("Trying to auto serialize string list %1").arg(name))); + + // Either can't serialize or not set. + if (value.toString().isEmpty()) { + continue; + } + + query.addQueryItem(name, property.read(this).toString()); + } + + return query; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/clients/commentclient.h b/src/bugzillaintegration/libbugzilla/clients/commentclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.h @@ -0,0 +1,40 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 COMMENTCLIENT_H +#define COMMENTCLIENT_H + +#include "clientbase.h" +#include "models/comment.h" + +namespace Bugzilla { + +class CommentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList getFromBug(KJob *kjob); + KJob *getFromBug(int bugId); +}; + +} // namespace Bugzilla + +#endif // COMMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp @@ -0,0 +1,51 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "commentclient.h" + +#include + +namespace Bugzilla { + +QList CommentClient::getFromBug(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + QJsonObject bugs = job->object().value(QStringLiteral("bugs")).toObject(); + + // The API should never return anything other than the single bug we asked for. + Q_ASSERT(bugs.keys().size() == 1); + + QJsonObject bug = bugs.value(bugs.keys().at(0)).toObject(); + QJsonArray comments = bug.value(QStringLiteral("comments")).toArray(); + + QList list; + for (auto it = comments.constBegin(); it != comments.constEnd(); ++it) { + list.append(new Comment((*it).toObject().toVariantHash())); + } + + return list; +} + +KJob *CommentClient::getFromBug(int bugId) +{ + return m_connection.get(QStringLiteral("/bug/%1/comment").arg(QString::number(bugId))); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/productclient.h b/src/bugzillaintegration/libbugzilla/clients/productclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/productclient.h @@ -0,0 +1,41 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 PRODUCTCLIENT_H +#define PRODUCTCLIENT_H + +#include "clientbase.h" +#include "models/product.h" + +namespace Bugzilla { + +class ProductClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + /// Gets a single Product by its name + Product::Ptr get(KJob *kjob); + KJob *get(const QString &idOrName); +}; + +} // namespace Bugzilla + +#endif // PRODUCTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/productclient.cpp b/src/bugzillaintegration/libbugzilla/clients/productclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/productclient.cpp @@ -0,0 +1,46 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "productclient.h" + +#include +#include +#include + +namespace Bugzilla { + +Product::Ptr ProductClient::get(KJob *kjob) +{ + auto job = qobject_cast(kjob); + + const QJsonArray productsArray = job->object().value(QLatin1String("products")).toArray(); + Q_ASSERT(productsArray.size() == 1); + + auto obj = productsArray.at(0).toObject().toVariantHash(); + + return Product::Ptr(new Product(obj, m_connection)); +} + +KJob *ProductClient::get(const QString &idOrName) +{ + return m_connection.get(QStringLiteral("/product/%1").arg(idOrName)); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/connection.h b/src/bugzillaintegration/libbugzilla/connection.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.h @@ -0,0 +1,90 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 CONNECTION_H +#define CONNECTION_H + +#include +#include +#include + +#include "exceptions.h" +#include "apijob.h" + +namespace Bugzilla { + +class APIJob; + +/** + * Base Connection. Has CRUD-y methods that need implementing in a specific + * connection variant. + */ +class Connection : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual void setToken(const QString &authToken) = 0; + + virtual APIJob *get(const QString &path, const QUrlQuery &query = QUrlQuery()) const = 0; + virtual APIJob *post(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const = 0; + virtual APIJob *put(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const = 0; +}; + +/** + * HTTP Connection. + */ +class HTTPConnection : public Connection +{ + Q_OBJECT +public: + explicit HTTPConnection(const QUrl &root = QUrl(QStringLiteral("http://bugstest.kde.org/")), + QObject *parent = nullptr); + ~HTTPConnection(); + + virtual void setToken(const QString &authToken) override; + + virtual APIJob *get(const QString &path, const QUrlQuery &query = QUrlQuery()) const override; + virtual APIJob *post(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const override; + virtual APIJob *put(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const override; + +private: + QUrl url(const QString &appendix, QUrlQuery query) const; + + QUrl m_root; + QString m_token; +}; + +/** + * @return the "default" global connection instance. This is the instance used + * by all clients unless another one is manually set on the client. + */ +Connection &connection(); + +/** + * Changes the "default" global connection. This generally shouldn't be used + * outside tests, where it is used to inject connection doubles. + */ +void setConnection(Connection *newConnection); + +} // namespace Bugzilla + +#endif // CONNECTION_H diff --git a/src/bugzillaintegration/libbugzilla/connection.cpp b/src/bugzillaintegration/libbugzilla/connection.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.cpp @@ -0,0 +1,110 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "connection.h" + +#include +#include + +#include + +#include "bugzilla_debug.h" + +namespace Bugzilla { + +// Static container for global default connection. +// We need a container here because the connection may be anything derived from +// Connection and its effective type may change (e.g. in autotests). +class GlobalConnection +{ +public: + ~GlobalConnection() + { + delete m_connection; + } + + Connection *m_connection = new HTTPConnection; +}; + +Q_GLOBAL_STATIC(GlobalConnection, s_connection) + +Connection &connection() +{ + return *(s_connection->m_connection); +} + +void setConnection(Connection *newConnection) +{ + delete s_connection->m_connection; + s_connection->m_connection = newConnection; +} + +HTTPConnection::HTTPConnection(const QUrl &root, QObject *parent) + : Connection(parent) + , m_root(root) +{ +} + +HTTPConnection::~HTTPConnection() +{ +} + +void HTTPConnection::setToken(const QString &authToken) +{ + m_token = authToken; +} + +APIJob *HTTPConnection::get(const QString &path, const QUrlQuery &query) const +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + auto job = new TransferAPIJob(KIO::get(url(path, query), KIO::Reload, KIO::HideProgressInfo)); + return job; +} + +APIJob *HTTPConnection::post(const QString &path, const QByteArray &data, const QUrlQuery &query) const +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + auto job = new TransferAPIJob(KIO::http_post(url(path, query), data, KIO::HideProgressInfo)); + return job; +} + +APIJob *HTTPConnection::put(const QString &path, const QByteArray &data, const QUrlQuery &query) const +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + auto job = new TransferAPIJob(KIO::put(url(path, query), KIO::HideProgressInfo)); + job->setPutData(data); + return job; +} + +QUrl HTTPConnection::url(const QString &appendix, QUrlQuery query) const +{ + QUrl url(m_root); + url.setPath(m_root.path() + appendix); + + if (!m_token.isEmpty()) { + query.addQueryItem(QStringLiteral("token"), m_token); + } + + url.setQuery(query); + return url; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/exceptions.h b/src/bugzillaintegration/libbugzilla/exceptions.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.h @@ -0,0 +1,116 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 EXCEPTIONS_H +#define EXCEPTIONS_H + +#include + +class QJsonDocument; +class QJsonObject; + +namespace KIO { +class TransferJob; +} + +namespace Bugzilla { + +class APIJob; + +/** + * Root class for exceptions. Simply a QException which has the what backed + * by a QString. + */ +class Exception : public QException +{ +public: + using QException::QException; + virtual ~Exception(); + + virtual QString whatString() const = 0; + virtual const char *what() const noexcept override; + +private: + char *m_what = nullptr; +}; + +/** + * Generic runtime exception. + */ +class RuntimeException : public Exception +{ +public: + RuntimeException(const QString &reason); + virtual RuntimeException *clone() const override { return new RuntimeException(*this); } + virtual QString whatString() const override; + +private: + QString m_reason; +}; + +/** + * Translates an API error into an excpetion for easy handling. + * Specifically when the API sends an error object in the body attempting to + * access the JSON blob through one of the convenience accessors + * (e.g. job.object()) will instead raise an exception. + */ +class APIException : public Exception +{ +public: + APIException(const QJsonDocument &document); + APIException(const QJsonObject &object); + APIException(const APIException &other); + + virtual void raise() const override { throw *this; } + virtual APIException *clone() const override { return new APIException(*this); } + virtual QString whatString() const override; + + bool isError() const { return m_isError; } + + static void maybeThrow(const QJsonDocument &document); + +private: + bool m_isError = false; + QString m_message; + int m_code = -1; +}; + +/** + * Translates an KJob/APIJob error into an excpetion for easy handling. + */ +class ProtocolException : public Exception +{ +public: + ProtocolException(const APIJob *job); + ProtocolException(const ProtocolException &other); + + virtual void raise() const override { throw *this; } + virtual ProtocolException *clone() const override { return new ProtocolException(*this); } + virtual QString whatString() const override; + + static void maybeThrow(const APIJob *job); + +private: + const APIJob *m_job = nullptr; +}; + +} // namespace Bugzilla + +#endif // EXCEPTIONS_H diff --git a/src/bugzillaintegration/libbugzilla/exceptions.cpp b/src/bugzillaintegration/libbugzilla/exceptions.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.cpp @@ -0,0 +1,120 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "exceptions.h" + +#include + +#include "apijob.h" + +namespace Bugzilla { + +APIException::APIException(const QJsonDocument &document) + : APIException(document.object()) +{ +} + +APIException::APIException(const QJsonObject &object) +{ + if (object.isEmpty()) { + return; + } + m_isError = object.value(QStringLiteral("error")).toBool(m_isError); + m_message = object.value(QStringLiteral("message")).toString(m_message); + m_code = object.value(QStringLiteral("code")).toInt(m_code); + // Our bugzilla is a bit bugged. It doesn't necessarily set error to true + // but instead keeps it at null. Because of this we need to possibly shimy + // the bool to align with reality. + if (object.value(QStringLiteral("error")).type() == QJsonValue::Null && + m_code > 0 && + !m_message.isNull()) { + m_isError = true; + } +} + +APIException::APIException(const APIException &other) + : m_isError(other.m_isError) + , m_message(other.m_message) + , m_code(other.m_code) +{ +} + +QString APIException::whatString() const +{ + return QStringLiteral("[%1] %2").arg(m_code).arg(m_message); +} + +void APIException::maybeThrow(const QJsonDocument &document) +{ + APIException ex(document); + + if (ex.isError()) { + ex.raise(); + } +} + +ProtocolException::ProtocolException(const APIJob *job) + : Exception() + , m_job(job) +{ +} + +ProtocolException::ProtocolException(const ProtocolException &other) + : m_job(other.m_job) +{ +} + +QString ProtocolException::whatString() const +{ + // String generally includes the error code, so no extra logic needed. + return m_job->errorString(); +} + +void ProtocolException::maybeThrow(const APIJob *job) +{ + if (job->error() == KJob::NoError) { + return; + } + throw ProtocolException(job); +} + +Exception::~Exception() +{ + delete m_what; +} + +const char *Exception::what() const noexcept +{ + strcpy(m_what, qUtf8Printable(whatString())); + return m_what; +} + +RuntimeException::RuntimeException(const QString &reason) + : m_reason(reason) +{ +} + +QString RuntimeException::whatString() const +{ + return m_reason; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/models/bug.h b/src/bugzillaintegration/libbugzilla/models/bug.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.h @@ -0,0 +1,150 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 BUG_H +#define BUG_H + +#include +#include + +#include + +#include "comment.h" + +namespace Bugzilla { + +// Models a bugzilla bug. +class Bug : public QObject +{ +public: + enum class Status { + Unknown, // First value is default if QMetaEnum can't map the key. + UNCONFIRMED, + CONFIRMED, + ASSIGNED, + REOPENED, + RESOLVED, + NEEDSINFO, + VERIFIED, + CLOSED + }; + Q_ENUM(Status) + + enum class Resolution { + Unknown, // First value is default if QMetaEnum can't map the key. + NONE, // Fake value, expresses unresoled. On the REST side this is an empty string. + FIXED, + INVALID, + WONTFIX, + LATER, + REMIND, + DUPLICATE, + WORKSFORME, + MOVED, + UPSTREAM, + DOWNSTREAM, + WAITINGFORINFO, + BACKTRACE, + UNMAINTAINED + }; + Q_ENUM(Resolution) + +private: + Q_OBJECT + Q_PROPERTY(qint64 id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString product READ product WRITE setProduct CONSTANT) + Q_PROPERTY(QString component READ component WRITE setComponent CONSTANT) + Q_PROPERTY(QString summary READ summary WRITE setSummary CONSTANT) + Q_PROPERTY(QString version READ version WRITE setVersion CONSTANT) + Q_PROPERTY(bool is_open READ is_open WRITE setIs_open CONSTANT) + // maybe should be camel mapped, who knows + Q_PROPERTY(QString op_sys READ op_sys WRITE setOp_sys CONSTANT) + Q_PROPERTY(QString priority READ priority WRITE setPriority CONSTANT) + Q_PROPERTY(QString severity READ severity WRITE setSeverity CONSTANT) + Q_PROPERTY(Status status READ status WRITE setStatus CONSTANT) + Q_PROPERTY(Resolution resolution READ resolution WRITE setResolution CONSTANT) + Q_PROPERTY(qint64 dupe_of READ dupe_of WRITE setDupe_of CONSTANT) + + // Custom fields (versionfixedin etc) are only available via customField(). + +public: + typedef QPointer Ptr; + + explicit Bug(const QVariantHash &object, QObject *parent = nullptr); + + qint64 id() const; + void setId(qint64 id); + + QVariant customField(const char *key); + + Status status() const; + void setStatus(Status status); + + Resolution resolution() const; + void setResolution(Resolution resolution); + + QString summary() const; + void setSummary(const QString &summary); + + QString version() const; + void setVersion(const QString &version); + + QString product() const; + void setProduct(const QString &product); + + QString component() const; + void setComponent(const QString &component); + + QString op_sys() const; + void setOp_sys(const QString &op_sys); + + QString priority() const; + void setPriority(const QString &priority); + + QString severity() const; + void setSeverity(const QString &severity); + + bool is_open() const; + void setIs_open(bool is_open); + + qint64 dupe_of() const; + void setDupe_of(qint64 dupe_of); + +Q_SIGNALS: + void commentsChanged(); + +private: + qint64 m_id = -1; + QString m_product; + QString m_component; + QString m_summary; + QString m_version; + bool m_is_open = false; + QString m_op_sys; + QString m_priority; + QString m_severity; + Status m_status = Status::Unknown; + Resolution m_resolution = Resolution::Unknown; + qint64 m_dupe_of = -1; +}; + +} // namespace Bugzilla + +#endif // BUG_H diff --git a/src/bugzillaintegration/libbugzilla/models/bug.cpp b/src/bugzillaintegration/libbugzilla/models/bug.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.cpp @@ -0,0 +1,186 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "bug.h" + +#include +#include +#include +#include +#include + +namespace Bugzilla { + +Bug::Bug(const QVariantHash &obj, QObject *parent) + : QObject(parent) +{ + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } + + // Enums are auto-translated from strings so long as the string is equal + // to the stringified enum key. Fail if the mapping failed. + const QString status = obj.value(QStringLiteral("status")).toString(); + if (m_status == Status::Unknown) { + // Intentionally uncategorized. Very important warning! + qWarning() << "Drkonqi status mapping failed on bug" + << id() << ":" << status + << "Please file a bug at bugs.kde.org"; + } + + const QString resolution = obj.value(QStringLiteral("resolution")).toString(); + if (resolution.isEmpty() && m_resolution == Resolution::Unknown) { + m_resolution = Resolution::NONE; + // The empty string is unresolved. This is expected and shouldn't trip + // the mapping guard. + } else if (m_resolution == Resolution::Unknown) { + // Intentionally uncategorized. Very important warning! + qWarning() << "Drkonqi resolution mapping failed on bug" + << id() << ":" << resolution + << "Please file a bug at bugs.kde.org"; + } +} + +Bug::Resolution Bug::resolution() const +{ + return m_resolution; +} + +void Bug::setResolution(Resolution resolution) +{ + m_resolution = resolution; +} + +QString Bug::summary() const +{ + return m_summary; +} + +void Bug::setSummary(const QString &summary) +{ + m_summary = summary; +} + +QString Bug::version() const +{ + return m_version; +} + +void Bug::setVersion(const QString &version) +{ + m_version = version; +} + +QString Bug::product() const +{ + return m_product; +} + +void Bug::setProduct(const QString &product) +{ + m_product = product; +} + +QString Bug::component() const +{ + return m_component; +} + +void Bug::setComponent(const QString &component) +{ + m_component = component; +} + +QString Bug::op_sys() const +{ + return m_op_sys; +} + +void Bug::setOp_sys(const QString &op_sys) +{ + m_op_sys = op_sys; +} + +QString Bug::priority() const +{ + return m_priority; +} + +void Bug::setPriority(const QString &priority) +{ + m_priority = priority; +} + +QString Bug::severity() const +{ + return m_severity; +} + +void Bug::setSeverity(const QString &severity) +{ + m_severity = severity; +} + +bool Bug::is_open() const +{ + return m_is_open; +} + +void Bug::setIs_open(bool is_open) +{ + m_is_open = is_open; +} + +qint64 Bug::dupe_of() const +{ + return m_dupe_of; +} + +void Bug::setDupe_of(qint64 dupe_of) +{ + m_dupe_of = dupe_of; +} + +qint64 Bug::id() const +{ + return m_id; +} + +void Bug::setId(qint64 id) +{ + m_id = id; +} + +QVariant Bug::customField(const char *key) +{ + return property(key); +} + +Bug::Status Bug::status() const +{ + return m_status; +} + +void Bug::setStatus(Status status) +{ + m_status = status; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/comment.h b/src/bugzillaintegration/libbugzilla/models/comment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/comment.h @@ -0,0 +1,52 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 COMMENT_H +#define COMMENT_H + +#include +#include + +namespace Bugzilla { + +class Comment : public QObject +{ + Q_OBJECT + Q_PROPERTY(int bug_id READ bug_id WRITE setBug_id CONSTANT) + Q_PROPERTY(QString text READ text WRITE setText CONSTANT) +public: + typedef QPointer Ptr; + + explicit Comment(const QVariantHash &object, QObject *parent = nullptr); + + int bug_id() const; + void setBug_id(int bug_id); + + QString text() const; + void setText(const QString &text); + +private: + int m_bug_id; + QString m_text; +}; + +} // namespace Bugzilla + +#endif // COMMENT_H diff --git a/src/bugzillaintegration/libbugzilla/models/comment.cpp b/src/bugzillaintegration/libbugzilla/models/comment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/comment.cpp @@ -0,0 +1,55 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "comment.h" + +#include + +namespace Bugzilla { + +Comment::Comment(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +int Comment::bug_id() const +{ + return m_bug_id; +} + +void Comment::setBug_id(int bug_id) +{ + m_bug_id = bug_id; +} + +QString Comment::text() const +{ + return m_text; +} + +void Comment::setText(const QString &text) +{ + m_text = text; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/logindetails.h b/src/bugzillaintegration/libbugzilla/models/logindetails.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/logindetails.h @@ -0,0 +1,36 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 LOGINDETAILS_H +#define LOGINDETAILS_H + +#include + +namespace Bugzilla { + +struct LoginDetails +{ + int id; + QString token; +}; + +} // namespace Bugzilla + +#endif // LOGINDETAILS_H diff --git a/src/bugzillaintegration/libbugzilla/models/logindetails.cpp b/src/bugzillaintegration/libbugzilla/models/logindetails.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/logindetails.cpp @@ -0,0 +1,24 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "logindetails.h" + +namespace Bugzilla { +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/product.h b/src/bugzillaintegration/libbugzilla/models/product.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/product.h @@ -0,0 +1,117 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 PRODUCT_H +#define PRODUCT_H + +#include +#include + +#include "connection.h" + +namespace Bugzilla { + +class ProductVersion : public QObject +{ + Q_OBJECT + Q_PROPERTY(int id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName CONSTANT) + Q_PROPERTY(bool active READ isActive WRITE setActive CONSTANT) +public: + int id() const { return m_id; } + QString name() const { return m_name; } + bool isActive() const { return m_active; } + + explicit ProductVersion(const QVariantHash &object, QObject *parent = nullptr); +private: + void setId(int id) { m_id = id; } + void setName(const QString &name) { m_name = name; } + void setActive(bool active) { m_active = active; } + + int m_id = -1; + QString m_name = QString(); + bool m_active = false; +}; + +class ProductComponent : public QObject +{ + Q_OBJECT + Q_PROPERTY(int id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName CONSTANT) +public: + int id() const { return m_id; } + QString name() const { return m_name; } + + explicit ProductComponent(const QVariantHash &object, QObject *parent = nullptr); +private: + void setId(int id) { m_id = id; } + void setName(const QString &name) { m_name = name; } + + int m_id = -1; + QString m_name = QString(); +}; + +class Product : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool is_active READ isActive WRITE setActive CONSTANT) + Q_PROPERTY(QList components READ components WRITE setComponents CONSTANT) + Q_PROPERTY(QList versions READ versions WRITE setVersions CONSTANT) +public: + typedef QSharedPointer Ptr; + + explicit Product(const QVariantHash &object, + const Connection &connection = Bugzilla::connection(), + QObject *parent = nullptr); + ~Product(); + + bool isActive() const; + void setActive(bool active); + + QList components() const; + void setComponents(const QList &components); + + QList versions() const; + void setVersions(const QList &versions); + + // Convenience methods to get useful content out of the + QStringList componentNames() const; + QStringList allVersions() const; + QStringList activeVersions() const; + QStringList inactiveVersions() const; + +private: + static void registerVariantConverters(); + + const Connection &m_connection; + + bool m_active = false; + QList m_components; + QList m_versions; +}; + +} // namespace Bugzilla + +Q_DECLARE_METATYPE(Bugzilla::ProductComponent *) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(Bugzilla::ProductVersion *) +Q_DECLARE_METATYPE(QList) + +#endif // PRODUCT_H diff --git a/src/bugzillaintegration/libbugzilla/models/product.cpp b/src/bugzillaintegration/libbugzilla/models/product.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/product.cpp @@ -0,0 +1,165 @@ +/* + Copyright 2019 Harald Sitter + + 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) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + 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 "product.h" + +#include + +#include +#include +#include + +namespace Bugzilla { + +Product::Product(const QVariantHash &object, const Connection &connection, QObject *parent) + : QObject(parent) + , m_connection(connection) +{ + registerVariantConverters(); + + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +QList Product::versions() const +{ + return m_versions; +} + +void Product::setVersions(const QList &versions) +{ + m_versions = versions; +} + +QList Product::components() const +{ + return m_components; +} + +void Product::setComponents(const QList &components) +{ + m_components = components; +} + +Product::~Product() +{ + qDeleteAll(m_components); + qDeleteAll(m_versions); +} + +bool Product::isActive() const +{ + return m_active; +} + +QStringList Product::componentNames() const +{ + QStringList ret; + for (const auto *component : m_components) { + ret << component->name(); + } + return ret; +} + +QStringList Product::allVersions() const +{ + QStringList ret; + for (const auto *version : m_versions) { + ret << version->name(); + } + return ret; +} + +QStringList Product::inactiveVersions() const +{ + QStringList ret; + for (const auto *version : m_versions) { + if (!version->isActive()) { + ret << version->name(); + } + } + return ret; +} + +void Product::registerVariantConverters() +{ + // The way List QVariant get converted to QList is a bit meh. + // A List variant by default only can convert to a QStringList, regardless + // of the T itself being a metatype known to QVariant. i.e. the QVariant + // may know how to iterate a QList, and it may know how to convert T, but + // it doesn't know how to put the two together into a list conversion. + // This is true for all Ts. You can have a QList, QVariant::fromValue + // that into a QVariant{QVariantList} but that variant will no longer + // convert>. + // To bridge the conversion gap we need to register custom converters which + // iterate the variant list and turn it into the relevant type. + + static bool convertersRegistered = false; + if (convertersRegistered) { + return; + } + convertersRegistered = true; + + QMetaType::registerConverter>( + [](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : qAsConst(v)) { + list.append(new ProductComponent(variant.toHash())); + } + return list; + }); + + QMetaType::registerConverter>( + [](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : qAsConst(v)) { + list.append(new ProductVersion(variant.toHash())); + } + return list; + }); +} + +void Product::setActive(bool active) +{ + m_active = active; +} + +ProductVersion::ProductVersion(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +ProductComponent::ProductComponent(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/parsebugbacktraces.h b/src/bugzillaintegration/parsebugbacktraces.h --- a/src/bugzillaintegration/parsebugbacktraces.h +++ b/src/bugzillaintegration/parsebugbacktraces.h @@ -34,7 +34,7 @@ { Q_OBJECT public: - explicit ParseBugBacktraces(const BugReport &bug, QObject *parent = nullptr); + explicit ParseBugBacktraces(const QList &comments, QObject *parent = nullptr); void parse(); @@ -56,7 +56,7 @@ private: BacktraceParser *m_parser = nullptr; - const BugReport m_bug; + const QList m_comments; QList > m_backtraces; }; diff --git a/src/bugzillaintegration/parsebugbacktraces.cpp b/src/bugzillaintegration/parsebugbacktraces.cpp --- a/src/bugzillaintegration/parsebugbacktraces.cpp +++ b/src/bugzillaintegration/parsebugbacktraces.cpp @@ -110,21 +110,18 @@ } } -ParseBugBacktraces::ParseBugBacktraces(const BugReport &bug, QObject *parent) - : QObject(parent), - m_bug(bug) +ParseBugBacktraces::ParseBugBacktraces(const QList &comments, QObject *parent) + : QObject(parent) + , m_comments(comments) { m_parser = BacktraceParser::newParser(QStringLiteral("gdb"), this); m_parser->connectToGenerator(this); } void ParseBugBacktraces::parse() { - parse(m_bug.description()); - - QStringList comments = m_bug.comments(); - foreach (const QString &comment, comments) { - parse(comment); + for (const auto &comment : m_comments) { + parse(comment->text()); } } diff --git a/src/bugzillaintegration/productmapping.h b/src/bugzillaintegration/productmapping.h --- a/src/bugzillaintegration/productmapping.h +++ b/src/bugzillaintegration/productmapping.h @@ -24,15 +24,20 @@ #include #include +#include "bugzillaintegration/libbugzilla/clients/productclient.h" + class Product; class BugzillaManager; class CrashedApplication; +/** + * Maps our crashed entity to a bugzilla product/component/version. + */ class ProductMapping: public QObject { Q_OBJECT public: - explicit ProductMapping(const CrashedApplication *, BugzillaManager *, QObject * parent = nullptr); + explicit ProductMapping(const CrashedApplication *, BugzillaManager *, QObject *parent = nullptr); QString bugzillaProduct() const; QString bugzillaComponent() const; @@ -43,25 +48,24 @@ bool bugzillaVersionDisabled() const; private Q_SLOTS: - void checkProductInfo(const Product &); + void checkProductInfo(const Bugzilla::Product::Ptr); private: void map(const QString&); void mapUsingInternalFile(const QString&); void getRelatedProductsUsingInternalFile(const QString&); QStringList m_relatedBugzillaProducts; - QString m_bugzillaProduct; - QString m_bugzillaComponent; + QString m_bugzillaProduct; + QString m_bugzillaComponent; - QString m_bugzillaVersionString; + QString m_bugzillaVersionString; - const CrashedApplication * m_crashedAppPtr; - BugzillaManager * m_bugzillaManagerPtr = nullptr; + const CrashedApplication *m_crashedAppPtr = nullptr; + BugzillaManager *m_bugzillaManagerPtr = nullptr; bool m_bugzillaProductDisabled; bool m_bugzillaVersionDisabled; - }; #endif diff --git a/src/bugzillaintegration/productmapping.cpp b/src/bugzillaintegration/productmapping.cpp --- a/src/bugzillaintegration/productmapping.cpp +++ b/src/bugzillaintegration/productmapping.cpp @@ -121,20 +121,20 @@ } } -void ProductMapping::checkProductInfo(const Product & product) +void ProductMapping::checkProductInfo(const Bugzilla::Product::Ptr product) { // check whether the product itself is disabled for new reports, // which usually means that product/application is unmaintained. - m_bugzillaProductDisabled = !product.isActive(); + m_bugzillaProductDisabled = !product->isActive(); // check whether the product on bugzilla contains the expected component - if (! product.components().contains(m_bugzillaComponent)) { + if (!product->componentNames().contains(m_bugzillaComponent)) { m_bugzillaComponent = QLatin1String("general"); } // find the appropriate version to use on bugzilla const QString version = m_crashedAppPtr->version(); - const QStringList& allVersions = product.allVersions(); + const QStringList &allVersions = product->allVersions(); if (allVersions.contains(version)) { //The version the crash application provided is a valid bugzilla version: use it ! @@ -149,7 +149,7 @@ // check whether that verions is disabled for new reports, which // usually means that version is outdated and not supported anymore. - const QStringList& inactiveVersions = product.inactiveVersions(); + const QStringList &inactiveVersions = product->inactiveVersions(); m_bugzillaVersionDisabled = inactiveVersions.contains(m_bugzillaVersionString); } diff --git a/src/bugzillaintegration/reportassistantpages_base.cpp b/src/bugzillaintegration/reportassistantpages_base.cpp --- a/src/bugzillaintegration/reportassistantpages_base.cpp +++ b/src/bugzillaintegration/reportassistantpages_base.cpp @@ -131,6 +131,13 @@ //BEGIN BugAwarenessPage +static QHash s_reproducibleIndex { + { 0, ReportInterface::ReproducibleUnsure }, + { 1, ReportInterface::ReproducibleNever }, + { 2, ReportInterface::ReproducibleSometimes }, + { 3, ReportInterface::ReproducibleEverytime } +}; + BugAwarenessPage::BugAwarenessPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { @@ -150,6 +157,12 @@ i18nc("@label examples about information the user can provide", "Examples: %1", reportInterface()->appDetailsExamples()->examples())); ui.m_appSpecificDetailsExamples->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); + + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + ui.m_rememberCrashSituationYes->setChecked(true); + ui.m_reproducibleBox->setCurrentIndex( + s_reproducibleIndex.key(ReportInterface::ReproducibleEverytime)); + } } void BugAwarenessPage::aboutToShow() @@ -161,24 +174,7 @@ { //Save data ReportInterface::Reproducible reproducible = ReportInterface::ReproducibleUnsure; - switch(ui.m_reproducibleBox->currentIndex()) { - case 0: { - reproducible = ReportInterface::ReproducibleUnsure; - break; - } - case 1: { - reproducible = ReportInterface::ReproducibleNever; - break; - } - case 2: { - reproducible = ReportInterface::ReproducibleSometimes; - break; - } - case 3: { - reproducible = ReportInterface::ReproducibleEverytime; - break; - } - } + reproducible = s_reproducibleIndex.value(ui.m_reproducibleBox->currentIndex()); reportInterface()->setBugAwarenessPageData(ui.m_rememberCrashSituationYes->isChecked(), reproducible, diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.h b/src/bugzillaintegration/reportassistantpages_bugzilla.h --- a/src/bugzillaintegration/reportassistantpages_bugzilla.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.h @@ -61,7 +61,6 @@ void updateWidget(bool enabled); bool kWalletEntryExists(const QString&); void openWallet(); - bool canSetCookies(); Ui::AssistantPageBugzillaLogin ui; diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp --- a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp @@ -127,7 +127,7 @@ { loginFinished(false); ui.m_statusWidget->setIdle(xi18nc("@info:status","Error when trying to login: " - "%1.", err)); + "%1", err)); if (!extendedMessage.isEmpty()) { new UnhandledErrorDialog(this, err, extendedMessage); } @@ -231,79 +231,9 @@ } } -bool BugzillaLoginPage::canSetCookies() -{ - if (bugzillaManager()->securityMethod() != BugzillaManager::UseCookies) { - qCDebug(DRKONQI_LOG) << "Bugzilla software no longer issues cookies."; - return false; - } - QDBusInterface kded(QStringLiteral("org.kde.kded5"), - QStringLiteral("/kded"), - QStringLiteral("org.kde.kded5")); - QDBusReply kcookiejarLoaded = kded.call(QStringLiteral("loadModule"), - QStringLiteral("kcookiejar")); - if (!kcookiejarLoaded.isValid()) { - KMessageBox::error(this, i18n("Failed to communicate with kded. Make sure it is running.")); - return false; - } else if (!kcookiejarLoaded.value()) { - KMessageBox::error(this, i18n("Failed to load KCookieServer. Check your KDE installation.")); - return false; - } - - - QDBusInterface kcookiejar(QStringLiteral("org.kde.kded5"), - QStringLiteral("/modules/kcookiejar"), - QStringLiteral("org.kde.KCookieServer")); - QDBusReply advice = kcookiejar.call(QStringLiteral("getDomainAdvice"), - KDE_BUGZILLA_URL); - - if (!advice.isValid()) { - KMessageBox::error(this, i18n("Failed to communicate with KCookieServer.")); - return false; - } - - qCDebug(DRKONQI_LOG) << "Got reply from KCookieServer:" << advice.value(); - - if (advice.value() == QLatin1String("Reject")) { - QString msg = i18nc("@info 1 is the bugzilla website url", - "Cookies are not allowed in your KDE network settings. In order to " - "proceed, you need to allow %1 to set cookies.", KDE_BUGZILLA_URL); - - KGuiItem yesItem = KStandardGuiItem::yes(); - yesItem.setText(i18nc("@action:button 1 is the bugzilla website url", - "Allow %1 to set cookies", KDE_BUGZILLA_URL)); - - KGuiItem noItem = KStandardGuiItem::no(); - noItem.setText(i18nc("@action:button do not allow the bugzilla website " - "to set cookies", "No, do not allow")); - - if (KMessageBox::warningYesNo(this, msg, QString(), yesItem, noItem) == KMessageBox::Yes) { - QDBusReply success = kcookiejar.call(QStringLiteral("setDomainAdvice"), - KDE_BUGZILLA_URL, - QStringLiteral("Accept")); - if (!success.isValid() || !success.value()) { - qCWarning(DRKONQI_LOG) << "Failed to set domain advice in KCookieServer"; - return false; - } else { - return true; - } - } else { - return false; - } - } - - return true; -} - void BugzillaLoginPage::loginClicked() { if (!(ui.m_userEdit->text().isEmpty() || ui.m_passwordEdit->password().isEmpty())) { - - if ((bugzillaManager()->securityMethod() == BugzillaManager::UseCookies) - && (!canSetCookies())) { - return; - } - updateWidget(false); if (ui.m_savePasswordCheckBox->checkState()==Qt::Checked) { //Wants to save data diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h @@ -1,6 +1,7 @@ /******************************************************************* * reportassistantpages_bugzilla_duplicates.h * Copyright 2009 Dario Andres Rodriguez +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -57,19 +58,16 @@ private Q_SLOTS: /* Search related methods */ void searchMore(); - void performSearch(); void stopCurrentSearch(); void markAsSearching(bool); bool canSearchMore(); - void searchFinished(const BugMapList&); + void searchFinished(const QList &); void searchError(QString); void analyzedDuplicates(KJob *job); - void resetDates(); - /* Duplicates list related methods */ void openSelectedReport(); void itemClicked(QTreeWidgetItem *, int); @@ -91,21 +89,17 @@ void informationClicked(const QString &activatedLink); private: - bool m_searching; - bool m_foundDuplicate; + bool m_searching = false; + bool m_foundDuplicate = false; - Ui::AssistantPageBugzillaDuplicates ui; + Ui::AssistantPageBugzillaDuplicates ui; - //Dates of current Results - QDate m_startDate; - QDate m_endDate; - //Dates of searching process - QDate m_searchingStartDate; - QDate m_searchingEndDate; + KGuiItem m_searchMoreGuiItem; + KGuiItem m_retrySearchGuiItem; + DuplicateFinderJob::Result m_result; - KGuiItem m_searchMoreGuiItem; - KGuiItem m_retrySearchGuiItem; - DuplicateFinderJob::Result m_result; + int m_offset = -1; + bool m_atEnd = false; }; /** Internal bug-info dialog **/ @@ -124,7 +118,10 @@ void cancelAssistant(); private Q_SLOTS: - void bugFetchFinished(BugReport,QObject *); + void bugFetchFinished(Bugzilla::Bug::Ptr bug, QObject *); + void onCommentsFetched(QList bugComments, + QObject *jobOwner); + void bugFetchError(QString, QObject *); void reloadReport(); @@ -146,6 +143,8 @@ QString m_closedStateString; int m_duplicatesCount; QPushButton* m_suggestButton; + + Bugzilla::Bug::Ptr m_bug = nullptr; }; class BugzillaReportConfirmationDialog : public QDialog diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp @@ -1,6 +1,7 @@ /******************************************************************* * reportassistantpages_bugzilla_duplicates.cpp * Copyright 2009 Dario Andres Rodriguez +* Copyright 2019 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -19,7 +20,7 @@ #include "reportassistantpages_bugzilla_duplicates.h" -#include +#include #include #include #include @@ -36,24 +37,21 @@ //BEGIN BugzillaDuplicatesPage -BugzillaDuplicatesPage::BugzillaDuplicatesPage(ReportAssistantDialog * parent): - ReportAssistantPage(parent), - m_searching(false), - m_foundDuplicate(false) +BugzillaDuplicatesPage::BugzillaDuplicatesPage(ReportAssistantDialog *parent) + : ReportAssistantPage(parent) { - resetDates(); - connect(bugzillaManager(), &BugzillaManager::searchFinished, - this, &BugzillaDuplicatesPage::searchFinished); - connect(bugzillaManager(), SIGNAL(searchError(QString)), - this, SLOT(searchError(QString))); + this, &BugzillaDuplicatesPage::searchFinished); + connect(bugzillaManager(), &BugzillaManager::searchError, + this, &BugzillaDuplicatesPage::searchError); ui.setupUi(this); ui.information->hide(); connect(ui.m_bugListWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(itemClicked(QTreeWidgetItem*,int))); - connect(ui.m_bugListWidget, &QTreeWidget::itemSelectionChanged, this, &BugzillaDuplicatesPage::itemSelectionChanged); + connect(ui.m_bugListWidget, &QTreeWidget::itemSelectionChanged, + this, &BugzillaDuplicatesPage::itemSelectionChanged); QHeaderView * header = ui.m_bugListWidget->header(); header->setSectionResizeMode(0, QHeaderView::ResizeToContents); @@ -79,8 +77,7 @@ m_searchMoreGuiItem = KGuiItem2(i18nc("@action:button", "Search for more reports"), QIcon::fromTheme(QStringLiteral("edit-find")), i18nc("@info:tooltip", "Use this button to " - "search for more similar bug reports on an " - "earlier date.")); + "search for more similar bug reports")); KGuiItem::assign(ui.m_searchMoreButton, m_searchMoreGuiItem); connect(ui.m_searchMoreButton, &QAbstractButton::clicked, this, &BugzillaDuplicatesPage::searchMore); @@ -206,38 +203,29 @@ //BEGIN Search related methods void BugzillaDuplicatesPage::searchMore() { - //1 year back - m_searchingEndDate = m_startDate; - m_searchingStartDate = m_searchingEndDate.addYears(-1); + if (m_offset < 0) { + m_offset = 0; // initialize, -1 means no search done yet + } - performSearch(); -} + // This is fairly inefficient, unfortunately the API's offset/limit system + // is not useful to us. The search is always sorting by lowest id, and + // negative offests are not a thing. So, offset=0&limit=1 gives the first + // ever reported bug in the product, while what we want is the latest. + // We also cannot query all perintent bug ids by default. While the API + // is reasonably fast, it'll still produce upwards of 2MiB just for the + // ids of a dolphin crash (as it includes all sorts of extra products). + // So we are left with somewhat shoddy time-based queries. -void BugzillaDuplicatesPage::performSearch() -{ markAsSearching(true); + ui.m_statusWidget->setBusy(i18nc("@info:status", "Searching for duplicates...")); - QString startDateStr = m_searchingStartDate.toString(QStringLiteral("yyyy-MM-dd")); - QString endDateStr = m_searchingEndDate.toString(QStringLiteral("yyyy-MM-dd")); + // Grab the default severity for newbugs + static QString severity = reportInterface()->newBugReportTemplate().severity; - ui.m_statusWidget->setBusy(i18nc("@info:status","Searching for duplicates (from %1 to %2)...", - startDateStr, endDateStr)); - - //Bugzilla will not search on Today bugs if we send the date. - //we need to send "Now" - if (m_searchingEndDate == QDate::currentDate()) { - endDateStr = QLatin1String("Now"); - } - -#if 1 - BugReport report = reportInterface()->newBugReportTemplate(); bugzillaManager()->searchBugs(reportInterface()->relatedBugzillaProducts(), - report.bugSeverity(), startDateStr, endDateStr, - reportInterface()->firstBacktraceFunctions().join(QStringLiteral(" "))); -#else //Test search - bugzillaManager()->searchBugs(QStringList() << "plasma", "crash", startDateStr, endDateStr, - "QGraphicsScenePrivate::processDirtyItemsRecursive"); -#endif + severity, + reportInterface()->firstBacktraceFunctions().join(QStringLiteral(" ")), + m_offset); } void BugzillaDuplicatesPage::stopCurrentSearch() @@ -247,12 +235,10 @@ markAsSearching(false); - if (m_startDate==m_endDate) { //Never searched - ui.m_statusWidget->setIdle(i18nc("@info:status","Search stopped.")); + if (m_offset < 0) { //Never searched + ui.m_statusWidget->setIdle(i18nc("@info:status", "Search stopped.")); } else { - ui.m_statusWidget->setIdle(i18nc("@info:status","Search stopped. Showing results from " - "%1 to %2", m_startDate.toString(QStringLiteral("yyyy-MM-dd")), - m_endDate.toString(QStringLiteral("yyyy-MM-dd")))); + ui.m_statusWidget->setIdle(i18nc("@info:status", "Search stopped. Showing results.")); } } } @@ -284,72 +270,87 @@ bool BugzillaDuplicatesPage::canSearchMore() { - return (m_startDate.year() >= 2009); + return !m_atEnd; +} + +static QString statusString(const Bugzilla::Bug::Ptr &bug) +{ + // Generate a non-geek readable status + switch(bug->status()) { + case Bugzilla::Bug::Status::UNCONFIRMED: + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return i18nc("@info bug status", "[Open]"); + + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + switch(bug->resolution()) { + case Bugzilla::Bug::Resolution::FIXED: + return i18nc("@info bug resolution", "[Fixed]"); + case Bugzilla::Bug::Resolution::WORKSFORME: + return i18nc("@info bug resolution", "[Non-reproducible]"); + case Bugzilla::Bug::Resolution::DUPLICATE: + return i18nc("@info bug resolution", "[Duplicate report]"); + case Bugzilla::Bug::Resolution::INVALID: + return i18nc("@info bug resolution", "[Invalid]"); + case Bugzilla::Bug::Resolution::UPSTREAM: + case Bugzilla::Bug::Resolution::DOWNSTREAM: + return i18nc("@info bug resolution", "[External problem]"); + case Bugzilla::Bug::Resolution::WONTFIX: + case Bugzilla::Bug::Resolution::LATER: + case Bugzilla::Bug::Resolution::REMIND: + case Bugzilla::Bug::Resolution::MOVED: + case Bugzilla::Bug::Resolution::WAITINGFORINFO: + case Bugzilla::Bug::Resolution::BACKTRACE: + case Bugzilla::Bug::Resolution::UNMAINTAINED: + case Bugzilla::Bug::Resolution::NONE: + return QStringLiteral("[%1]").arg(QVariant::fromValue(bug->resolution()).toString()); + case Bugzilla::Bug::Resolution::Unknown: + Q_FALLTHROUGH(); + } + break; + case Bugzilla::Bug::Status::NEEDSINFO: + return i18nc("@info bug status", "[Incomplete]"); + + case Bugzilla::Bug::Status::Unknown: + Q_FALLTHROUGH(); + } + return QString(); } -void BugzillaDuplicatesPage::searchFinished(const BugMapList & list) +void BugzillaDuplicatesPage::searchFinished(const QList &list) { KGuiItem::assign(ui.m_searchMoreButton, m_searchMoreGuiItem); - m_startDate = m_searchingStartDate; int results = list.count(); + m_offset += results; if (results > 0) { + m_atEnd = false; + markAsSearching(false); - ui.m_statusWidget->setIdle(i18nc("@info:status","Showing results from %1 to %2", - m_startDate.toString(QStringLiteral("yyyy-MM-dd")), - m_endDate.toString(QStringLiteral("yyyy-MM-dd")))); + ui.m_statusWidget->setIdle(i18nc("@info:status", "Showing results.")); - QList bugIds; for (int i = 0; i < results; i++) { - BugMap bug = list.at(i); - - bool ok; - int bugId = bug.value(QStringLiteral("bug_id")).toInt(&ok); - if (ok) { - bugIds << bugId; - } + Bugzilla::Bug::Ptr bug = list.at(i); - QString title; - - //Generate a non-geek readable status - QString customStatusString; - BugReport::Status status = BugReport::parseStatus(bug.value(QStringLiteral("bug_status"))); - BugReport::Resolution resolution = BugReport::parseResolution(bug.value(QStringLiteral("resolution"))); - if (BugReport::isOpen(status)) { - customStatusString = i18nc("@info bug status", "[Open]"); - } else if (BugReport::isClosed(status) && status != BugReport::NeedsInfo) { - if (resolution == BugReport::Fixed) { - customStatusString = i18nc("@info bug resolution", "[Fixed]"); - } else if (resolution == BugReport::WorksForMe) { - customStatusString = i18nc("@info bug resolution", "[Non-reproducible]"); - } else if (resolution == BugReport::Duplicate) { - customStatusString = i18nc("@info bug resolution", "[Duplicate report]"); - } else if (resolution == BugReport::Invalid) { - customStatusString = i18nc("@info bug resolution", "[Invalid]"); - } else if (resolution == BugReport::Downstream - || resolution == BugReport::Upstream) { - customStatusString = i18nc("@info bug resolution", "[External problem]"); - } - } else if (status == BugReport::NeedsInfo) { - customStatusString = i18nc("@info bug status", "[Incomplete]"); - } - - title = customStatusString + QLatin1Char(' ') + bug[QStringLiteral("short_desc")]; + QString title = statusString(bug) + QLatin1Char(' ') + bug->summary(); - QStringList fields = QStringList() << bug[QStringLiteral("bug_id")] << title; + QStringList fields = QStringList() << QString::number(bug->id()) << title; QTreeWidgetItem * item = new QTreeWidgetItem(fields); - item->setToolTip(0, bug[QStringLiteral("short_desc")]); - item->setToolTip(1, bug[QStringLiteral("short_desc")]); + item->setToolTip(0, bug->summary()); + item->setToolTip(1, bug->summary()); ui.m_bugListWidget->addTopLevelItem(item); } if (!m_foundDuplicate) { markAsSearching(true); - DuplicateFinderJob *job = new DuplicateFinderJob(bugIds, bugzillaManager(), this); - connect(job, SIGNAL(result(KJob*)), this, SLOT(analyzedDuplicates(KJob*))); + DuplicateFinderJob *job = new DuplicateFinderJob(list, bugzillaManager(), this); + connect(job, &KJob::result, this, &BugzillaDuplicatesPage::analyzedDuplicates); job->start(); } @@ -361,6 +362,7 @@ } } else { + m_atEnd = true; if (canSearchMore()) { //We don't call markAsSearching(false) to avoid flicker @@ -380,6 +382,31 @@ } } +static bool isStatusOpen(Bugzilla::Bug::Status status) +{ + switch(status) { + case Bugzilla::Bug::Status::UNCONFIRMED: + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return true; + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::NEEDSINFO: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + return false; + + case Bugzilla::Bug::Status::Unknown: + Q_FALLTHROUGH(); + } + return false; +} + +static bool isStatusClosed(Bugzilla::Bug::Status status) +{ + return !isStatusOpen(status); +} + void BugzillaDuplicatesPage::analyzedDuplicates(KJob *j) { markAsSearching(false); @@ -390,7 +417,7 @@ reportInterface()->setDuplicateId(m_result.parentDuplicate); ui.m_searchMoreButton->setEnabled(!m_foundDuplicate); ui.information->setVisible(m_foundDuplicate); - BugReport::Status status = m_result.status; + auto status = m_result.status; const int duplicate = m_result.duplicate; const int parentDuplicate = m_result.parentDuplicate; @@ -404,13 +431,13 @@ } QString text; - if (BugReport::isOpen(status) || (BugReport::isClosed(status) && status == BugReport::NeedsInfo)) { + if (isStatusOpen(status) || status == Bugzilla::Bug::Status::NEEDSINFO) { text = (parentDuplicate == duplicate ? i18nc("@label", "Your crash is a duplicate and has already been reported as Bug %1.", QString::number(duplicate)) : - i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of Bug %2", QString::number(duplicate), QString::number(parentDuplicate))) + - QLatin1Char('\n') + i18nc("@label", "Only attach if you can add needed information to the bug report.", QStringLiteral("attach")); - } else if (BugReport::isClosed(status)) { + i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of Bug %2", QString::number(duplicate), QString::number(parentDuplicate))) + + QLatin1Char('\n') + i18nc("@label", "Only attach if you can add needed information to the bug report.", QStringLiteral("attach")); + } else if (isStatusClosed(status)) { text = (parentDuplicate == duplicate ? i18nc("@label", "Your crash has already been reported as Bug %1 which has been closed.", QString::number(duplicate)) : - i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of the closed Bug %2.", QString::number(duplicate), QString::number(parentDuplicate))); + i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of the closed Bug %2.", QString::number(duplicate), QString::number(parentDuplicate))); } ui.information->setText(text); } @@ -440,11 +467,6 @@ "Please wait some time and try again.", err)); } -void BugzillaDuplicatesPage::resetDates() -{ - m_endDate = QDate::currentDate(); - m_startDate = m_endDate; -} //END Search related methods //BEGIN Duplicates list related methods @@ -588,9 +610,13 @@ //Connect bugzillalib signals connect(m_parent->bugzillaManager(), &BugzillaManager::bugReportFetched, - this, &BugzillaReportInformationDialog::bugFetchFinished); - connect(m_parent->bugzillaManager(), SIGNAL(bugReportError(QString,QObject*)), - this, SLOT(bugFetchError(QString,QObject*))); + this, &BugzillaReportInformationDialog::bugFetchFinished); + connect(m_parent->bugzillaManager(), &BugzillaManager::bugReportError, + this, &BugzillaReportInformationDialog::bugFetchError); + connect(m_parent->bugzillaManager(), &BugzillaManager::commentsFetched, + this, &BugzillaReportInformationDialog::onCommentsFetched); + connect(m_parent->bugzillaManager(), &BugzillaManager::commentsError, + this, &BugzillaReportInformationDialog::bugFetchError); KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); KWindowConfig::restoreWindowSize(windowHandle(), config); @@ -644,173 +670,225 @@ show(); } -void BugzillaReportInformationDialog::bugFetchFinished(BugReport report, QObject * jobOwner) +struct Status2 { + QString statusString; + QString closedStateString; +}; + +static Status2 statusString2(const Bugzilla::Bug::Ptr &bug) { - if (jobOwner == this && isVisible()) { - if (report.isValid()) { - - //Handle duplicate state - QString duplicate = report.markedAsDuplicateOf(); - if (!duplicate.isEmpty()) { - bool ok = false; - int dupId = duplicate.toInt(&ok); - if (ok && dupId > 0) { - ui.m_statusWidget->setIdle(QString()); - - KGuiItem yesItem = KStandardGuiItem::yes(); - yesItem.setText(i18nc("@action:button let the user to choose to read the " - "main report", "Yes, read the main report")); - - KGuiItem noItem = KStandardGuiItem::no(); - noItem.setText(i18nc("@action:button let the user choose to read the original " - "report", "No, let me read the report I selected")); - - if (KMessageBox::questionYesNo(this, - xi18nc("@info","The report you selected (bug %1) is already " - "marked as duplicate of bug %2. " - "Do you want to read that report instead? (recommended)", - report.bugNumber(), QString::number(dupId)), - i18nc("@title:window","Nested duplicate detected"), yesItem, noItem) - == KMessageBox::Yes) { - showBugReport(dupId); - return; - } - } + // Generate a non-geek readable status + switch(bug->status()) { + case Bugzilla::Bug::Status::UNCONFIRMED: + return { i18nc("@info bug status", "Opened (Unconfirmed)"), QString() }; + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return { i18nc("@info bug status", "Opened (Unfixed)"), QString() }; + + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + switch(bug->resolution()) { + case Bugzilla::Bug::Resolution::FIXED: { + auto fixedIn = bug->customField("cf_versionfixedin").toString(); + if (!fixedIn.isEmpty()) { + return { i18nc("@info bug resolution, fixed in version", + "Fixed in version \"%1\"", + fixedIn), + i18nc("@info bug resolution, fixed by kde devs in version", + "the bug was fixed by KDE developers in version \"%1\"", + fixedIn) + }; } + return { + i18nc("@info bug resolution", "Fixed"), + i18nc("@info bug resolution", "the bug was fixed by KDE developers") + }; + } - //Generate html for comments (with proper numbering) - QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); - - QString comments; - QStringList commentList = report.comments(); - for (int i = 0; i < commentList.count(); i++) { - QString comment = commentList.at(i); - //Don't add duplicates mark comments - if (!comment.contains(duplicatesMark)) { - comment.replace(QLatin1Char('\n'), QLatin1String("
")); - comments += i18nc("comment $number to use as subtitle", "

Comment %1:

", (i+1)) - + QStringLiteral("

") + comment + QStringLiteral("


"); - //Count the inline attached crashes (DrKonqi feature) - QLatin1String attachedCrashMark = - QLatin1String("New crash information added by DrKonqi"); - if (comment.contains(attachedCrashMark)) { - m_duplicatesCount++; - } - } else { - //Count duplicate - m_duplicatesCount++; - } - } + case Bugzilla::Bug::Resolution::WORKSFORME: + return { i18nc("@info bug resolution", "Non-reproducible"), QString() }; + case Bugzilla::Bug::Resolution::DUPLICATE: + return { i18nc("@info bug resolution", "Duplicate report (Already reported before)"), QString() }; + case Bugzilla::Bug::Resolution::INVALID: + return { i18nc("@info bug resolution", "Not a valid report/crash"), QString() }; + case Bugzilla::Bug::Resolution::UPSTREAM: + case Bugzilla::Bug::Resolution::DOWNSTREAM: + return { i18nc("@info bug resolution", "Not caused by a problem in the KDE's Applications or libraries"), + i18nc("@info bug resolution", "the bug is caused by a problem in an external application or library, or by a distribution or packaging issue") }; + case Bugzilla::Bug::Resolution::WONTFIX: + case Bugzilla::Bug::Resolution::LATER: + case Bugzilla::Bug::Resolution::REMIND: + case Bugzilla::Bug::Resolution::MOVED: + case Bugzilla::Bug::Resolution::WAITINGFORINFO: + case Bugzilla::Bug::Resolution::BACKTRACE: + case Bugzilla::Bug::Resolution::UNMAINTAINED: + case Bugzilla::Bug::Resolution::NONE: + return { QVariant::fromValue(bug->resolution()).toString(), QString() }; + case Bugzilla::Bug::Resolution::Unknown: + Q_FALLTHROUGH(); + } + return {}; - //Generate a non-geek readable status - QString customStatusString; - BugReport::Status status = report.statusValue(); - BugReport::Resolution resolution = report.resolutionValue(); - if (status == BugReport::Unconfirmed) { - customStatusString = i18nc("@info bug status", "Opened (Unconfirmed)"); - } else if (report.isOpen()) { - customStatusString = i18nc("@info bug status", "Opened (Unfixed)"); - } else if (report.isClosed() && status != BugReport::NeedsInfo) { - QString customResolutionString; - if (resolution == BugReport::Fixed) { - if (!report.versionFixedIn().isEmpty()) { - customResolutionString = i18nc("@info bug resolution, fixed in version", - "Fixed in version \"%1\"", - report.versionFixedIn()); - m_closedStateString = i18nc("@info bug resolution, fixed by kde devs in version", - "the bug was fixed by KDE developers in version \"%1\"", - report.versionFixedIn()); - } else { - customResolutionString = i18nc("@info bug resolution", "Fixed"); - m_closedStateString = i18nc("@info bug resolution", "the bug was fixed by KDE developers"); - } - } else if (resolution == BugReport::WorksForMe) { - customResolutionString = i18nc("@info bug resolution", "Non-reproducible"); - } else if (resolution == BugReport::Duplicate) { - customResolutionString = i18nc("@info bug resolution", "Duplicate report " - "(Already reported before)"); - } else if (resolution == BugReport::Invalid) { - customResolutionString = i18nc("@info bug resolution", "Not a valid report/crash"); - } else if (resolution == BugReport::Downstream || resolution == BugReport::Upstream) { - customResolutionString = i18nc("@info bug resolution", "Not caused by a problem " - "in the KDE's Applications or libraries"); - m_closedStateString = i18nc("@info bug resolution", "the bug is caused by a " - "problem in an external application or library, or " - "by a distribution or packaging issue"); - } else { - customResolutionString = report.resolution(); - } - - customStatusString = i18nc("@info bug status, %1 is the resolution", "Closed (%1)", - customResolutionString); - } else if (status == BugReport::NeedsInfo) { - customStatusString = i18nc("@info bug status", "Temporarily closed, because of a lack " - "of information"); - } else { //Fallback to other raw values - customStatusString = QStringLiteral("%1 (%2)").arg(report.bugStatus(), report.resolution()); - } + case Bugzilla::Bug::Status::NEEDSINFO: + return { i18nc("@info bug status", "Temporarily closed, because of a lack of information"), QString() }; - //Generate notes - QString notes = xi18n("

The bug report's title is often written by its reporter " - "and may not reflect the bug's nature, root cause or other visible " - "symptoms you could use to compare to your crash. Please read the " - "complete report and all the comments below.

"); - - if (m_duplicatesCount >= 10) { //Consider a possible mass duplicate crash - notes += xi18np("

This bug report has %1 duplicate report. That means this " - "is probably a common crash. Please consider only " - "adding a comment or a note if you can provide new valuable " - "information which was not already mentioned.

", - "

This bug report has %1 duplicate reports. That means this " - "is probably a common crash. Please consider only " - "adding a comment or a note if you can provide new valuable " - "information which was not already mentioned.

", - m_duplicatesCount); - } + case Bugzilla::Bug::Status::Unknown: + Q_FALLTHROUGH(); + } + return {}; +} - //A manually entered bug ID could represent a normal bug - if (report.bugSeverity() != QLatin1String("crash") - && report.bugSeverity() != QLatin1String("major") - && report.bugSeverity() != QLatin1String("grave") - && report.bugSeverity() != QLatin1String("critical")) - { - notes += xi18n("

This bug report is not about a crash or about any other " - "critical bug.

"); - } +void BugzillaReportInformationDialog::bugFetchFinished(Bugzilla::Bug::Ptr bug, QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + return; + } - //Generate HTML text - QString text = - i18nc("@info bug report title (quoted)", - "

\"%1\"

", report.shortDescription()) + - notes + - i18nc("@info bug report status", - "

Bug Report Status: %1

", customStatusString) + - i18nc("@info bug report product and component", - "

Affected Component: %1 (%2)

", - report.product(), report.component()) + - i18nc("@info bug report description", - "

Description of the bug

%1

", - report.description().replace(QLatin1Char('\n'), QLatin1String("
"))); - - if (!comments.isEmpty()) { - text += i18nc("@label:textbox bug report comments (already formatted)", - "

Additional Comments

%1", comments); - } + if (!bug) { + bugFetchError(i18nc("@info", "Invalid report information (malformed data). This could " + "mean that the bug report does not exist, or the bug tracking site " + "is experiencing a problem."), this); + return; + } + + Q_ASSERT(!m_bug); // m_bug must only be set once we've selected one! + + // Handle duplicate state + if (bug->dupe_of() > 0) { + ui.m_statusWidget->setIdle(QString()); + + KGuiItem yesItem = KStandardGuiItem::yes(); + yesItem.setText(i18nc("@action:button let the user to choose to read the " + "main report", "Yes, read the main report")); + + KGuiItem noItem = KStandardGuiItem::no(); + noItem.setText(i18nc("@action:button let the user choose to read the original " + "report", "No, let me read the report I selected")); + + auto ret = KMessageBox::questionYesNo( + this, + xi18nc("@info","The report you selected (bug %1) is already " + "marked as duplicate of bug %2. " + "Do you want to read that report instead? (recommended)", + bug->id(), QString::number(bug->dupe_of())), + i18nc("@title:window","Nested duplicate detected"), + yesItem, + noItem); + if (ret == KMessageBox::Yes) { + qDebug() << "REDIRECT"; + showBugReport(bug->dupe_of()); + return; + } + } + + // Process comments... + m_bug = bug; + m_parent->bugzillaManager()->fetchComments(m_bug, this); +} - ui.m_infoBrowser->setText(text); - ui.m_infoBrowser->setEnabled(true); +void BugzillaReportInformationDialog::onCommentsFetched(QList bugComments, + QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + return; + } + + Q_ASSERT(m_bug); - m_suggestButton->setEnabled(m_relatedButtonEnabled); - m_suggestButton->setVisible(m_relatedButtonEnabled); + // Generate html for comments (with proper numbering) + QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); - ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", - QString::number(report.bugNumberAsInt()))); + // TODO: the way comment objects are turned into comment strings is fairly + // awkward and does not particularly object-centric. May benefit from a + // slight redesign. + QString comments; + QString description; // aka first comment + if (bugComments.size() > 0) { + description = bugComments.takeFirst()->text(); + } + for (auto it = bugComments.constBegin(); it != bugComments.constEnd(); ++it) { + QString comment = (*it)->text(); + // Don't add duplicates mark comments + if (!comment.contains(duplicatesMark)) { + comment.replace(QLatin1Char('\n'), QLatin1String("
")); + const int i = it - bugComments.constBegin(); + comments += i18nc("comment $number to use as subtitle", "

Comment %1:

", (i+1)) + + QStringLiteral("

") + comment + QStringLiteral("


"); + // Count the inline attached crashes (DrKonqi feature) + QLatin1String attachedCrashMark = + QLatin1String("New crash information added by DrKonqi"); + if (comment.contains(attachedCrashMark)) { + m_duplicatesCount++; + } } else { - bugFetchError(i18nc("@info", "Invalid report information (malformed data). This could " - "mean that the bug report does not exist, or the bug tracking site " - "is experiencing a problem."), this); + // Count duplicate + m_duplicatesCount++; } } + + // Generate a non-geek readable status + auto str = statusString2(m_bug); + QString customStatusString = str.statusString; + m_closedStateString = str.closedStateString; + + // Generate notes + QString notes = xi18n("

The bug report's title is often written by its reporter " + "and may not reflect the bug's nature, root cause or other visible " + "symptoms you could use to compare to your crash. Please read the " + "complete report and all the comments below.

"); + + if (m_duplicatesCount >= 10) { //Consider a possible mass duplicate crash + notes += xi18np("

This bug report has %1 duplicate report. That means this " + "is probably a common crash. Please consider only " + "adding a comment or a note if you can provide new valuable " + "information which was not already mentioned.

", + "

This bug report has %1 duplicate reports. That means this " + "is probably a common crash. Please consider only " + "adding a comment or a note if you can provide new valuable " + "information which was not already mentioned.

", + m_duplicatesCount); + } + + // A manually entered bug ID could represent a normal bug + if (m_bug->severity() != QLatin1String("crash") + && m_bug->severity() != QLatin1String("major") + && m_bug->severity() != QLatin1String("grave") + && m_bug->severity() != QLatin1String("critical")) + { + notes += xi18n("

This bug report is not about a crash or about any other " + "critical bug.

"); + } + + // Generate HTML text + QString text = + i18nc("@info bug report title (quoted)", + "

\"%1\"

", m_bug->summary()) + + notes + + i18nc("@info bug report status", + "

Bug Report Status: %1

", customStatusString) + + i18nc("@info bug report product and component", + "

Affected Component: %1 (%2)

", + m_bug->product(), + m_bug->component()) + + i18nc("@info bug report description", + "

Description of the bug

%1

", + description.replace(QLatin1Char('\n'), QLatin1String("
"))); + + if (!comments.isEmpty()) { + text += i18nc("@label:textbox bug report comments (already formatted)", + "

Additional Comments

%1", comments); + } + + ui.m_infoBrowser->setText(text); + ui.m_infoBrowser->setEnabled(true); + + m_suggestButton->setEnabled(m_relatedButtonEnabled); + m_suggestButton->setVisible(m_relatedButtonEnabled); + + ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", + QString::number(m_bug->id()))); } void BugzillaReportInformationDialog::markAsDuplicate() diff --git a/src/bugzillaintegration/reportinterface.h b/src/bugzillaintegration/reportinterface.h --- a/src/bugzillaintegration/reportinterface.h +++ b/src/bugzillaintegration/reportinterface.h @@ -24,7 +24,9 @@ #include #include -class BugReport; +namespace Bugzilla { +class NewBug; +} class BugzillaManager; class ProductMapping; @@ -58,7 +60,7 @@ QString generateReportFullText(bool drKonqiStamp) const; - BugReport newBugReportTemplate() const; + Bugzilla::NewBug newBugReportTemplate() const; void sendBugReport() const; QStringList relatedBugzillaProducts() const; diff --git a/src/bugzillaintegration/reportinterface.cpp b/src/bugzillaintegration/reportinterface.cpp --- a/src/bugzillaintegration/reportinterface.cpp +++ b/src/bugzillaintegration/reportinterface.cpp @@ -261,44 +261,26 @@ return comment; } -BugReport ReportInterface::newBugReportTemplate() const +Bugzilla::NewBug ReportInterface::newBugReportTemplate() const { - //Generate a new bug report template with some values on it - BugReport report; + const SystemInformation *sysInfo = DrKonqi::systemInformation(); - const SystemInformation * sysInfo = DrKonqi::systemInformation(); - - report.setProduct(m_productMapping->bugzillaProduct()); - report.setComponent(m_productMapping->bugzillaComponent()); - report.setVersion(m_productMapping->bugzillaVersion()); - report.setOperatingSystem(sysInfo->bugzillaOperatingSystem()); + Bugzilla::NewBug bug; + bug.product = m_productMapping->bugzillaProduct(); + bug.component = m_productMapping->bugzillaComponent(); + bug.version = m_productMapping->bugzillaVersion(); + bug.op_sys = sysInfo->bugzillaOperatingSystem(); if (sysInfo->compiledSources()) { - report.setPlatform(QLatin1String("Compiled Sources")); + bug.platform = QLatin1String("Compiled Sources"); } else { - report.setPlatform(sysInfo->bugzillaPlatform()); + bug.platform = sysInfo->bugzillaPlatform(); } - report.setKeywords(QStringList() << QStringLiteral("drkonqi")); - report.setPriority(QLatin1String("NOR")); - report.setBugSeverity(QLatin1String("crash")); - - /* - Disable the backtrace functions on title for RELEASE. - It also needs a bit of polishment - - QString title = m_reportTitle; - - //If there are not too much possible duplicates by query then there are more possibilities - //that this report is unique. Let's add the backtrace functions to the title - if (m_allPossibleDuplicatesByQuery.count() <= 2) { - if (!m_firstBacktraceFunctions.isEmpty()) { - title += (QLatin1String(" [") + m_firstBacktraceFunctions.join(", ").trimmed() - + QLatin1String("]")); - } - } - */ + bug.keywords = QStringList { QStringLiteral("drkonqi") }; + bug.priority = QLatin1String("NOR"); + bug.severity = QLatin1String("crash"); + bug.summary = m_reportTitle; - report.setShortDescription(m_reportTitle); - return report; + return bug; } void ReportInterface::sendBugReport() const @@ -312,9 +294,9 @@ m_bugzillaManager->addMeToCC(m_attachToBugNumber); } else { //Creating a new bug report - BugReport report = newBugReportTemplate(); - report.setDescription(generateReportFullText(true)); - report.setValid(true); + Bugzilla::NewBug report = newBugReportTemplate(); + report.description = generateReportFullText(true); + Q_ASSERT(!report.description.isEmpty()); connect(m_bugzillaManager, &BugzillaManager::sendReportErrorInvalidValues, this, &ReportInterface::sendUsingDefaultProduct); connect(m_bugzillaManager, &BugzillaManager::reportSent, this, &ReportInterface::reportSent); @@ -327,13 +309,12 @@ { //Fallback function: if some of the custom values fail, we need to reset all the fields to the default //(and valid) bugzilla values; and try to resend - BugReport report = newBugReportTemplate(); - report.setProduct(QLatin1String("kde")); - report.setComponent(QLatin1String("general")); - report.setPlatform(QLatin1String("unspecified")); - report.setDescription(generateReportFullText(true)); - report.setValid(true); - m_bugzillaManager->sendReport(report); + Bugzilla::NewBug bug = newBugReportTemplate(); + bug.product = QLatin1String("kde"); + bug.component = QLatin1String("general"); + bug.platform = QLatin1String("unspecified"); + bug.description = generateReportFullText(true); + m_bugzillaManager->sendReport(bug); } void ReportInterface::addedToCC() diff --git a/src/drkonqi.cpp b/src/drkonqi.cpp --- a/src/drkonqi.cpp +++ b/src/drkonqi.cpp @@ -336,13 +336,18 @@ bool DrKonqi::ignoreQuality() { - return qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY"); + static bool ignore = qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY") || + qEnvironmentVariableIsSet("DRKONQI_TEST_MODE"); + return ignore; } const QString &DrKonqi::kdeBugzillaURL() { - // NB: for practical reasons this cannot use the shared instance. Initing the instances requires - // knowing the URL already, so we'd have an init loop. Use a local static instead. + // WARNING: for practical reasons this cannot use the shared instance + // Initing the instances requires knowing the URL already, so we'd have + // an init loop. Use a local static instead. Otherwise we'd crash on + // initialization of global statics derived from our return value. + // Always copy into the local static and return that! static QString url; if (!url.isEmpty()) { return url; @@ -353,6 +358,11 @@ return url; } - url = QStringLiteral("https://bugs.kde.org/"); + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + url = QStringLiteral("https://bugstest.kde.org/"); + } else { + url = QStringLiteral("https://bugs.kde.org/"); + } + return url; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,18 +1,34 @@ +if(NOT BUILD_TESTING) + # Skip everything. Particularly trying to look for integration test deps. + return() +endif() + +if(BUILD_COVERAGE) + add_custom_target(coverage) + add_custom_command(TARGET coverage + COMMAND ctest + COMMAND lcov -o ${CMAKE_BINARY_DIR}/lcov.report -c -d ${CMAKE_BINARY_DIR} + COMMAND genhtml -o ${CMAKE_BINARY_DIR}/coverage ${CMAKE_BINARY_DIR}/lcov.report + COMMAND find ${CMAKE_BINARY_DIR} -iname *.gcda | xargs rm -v + DEPENDS test + ) + add_custom_target(show-coverage + COMMAND xdg-open ${CMAKE_BINARY_DIR}/coverage/index.html + DEPENDS coverage + ) +endif() + add_subdirectory(crashtest) add_subdirectory(backtraceparsertest) -if(KF5XmlRpcClient_FOUND) - add_subdirectory(bugzillalibtest) -endif() +add_subdirectory(bugzillalibtest) if(NOT APPLE) if(NOT RUBY_EXECTUABLE) find_program(RUBY_EXECUTABLE ruby) endif() if(RUBY_EXECUTABLE) execute_process(COMMAND ${RUBY_EXECUTABLE} -e "require 'atspi'" RESULT_VARIABLE RUBY_ATSPI) - execute_process(COMMAND ${RUBY_EXECUTABLE} -e "require 'xmlrpc/server'" - RESULT_VARIABLE RUBY_XMLRPC) endif() if(NOT GDB_EXECUTABLE) # Needed so drkonqi can actually trace something. @@ -41,10 +57,10 @@ if(RUBY_EXECUTABLE AND XVFB_RUN_EXECTUABLE AND ATSPI_BUS_LAUNCHER_EXECUTABLE AND ATSPI_REGISTRYD_EXECUTABLE AND GDB_EXECUTABLE - AND RUBY_ATSPI EQUAL 0 AND RUBY_XMLRPC EQUAL 0) + AND RUBY_ATSPI EQUAL 0) set(WITH_DRKONI_INTEGRATION_TESTING TRUE) add_subdirectory(integration) endif() add_feature_info(DrKonqiIntegrationTesting WITH_DRKONI_INTEGRATION_TESTING - "Needs Ruby, functional atspi and xmlrpc gems, gdb, as well as xvfb-run.") + "Needs Ruby, functional atspi gem, gdb, as well as xvfb-run.") endif() diff --git a/src/tests/bugzillalibtest/CMakeLists.txt b/src/tests/bugzillalibtest/CMakeLists.txt --- a/src/tests/bugzillalibtest/CMakeLists.txt +++ b/src/tests/bugzillalibtest/CMakeLists.txt @@ -1,4 +1,3 @@ - set(bugzillalibtest_SRCS bugzillalibtest.cpp ../../bugzillaintegration/bugzillalib.cpp @@ -9,5 +8,4 @@ add_executable(bugzillalibtest ${bugzillalibtest_SRCS}) ecm_mark_as_test(bugzillalibtest) -target_link_libraries(bugzillalibtest KF5::KIOWidgets KF5::XmlRpcClient Qt5::Xml KF5::I18n) - +target_link_libraries(bugzillalibtest KF5::KIOWidgets KF5::I18n qbugzilla) diff --git a/src/tests/bugzillalibtest/bugzillalibtest.cpp b/src/tests/bugzillalibtest/bugzillalibtest.cpp --- a/src/tests/bugzillalibtest/bugzillalibtest.cpp +++ b/src/tests/bugzillalibtest/bugzillalibtest.cpp @@ -75,35 +75,33 @@ void sendBR() { - BugReport br; - br.setValid(true); - br.setProduct(QStringLiteral("konqueror")); - br.setComponent(QStringLiteral("general")); - br.setVersion(QStringLiteral("undefined")); - br.setOperatingSystem(QStringLiteral("Linux")); - br.setPriority(QStringLiteral("NOR")); - br.setPlatform(QStringLiteral("random test")); - br.setBugSeverity(QStringLiteral("crash")); - br.setShortDescription(QStringLiteral("bla bla")); - br.setDescription(QStringLiteral("bla bla large")); + Bugzilla::NewBug br; + br.product = QStringLiteral("konqueror"); + br.component = QStringLiteral("general"); + br.version = QStringLiteral("undefined"); + br.op_sys = QStringLiteral("Linux"); + br.priority = QStringLiteral("NOR"); + br.platform = QStringLiteral("random test"); + br.severity = QStringLiteral("crash"); + br.summary = QStringLiteral("bla bla"); + br.description = QStringLiteral("bla bla large"); manager->sendReport(br); qDebug() << "Trying to send bug report"; } void sendBR2() { - BugReport br; - br.setValid(true); - br.setProduct(QStringLiteral("konqueror")); - br.setComponent(QStringLiteral("general")); - br.setVersion(QStringLiteral("undefined")); - br.setOperatingSystem(QStringLiteral("Linux")); - br.setPriority(QStringLiteral("NOR")); - br.setPlatform(QStringLiteral("unspecified")); - br.setBugSeverity(QStringLiteral("crash")); - br.setShortDescription(QStringLiteral("bla bla")); - br.setDescription(QStringLiteral("bla bla large")); + Bugzilla::NewBug br; + br.product = QStringLiteral("konqueror"); + br.component = QStringLiteral("general"); + br.version = QStringLiteral("undefined"); + br.op_sys = QStringLiteral("Linux"); + br.priority = QStringLiteral("NOR"); + br.platform = QStringLiteral("unspecified"); + br.severity = QStringLiteral("crash"); + br.summary = QStringLiteral("bla bla"); + br.description = QStringLiteral("bla bla large"); manager->sendReport(br); qDebug() << "Trying to send bug report"; diff --git a/src/tests/integration/data/bugs b/src/tests/integration/data/bugs new file mode 100644 --- /dev/null +++ b/src/tests/integration/data/bugs @@ -0,0 +1,67 @@ +{ + "bugs" : [ + { + "alias" : [], + "assigned_to" : "dolphin-bugs-null@kde.org", + "assigned_to_detail" : { + "email" : "dolphin-bugs-null@kde.org", + "id" : 168329, + "name" : "dolphin-bugs-null@kde.org", + "real_name" : "Dolphin Bug Assignee" + }, + "blocks" : [], + "cc" : [ + "elvis.angelaccio@kde.org" + ], + "cc_detail" : [ + { + "email" : "elvis.angelaccio@kde.org", + "id" : 183313, + "name" : "elvis.angelaccio@kde.org", + "real_name" : "Elvis Angelaccio" + } + ], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2017-01-16T21:57:27Z", + "creator" : "j.peter0123@gmail.com", + "creator_detail" : { + "email" : "j.peter0123@gmail.com", + "id" : 199606, + "name" : "j.peter0123@gmail.com", + "real_name" : "Jung Péter" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 375161, + "is_cc_accessible" : null, + "is_confirmed" : null, + "is_creator_accessible" : null, + "is_open" : null, + "keywords" : [ + "drkonqi" + ], + "last_change_time" : "2017-10-28T09:58:26Z", + "op_sys" : "Linux", + "platform" : "openSUSE RPMs", + "priority" : "NOR", + "product" : "dolphin", + "qa_contact" : "", + "resolution" : "", + "see_also" : [], + "severity" : "crash", + "status" : "UNCONFIRMED", + "summary" : "Dolphin crash, copy from Samba share", + "target_milestone" : "---", + "url" : "", + "version" : "16.08.2", + "whiteboard" : "" + } + ], + "faults" : [] +} diff --git a/src/tests/integration/data/comments b/src/tests/integration/data/comments new file mode 100644 --- /dev/null +++ b/src/tests/integration/data/comments @@ -0,0 +1,48 @@ + + +{ + "bugs" : { + "375161" : { + "comments" : [ + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 0, + "creation_time" : "2017-01-16T21:57:27Z", + "creator" : "j.peter0123@gmail.com", + "id" : 1654770, + "is_private" : null, + "tags" : [], + "text" : "Application: dolphin (16.08.2)\n\nQt Version: 5.6.1\nFrameworks Version: 5.26.0\nOperating System: Linux 4.4.36-8-default x86_64\nDistribution: \"openSUSE Leap 42.2\"\n\n-- Information about the crash:\n- What I was doing when the application crashed:\nI had copyed files from Samba share (with drag and drop methode).\n\n-- Backtrace:\nApplication: Dolphin (dolphin), signal: Segmentation fault\nUsing host libthread_db library \"/lib64/libthread_db.so.1\".\n[Current thread is 1 (Thread 0x7f03846b5900 (LWP 4820))]\n\nThread 4 (Thread 0x7f035bfff700 (LWP 4823)):\n#0 0x00007f0383f8951d in read () at /lib64/libc.so.6\n#1 0x00007f037430b073 in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#2 0x00007f0378dbb670 in () at /usr/lib64/libglib-2.0.so.0\n#3 0x00007f0378d7ae49 in g_main_context_check () at /usr/lib64/libglib-2.0.so.0\n#4 0x00007f0378d7b2a8 in () at /usr/lib64/libglib-2.0.so.0\n#5 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#6 0x00007f037e26932b in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#7 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#8 0x00007f037e051f1a in QThread::exec() () at /usr/lib64/libQt5Core.so.5\n#9 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#10 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#11 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 3 (Thread 0x7f036a0f2700 (LWP 4822)):\n#0 0x00007f0383f8951d in read () at /lib64/libc.so.6\n#1 0x00007f037430b073 in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#2 0x00007f0378dbb670 in () at /usr/lib64/libglib-2.0.so.0\n#3 0x00007f0378d7ae49 in g_main_context_check () at /usr/lib64/libglib-2.0.so.0\n#4 0x00007f0378d7b2a8 in () at /usr/lib64/libglib-2.0.so.0\n#5 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#6 0x00007f037e26932b in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#7 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#8 0x00007f037e051f1a in QThread::exec() () at /usr/lib64/libQt5Core.so.5\n#9 0x00007f037e67d1d5 in () at /usr/lib64/libQt5DBus.so.5\n#10 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#11 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#12 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 2 (Thread 0x7f036c09b700 (LWP 4821)):\n#0 0x00007f0383f8d49d in poll () at /lib64/libc.so.6\n#1 0x00007f0376c703e2 in () at /usr/lib64/libxcb.so.1\n#2 0x00007f0376c71fcf in xcb_wait_for_event () at /usr/lib64/libxcb.so.1\n#3 0x00007f036ec27839 in () at /usr/lib64/libQt5XcbQpa.so.5\n#4 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#5 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#6 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 1 (Thread 0x7f03846b5900 (LWP 4820)):\n[KCrash Handler]\n#6 0x00007f0383f28296 in malloc_usable_size () at /lib64/libc.so.6\n#7 0x00007f03783d5139 in () at /usr/X11R6/lib64/libGL.so.1\n#8 0x00007f037430b1fc in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#9 0x00007f037e2414a4 in QObjectPrivate::Connection::~Connection() () at /usr/lib64/libQt5Core.so.5\n#10 0x00007f037e246dcc in QObjectPrivate::cleanConnectionLists() () at /usr/lib64/libQt5Core.so.5\n#11 0x00007f037e246eab in QObjectPrivate::addConnection(int, QObjectPrivate::Connection*) () at /usr/lib64/libQt5Core.so.5\n#12 0x00007f037e2475c6 in () at /usr/lib64/libQt5Core.so.5\n#13 0x00007f037e247d57 in QObject::connect(QObject const*, char const*, QObject const*, char const*, Qt::ConnectionType) () at /usr/lib64/libQt5Core.so.5\n#14 0x00007f038207b54d in () at /usr/lib64/libKF5KIOCore.so.5\n#15 0x00007f038208c686 in () at /usr/lib64/libKF5KIOCore.so.5\n#16 0x00007f037e242bb1 in QMetaObject::activate(QObject*, int, int, void**) () at /usr/lib64/libQt5Core.so.5\n#17 0x00007f037e250112 in QTimer::timerEvent(QTimerEvent*) () at /usr/lib64/libQt5Core.so.5\n#18 0x00007f037e243f34 in QObject::event(QEvent*) () at /usr/lib64/libQt5Core.so.5\n#19 0x00007f037f1a0e3c in QApplicationPrivate::notify_helper(QObject*, QEvent*) () at /usr/lib64/libQt5Widgets.so.5\n#20 0x00007f037f1a549a in QApplication::notify(QObject*, QEvent*) () at /usr/lib64/libQt5Widgets.so.5\n#21 0x00007f037e218fc5 in QCoreApplication::notifyInternal2(QObject*, QEvent*) () at /usr/lib64/libQt5Core.so.5\n#22 0x00007f037e268c7e in QTimerInfoList::activateTimers() () at /usr/lib64/libQt5Core.so.5\n#23 0x00007f037e269079 in () at /usr/lib64/libQt5Core.so.5\n#24 0x00007f0378d7b134 in g_main_context_dispatch () at /usr/lib64/libglib-2.0.so.0\n#25 0x00007f0378d7b388 in () at /usr/lib64/libglib-2.0.so.0\n#26 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#27 0x00007f037e26930c in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#28 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#29 0x00007f037e21eec6 in QCoreApplication::exec() () at /usr/lib64/libQt5Core.so.5\n#30 0x00007f03842b196a in kdemain () at /usr/lib64/libkdeinit5_dolphin.so\n#31 0x00007f0383ecc6e5 in __libc_start_main () at /lib64/libc.so.6\n#32 0x0000000000400789 in _start ()\n\nReported using DrKonqi", + "time" : "2017-01-16T21:57:27Z" + }, + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 1, + "creation_time" : "2017-01-16T22:45:08Z", + "creator" : "elvis.angelaccio@kde.org", + "id" : 1654785, + "is_private" : null, + "tags" : [], + "text" : "Thanks for the report. Unfortunately the backtracke is not very useful. If you can reproduce the crash, please install debug symbols and attach a new backtrace here. See also https://community.kde.org/Dolphin/FAQ/Crashes", + "time" : "2017-01-16T22:45:08Z" + }, + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 2, + "creation_time" : "2017-10-28T09:58:26Z", + "creator" : "elvis.angelaccio@kde.org", + "id" : 1708610, + "is_private" : null, + "tags" : [], + "text" : "\n\n*** This bug has been marked as a duplicate of bug 386277 ***", + "time" : "2017-10-28T09:58:26Z" + } + ] + } + }, + "comments" : {} +} + diff --git a/src/tests/integration/data/product.dolphin b/src/tests/integration/data/product.dolphin new file mode 100644 --- /dev/null +++ b/src/tests/integration/data/product.dolphin @@ -0,0 +1,2354 @@ + + +{ + "products" : [ + { + "classification" : "Unclassified", + "components" : [ + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Filter Bar", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2107, + "is_active" : null, + "name" : "bars: filter", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Location Bar", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2105, + "is_active" : null, + "name" : "bars: location", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Status Bar", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2106, + "is_active" : null, + "name" : "bars: status", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs that do not belong to other components.", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1072, + "is_active" : null, + "name" : "general", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Folders Panel", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1815, + "is_active" : null, + "name" : "panels: folders", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Information Panel", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1816, + "is_active" : null, + "name" : "panels: information", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Places Panel", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1817, + "is_active" : null, + "name" : "panels: places", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Search Panel", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1814, + "is_active" : null, + "name" : "panels: search", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Terminal Panel", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1818, + "is_active" : null, + "name" : "panels: terminal", + "sort_key" : 0 + }, + { + "default_assigned_to" : "jr@jriddell.org", + "default_qa_contact" : "", + "description" : "Version control plugin for Bazaar", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1942, + "is_active" : null, + "name" : "plugins: bazaar", + "sort_key" : 0 + }, + { + "default_assigned_to" : "emmanuelpescosta099@gmail.com", + "default_qa_contact" : "", + "description" : "Dropbox Plugin", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1825, + "is_active" : null, + "name" : "plugins: dropbox", + "sort_key" : 0 + }, + { + "default_assigned_to" : "sebastian@sebastian-doerner.de", + "default_qa_contact" : "", + "description" : "Git Plugin", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1824, + "is_active" : null, + "name" : "plugins: git", + "sort_key" : 0 + }, + { + "default_assigned_to" : "vishesh3y@yahoo.com", + "default_qa_contact" : "", + "description" : "Mercurial Plugin", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1974, + "is_active" : null, + "name" : "plugins: mercurial", + "sort_key" : 0 + }, + { + "default_assigned_to" : "sebastian@sebastian-doerner.de", + "default_qa_contact" : "", + "description" : "Subversion Plugin", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1823, + "is_active" : null, + "name" : "plugins: svn", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs related to searching of files or metadata", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1819, + "is_active" : null, + "name" : "search", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "The split view feature", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2108, + "is_active" : null, + "name" : "split view", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Columns", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1822, + "is_active" : null, + "name" : "view-engine: columns mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Compact", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1989, + "is_active" : null, + "name" : "view-engine: compact mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Details", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1821, + "is_active" : null, + "name" : "view-engine: details mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "General view-engine issue that are not related to the view-mode", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1982, + "is_active" : null, + "name" : "view-engine: general", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Icons", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1820, + "is_active" : null, + "name" : "view-engine: icons mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Tooltip", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2109, + "is_active" : null, + "name" : "view-engine: tooltip", + "sort_key" : 0 + } + ], + "default_milestone" : "---", + "description" : "File manager", + "has_unconfirmed" : null, + "id" : 371, + "is_active" : null, + "milestones" : [ + { + "id" : 371, + "is_active" : null, + "name" : "---", + "sort_key" : 0 + } + ], + "name" : "dolphin", + "versions" : [ + { + "id" : 6326, + "is_active" : null, + "name" : "1.3", + "sort_key" : 0 + }, + { + "id" : 6327, + "is_active" : null, + "name" : "1.4", + "sort_key" : 0 + }, + { + "id" : 6328, + "is_active" : null, + "name" : "1.5", + "sort_key" : 0 + }, + { + "id" : 4407, + "is_active" : null, + "name" : "1.6.1", + "sort_key" : 0 + }, + { + "id" : 4516, + "is_active" : null, + "name" : "1.7", + "sort_key" : 0 + }, + { + "id" : 4536, + "is_active" : null, + "name" : "1.99", + "sort_key" : 0 + }, + { + "id" : 4681, + "is_active" : null, + "name" : "2.0", + "sort_key" : 0 + }, + { + "id" : 4785, + "is_active" : null, + "name" : "2.0.95", + "sort_key" : 0 + }, + { + "id" : 5033, + "is_active" : null, + "name" : "2.1", + "sort_key" : 0 + }, + { + "id" : 5462, + "is_active" : null, + "name" : "2.1.80", + "sort_key" : 0 + }, + { + "id" : 5504, + "is_active" : null, + "name" : "2.1.85", + "sort_key" : 0 + }, + { + "id" : 5605, + "is_active" : null, + "name" : "2.1.97", + "sort_key" : 0 + }, + { + "id" : 5711, + "is_active" : null, + "name" : "2.2", + "sort_key" : 0 + }, + { + "id" : 6666, + "is_active" : null, + "name" : "2.2.60", + "sort_key" : 0 + }, + { + "id" : 8071, + "is_active" : null, + "name" : "4.9.5", + "sort_key" : 0 + }, + { + "id" : 6782, + "is_active" : null, + "name" : "4.10.5", + "sort_key" : 0 + }, + { + "id" : 6745, + "is_active" : null, + "name" : "4.10.90", + "sort_key" : 0 + }, + { + "id" : 6876, + "is_active" : null, + "name" : "4.10.95", + "sort_key" : 0 + }, + { + "id" : 6909, + "is_active" : null, + "name" : "4.10.97", + "sort_key" : 0 + }, + { + "id" : 7065, + "is_active" : null, + "name" : "4.11.0", + "sort_key" : 0 + }, + { + "id" : 7177, + "is_active" : null, + "name" : "4.11.1", + "sort_key" : 0 + }, + { + "id" : 7263, + "is_active" : null, + "name" : "4.11.2", + "sort_key" : 0 + }, + { + "id" : 7370, + "is_active" : null, + "name" : "4.11.3", + "sort_key" : 0 + }, + { + "id" : 7684, + "is_active" : null, + "name" : "4.11.4", + "sort_key" : 0 + }, + { + "id" : 7951, + "is_active" : null, + "name" : "4.11.5", + "sort_key" : 0 + }, + { + "id" : 7027, + "is_active" : null, + "name" : "4.11.60", + "sort_key" : 0 + }, + { + "id" : 7446, + "is_active" : null, + "name" : "4.11.80", + "sort_key" : 0 + }, + { + "id" : 7507, + "is_active" : null, + "name" : "4.11.90", + "sort_key" : 0 + }, + { + "id" : 7561, + "is_active" : null, + "name" : "4.11.95", + "sort_key" : 0 + }, + { + "id" : 7622, + "is_active" : null, + "name" : "4.11.97", + "sort_key" : 0 + }, + { + "id" : 7758, + "is_active" : null, + "name" : "4.12.0", + "sort_key" : 0 + }, + { + "id" : 8023, + "is_active" : null, + "name" : "4.12.1", + "sort_key" : 0 + }, + { + "id" : 8109, + "is_active" : null, + "name" : "4.12.2", + "sort_key" : 0 + }, + { + "id" : 8208, + "is_active" : null, + "name" : "4.12.3", + "sort_key" : 0 + }, + { + "id" : 8315, + "is_active" : null, + "name" : "4.12.4", + "sort_key" : 0 + }, + { + "id" : 8741, + "is_active" : null, + "name" : "4.12.5", + "sort_key" : 0 + }, + { + "id" : 7823, + "is_active" : null, + "name" : "4.12.60", + "sort_key" : 0 + }, + { + "id" : 8427, + "is_active" : null, + "name" : "4.12.80", + "sort_key" : 0 + }, + { + "id" : 8482, + "is_active" : null, + "name" : "4.12.90", + "sort_key" : 0 + }, + { + "id" : 8365, + "is_active" : null, + "name" : "4.12.95", + "sort_key" : 0 + }, + { + "id" : 8366, + "is_active" : null, + "name" : "4.12.97", + "sort_key" : 0 + }, + { + "id" : 8674, + "is_active" : null, + "name" : "4.13.0", + "sort_key" : 0 + }, + { + "id" : 8847, + "is_active" : null, + "name" : "4.13.1", + "sort_key" : 0 + }, + { + "id" : 8990, + "is_active" : null, + "name" : "4.13.2", + "sort_key" : 0 + }, + { + "id" : 9260, + "is_active" : null, + "name" : "4.13.3", + "sort_key" : 0 + }, + { + "id" : 9261, + "is_active" : null, + "name" : "4.14.0", + "sort_key" : 0 + }, + { + "id" : 9437, + "is_active" : null, + "name" : "4.14.1", + "sort_key" : 0 + }, + { + "id" : 9438, + "is_active" : null, + "name" : "4.14.2", + "sort_key" : 0 + }, + { + "id" : 9439, + "is_active" : null, + "name" : "4.14.3", + "sort_key" : 0 + }, + { + "id" : 8974, + "is_active" : null, + "name" : "4.60", + "sort_key" : 0 + }, + { + "id" : 10397, + "is_active" : null, + "name" : "14.12.3", + "sort_key" : 0 + }, + { + "id" : 10390, + "is_active" : null, + "name" : "15.04.0", + "sort_key" : 0 + }, + { + "id" : 10393, + "is_active" : null, + "name" : "15.04.3", + "sort_key" : 0 + }, + { + "id" : 10386, + "is_active" : null, + "name" : "15.08.0", + "sort_key" : 0 + }, + { + "id" : 10387, + "is_active" : null, + "name" : "15.08.1", + "sort_key" : 0 + }, + { + "id" : 10388, + "is_active" : null, + "name" : "15.08.2", + "sort_key" : 0 + }, + { + "id" : 10389, + "is_active" : null, + "name" : "15.08.3", + "sort_key" : 0 + }, + { + "id" : 10506, + "is_active" : null, + "name" : "15.12.0", + "sort_key" : 0 + }, + { + "id" : 10653, + "is_active" : null, + "name" : "15.12.1", + "sort_key" : 0 + }, + { + "id" : 11976, + "is_active" : null, + "name" : "15.12.3", + "sort_key" : 0 + }, + { + "id" : 13008, + "is_active" : null, + "name" : "16.08.0", + "sort_key" : 0 + }, + { + "id" : 13009, + "is_active" : null, + "name" : "16.08.1", + "sort_key" : 0 + }, + { + "id" : 13233, + "is_active" : null, + "name" : "16.08.2", + "sort_key" : 0 + }, + { + "id" : 13414, + "is_active" : null, + "name" : "16.08.3", + "sort_key" : 0 + }, + { + "id" : 13562, + "is_active" : null, + "name" : "16.12.0", + "sort_key" : 0 + }, + { + "id" : 14201, + "is_active" : null, + "name" : "16.12.1", + "sort_key" : 0 + }, + { + "id" : 3098, + "is_active" : null, + "name" : "16.12.2", + "sort_key" : 0 + }, + { + "id" : 14202, + "is_active" : null, + "name" : "16.12.3", + "sort_key" : 0 + }, + { + "id" : 14212, + "is_active" : null, + "name" : "17.03.80", + "sort_key" : 0 + }, + { + "id" : 14292, + "is_active" : null, + "name" : "17.04.0", + "sort_key" : 0 + }, + { + "id" : 14512, + "is_active" : null, + "name" : "17.04.1", + "sort_key" : 0 + }, + { + "id" : 14640, + "is_active" : null, + "name" : "17.04.2", + "sort_key" : 0 + }, + { + "id" : 14901, + "is_active" : null, + "name" : "17.04.3", + "sort_key" : 0 + }, + { + "id" : 14951, + "is_active" : null, + "name" : "17.07.80", + "sort_key" : 0 + }, + { + "id" : 15107, + "is_active" : null, + "name" : "17.08.0", + "sort_key" : 0 + }, + { + "id" : 15293, + "is_active" : null, + "name" : "17.08.1", + "sort_key" : 0 + }, + { + "id" : 15408, + "is_active" : null, + "name" : "17.08.2", + "sort_key" : 0 + }, + { + "id" : 15754, + "is_active" : null, + "name" : "17.08.3", + "sort_key" : 0 + }, + { + "id" : 15913, + "is_active" : null, + "name" : "17.12.0", + "sort_key" : 0 + }, + { + "id" : 15977, + "is_active" : null, + "name" : "17.12.1", + "sort_key" : 0 + }, + { + "id" : 16244, + "is_active" : null, + "name" : "17.12.2", + "sort_key" : 0 + }, + { + "id" : 16542, + "is_active" : null, + "name" : "17.12.3", + "sort_key" : 0 + }, + { + "id" : 16777, + "is_active" : null, + "name" : "18.04.0", + "sort_key" : 0 + }, + { + "id" : 16956, + "is_active" : null, + "name" : "18.04.1", + "sort_key" : 0 + }, + { + "id" : 17143, + "is_active" : null, + "name" : "18.04.2", + "sort_key" : 0 + }, + { + "id" : 17319, + "is_active" : null, + "name" : "18.04.3", + "sort_key" : 0 + }, + { + "id" : 17541, + "is_active" : null, + "name" : "18.08.0", + "sort_key" : 0 + }, + { + "id" : 17676, + "is_active" : null, + "name" : "18.08.1", + "sort_key" : 0 + }, + { + "id" : 17903, + "is_active" : null, + "name" : "18.08.2", + "sort_key" : 0 + }, + { + "id" : 18095, + "is_active" : null, + "name" : "18.08.3", + "sort_key" : 0 + }, + { + "id" : 18355, + "is_active" : null, + "name" : "18.12.0", + "sort_key" : 0 + }, + { + "id" : 18435, + "is_active" : null, + "name" : "18.12.1", + "sort_key" : 0 + }, + { + "id" : 18574, + "is_active" : null, + "name" : "18.12.2", + "sort_key" : 0 + }, + { + "id" : 18828, + "is_active" : null, + "name" : "18.12.3", + "sort_key" : 0 + }, + { + "id" : 19090, + "is_active" : null, + "name" : "19.04.0", + "sort_key" : 0 + }, + { + "id" : 19179, + "is_active" : null, + "name" : "19.04.1", + "sort_key" : 0 + }, + { + "id" : 19323, + "is_active" : null, + "name" : "19.04.2", + "sort_key" : 0 + }, + { + "id" : 14216, + "is_active" : null, + "name" : "unspecified", + "sort_key" : 0 + } + ] + } + ] +} + diff --git a/src/tests/integration/duplicate_attach_test.rb b/src/tests/integration/duplicate_attach_test.rb --- a/src/tests/integration/duplicate_attach_test.rb +++ b/src/tests/integration/duplicate_attach_test.rb @@ -18,92 +18,78 @@ require_relative 'test_helper' -require 'xmlrpc/client' -require 'xmlrpc/server' - -# Monkey patch the xmlrpc server to let us handle regular GET requests. -# Drkonqi partially goes through regular bugzilla cgi's simply requesting xml -# output. -module XMLServerInterceptor - # raw xml data is here - # def process(*args) - # warn "+++ #{__method__} +++" - # p args - # warn "--- #{__method__} ---" - # super - # end - - def service(req, resp) - # Where webrick comes in with request, server rejects non-xmlrpc requests - # so we'll manually handle GET requests as necessary and forward to an - # actual bugzilla so we don't have to reimplement everything. - warn "+++ #{__method__} +++" - if req.request_method == 'GET' - if req.request_uri.path.include?('buglist.cgi') # Returns CSV. - resp.body = <<-EOF -bug_id,"bug_severity","priority","bug_status","product","short_desc","resolution" -375161,"crash","NOR","NEEDSINFO","dolphin","Dolphin crash, copy from Samba share","BACKTRACE" - EOF - return - end - - if req.request_uri.path.include?('show_bug.cgi') - uri = req.request_uri.dup - uri.host = 'bugstest.kde.org' - uri.scheme = 'https' - uri.port = nil - resp.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, uri.to_s) - return - end - end - warn "--- #{__method__} ---" - super - end -end - -class XMLRPC::Server - prepend XMLServerInterceptor +require 'json' +require 'webrick' - # Expose webrick server so we can get our port :| - # https://github.com/ruby/xmlrpc/issues/17 - attr_accessor :server +# monkey patch alias from put to get, upstream only aliases post -.- +class WEBrick::HTTPServlet::ProcHandler + alias do_PUT do_GET end class TestDuplicateAttach < ATSPITest def setup - server = XMLRPC::Server.new(0) - port = server.server.config.fetch(:Port) + server = WEBrick::HTTPServer.new(Port: 0) + + port = server.config.fetch(:Port) ENV['DRKONQI_KDE_BUGZILLA_URL'] = "http://localhost:#{port}/" @got_comment = false - server.set_default_handler do |name, args| - puts '+++ handler +++' - p name, args - if name == 'User.login' - next {"id"=>12345, "token"=>"12345-cJ5o717AbC"} - end - if name == 'Bug.update' - id = args.fetch('ids').fetch(0) - cc_to_add = args.fetch('cc').fetch('add') - next {"bugs"=>[{"last_change_time"=>DateTime.now, "id"=>id, "changes"=>{"cc"=>{"removed"=>"", "added"=>cc_to_add}}, "alias"=>[]}]} - end - if name == 'Bug.add_attachment' - # Check for garbage string from test - @got_comment = args.fetch('comment').include?('yyyyyyyyyyyyyyyy') - next { "ids" => [1234] } + server.mount_proc '/' do |req, res| + query = req.request_uri.query + + case req.request_method + when 'GET' + case req.path_info + when '/rest/version' + res.body = JSON.generate(version: '5.0.6') + next + when '/rest/product/ruby' # this is off because the product detection is a bit meh + res.body = File.read("#{__dir__}/data/product.dolphin") + next + when '/rest/login' + unless query.include?('login=xxx') && query.include?('password=yyy') + abort + end + + res.body = JSON.generate(token: '123', id: '321') + next + when '/rest/bug', '/rest/bug/375161' + res.body = File.read("#{__dir__}/data/bugs") + next + when '/rest/bug/375161/comment' + res.body = File.read("#{__dir__}/data/comments") + next + end + when 'PUT' + case req.path_info + when '/rest/bug/375161' + input = JSON.parse(req.body) + if input['cc']&.[]('add')&.include?('xxx') + res.body = JSON.generate(id: 375161) + next + end + end + when 'POST' + case req.path_info + when '/rest/bug/375161/attachment' + input = JSON.parse(req.body) + if input['comment']&.include?('yyyyyyyyy') + res.body = JSON.generate(ids: [375161]) + @got_comment = true + next + end + end end - puts '~~~ bugzilla ~~~' - # Pipe request through bugstest. - # The arguments are killing me. - client = XMLRPC::Client.new('bugstest.kde.org', '/xmlrpc.cgi', 443, - nil, nil, nil, nil, true) - bugzilla = client.call(name, *args) - p bugzilla - next bugzilla + + warn "!!!!!!!!" + res.keep_alive = false + abort "ERROR Unexpected request #{req}" end - @xml_server_thread = Thread.start { server.serve } + Thread.report_on_exception = true + @api_server_thread = Thread.start { server.start } + @api_server_thread.report_on_exception=true @tracee = fork { loop { sleep(999_999_999) } } @@ -119,8 +105,8 @@ def teardown Process.kill('KILL', @tracee) Process.waitpid2(@tracee) - @xml_server_thread.kill - @xml_server_thread.join + @api_server_thread.kill + @api_server_thread.join end def drkonqi_running? @@ -171,7 +157,8 @@ # Set pseudo login data if there are none. accessible = find_in(window, name: 'Username input') accessible.text.set_to 'xxx' if accessible.text.length <= 0 - accessible = find_in(window, name: 'Password input') + # the lineedit is in fact an element on the input. why wouldn't it be... + accessible = find_in(window, name: 'Password input').children[0] accessible.text.set_to 'yyy' if accessible.text.length <= 0 accessible = find_in(window, name: 'Login')