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);