diff --git a/drkonqi/CMakeLists.txt b/drkonqi/CMakeLists.txt --- a/drkonqi/CMakeLists.txt +++ b/drkonqi/CMakeLists.txt @@ -17,7 +17,6 @@ add_subdirectory( data ) add_subdirectory( parser ) -add_subdirectory( tests ) if ( WIN32 ) add_subdirectory( kdbgwin ) endif () @@ -114,3 +113,7 @@ endif() install(TARGETS drkonqi DESTINATION ${KDE_INSTALL_LIBEXECDIR}) + +# Only go into tests once we have a drkonqi target so the tests can reference +# it. +add_subdirectory( tests ) diff --git a/drkonqi/aboutbugreportingdialog.cpp b/drkonqi/aboutbugreportingdialog.cpp --- a/drkonqi/aboutbugreportingdialog.cpp +++ b/drkonqi/aboutbugreportingdialog.cpp @@ -130,7 +130,7 @@ "you " "need to have an account on the KDE bug tracking system. If you do " "not have one, you can create one here: %1", - QStringLiteral(KDE_BUGZILLA_CREATE_ACCOUNT_URL)), + KDE_BUGZILLA_CREATE_ACCOUNT_URL), xi18nc("@info/rich","Then, enter your username and password and " "press the Login button. You can use this login to directly access the " "KDE bug tracking system later."), diff --git a/drkonqi/bugreportaddress.h b/drkonqi/bugreportaddress.h --- a/drkonqi/bugreportaddress.h +++ b/drkonqi/bugreportaddress.h @@ -26,13 +26,12 @@ public: inline BugReportAddress() : QString() {} inline BugReportAddress(const QString & address) - : QString(address == QLatin1String("submit@bugs.kde.org") ? - QLatin1String(KDE_BUGZILLA_URL) : address) + : QString(address == QLatin1String("submit@bugs.kde.org") ? KDE_BUGZILLA_URL : address) {} inline bool isKdeBugzilla() const { - return *this == QLatin1String(KDE_BUGZILLA_URL); + return *this == KDE_BUGZILLA_URL; } inline bool isEmail() const diff --git a/drkonqi/bugzillaintegration/reportassistantpages_base.cpp b/drkonqi/bugzillaintegration/reportassistantpages_base.cpp --- a/drkonqi/bugzillaintegration/reportassistantpages_base.cpp +++ b/drkonqi/bugzillaintegration/reportassistantpages_base.cpp @@ -104,6 +104,10 @@ BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness(); + if (DrKonqi::ignoreQuality()) { + return true; + } + if ((use == BacktraceParser::InvalidUsefulness || use == BacktraceParser::ProbablyUseless || use == BacktraceParser::Useless) && m_backtraceWidget->canInstallDebugPackages()) { if ( KMessageBox::Yes == KMessageBox::questionYesNo(this, @@ -137,6 +141,8 @@ DrKonqi::crashedApplication()->name())); connect(ui.m_rememberGroup, static_cast(&QButtonGroup::buttonClicked), this, &BugAwarenessPage::updateCheckBoxes); + // Also listen to toggle so radio buttons are covered. + connect(ui.m_rememberGroup, static_cast(&QButtonGroup::buttonToggled), this, &BugAwarenessPage::updateCheckBoxes); ui.m_appSpecificDetailsExamples->setVisible(reportInterface()->appDetailsExamples()->hasExamples()); ui.m_appSpecificDetailsExamples->setContextMenuPolicy(Qt::NoContextMenu); @@ -309,7 +315,7 @@ "Reporting Guide by clicking on the " "Help button.")); //but this guide doesn't mention bt packages? that's techbase - //->>and the help guide mention techbase page... + //->>and the help guide mention techbase page... } break; } diff --git a/drkonqi/bugzillaintegration/reportassistantpages_bugzilla.cpp b/drkonqi/bugzillaintegration/reportassistantpages_bugzilla.cpp --- a/drkonqi/bugzillaintegration/reportassistantpages_bugzilla.cpp +++ b/drkonqi/bugzillaintegration/reportassistantpages_bugzilla.cpp @@ -59,7 +59,7 @@ static const char kWalletEntryUsername[] = "username"; static const char kWalletEntryPassword[] = "password"; -static const char konquerorKWalletEntryName[] = KDE_BUGZILLA_URL "index.cgi#login"; +static QString konquerorKWalletEntryName = KDE_BUGZILLA_URL + "index.cgi#login"; static const char konquerorKWalletEntryUsername[] = "Bugzilla_login"; static const char konquerorKWalletEntryPassword[] = "Bugzilla_password"; @@ -102,7 +102,7 @@ "one, you can freely create one here. " "Please do not use disposable email accounts.", DrKonqi::crashedApplication()->bugReportAddress(), - QLatin1String(KDE_BUGZILLA_CREATE_ACCOUNT_URL))); + KDE_BUGZILLA_CREATE_ACCOUNT_URL)); } bool BugzillaLoginPage::isComplete() @@ -202,15 +202,15 @@ ui.m_passwordEdit->setText(password); } } - } else if (kWalletEntryExists(QLatin1String(konquerorKWalletEntryName))) { + } else if (kWalletEntryExists(konquerorKWalletEntryName)) { //If the DrKonqi entry is empty, but a Konqueror entry exists, use and copy it. openWallet(); if (m_wallet) { m_wallet->setFolder(KWallet::Wallet::FormDataFolder()); //Fetch Konqueror data QMap values; - m_wallet->readMap(QLatin1String(konquerorKWalletEntryName), values); + m_wallet->readMap(konquerorKWalletEntryName, values); QString username = values.value(QLatin1String(konquerorKWalletEntryUsername)); QString password = values.value(QLatin1String(konquerorKWalletEntryPassword)); @@ -256,7 +256,7 @@ QLatin1String("/modules/kcookiejar"), QLatin1String("org.kde.KCookieServer")); QDBusReply advice = kcookiejar.call(QLatin1String("getDomainAdvice"), - QLatin1String(KDE_BUGZILLA_URL)); + KDE_BUGZILLA_URL); if (!advice.isValid()) { KMessageBox::error(this, i18n("Failed to communicate with KCookieServer.")); @@ -279,9 +279,9 @@ "to set cookies", "No, do not allow")); if (KMessageBox::warningYesNo(this, msg, QString(), yesItem, noItem) == KMessageBox::Yes) { - QDBusReply success = kcookiejar.call(QLatin1String("setDomainAdvice"), - QLatin1String(KDE_BUGZILLA_URL), - QLatin1String("Accept")); + QDBusReply success = kcookiejar.call(QStringLiteral("setDomainAdvice"), + KDE_BUGZILLA_URL, + QStringLiteral("Accept")); if (!success.isValid() || !success.value()) { qWarning() << "Failed to set domain advice in KCookieServer"; return false; diff --git a/drkonqi/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp b/drkonqi/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp --- a/drkonqi/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp +++ b/drkonqi/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp @@ -938,6 +938,11 @@ connect(ui.buttonGroupProceed, SIGNAL(buttonClicked(int)), this, SLOT(checkProceed())); connect(ui.buttonGroupProceedQuestion, SIGNAL(buttonClicked(int)), this, SLOT(checkProceed())); + // Also listen to toggle so radio buttons are covered. + connect(ui.buttonGroupProceed, static_cast(&QButtonGroup::buttonToggled), + this, &BugzillaReportConfirmationDialog::checkProceed); + connect(ui.buttonGroupProceedQuestion, static_cast(&QButtonGroup::buttonToggled), + this, &BugzillaReportConfirmationDialog::checkProceed); if (!m_showProceedQuestion) { ui.proceedLabel->setEnabled(false); diff --git a/drkonqi/bugzillaintegration/reportinterface.cpp b/drkonqi/bugzillaintegration/reportinterface.cpp --- a/drkonqi/bugzillaintegration/reportinterface.cpp +++ b/drkonqi/bugzillaintegration/reportinterface.cpp @@ -54,7 +54,7 @@ } void ReportInterface::setBugAwarenessPageData(bool rememberSituation, - Reproducible reproducible, bool actions, + Reproducible reproducible, bool actions, bool unusual, bool configuration) { //Save the information the user can provide about the crash from the assistant page @@ -367,6 +367,10 @@ bool ReportInterface::isWorthReporting() const { + if (DrKonqi::ignoreQuality()) { + return true; + } + //Evaluate if the provided information is useful enough to enable the automatic report bool needToReport = false; diff --git a/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_information.ui b/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_information.ui --- a/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_information.ui +++ b/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_information.ui @@ -44,6 +44,9 @@ + + Information about the crash text + false diff --git a/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_login.ui b/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_login.ui --- a/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_login.ui +++ b/drkonqi/bugzillaintegration/ui/assistantpage_bugzilla_login.ui @@ -48,6 +48,9 @@ + + Password input + true @@ -58,6 +61,9 @@ + + Username input + true diff --git a/drkonqi/drkonqi.h b/drkonqi/drkonqi.h --- a/drkonqi/drkonqi.h +++ b/drkonqi/drkonqi.h @@ -63,6 +63,8 @@ static bool isRestarted(); static bool isKeepRunning(); static int thread(); + static bool ignoreQuality(); + static const QString &kdeBugzillaURL(); private: DrKonqi(); diff --git a/drkonqi/drkonqi.cpp b/drkonqi/drkonqi.cpp --- a/drkonqi/drkonqi.cpp +++ b/drkonqi/drkonqi.cpp @@ -327,3 +327,26 @@ { return instance()->m_thread; } + +bool DrKonqi::ignoreQuality() +{ + return qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY"); +} + +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. + static QString url; + if (!url.isEmpty()) { + return url; + } + + url = QString::fromLocal8Bit(qgetenv("DRKONQI_KDE_BUGZILLA_URL")); + if (!url.isEmpty()) { + return url; + } + + url = QStringLiteral("https://bugs.kde.org/"); + return url; +} diff --git a/drkonqi/drkonqi_globals.h b/drkonqi/drkonqi_globals.h --- a/drkonqi/drkonqi_globals.h +++ b/drkonqi/drkonqi_globals.h @@ -20,6 +20,8 @@ #include #include +#include "drkonqi.h" + /** This class provides a custom constructor to fill the "toolTip" * and "whatsThis" texts of KGuiItem with the same text. */ @@ -31,8 +33,8 @@ }; /* Urls are defined globally here, so that they can change easily */ -#define KDE_BUGZILLA_URL "https://bugs.kde.org/" -#define KDE_BUGZILLA_CREATE_ACCOUNT_URL KDE_BUGZILLA_URL "createaccount.cgi" +#define KDE_BUGZILLA_URL DrKonqi::kdeBugzillaURL() +#define KDE_BUGZILLA_CREATE_ACCOUNT_URL KDE_BUGZILLA_URL + QStringLiteral("createaccount.cgi") #define KDE_BUGZILLA_SHORT_URL "bugs.kde.org" #define TECHBASE_HOWTO_DOC "https://community.kde.org/Guidelines_and_HOWTOs/Debugging/How_to_create_useful_crash_reports#Preparing_your_KDE_packages" diff --git a/drkonqi/drkonqidialog.cpp b/drkonqi/drkonqidialog.cpp --- a/drkonqi/drkonqidialog.cpp +++ b/drkonqi/drkonqidialog.cpp @@ -42,8 +42,7 @@ #endif static const char ABOUT_BUG_REPORTING_URL[] = "#aboutbugreporting"; -static const char DRKONQI_REPORT_BUG_URL[] = - KDE_BUGZILLA_URL "enter_bug.cgi?product=drkonqi&format=guided"; +static QString DRKONQI_REPORT_BUG_URL = KDE_BUGZILLA_URL + QStringLiteral("enter_bug.cgi?product=drkonqi&format=guided"); DrKonqiDialog::DrKonqiDialog(QWidget * parent) : QDialog(parent), @@ -121,7 +120,7 @@ "to the KDE bug tracking system. Do not forget to include " "the backtrace from the Developer Information " "tab.", - QLatin1String(DRKONQI_REPORT_BUG_URL)); + DRKONQI_REPORT_BUG_URL); } else if (DrKonqi::isSafer()) { reportMessage = xi18nc("@info", "The reporting assistant is disabled because " "the crash handler dialog was started in safe mode." @@ -279,7 +278,7 @@ { if (link == QLatin1String(ABOUT_BUG_REPORTING_URL)) { showAboutBugReporting(); - } else if (link == QLatin1String(DRKONQI_REPORT_BUG_URL)) { + } else if (link == DRKONQI_REPORT_BUG_URL) { QDesktopServices::openUrl(QUrl(link)); } else if (link.startsWith(QLatin1String("http"))) { qWarning() << "unexpected link"; diff --git a/drkonqi/main.cpp b/drkonqi/main.cpp --- a/drkonqi/main.cpp +++ b/drkonqi/main.cpp @@ -100,6 +100,7 @@ QCommandLineOption keepRunningOption(QStringLiteral("keeprunning"), i18nc("@info:shell","Keep the program running and generate " "the backtrace at startup")); QCommandLineOption threadOption(QStringLiteral("thread"), i18nc("@info:shell","The of the failing thread"), QStringLiteral("threadid")); + QCommandLineOption dialogOption(QStringLiteral("dialog"), i18nc("@info:shell","Do not show a notification but launch the debug dialog directly")); parser.addOption(signalOption); parser.addOption(appNameOption); @@ -114,6 +115,7 @@ parser.addOption(restartedOption); parser.addOption(keepRunningOption); parser.addOption(threadOption); + parser.addOption(dialogOption); aboutData.setupCommandLine(&parser); parser.process(qa); @@ -131,6 +133,7 @@ DrKonqi::setRestarted(parser.isSet(restartedOption)); DrKonqi::setKeepRunning(parser.isSet(keepRunningOption)); DrKonqi::setThread(parser.value(threadOption).toInt()); + auto forceDialog = parser.isSet(dialogOption); #if HAVE_X11 const QString startupId = parser.value(startupIdOption); @@ -156,7 +159,7 @@ // if no notification service is running (eg. shell crashed, or other desktop environment) // and we didn't auto-restart the app, open DrKonqi dialog instead of showing an SNI // and emitting a desktop notification - if (!restarted && !StatusNotifier::notificationServiceRegistered()) { + if (forceDialog || (!restarted && !StatusNotifier::notificationServiceRegistered())) { openDrKonqiDialog(); } else { StatusNotifier *statusNotifier = new StatusNotifier(); diff --git a/drkonqi/tests/CMakeLists.txt b/drkonqi/tests/CMakeLists.txt --- a/drkonqi/tests/CMakeLists.txt +++ b/drkonqi/tests/CMakeLists.txt @@ -3,3 +3,30 @@ if(KF5XmlRpcClient_FOUND) add_subdirectory(bugzillalibtest) endif() + +if(NOT RUBY_EXECTUABLE) + find_program(RUBY_EXECTUABLE 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 XVFB_RUN_EXECTUABLE) + find_program(XVFB_RUN_EXECTUABLE xvfb-run) +endif() +if(NOT ATSPI_BUS_LAUNCHER_EXECUTABLE) + find_program(ATSPI_BUS_LAUNCHER_EXECUTABLE + NAMES at-spi-bus-launcher + PATHS /usr/lib/at-spi2-core/ + DOC "AT-SPI accessibility dbus launcher") +endif() + +if(RUBY_EXECUTABLE AND XVFB_RUN_EXECTUABLE AND ATSPI_BUS_LAUNCHER_EXECUTABLE + AND RUBY_ATSPI EQUAL 0 AND RUBY_XMLRPC EQUAL 0) + set(WITH_DRKONI_INTEGRATION_TESTING TRUE) +endif() +add_feature_info(DrKonqiIntegrationTesting WITH_DRKONI_INTEGRATION_TESTING + "Needs Ruby, functional atspi and xmlrpc gems, as well as xvfb-run.") +add_subdirectory(integration) diff --git a/drkonqi/tests/integration/CMakeLists.txt b/drkonqi/tests/integration/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/drkonqi/tests/integration/CMakeLists.txt @@ -0,0 +1,9 @@ +add_test(NAME drkonqi_integration_suite + COMMAND ${RUBY_EXECTUABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/suite + --drkonqi $ + --at-spi-bus-launcher ${ATSPI_BUS_LAUNCHER_EXECUTABLE}) + +# Hack to get rb files to show in qtc. +file(GLOB RUBIES suite *.rb) +add_custom_target(Rubies ALL echo SOURCES ${RUBIES}) diff --git a/drkonqi/tests/integration/duplicate_attach_test.rb b/drkonqi/tests/integration/duplicate_attach_test.rb new file mode 100644 --- /dev/null +++ b/drkonqi/tests/integration/duplicate_attach_test.rb @@ -0,0 +1,215 @@ +# Copyright (C) 2017 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 +# published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +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 + + # Expose webrick server so we can get our port :| + # https://github.com/ruby/xmlrpc/issues/17 + attr_accessor :server +end + +class TestDuplicateAttach < ATSPITest + def setup + server = XMLRPC::Server.new(0) + port = server.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] } + 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 + end + + @xml_server_thread = Thread.start { server.serve } + + @tracee = fork { loop { sleep(999_999_999) } } + + assert File.exist?(DRKONQI_PATH), "drkonqi not at #{DRKONQI_PATH}" + # Will die with our Xephyr in case of errors. + pid = spawn("#{DRKONQI_PATH} --signal 11 --pid #{@tracee} --bugaddress submit@bugs.kde.org --dialog") + sleep 4 # Grace to give time to appear on at-spi + assert_nil Process.waitpid(pid, Process::WNOHANG), + 'drkonqi failed to start or died' + end + + def teardown + Process.kill('KILL', @tracee) + Process.waitpid2(@tracee) + @xml_server_thread.kill + @xml_server_thread.join + end + + # When evaluating duplicates + def test_duplicate_attach + drkonqi = ATSPI.desktops[0].applications.find { |x| x.name == 'drkonqi' } + refute_nil drkonqi + + accessible = find_in(drkonqi.windows[-1], name: 'Report Bug') + press(accessible) + + find_in(drkonqi, name: 'Crash Reporting Assistant') do |window| + accessible = find_in(window, name: 'Next') + press(accessible) + + accessible = find_in(window, name: 'Yes') + toggle_on(accessible) + + accessible = find_in(window, name: /^What I was doing when the application.+/) + toggle_on(accessible) + + accessible = find_in(window, name: 'Next') + press(accessible) + + loop do + # Drkonqi is now doing the trace, wait until it is done. + accessible = find_in(window, name: 'Next') + refute_nil accessible + if accessible.states.include?(:sensitive) + press(accessible) + break + end + warn accessible.states + sleep 2 + end + + # 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') + accessible.text.set_to 'yyy' if accessible.text.length <= 0 + + accessible = find_in(window, name: 'Login') + press(accessible) + + sleep 2 # Wait for login and bug listing + + accessible = find_in(window, name: '375161') + toggle_on(accessible) + + accessible = find_in(window, name: 'Open selected report') + press(accessible) + end + + find_in(drkonqi, name: 'Bug Description') do |window| + accessible = find_in(window, name: 'Suggest this crash is related') + press(accessible) + end + + find_in(drkonqi, name: 'Related Bug Report') do |window| + accessible = find_in(window, name: /^Completely sure: attach my information.+/) + toggle_on(accessible) + + accessible = find_in(window, name: 'Continue') + press(accessible) + end + + find_in(drkonqi, name: 'Crash Reporting Assistant') do |window| + accessible = find_in(window, name: /^The report is going to be attached.+/) + refute_nil accessible + + accessible = find_in(window, name: 'Next') + press(accessible) + + accessible = find_in(window, name: 'Information about the crash text') + accessible.text.set_to(accessible.text.to_s + + Array.new(128).collect { 'y' }.join) + + accessible = find_in(window, name: 'Next') + press(accessible) + + accessible = find_in(window, name: 'Next') + press(accessible) + + accessible = find_in(window, name: /.*Crash report sent.*/) + refute_nil accessible + + accessible = find_in(window, name: 'Finish') + press(accessible) + end + + assert @got_comment # only true iff the server go tour yyyyyy garbage string + end +end diff --git a/drkonqi/tests/integration/suite b/drkonqi/tests/integration/suite new file mode 100755 --- /dev/null +++ b/drkonqi/tests/integration/suite @@ -0,0 +1,81 @@ +#!/usr/bin/env ruby +# +# Copyright (C) 2017 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 +# published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +require 'fileutils' +require 'optparse' +require 'tmpdir' + +# Isolates GUI via xephyr +class XephyrIsolator + def run(cmd) + ephemeral('Xephyr -screen 1024x768x24+32 :666') do + ENV['DISPLAY'] = ':666' + system(cmd) || raise + end + end + + private + + def ephemeral(*args) + pid = spawn(*args) + yield + ensure + Process.kill('KILL', pid) if pid + Process.waitpid2(pid) if pid + end +end + +# Isolates GUI via xvfb-run +class XvfbIsolator + def run(cmd) + system("xvfb-run -a --server-args=\"-screen 0 1024x768x24\" #{cmd}") || raise + end +end + +OptionParser.new do |opts| + opts.banner = "Usage: #{$0} ARGS" + + opts.separator('') + + opts.on('--drkonqi PATH', 'Path to drkonqi bin to test.') do |v| + ENV['DRKONQI_PATH'] = v + end + + opts.on('--at-spi-bus-launcher PATH', + 'Path to --at-spi-bus-launcher bin to use for testing.') do |v| + ENV['AT_SPI_BUS_LAUNCHER_PATH'] = v + end +end.parse! + +ENV['DRKONQI_PATH'] ||= '/usr/lib/x86_64-linux-gnu/libexec/drkonqi' +ENV['AT_SPI_BUS_LAUNCHER_PATH'] ||= '/usr/lib/at-spi2-core/at-spi-bus-launcher' + +# Isolate ourselves by forcing into a separate home and unsetting the XDG path +# variables. Then spin up a suitable virtual X, run a new dbus session bus +# and our test in that environment. +Dir.mktmpdir do |tmpdir| + ENV['HOME'] = tmpdir + ENV.keys.each { |k| ENV.delete(k) if k.start_with?('XDG_') } + Dir.glob("#{__dir__}/*_test.rb").each do |test| + isolator = ENV['XEPHYR'] ? XephyrIsolator.new : XvfbIsolator.new + isolator.run("dbus-run-session -- ruby #{test} -p") + end + sleep 8 # Wait a bit to make sure all children are dead. +end diff --git a/drkonqi/tests/integration/test_helper.rb b/drkonqi/tests/integration/test_helper.rb new file mode 100644 --- /dev/null +++ b/drkonqi/tests/integration/test_helper.rb @@ -0,0 +1,85 @@ +# Copyright (C) 2017 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 +# published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +ENV['DRKONQI_IGNORE_QUALITY'] = '1' +ENV['KDE_FORK_SLAVES'] = '1' + +DRKONQI_PATH = ENV['DRKONQI_PATH'] +AT_SPI_BUS_LAUNCHER_PATH = ENV['AT_SPI_BUS_LAUNCHER_PATH'] +warn "Testing against #{DRKONQI_PATH} with #{AT_SPI_BUS_LAUNCHER_PATH}" + +# Dies together with our dbus. +spawn "#{AT_SPI_BUS_LAUNCHER_PATH} --launch-immediately" + +require 'atspi' +require 'minitest/autorun' + +# Adds convenience methods for ATSPI on top of minitest. +class ATSPITest < Minitest::Test + def find_in(parent, name: nil, recursion: false) + raise 'no accessible' if parent.nil? + accessibles = parent.children.collect do |child| + ret = [] + if child.children.size != 0 # recurse + ret += find_in(child, name: name, recursion: true) + end + if name && child.states.include?(:showing) + if (name.is_a?(Regexp) && child.name.match(name)) || + (name.is_a?(String) && child.name == name) + ret << child + end + end + ret + end.compact.uniq.flatten + return accessibles if recursion + raise "not exactly one accessible for #{name} => #{accessibles.collect {|x| x.name}.join(', ')}" if accessibles.size > 1 + raise "cannot find accessible(#{name})" if accessibles.size < 1 + yield accessibles[0] if block_given? + accessibles[0] + end + + def press(accessible) + raise 'no accessible' if accessible.nil? + action = accessible.actions.find { |x| x.name == 'Press' } + refute_nil action, 'expected accessible to be pressable' + action.do_it! + sleep 0.25 + end + + def focus(accessible) + raise 'no accessible' if accessible.nil? + action = accessible.actions.find { |x| x.name == 'SetFocus' } + refute_nil action, 'expected accessible to be focusable' + action.do_it! + sleep 0.1 + end + + def toggle(accessible) + raise 'no accessible' if accessible.nil? + action = accessible.actions.find { |x| x.name == 'Toggle' } + refute_nil action, 'expected accessible to be toggle' + action.do_it! + sleep 0.1 + end + + def toggle_on(accessible) + raise 'no accessible' if accessible.nil? + return if accessible.states.any? { |x| %i[checked selected].include?(x) } + toggle(accessible) + end +end