diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,11 @@ TYPE RUNTIME ) +if (BUILD_TESTING) + include(ECMAddTests) + find_package(Qt5Test 5.5.0 CONFIG REQUIRED) +endif (BUILD_TESTING) + add_subdirectory(src) add_subdirectory(icons) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ cargoplugin.cpp cargobuildjob.cpp cargoexecutionconfig.cpp + cargofindtestsjob.cpp ${cargo_LOG_SRCS} ) @@ -23,5 +24,7 @@ KDev::OutputView ) -## Unittests -add_subdirectory( tests ) +## Unittests are only built if KDevPlatform was built with testing support +if (BUILD_TESTING) + add_subdirectory(tests) +endif (BUILD_TESTING) diff --git a/src/cargobuildjob.h b/src/cargobuildjob.h --- a/src/cargobuildjob.h +++ b/src/cargobuildjob.h @@ -70,7 +70,7 @@ QString builddir; QUrl installPrefix; QStringList runArguments; - KDevelop::CommandExecutor* exec; + KDevelop::CommandExecutor* executor; bool killed; bool enabled; KDevelop::IOutputView::StandardToolView standardViewType; diff --git a/src/cargobuildjob.cpp b/src/cargobuildjob.cpp --- a/src/cargobuildjob.cpp +++ b/src/cargobuildjob.cpp @@ -152,7 +152,7 @@ CargoBuildJob::CargoBuildJob( CargoPlugin* plugin, KDevelop::ProjectBaseItem* item, const QString& command ) : OutputJob( plugin ) , command( command) - , exec(nullptr) + , executor(nullptr) , killed( false ) , enabled( false ) { @@ -201,26 +201,26 @@ startOutput(); - exec = new KDevelop::CommandExecutor( cmd, this ); + executor = new KDevelop::CommandExecutor( cmd, this ); - exec->setArguments( arguments ); - exec->setWorkingDirectory( builddir ); + executor->setArguments( arguments ); + executor->setWorkingDirectory( builddir ); - connect( exec, &CommandExecutor::completed, this, &CargoBuildJob::procFinished ); - connect( exec, &CommandExecutor::failed, this, &CargoBuildJob::procError ); + connect( executor, &CommandExecutor::completed, this, &CargoBuildJob::procFinished ); + connect( executor, &CommandExecutor::failed, this, &CargoBuildJob::procError ); - connect( exec, &CommandExecutor::receivedStandardError, model, &OutputModel::appendLines ); - connect( exec, &CommandExecutor::receivedStandardOutput, model, &OutputModel::appendLines ); + connect( executor, &CommandExecutor::receivedStandardError, model, &OutputModel::appendLines ); + connect( executor, &CommandExecutor::receivedStandardOutput, model, &OutputModel::appendLines ); model->appendLine( QStringLiteral("%1> %2 %3").arg( builddir ).arg( cmd ).arg( KShell::joinArgs(arguments) ) ); - exec->start(); + executor->start(); } } bool CargoBuildJob::doKill() { killed = true; - exec->kill(); + executor->kill(); return true; } diff --git a/src/cargobuildjob.h b/src/cargofindtestsjob.h copy from src/cargobuildjob.h copy to src/cargofindtestsjob.h --- a/src/cargobuildjob.h +++ b/src/cargofindtestsjob.h @@ -20,8 +20,8 @@ * along with this program. If not, see . */ -#ifndef CARGOBUILDJOB_H -#define CARGOBUILDJOB_H +#ifndef CARGOFINDTESTSJOB_H +#define CARGOFINDTESTSJOB_H #include #include @@ -36,44 +36,38 @@ class IProject; } -class CargoBuildJob : public KDevelop::OutputJob + + +class CargoFindTestsJob : public KJob { Q_OBJECT public: enum ErrorType { - UndefinedBuildType = UserDefinedError, - FailedToStart, - UnknownExecError, - Crashed, - WrongArgs, - ToolDisabled, - NoCommand + TargetsDirDoesNotExist = UserDefinedError, }; - CargoBuildJob( CargoPlugin*, KDevelop::ProjectBaseItem*, const QString& command ); + CargoFindTestsJob(CargoPlugin*, KDevelop::ProjectBaseItem*); void start() override; bool doKill() override; - void setInstallPrefix(const QUrl &installPrefix) { this->installPrefix = installPrefix; } - void setRunArguments(const QStringList &arguments) { this->runArguments = arguments; } - void setStandardViewType(KDevelop::IOutputView::StandardToolView view) { this->standardViewType = view; } - private slots: - void procFinished(int); - void procError( QProcess::ProcessError ); + void procFinished(const QString& suiteName, const QString& executable, int); + void procError(const QString& suiteName, QProcess::ProcessError); private: - KDevelop::OutputModel* model(); - QString command; - QString projectName; - QString cmd; - QString environment; + void addSuiteCases(const QString& suiteName, const QStringList& lines); + void addIgnoredCases(const QString& suiteName, const QStringList& lines); + + CargoPlugin* plugin; + KDevelop::IProject* project; QString builddir; - QUrl installPrefix; - QStringList runArguments; - KDevelop::CommandExecutor* exec; + + QList executors; + int numExecutorsFinished; + QHash suiteCases; + QHash ignoredCases; + bool killed; bool enabled; - KDevelop::IOutputView::StandardToolView standardViewType; }; #endif diff --git a/src/cargofindtestsjob.cpp b/src/cargofindtestsjob.cpp new file mode 100644 --- /dev/null +++ b/src/cargofindtestsjob.cpp @@ -0,0 +1,504 @@ +/* + * This file is part of the Cargo plugin for KDevelop. + * + * Copyright 2017 Miha Čančula + * + * 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 . + */ + +#include "cargofindtestsjob.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cargoplugin.h" +#include "debug.h" + +using namespace KDevelop; + +class JobList : public KJob +{ +public: + JobList(const QList jobs) + : KJob() + , jobs(jobs) + { + KJob::Capabilities capabilities = Killable | Suspendable; + for (KJob* job : jobs) + { + capabilities &= job->capabilities(); + } + setCapabilities(capabilities); + } + + void start() override + { + for (KJob* job : jobs) + { + job->start(); + } + } + + bool doKill() override + { + bool ok = true; + for (KJob* job : jobs) + { + ok &= job->kill(); + } + return ok; + } + + bool doSuspend() override + { + bool ok = true; + for (KJob* job : jobs) + { + ok &= job->suspend(); + } + return ok; + } + + QList jobs; +}; + +class CargoTestSuite : public KDevelop::ITestSuite +{ +public: + CargoTestSuite(const QString& suiteName, const Path& executable, const QStringList& cases, const QStringList& ignoredCases, IProject* project); + virtual ~CargoTestSuite(); + + QString name() const override { return m_suiteName; } + Path executable() const { return m_executable; } + QStringList cases() const override { return m_cases; } + KDevelop::IProject * project() const override { return m_project; } + + KDevelop::IndexedDeclaration declaration() const override + { + return IndexedDeclaration(); + } + + KDevelop::IndexedDeclaration caseDeclaration(const QString & testCase) const override + { + Q_UNUSED(testCase); + return IndexedDeclaration(); + } + + bool isIgnored(const QString& caseName) + { + return m_ignoredCases.contains(caseName); + } + + KJob * launchCase(const QString & testCase, KDevelop::ITestSuite::TestJobVerbosity verbosity) override; + KJob * launchAllCases(KDevelop::ITestSuite::TestJobVerbosity verbosity) override; + KJob * launchCases(const QStringList & testCases, KDevelop::ITestSuite::TestJobVerbosity verbosity) override; + +private: + QString m_suiteName; + Path m_executable; + QStringList m_cases; + QStringList m_ignoredCases; + IProject* m_project; +}; + +CargoTestSuite::CargoTestSuite(const QString& suiteName, const KDevelop::Path& executable, const QStringList& cases, const QStringList& ignoredCases, KDevelop::IProject* project) + : m_suiteName(suiteName) + , m_executable(executable) + , m_cases(cases) + , m_ignoredCases(ignoredCases) + , m_project(project) +{} + +CargoTestSuite::~CargoTestSuite() +{} + +class CargoRunTestsJob : public KDevelop::OutputJob +{ + Q_OBJECT +public: + CargoRunTestsJob(CargoTestSuite* suite, const QString& caseName, KDevelop::ITestSuite::TestJobVerbosity verbosity) + : KDevelop::OutputJob() + , killed(false) + , suite(suite) + , caseName(caseName) + , verbosity(verbosity) + { + } + + TestResult::TestCaseResult parseResult(const QString& res) + { + if (res == QStringLiteral("ok")) + { + return TestResult::Passed; + } + else if (res == QStringLiteral("FAILED")) + { + return TestResult::Failed; + } + else if (res == QStringLiteral("ignored")) + { + return TestResult::NotRun; + } + + return TestResult::Error; + } + + void start() override + { + setStandardToolView(KDevelop::IOutputView::TestView); + QStringList arguments; + arguments << QStringLiteral("--test"); + + if (verbosity == ITestSuite::Verbose) + { + arguments << QStringLiteral("--nocapture"); + } + + if (!caseName.isEmpty()) + { + arguments << QStringLiteral("--exact") << caseName; + + if (suite->isIgnored(caseName)) + { + // When running a single test case, we also allow running ignored cases. + // They will only be skipped when running the whole suite. + arguments << QStringLiteral("--ignored"); + } + } + + setStandardToolView( KDevelop::IOutputView::TestView ); + setVerbosity( verbosity == ITestSuite::Verbose ? KDevelop::OutputJob::Verbose : KDevelop::OutputJob::Silent ); + setBehaviours( KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll ); + + KDevelop::OutputModel* model = new KDevelop::OutputModel(); + setModel( model ); + + startOutput(); + + exec = new KDevelop::CommandExecutor( suite->executable().path(), this ); + + exec->setArguments( arguments ); + + connect( exec, &CommandExecutor::completed, this, &CargoRunTestsJob::procFinished ); + connect( exec, &CommandExecutor::failed, this, &CargoRunTestsJob::procError ); + + connect( exec, &CommandExecutor::receivedStandardError, model, &OutputModel::appendLines ); + connect( exec, &CommandExecutor::receivedStandardOutput, [this, model](const QStringList& lines) { + model->appendLines(lines); + + for (auto& line : lines) + { + qCDebug(KDEV_CARGO) << "Received output line" << line; + QStringList elements = line.split(' '); + if (elements.size() == 4 && elements[0] == QStringLiteral("test")) + { + QString testCase = elements[1]; + TestResult::TestCaseResult result = parseResult(elements[3]); + + qCDebug(KDEV_CARGO) << "Received test case result" << testCase << elements[3] << result; + + caseResults.insert(testCase, result); + } + } + }); + + model->appendLine( QStringLiteral("Test %1 %2").arg( suite->name() ).arg( caseName ) ); + exec->start(); + } + + bool doKill() override + { + killed = true; + exec->kill(); + return true; + } + +private: + + void procFinished(int); + void procError(QProcess::ProcessError); + + bool killed; + CargoTestSuite* suite; + QString caseName; + ITestSuite::TestJobVerbosity verbosity; + KDevelop::CommandExecutor* exec; + QHash caseResults; +}; + + +void CargoRunTestsJob::procError( QProcess::ProcessError err ) +{ + Q_UNUSED(err); + if( !killed ) { + setError( FailedShownError ); + setErrorText( i18n( "Error running test command." ) ); + } + + TestResult result; + result.suiteResult = TestResult::Error; + ICore::self()->testController()->notifyTestRunFinished(suite, result); + + emitResult(); +} + +void CargoRunTestsJob::procFinished(int code) +{ + TestResult result; + + if (code != 0) { + setError(FailedShownError); + result.suiteResult = TestResult::Failed; + } else { + result.suiteResult = TestResult::Passed; + } + + for (auto it = caseResults.constBegin(); it != caseResults.constEnd(); ++it) + { + result.testCaseResults.insert(it.key(), it.value()); + } + + ICore::self()->testController()->notifyTestRunFinished(suite, result); + + emitResult(); +} + +KJob* CargoTestSuite::launchCases(const QStringList & testCases, KDevelop::ITestSuite::TestJobVerbosity verbosity) +{ + /* + * Rust test executable have no way of specifying a list of test cases to run. + * Either all test cases, or only a single test case can be specified at a time. + * + * To work around this, when a list of tests is specified, we run one job per test case, + * and run exactly one test case in each job. + */ + + Q_UNUSED(verbosity); + // We do not want to run multiple verbose jobs at the same time + + QList jobs; + for (auto& testCase : testCases) + { + jobs << launchCase(testCase, Silent); + } + return new JobList(jobs); +} + +KJob * CargoTestSuite::launchCase(const QString& testCase, KDevelop::ITestSuite::TestJobVerbosity verbosity) +{ + return new CargoRunTestsJob(this, testCase, verbosity); +} + +KJob * CargoTestSuite::launchAllCases(KDevelop::ITestSuite::TestJobVerbosity verbosity) +{ + return new CargoRunTestsJob(this, QString(), verbosity); +} + +CargoFindTestsJob::CargoFindTestsJob(CargoPlugin* plugin, KDevelop::ProjectBaseItem* item) + : KJob(plugin) + , plugin(plugin) + , killed(false) +{ + setCapabilities( Killable ); + + project = item->project(); + QString projectName = item->project()->name(); + builddir = plugin->buildDirectory( item ).toLocalFile(); + + QString title = i18n("Find tests for Cargo project %1", projectName); + setObjectName(title); +} + +void CargoFindTestsJob::start() +{ + QDir testDir(builddir + "/target/debug"); + + if (!testDir.exists()) + { + setError( TargetsDirDoesNotExist ); + setErrorText( i18n( "The targets directory %1 does not exist", testDir.path() ) ); + emitResult(); + return; + } + + executors.clear(); + numExecutorsFinished = 0; + + QDir::Filters filters = QDir::Files | QDir::Executable; + for (auto info : testDir.entryInfoList(filters)) + { + /* + * Test executable built by cargo are named + * -, where crate_name never includes dashes + * but may include underscores. + * + * We thus try to split each executable named into the (crate_name, hash) + * pair, then ignore the hash and use only the crate name as the + * test suite name. + */ + + QStringList fileNameParts = info.fileName().split('-'); + if (fileNameParts.size() != 2) + { + continue; + } + QString suiteName = fileNameParts.first(); + QString executable = info.absoluteFilePath(); + + auto exec = new KDevelop::CommandExecutor( executable, this ); + + qCDebug(KDEV_CARGO) << "Finding tests in executable" << executable << "in dir" << builddir << ", suite" << suiteName; + + exec->setArguments(QStringList() << QStringLiteral("--list")); + exec->setWorkingDirectory(builddir); + + connect(exec, &CommandExecutor::completed, [this, suiteName, executable](int code){ + procFinished(suiteName, executable, code); + } ); + connect(exec, &CommandExecutor::failed, [this, suiteName](QProcess::ProcessError error){ + procError(suiteName, error); + }); + + connect(exec, &CommandExecutor::receivedStandardOutput, [this, suiteName](const QStringList& output) { + addSuiteCases(suiteName, output); + }); + + exec->start(); + executors.append(exec); + + /* + * We separately track the list of ignored test cases. + * This is needed for the ability to run individual ignored test cases. + */ + + exec = new KDevelop::CommandExecutor( executable, this ); + exec->setArguments(QStringList() << QStringLiteral("--list") << QStringLiteral("--ignored")); + exec->setWorkingDirectory(builddir); + + connect(exec, &CommandExecutor::completed, [this, suiteName, executable](int code){ + procFinished(suiteName, executable, code); + } ); + connect(exec, &CommandExecutor::failed, [this, suiteName](QProcess::ProcessError error){ + procError(suiteName, error); + }); + + connect(exec, &CommandExecutor::receivedStandardOutput, [this, suiteName](const QStringList& output) { + addIgnoredCases(suiteName, output); + }); + + exec->start(); + executors.append(exec); + } +} + +bool CargoFindTestsJob::doKill() +{ + killed = true; + for (auto exec : executors) + { + exec->kill(); + } + return true; +} + +void CargoFindTestsJob::procFinished(const QString& suiteName, const QString& executable, int) +{ + qCDebug(KDEV_CARGO) << "Proc finished" << suiteName; + numExecutorsFinished++; + + if (suiteCases.contains(suiteName)) + { + QStringList all = suiteCases[suiteName]; + QStringList ignored = ignoredCases.value(suiteName); + + CargoTestSuite* suite = new CargoTestSuite(suiteName, Path(executable), all, ignored, project); + plugin->core()->testController()->addTestSuite(suite); + } + + if (numExecutorsFinished == executors.size()) + { + emitResult(); + } +} + +void CargoFindTestsJob::procError(const QString& suiteName, QProcess::ProcessError err) +{ + qCDebug(KDEV_CARGO) << "Proc error" << suiteName << err; + numExecutorsFinished++; + + if (numExecutorsFinished == executors.size()) + { + emitResult(); + } +} + +void CargoFindTestsJob::addSuiteCases(const QString& suiteName, const QStringList& lines) +{ + qCDebug(KDEV_CARGO) << "Received lines for suite" << suiteName; + + if (!suiteCases.contains(suiteName)) + { + suiteCases.insert(suiteName, QStringList()); + } + + for (const auto& line : lines) + { + QStringList elements = line.split(QStringLiteral(": ")); + if (elements.size() == 2 && elements[1] == QStringLiteral("test")) + { + qCDebug(KDEV_CARGO) << "Adding case" << elements[0] << "to suite" << suiteName; + + suiteCases[suiteName] << elements[0]; + } + } +} + +void CargoFindTestsJob::addIgnoredCases(const QString& suiteName, const QStringList& lines) +{ + qCDebug(KDEV_CARGO) << "Received ignored lines for suite" << suiteName; + + if (!ignoredCases.contains(suiteName)) + { + ignoredCases.insert(suiteName, QStringList()); + } + + for (const auto& line : lines) + { + QStringList elements = line.split(QStringLiteral(": ")); + if (elements.size() == 2 && elements[1] == QStringLiteral("test")) + { + qCDebug(KDEV_CARGO) << "Adding ignored case" << elements[0] << "to suite" << suiteName; + + ignoredCases[suiteName] << elements[0]; + } + } +} + +#include "cargofindtestsjob.moc" diff --git a/src/cargoplugin.h b/src/cargoplugin.h --- a/src/cargoplugin.h +++ b/src/cargoplugin.h @@ -30,8 +30,6 @@ #include #include -#define VERSION_5_2 ((5<<16)|(2<<8)|(0)) - class KConfigGroup; class KDialogBase; class CargoExecutionConfigType; @@ -96,6 +94,8 @@ int perProjectConfigPages() const override; KDevelop::ConfigPage* perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) override; + KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context, QWidget* parent) override; + // IExecutePlugin API QUrl executable(KDevelop::ILaunchConfiguration* config, QString& error) const override; QStringList arguments(KDevelop::ILaunchConfiguration* config, QString& error) const override; @@ -112,7 +112,11 @@ void unload() override; private: + void runBuildTestsJob(KDevelop::ProjectBaseItem* item, bool run); + CargoExecutionConfigType* m_configType; + QAction* m_buildTestsAction; + QAction* m_runTestsAction; }; #endif diff --git a/src/cargoplugin.cpp b/src/cargoplugin.cpp --- a/src/cargoplugin.cpp +++ b/src/cargoplugin.cpp @@ -33,9 +33,14 @@ #include #include #include +#include +#include +#include #include "cargobuildjob.h" +#include "cargofindtestsjob.h" #include "cargoexecutionconfig.h" +#include "debug.h" using KDevelop::ProjectTargetItem; using KDevelop::ProjectFolderItem; @@ -58,6 +63,24 @@ m_configType = new CargoExecutionConfigType(); m_configType->addLauncher( new CargoLauncher( this ) ); core()->runController()->addConfigurationType( m_configType ); + + m_buildTestsAction = new QAction(this); + m_buildTestsAction->setIcon(QIcon::fromTheme(QStringLiteral("preflight-verifier"))); + m_buildTestsAction->setText(i18n("Build Cargo Tests")); + + m_runTestsAction = new QAction(this); + m_runTestsAction->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); + m_runTestsAction->setText(i18n("Run Cargo Tests")); + + // QLoggingCategory::setFilterRules(QStringLiteral("kdevelop.projectmanagers.cargo.debug = true")); + + connect(core()->projectController(), &KDevelop::IProjectController::projectOpened, [this](IProject* project) { + if (project->buildSystemManager() == this) + { + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(this, project->projectItem()); + core()->runController()->registerJob(findTestsJob); + } + }); } CargoPlugin::~CargoPlugin() @@ -240,4 +263,57 @@ return QString(); } +KDevelop::ContextMenuExtension CargoPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) +{ + Q_UNUSED(parent); + KDevelop::ContextMenuExtension menuExt; + + if (context->hasType(KDevelop::Context::ProjectItemContext)) + { + KDevelop::ProjectItemContext* projectContext = static_cast(context); + + if (projectContext->items().size() == 1) + { + auto item = projectContext->items().first(); + if (item->isProjectRoot() && item->project()->buildSystemManager() == this) + { + m_buildTestsAction->disconnect(); + connect(m_buildTestsAction, &QAction::triggered, this, [this, item](){ + runBuildTestsJob(item, false); + }); + m_runTestsAction->disconnect(); + connect(m_runTestsAction, &QAction::triggered, this, [this, item](){ + runBuildTestsJob(item, true); + }); + menuExt.addAction(KDevelop::ContextMenuExtension::RunGroup, m_buildTestsAction); + menuExt.addAction(KDevelop::ContextMenuExtension::RunGroup, m_runTestsAction); + } + } + } + + return menuExt; +} + +void CargoPlugin::runBuildTestsJob(KDevelop::ProjectBaseItem* item, bool run) +{ + CargoBuildJob* job = new CargoBuildJob(this, item, QStringLiteral("test")); + if (run) + { + job->setRunArguments({ QStringLiteral("--all") }); + job->setStandardViewType(KDevelop::IOutputView::RunView); + } + else + { + job->setRunArguments({ QStringLiteral("--all"), QStringLiteral("--no-run") }); + job->setStandardViewType(KDevelop::IOutputView::BuildView); + } + + connect(job, &KJob::finished, [this, item](){ + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(this, item); + core()->runController()->registerJob(findTestsJob); + }); + + core()->runController()->registerJob(job); +} + #include "cargoplugin.moc" diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -0,0 +1,26 @@ +## KDevelop Plugin +set(test_cargo_SRCS + test_cargo.cpp + + ../cargoplugin.cpp + ../cargobuildjob.cpp + ../cargoexecutionconfig.cpp + ../cargofindtestsjob.cpp + ${cargo_LOG_SRCS} +) + +include_directories( + .. + ${CMAKE_CURRENT_BINARY_DIR}/.. +) + +configure_file("paths.h.cmake" "cargo-test-paths.h" ESCAPE_QUOTES) + +ki18n_wrap_ui(test_cargo_SRCS ../cargoexecutionconfig.ui) + +ecm_add_test( + ${test_cargo_SRCS} + + TEST_NAME test_cargo + LINK_LIBRARIES Qt5::Test KDev::Tests +) diff --git a/src/tests/data/kdev-cargo-test/Cargo.lock b/src/tests/data/kdev-cargo-test/Cargo.lock new file mode 100644 --- /dev/null +++ b/src/tests/data/kdev-cargo-test/Cargo.lock @@ -0,0 +1,4 @@ +[[package]] +name = "kdev-cargo-test" +version = "0.1.0" + diff --git a/src/tests/data/kdev-cargo-test/Cargo.toml b/src/tests/data/kdev-cargo-test/Cargo.toml new file mode 100644 --- /dev/null +++ b/src/tests/data/kdev-cargo-test/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "kdev-cargo-test" +version = "0.1.0" +authors = ["Miha Čančula "] + +[dependencies] diff --git a/src/tests/data/kdev-cargo-test/src/lib.rs b/src/tests/data/kdev-cargo-test/src/lib.rs new file mode 100644 --- /dev/null +++ b/src/tests/data/kdev-cargo-test/src/lib.rs @@ -0,0 +1,36 @@ +#[cfg(test)] +mod tests { + #[test] + fn passes() { + assert_eq!(2 + 2, 4); + } + + #[test] + fn fails() { + assert_eq!(2 + 2, 5); + } + + #[test] + #[should_panic] + fn should_fail_and_fails() { + assert_eq!(2 + 2, 5); + } + + #[test] + #[should_panic] + fn should_fail_and_passes() { + assert_eq!(2 + 2, 4); + } + + #[test] + #[ignore] + fn is_ignored_and_passes() { + assert_eq!(2 + 2, 4); + } + + #[test] + #[ignore] + fn is_ignored_and_fails() { + assert_eq!(2 + 2, 5); + } +} diff --git a/src/tests/paths.h.cmake b/src/tests/paths.h.cmake new file mode 100644 --- /dev/null +++ b/src/tests/paths.h.cmake @@ -0,0 +1,2 @@ +#define CARGO_TESTS_PROJECTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data" +#define CARGO_TESTS_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" diff --git a/src/tests/test_cargo.h b/src/tests/test_cargo.h new file mode 100644 --- /dev/null +++ b/src/tests/test_cargo.h @@ -0,0 +1,51 @@ +/* + * This file is part of the Cargo plugin for KDevelop. + * + * Copyright 2017 Miha Čančula + * + * 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 . + */ + +#ifndef KDEV_CARGO_TEST_H +#define KDEV_CARGO_TEST_H + +#include + +class CargoPlugin; + +class CargoPluginTest: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void cleanup(); + + void testOpenProject(); + void testBuildProject(); + void testFindTests(); + void testRunTests(); + void testRunSingleCases(); + void testRunIgnoredCases(); + +private: + CargoPlugin* m_plugin; +}; + +#endif // KDEV_CARGO_TEST_H diff --git a/src/tests/test_cargo.cpp b/src/tests/test_cargo.cpp new file mode 100644 --- /dev/null +++ b/src/tests/test_cargo.cpp @@ -0,0 +1,306 @@ +/* + * This file is part of the Cargo plugin for KDevelop. + * + * Copyright 2017 Miha Čančula + * + * 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 . + */ + +#include "test_cargo.h" +#include "cargo-test-paths.h" +#include "../cargobuildjob.h" +#include "../cargofindtestsjob.h" +#include "../cargoplugin.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace KDevelop; + +Q_DECLARE_METATYPE(KDevelop::TestResult); +Q_DECLARE_METATYPE(KDevelop::ITestSuite*); + +IProject* loadProject(const QString& name) +{ + Path path(QStringLiteral(CARGO_TESTS_PROJECTS_DIR)); + path.addPath(name); + path.addPath(name + QStringLiteral(".kdev4")); + + QSignalSpy spy(Core::self()->projectController(), &IProjectController::projectOpened); + Q_ASSERT(spy.isValid()); + + Core::self()->projectController()->openProject(path.toUrl()); + + if ( spy.isEmpty() && !spy.wait(30000) ) { + qFatal( "Timeout while waiting for opened signal" ); + } + + IProject* project = Core::self()->projectController()->findProjectByName(name); + Q_ASSERT(project); + Q_ASSERT(project->buildSystemManager()); + Q_ASSERT(project->projectFile() == path); + + return project; +} + +void CargoPluginTest::initTestCase() +{ + AutoTestShell::init(); + TestCore::initialize(); + + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + cleanup(); +} + +void CargoPluginTest::cleanupTestCase() +{ + TestCore::shutdown(); +} + +void CargoPluginTest::cleanup() +{ + QTest::qWait(200); + Core::self()->projectController()->closeAllProjects(); +} + +void CargoPluginTest::testOpenProject() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); +} + +void CargoPluginTest::testBuildProject() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); + + IBuildSystemManager* build = project->buildSystemManager(); + IProjectBuilder* builder = build->builder(); + bool built = builder->build(project->projectItem())->exec(); + QVERIFY(built); +} + +void CargoPluginTest::testFindTests() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); + + CargoPlugin* plugin = new CargoPlugin(Core::self()); + + CargoBuildJob* job = new CargoBuildJob(plugin, project->projectItem(), QStringLiteral("test")); + job->setRunArguments({ QStringLiteral("--all"), QStringLiteral("--no-run") }); + job->setStandardViewType(KDevelop::IOutputView::BuildView); + QVERIFY(static_cast(job)->exec()); + + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(plugin, project->projectItem()); + QVERIFY(findTestsJob->exec()); + + QList suites = Core::self()->testController()->testSuitesForProject(project); + QCOMPARE(suites.size(), 1); + + if (suites.size() == 1) + { + ITestSuite* suite = suites.first(); + QCOMPARE(suite->name(), QStringLiteral("kdev_cargo_test")); + + QStringList cases = suite->cases(); + QSet expectedCases = { + QStringLiteral("tests::passes"), + QStringLiteral("tests::fails"), + QStringLiteral("tests::should_fail_and_fails"), + QStringLiteral("tests::should_fail_and_passes"), + QStringLiteral("tests::is_ignored_and_passes"), + QStringLiteral("tests::is_ignored_and_fails"), + }; + QCOMPARE(suite->cases().toSet(), expectedCases); + } +} + +void CargoPluginTest::testRunTests() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); + + CargoPlugin* plugin = new CargoPlugin(Core::self()); + + CargoBuildJob* job = new CargoBuildJob(plugin, project->projectItem(), QStringLiteral("test")); + job->setRunArguments({ QStringLiteral("--all"), QStringLiteral("--no-run") }); + job->setStandardViewType(KDevelop::IOutputView::BuildView); + QVERIFY(static_cast(job)->exec()); + + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(plugin, project->projectItem()); + QVERIFY(findTestsJob->exec()); + + QList suites = Core::self()->testController()->testSuitesForProject(project); + QCOMPARE(suites.size(), 1); + + if (suites.size() == 1) + { + ITestSuite* suite = suites.first(); + + QSignalSpy spy(Core::self()->testController(), &ITestController::testRunFinished); + QVERIFY(spy.isValid()); + + suite->launchAllCases(ITestSuite::Silent)->exec(); + + QCOMPARE(spy.count(), 1); + + TestResult result = qvariant_cast(spy.at(0).at(1)); + QCOMPARE(result.suiteResult, TestResult::Failed); + + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::passes")), TestResult::Passed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::fails")), TestResult::Failed); + + /* + * Rust tests do have a #[should_panic] attribute, but that does not change the test output. + * We thus cannot infer ExpectedFail or UnexpectedPass states, so such tests are marked with Passed or Failed. + */ + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::should_fail_and_fails")), TestResult::Passed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::should_fail_and_passes")), TestResult::Failed); + + /* + * Ignored test cases are not run when running the entire suite. + * They are only run when selected individually + */ + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::is_ignored_and_passes")), TestResult::NotRun); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::is_ignored_and_fails")), TestResult::NotRun); + } +} + +void CargoPluginTest::testRunSingleCases() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); + + CargoPlugin* plugin = new CargoPlugin(Core::self()); + + CargoBuildJob* job = new CargoBuildJob(plugin, project->projectItem(), QStringLiteral("test")); + job->setRunArguments({ QStringLiteral("--all"), QStringLiteral("--no-run") }); + job->setStandardViewType(KDevelop::IOutputView::BuildView); + QVERIFY(static_cast(job)->exec()); + + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(plugin, project->projectItem()); + QVERIFY(findTestsJob->exec()); + + QList suites = Core::self()->testController()->testSuitesForProject(project); + QCOMPARE(suites.size(), 1); + + if (suites.size() == 1) + { + ITestSuite* suite = suites.first(); + + { + QSignalSpy spy(Core::self()->testController(), &ITestController::testRunFinished); + QVERIFY(spy.isValid()); + + suite->launchCase(QStringLiteral("tests::passes"), ITestSuite::Silent)->exec(); + + QCOMPARE(spy.count(), 1); + + TestResult result = qvariant_cast(spy.at(0).at(1)); + QCOMPARE(result.suiteResult, TestResult::Passed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::passes")), TestResult::Passed); + } + + { + QSignalSpy spy(Core::self()->testController(), &ITestController::testRunFinished); + QVERIFY(spy.isValid()); + + suite->launchCase(QStringLiteral("tests::fails"), ITestSuite::Silent)->exec(); + + QCOMPARE(spy.count(), 1); + + TestResult result = qvariant_cast(spy.at(0).at(1)); + QCOMPARE(result.suiteResult, TestResult::Failed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::fails")), TestResult::Failed); + } + } +} + +void CargoPluginTest::testRunIgnoredCases() +{ + IProject* project = loadProject(QStringLiteral("kdev-cargo-test")); + QVERIFY(project); + + CargoPlugin* plugin = new CargoPlugin(Core::self()); + + CargoBuildJob* job = new CargoBuildJob(plugin, project->projectItem(), QStringLiteral("test")); + job->setRunArguments({ QStringLiteral("--all"), QStringLiteral("--no-run") }); + job->setStandardViewType(KDevelop::IOutputView::BuildView); + QVERIFY(static_cast(job)->exec()); + + CargoFindTestsJob* findTestsJob = new CargoFindTestsJob(plugin, project->projectItem()); + QVERIFY(findTestsJob->exec()); + + QList suites = Core::self()->testController()->testSuitesForProject(project); + QCOMPARE(suites.size(), 1); + + if (suites.size() == 1) + { + ITestSuite* suite = suites.first(); + + { + QSignalSpy spy(Core::self()->testController(), &ITestController::testRunFinished); + QVERIFY(spy.isValid()); + + suite->launchCase(QStringLiteral("tests::is_ignored_and_passes"), ITestSuite::Silent)->exec(); + + QCOMPARE(spy.count(), 1); + + TestResult result = qvariant_cast(spy.at(0).at(1)); + QCOMPARE(result.suiteResult, TestResult::Passed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::is_ignored_and_passes")), TestResult::Passed); + } + + { + QSignalSpy spy(Core::self()->testController(), &ITestController::testRunFinished); + QVERIFY(spy.isValid()); + + suite->launchCase(QStringLiteral("tests::is_ignored_and_fails"), ITestSuite::Silent)->exec(); + + QCOMPARE(spy.count(), 1); + + TestResult result = qvariant_cast(spy.at(0).at(1)); + QCOMPARE(result.suiteResult, TestResult::Failed); + QCOMPARE(result.testCaseResults.value(QStringLiteral("tests::is_ignored_and_fails")), TestResult::Failed); + } + + } +} + + + +QTEST_MAIN(CargoPluginTest);