diff --git a/plugins/cmake/cmakeprojectdata.h b/plugins/cmake/cmakeprojectdata.h index dfe0a49b2d..a533630864 100644 --- a/plugins/cmake/cmakeprojectdata.h +++ b/plugins/cmake/cmakeprojectdata.h @@ -1,106 +1,107 @@ /* KDevelop CMake Support * * Copyright 2013-2017 Aleix Pol * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef CMAKEPROJECTDATA_H #define CMAKEPROJECTDATA_H #include #include #include #include #include class CMakeServer; /** * Represents any file in a cmake project that has been added * to the project. * * Contains the required information to compile it properly */ struct CMakeFile { KDevelop::Path::List includes; KDevelop::Path::List frameworkDirectories; QString compileFlags; QHash defines; }; inline QDebug &operator<<(QDebug debug, const CMakeFile& file) { debug << "CMakeFile(-I" << file.includes << ", -F" << file.frameworkDirectories << ", -D" << file.defines << ")"; return debug.maybeSpace(); } struct CMakeFilesCompilationData { QHash files; bool isValid = false; }; struct CMakeTarget { Q_GADGET public: enum Type { Library, Executable, Custom }; Q_ENUM(Type) Type type; QString name; KDevelop::Path::List artifacts; + KDevelop::Path::List sources; }; Q_DECLARE_TYPEINFO(CMakeTarget, Q_MOVABLE_TYPE); inline QDebug &operator<<(QDebug debug, const CMakeTarget& target) { debug << target.type << ':' << target.name; return debug.maybeSpace(); } inline bool operator==(const CMakeTarget& lhs, const CMakeTarget& rhs) { return lhs.type == rhs.type && lhs.name == rhs.name && lhs.artifacts == rhs.artifacts; } struct Test { Test() {} QString name; QString executable; QStringList arguments; QHash properties; }; Q_DECLARE_TYPEINFO(Test, Q_MOVABLE_TYPE); struct CMakeProjectData { CMakeProjectData(const QHash> &targets, const CMakeFilesCompilationData &data, const QVector &tests); CMakeProjectData() : watcher(new QFileSystemWatcher) {} ~CMakeProjectData() {} CMakeFilesCompilationData compilationData; QHash> targets; QSharedPointer watcher; QSharedPointer m_server; QVector m_testSuites; }; #endif diff --git a/plugins/cmake/cmakeserverimportjob.cpp b/plugins/cmake/cmakeserverimportjob.cpp index 403ffe19c7..1198634664 100644 --- a/plugins/cmake/cmakeserverimportjob.cpp +++ b/plugins/cmake/cmakeserverimportjob.cpp @@ -1,212 +1,217 @@ /* KDevelop CMake Support * * Copyright 2017 Aleix Pol Gonzalez * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "cmakeserverimportjob.h" #include "cmakeutils.h" #include "cmakeserver.h" #include #include #include #include #include #include #include #include #include #include "debug.h" static QString unescape(const QStringRef& input) { QString output; output.reserve(input.length()); bool isEscaped = false; for (auto it = input.data(), end = it + input.length(); it != end; ++it) { QChar c = *it; if (!isEscaped && c == '\\') { isEscaped = true; } else { output.append(c); isEscaped = false; } } return output; } static QHash processDefines(const QString &compileFlags, const QJsonArray &defines) { QHash ret; const auto& defineRx = MakeFileResolver::defineRegularExpression(); auto it = defineRx.globalMatch(compileFlags); while (it.hasNext()) { const auto match = it.next(); QString value; if (match.lastCapturedIndex() > 1) { value = unescape(match.capturedRef(match.lastCapturedIndex())); } ret[match.captured(1)] = value; } for (const QJsonValue& defineValue: defines) { const QString define = defineValue.toString(); const int eqIdx = define.indexOf(QLatin1Char('=')); if (eqIdx<0) { ret[define] = QString(); } else { ret[define.left(eqIdx)] = define.mid(eqIdx+1); } } return ret; } CMakeTarget::Type typeToEnum(const QJsonObject& target) { static const QHash s_types = { {QStringLiteral("EXECUTABLE"), CMakeTarget::Executable}, {QStringLiteral("STATIC_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("MODULE_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("SHARED_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("OBJECT_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("INTERFACE_LIBRARY"), CMakeTarget::Library} }; const auto value = target.value(QLatin1String("type")).toString(); return s_types.value(value, CMakeTarget::Custom); } void CMakeServerImportJob::processCodeModel(const QJsonObject &response, CMakeProjectData &data) { const auto configs = response.value(QStringLiteral("configurations")).toArray(); qCDebug(CMAKE) << "process response" << response; data.targets.clear(); data.compilationData.files.clear(); const auto rt = KDevelop::ICore::self()->runtimeController()->currentRuntime(); for (const auto &config: configs) { const auto projects = config.toObject().value(QStringLiteral("projects")).toArray(); for (const auto &project: projects) { const auto targets = project.toObject().value(QStringLiteral("targets")).toArray(); for (const auto &targetObject: targets) { const auto target = targetObject.toObject(); const KDevelop::Path targetDir = rt->pathInHost(KDevelop::Path(target.value(QStringLiteral("sourceDirectory")).toString())); - CMakeTarget cmakeTarget{ - typeToEnum(target), - target.value(QStringLiteral("name")).toString(), - kTransform(target[QLatin1String("artifacts")].toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toString()); }) - }; - - // ensure we don't add the same target multiple times, for different projects - // cf.: https://bugs.kde.org/show_bug.cgi?id=387095 - auto& dirTargets = data.targets[targetDir]; - if (dirTargets.contains(cmakeTarget)) - continue; - dirTargets += cmakeTarget; - + KDevelop::Path::List targetSources; const auto fileGroups = target.value(QStringLiteral("fileGroups")).toArray(); for (const auto &fileGroupValue: fileGroups) { const auto fileGroup = fileGroupValue.toObject(); CMakeFile file; file.includes = kTransform(fileGroup.value(QStringLiteral("includePath")).toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toObject().value(QStringLiteral("path")).toString()); }); file.compileFlags = fileGroup.value(QStringLiteral("compileFlags")).toString(); file.defines = processDefines(file.compileFlags, fileGroup.value(QStringLiteral("defines")).toArray()); const auto sourcesArray = fileGroup.value(QStringLiteral("sources")).toArray(); const KDevelop::Path::List sources = kTransform(sourcesArray, [targetDir](const QJsonValue& val) { return KDevelop::Path(targetDir, val.toString()); }); for (const auto& source: sources) { // NOTE: we use the canonical file path to prevent issues with symlinks in the path // leading to lookup failures const auto localFile = rt->pathInHost(source); const auto canonicalFile = QFileInfo(source.toLocalFile()).canonicalFilePath(); const auto sourcePath = localFile.toLocalFile() == canonicalFile ? localFile : KDevelop::Path(canonicalFile); data.compilationData.files[sourcePath] = file; + targetSources << sourcePath; } qCDebug(CMAKE) << "registering..." << sources << file; } + + CMakeTarget cmakeTarget{ + typeToEnum(target), + target.value(QStringLiteral("name")).toString(), + kTransform(target[QLatin1String("artifacts")].toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toString()); }), + targetSources, + }; + + // ensure we don't add the same target multiple times, for different projects + // cf.: https://bugs.kde.org/show_bug.cgi?id=387095 + auto& dirTargets = data.targets[targetDir]; + if (dirTargets.contains(cmakeTarget)) + continue; + dirTargets += cmakeTarget; + + qCDebug(CMAKE) << "adding target" << cmakeTarget.name << "with sources" << cmakeTarget.sources; } } } } CMakeServerImportJob::CMakeServerImportJob(KDevelop::IProject* project, CMakeServer* server, QObject* parent) : KJob(parent) , m_server(server) , m_project(project) { connect(m_server.data(), &CMakeServer::disconnected, this, [this]() { setError(UnexpectedDisconnect); emitResult(); }); } void CMakeServerImportJob::start() { if (m_server->isServerAvailable()) doStart(); else connect(m_server.data(), &CMakeServer::connected, this, &CMakeServerImportJob::doStart); } void CMakeServerImportJob::doStart() { connect(m_server.data(), &CMakeServer::response, this, &CMakeServerImportJob::processResponse); m_server->handshake(m_project->path(), CMake::currentBuildDir(m_project)); } void CMakeServerImportJob::processResponse(const QJsonObject& response) { const auto responseType = response.value(QStringLiteral("type")); if (responseType == QLatin1String("reply")) { const auto inReplyTo = response.value(QStringLiteral("inReplyTo")); qCDebug(CMAKE) << "replying..." << inReplyTo; if (inReplyTo == QLatin1String("handshake")) { m_server->configure({}); } else if (inReplyTo == QLatin1String("configure")) { m_server->compute(); } else if (inReplyTo == QLatin1String("compute")) { m_server->codemodel(); } else if(inReplyTo == QLatin1String("codemodel")) { processCodeModel(response, m_data); m_data.m_testSuites = CMake::importTestSuites(CMake::currentBuildDir(m_project)); m_data.m_server = m_server; emitResult(); } else { qCDebug(CMAKE) << "unhandled reply" << response; } } else if(responseType == QLatin1String("error")) { setError(ErrorResponse); setErrorText(response.value(QStringLiteral("errorMessage")).toString()); qCWarning(CMAKE) << "error!!" << response; emitResult(); } else if (responseType == QLatin1String("progress")) { int progress = response.value(QStringLiteral("progressCurrent")).toInt(); int total = response.value(QStringLiteral("progressMaximum")).toInt(); if (progress >= 0 && total > 0) { setPercent(100.0 * progress / total); } } else if (responseType == QLatin1String("message") || responseType == QLatin1String("hello")) { // Known, but not used for anything currently. } else { qCDebug(CMAKE) << "unhandled message" << response; } } diff --git a/plugins/cmake/testing/ctestfindjob.cpp b/plugins/cmake/testing/ctestfindjob.cpp index ca40023dc4..035ae1e451 100644 --- a/plugins/cmake/testing/ctestfindjob.cpp +++ b/plugins/cmake/testing/ctestfindjob.cpp @@ -1,91 +1,96 @@ /* This file is part of KDevelop Copyright 2012 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestfindjob.h" #include "ctestsuite.h" #include #include #include #include #include #include #include CTestFindJob::CTestFindJob(CTestSuite* suite, QObject* parent) : KJob(parent) , m_suite(suite) { qCDebug(CMAKE) << "Created a CTestFindJob"; setObjectName(i18n("Parse test suite %1", suite->name())); setCapabilities(Killable); } void CTestFindJob::start() { qCDebug(CMAKE); QMetaObject::invokeMethod(this, "findTestCases", Qt::QueuedConnection); } void CTestFindJob::findTestCases() { - qCDebug(CMAKE); - if (!m_suite->arguments().isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); return; } - m_pendingFiles = m_suite->sourceFiles(); + m_pendingFiles.clear(); + for (const auto& file : m_suite->sourceFiles()) + { + if (!file.isEmpty()) + { + m_pendingFiles << file; + } + } qCDebug(CMAKE) << "Source files to update:" << m_pendingFiles; if (m_pendingFiles.isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); return; } foreach (const KDevelop::Path &file, m_pendingFiles) { KDevelop::DUChain::self()->updateContextForUrl(KDevelop::IndexedString(file.toUrl()), KDevelop::TopDUContext::AllDeclarationsAndContexts, this); } } void CTestFindJob::updateReady(const KDevelop::IndexedString& document, const KDevelop::ReferencedTopDUContext& context) { - qCDebug(CMAKE) << m_pendingFiles << document.str(); + qCDebug(CMAKE) << "context update ready" << m_pendingFiles << document.str(); m_suite->loadDeclarations(document, context); m_pendingFiles.removeAll(KDevelop::Path(document.toUrl())); if (m_pendingFiles.isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); } } bool CTestFindJob::doKill() { KDevelop::ICore::self()->languageController()->backgroundParser()->revertAllRequests(this); return true; } diff --git a/plugins/cmake/testing/ctestrunjob.cpp b/plugins/cmake/testing/ctestrunjob.cpp index ffc74bd39f..70ddd69022 100644 --- a/plugins/cmake/testing/ctestrunjob.cpp +++ b/plugins/cmake/testing/ctestrunjob.cpp @@ -1,222 +1,237 @@ /* This file is part of KDevelop Copyright 2012 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestrunjob.h" #include "ctestsuite.h" #include "qttestdelegate.h" #include #include #include #include #include #include #include #include #include +#include #include #include using namespace KDevelop; CTestRunJob::CTestRunJob(CTestSuite* suite, const QStringList& cases, OutputJob::OutputJobVerbosity verbosity, QObject* parent) : KJob(parent) , m_suite(suite) , m_cases(cases) , m_job(nullptr) -, m_outputJob(nullptr) +, m_outputModel(nullptr) , m_verbosity(verbosity) { foreach (const QString& testCase, cases) { m_caseResults[testCase] = TestResult::NotRun; } setCapabilities(Killable); } static KJob* createTestJob(const QString& launchModeId, const QStringList& arguments, const QString &workingDirectory) { LaunchConfigurationType* type = ICore::self()->runController()->launchConfigurationTypeForId( QStringLiteral("Native Application") ); ILaunchMode* mode = ICore::self()->runController()->launchModeForId( launchModeId ); qCDebug(CMAKE) << "got mode and type:" << type << type->id() << mode << mode->id(); Q_ASSERT(type && mode); ILauncher* launcher = nullptr; foreach (ILauncher *l, type->launchers()) { //qCDebug(CMAKE) << "available launcher" << l << l->id() << l->supportedModes(); if (l->supportedModes().contains(mode->id())) { launcher = l; break; } } Q_ASSERT(launcher); ILaunchConfiguration* ilaunch = nullptr; QList launchConfigurations = ICore::self()->runController()->launchConfigurations(); foreach (ILaunchConfiguration *l, launchConfigurations) { if (l->type() == type && l->config().readEntry("ConfiguredByCTest", false)) { ilaunch = l; break; } } if (!ilaunch) { ilaunch = ICore::self()->runController()->createLaunchConfiguration( type, qMakePair( mode->id(), launcher->id() ), nullptr, //TODO add project i18n("CTest") ); ilaunch->config().writeEntry("ConfiguredByCTest", true); //qCDebug(CMAKE) << "created config, launching"; } else { //qCDebug(CMAKE) << "reusing generated config, launching"; } if (!workingDirectory.isEmpty()) ilaunch->config().writeEntry( "Working Directory", QUrl::fromLocalFile( workingDirectory ) ); type->configureLaunchFromCmdLineArguments( ilaunch->config(), arguments ); return ICore::self()->runController()->execute(launchModeId, ilaunch); } void CTestRunJob::start() { // if (!m_suite->cases().isEmpty()) // { // TODO: Find a better way of determining whether QTestLib is used by this test // qCDebug(CMAKE) << "Setting a QtTestDelegate"; // setDelegate(new QtTestDelegate); // } // setStandardToolView(IOutputView::RunView); QStringList arguments = m_cases; if (m_cases.isEmpty() && !m_suite->arguments().isEmpty()) { arguments = m_suite->arguments(); } QStringList cases_selected = arguments; arguments.prepend(m_suite->executable().toLocalFile()); const QString workingDirectory = m_suite->properties().value(QLatin1String("WORKING_DIRECTORY"), QString()); m_job = createTestJob(QStringLiteral("execute"), arguments, workingDirectory); if (ExecuteCompositeJob* cjob = qobject_cast(m_job)) { - m_outputJob = cjob->findChild(); - Q_ASSERT(m_outputJob); - m_outputJob->setVerbosity(m_verbosity); + OutputJob* outputJob = cjob->findChild(); + Q_ASSERT(outputJob); + outputJob->setVerbosity(m_verbosity); QString testName = m_suite->name(); QString title; if (cases_selected.count() == 1) title = i18nc("running test %1, %2 test case", "CTest %1: %2", testName, cases_selected.value(0)); else title = i18ncp("running test %1, %2 number of test cases", "CTest %2 (%1)", "CTest %2 (%1)", cases_selected.count(), testName); - m_outputJob->setTitle(title); + outputJob->setTitle(title); - connect(m_outputJob->model(), &QAbstractItemModel::rowsInserted, this, &CTestRunJob::rowsInserted); + m_outputModel = qobject_cast(outputJob->model()); + connect(m_outputModel, &QAbstractItemModel::rowsInserted, this, &CTestRunJob::rowsInserted); } connect(m_job, &KJob::finished, this, &CTestRunJob::processFinished); ICore::self()->testController()->notifyTestRunStarted(m_suite, cases_selected); } bool CTestRunJob::doKill() { if (m_job) { m_job->kill(); } return true; } void CTestRunJob::processFinished(KJob* job) { - TestResult result; - result.testCaseResults = m_caseResults; - if (job->error() == OutputJob::FailedShownError) { - result.suiteResult = TestResult::Failed; - } else if (job->error() == KJob::NoError) { - result.suiteResult = TestResult::Passed; - } else { - result.suiteResult = TestResult::Error; - } + int error = job->error(); + auto finished = [this,error]() { + TestResult result; + result.testCaseResults = m_caseResults; + if (error == OutputJob::FailedShownError) { + result.suiteResult = TestResult::Failed; + } else if (error == KJob::NoError) { + result.suiteResult = TestResult::Passed; + } else { + result.suiteResult = TestResult::Error; + } - // in case the job was killed, mark this job as killed as well - if (job->error() == KJob::KilledJobError) { - setError(KJob::KilledJobError); - setErrorText(QStringLiteral("Child job was killed.")); - } + // in case the job was killed, mark this job as killed as well + if (error == KJob::KilledJobError) { + setError(KJob::KilledJobError); + setErrorText(QStringLiteral("Child job was killed.")); + } - qCDebug(CMAKE) << result.suiteResult << result.testCaseResults; - ICore::self()->testController()->notifyTestRunFinished(m_suite, result); - emitResult(); + qCDebug(CMAKE) << result.suiteResult << result.testCaseResults; + ICore::self()->testController()->notifyTestRunFinished(m_suite, result); + emitResult(); + }; + + if (m_outputModel) + { + connect(m_outputModel, &OutputModel::allDone, this, finished, Qt::QueuedConnection); + m_outputModel->ensureAllDone(); + } + else + { + finished(); + } } void CTestRunJob::rowsInserted(const QModelIndex &parent, int startRow, int endRow) { // This regular expression matches the name of the testcase (whatever between "::" and "(", indeed ) // For example, from: // PASS : ExpTest::testExp(sum) // matches "testExp" static QRegExp caseRx("::(.*)\\(", Qt::CaseSensitive, QRegExp::RegExp2); for (int row = startRow; row <= endRow; ++row) { - QString line = m_outputJob->model()->data(m_outputJob->model()->index(row, 0, parent), Qt::DisplayRole).toString(); + QString line = m_outputModel->data(m_outputModel->index(row, 0, parent), Qt::DisplayRole).toString(); QString testCase; if (caseRx.indexIn(line) >= 0) { testCase = caseRx.cap(1); } TestResult::TestCaseResult prevResult = m_caseResults.value(testCase, TestResult::NotRun); if (prevResult == TestResult::Passed || prevResult == TestResult::NotRun) { TestResult::TestCaseResult result = TestResult::NotRun; const bool expectFail = m_suite->properties().value(QStringLiteral("WILL_FAIL"), QStringLiteral("FALSE")) == QLatin1String("TRUE"); if (line.startsWith(QLatin1String("PASS :"))) { result = expectFail ? TestResult::UnexpectedPass : TestResult::Passed; } else if (line.startsWith(QLatin1String("FAIL! :"))) { result = expectFail ? TestResult::ExpectedFail : TestResult::Failed; } else if (line.startsWith(QLatin1String("XFAIL :"))) { result = TestResult::ExpectedFail; } else if (line.startsWith(QLatin1String("XPASS :"))) { result = TestResult::UnexpectedPass; } else if (line.startsWith(QLatin1String("SKIP :"))) { result = TestResult::Skipped; } if (result != TestResult::NotRun) { m_caseResults[testCase] = result; } } } } diff --git a/plugins/cmake/testing/ctestrunjob.h b/plugins/cmake/testing/ctestrunjob.h index 170cd7db58..526f894932 100644 --- a/plugins/cmake/testing/ctestrunjob.h +++ b/plugins/cmake/testing/ctestrunjob.h @@ -1,52 +1,56 @@ /* This file is part of KDevelop Copyright 2012 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CTESTRUNJOB_H #define CTESTRUNJOB_H #include #include #include class CTestSuite; +namespace KDevelop { + class OutputModel; +} + class CTestRunJob : public KJob { Q_OBJECT public: CTestRunJob(CTestSuite* suite, const QStringList& cases, KDevelop::OutputJob::OutputJobVerbosity verbosity, QObject* parent = nullptr); void start() override; protected: bool doKill() override; private Q_SLOTS: void processFinished(KJob* job); void rowsInserted(const QModelIndex &parent, int startRow, int endRow); private: CTestSuite* m_suite; QStringList m_cases; QHash m_caseResults; KJob* m_job; - KDevelop::OutputJob* m_outputJob; + KDevelop::OutputModel* m_outputModel; KDevelop::OutputJob::OutputJobVerbosity m_verbosity; }; #endif // CTESTRUNJOB_H diff --git a/plugins/cmake/testing/ctestsuite.cpp b/plugins/cmake/testing/ctestsuite.cpp index 264c91b438..b7c4e5f79f 100644 --- a/plugins/cmake/testing/ctestsuite.cpp +++ b/plugins/cmake/testing/ctestsuite.cpp @@ -1,198 +1,266 @@ /* This file is part of KDevelop Copyright 2012 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestsuite.h" #include "ctestrunjob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +Declaration* findTestClassDeclaration(const CursorInRevision& c, DUContext* ctx, RangeInRevision::ContainsBehavior behavior) +{ + /* + * This code is mostly copied from DUChainUtils::itemUnderCursorInternal. + * However, it is simplified because we are only looking for uses of the test class, + * so we can skip the search through local declarations, which speeds up the search. + * Additionally, we are only interested in the declaration itself, not in its context + * or range. + */ + + foreach(DUContext* subCtx, ctx->childContexts()) + { + //This is a little hacky, but we need it in case of foreach macros and similar stuff + if(subCtx->range().contains(c, behavior) || subCtx->range().isEmpty() || subCtx->range().start.line == c.line || subCtx->range().end.line == c.line) + { + Declaration *d = findTestClassDeclaration(c, subCtx, behavior); + if (d) + { + return d; + } + } + } + + for(int a = 0; a < ctx->usesCount(); ++a) + { + if(ctx->uses()[a].m_range.contains(c, behavior)) + { + return ctx->topContext()->usedDeclarationForIndex(ctx->uses()[a].m_declarationIndex); + } + } + + return nullptr; +} using namespace KDevelop; CTestSuite::CTestSuite(const QString& name, const KDevelop::Path &executable, const QList& files, IProject* project, const QStringList& args, const QHash& properties): m_executable(executable), m_name(name), m_args(args), m_files(files), m_project(project), m_properties(properties) { Q_ASSERT(project); qCDebug(CMAKE) << m_name << m_executable << m_project->name(); } CTestSuite::~CTestSuite() { } void CTestSuite::loadDeclarations(const IndexedString& document, const KDevelop::ReferencedTopDUContext& ref) { DUChainReadLocker locker(DUChain::lock()); TopDUContext* topContext = DUChainUtils::contentContextFromProxyContext(ref.data()); if (!topContext) { qCDebug(CMAKE) << "No top context in" << document.str(); return; } Declaration* testClass = nullptr; Identifier testCaseIdentifier(QStringLiteral("tc")); - foreach (Declaration* declaration, topContext->findLocalDeclarations(Identifier("main"))) + + foreach (Declaration* declaration, topContext->findLocalDeclarations(Identifier(QStringLiteral("main")))) { if (declaration->isDefinition()) { - qCDebug(CMAKE) << "Found a definition for a function 'main()' "; - FunctionDefinition* def = dynamic_cast(declaration); - DUContext* main = def->internalContext(); - foreach (Declaration* mainDeclaration, main->localDeclarations(topContext)) + qCDebug(CMAKE) << "Found a definition for a function 'main()' at" << declaration->range(); + + /* + * This is a rather hacky soluction to get the test class for a Qt test. + * + * The class is used as the argument to the QTEST_MAIN or QTEST_GUILESS_MAIN macro. + * This macro expands to a main() function with a variable declaration with 'tc' as + * the name and with the test class as the type. + * + * Unfortunately, we cannot get to the function body context in order to find + * this variable declaration. + * Instead, we find the cursor to the beginning of the main() function, offset + * the cursor to the inside of the QTEST_MAIN(x) call, and find the declaration there. + * If it is a type declaration, that type is the main test class. + */ + + CursorInRevision cursor = declaration->range().start; + Declaration* testClassDeclaration = nullptr; + int mainDeclarationColumn = cursor.column; + + // cursor points to the start of QTEST_MAIN(x) invocation, we offset it to point inside it + cursor.column += 12; + testClassDeclaration = findTestClassDeclaration(cursor, topContext, RangeInRevision::Default); + + while (!testClassDeclaration || testClassDeclaration->kind() != Declaration::Kind::Type) + { + // If the first found declaration was not a type, the macro may be QTEST_GUILESS_MAIN rather than QTEST_MAIN. + // Alternatively, it may be called as QTEST_MAIN(KDevelop::TestCase), or something similar. + // So we just try a couple of different positions. + cursor.column += 8; + if (cursor.column > mainDeclarationColumn + 60) + { + break; + } + testClassDeclaration = findTestClassDeclaration(cursor, topContext, RangeInRevision::Default); + } + + if (testClassDeclaration && testClassDeclaration->kind() == Declaration::Kind::Type) { - if (mainDeclaration->identifier() == testCaseIdentifier) + qCDebug(CMAKE) << "Found test class declaration" << testClassDeclaration->identifier().toString() << testClassDeclaration->kind(); + if (StructureType::Ptr type = testClassDeclaration->type()) { - qCDebug(CMAKE) << "Found tc declaration in main:" << mainDeclaration->identifier().toString(); - qCDebug(CMAKE) << "Its type is" << mainDeclaration->abstractType()->toString(); - if (StructureType::Ptr type = mainDeclaration->abstractType().cast()) + testClass = type->declaration(topContext); + if (testClass && testClass->internalContext()) { - testClass = type->declaration(topContext); + break; } } } } } if (!testClass || !testClass->internalContext()) { qCDebug(CMAKE) << "No test class found or internal context missing in " << document.str(); return; } if (!m_suiteDeclaration.data()) { m_suiteDeclaration = IndexedDeclaration(testClass); } foreach (Declaration* decl, testClass->internalContext()->localDeclarations(topContext)) { qCDebug(CMAKE) << "Found declaration" << decl->toString() << decl->identifier().identifier().byteArray(); if (ClassFunctionDeclaration* function = dynamic_cast(decl)) { if (function->accessPolicy() == Declaration::Private && function->isSlot()) { QString name = function->qualifiedIdentifier().last().toString(); qCDebug(CMAKE) << "Found private slot in test" << name; if (name.endsWith(QLatin1String("_data"))) { continue; } if (name != QLatin1String("initTestCase") && name != QLatin1String("cleanupTestCase") && name != QLatin1String("init") && name != QLatin1String("cleanup")) { m_cases << name; } qCDebug(CMAKE) << "Found test case function declaration" << function->identifier().toString(); FunctionDefinition* def = FunctionDefinition::definition(decl); m_declarations[name] = def ? IndexedDeclaration(def) : IndexedDeclaration(function); } } } } KJob* CTestSuite::launchCase(const QString& testCase, TestJobVerbosity verbosity) { return launchCases(QStringList() << testCase, verbosity); } KJob* CTestSuite::launchCases(const QStringList& testCases, ITestSuite::TestJobVerbosity verbosity) { qCDebug(CMAKE) << "Launching test run" << m_name << "with cases" << testCases; OutputJob::OutputJobVerbosity outputVerbosity = (verbosity == Verbose) ? OutputJob::Verbose : OutputJob::Silent; return new CTestRunJob(this, testCases, outputVerbosity); } KJob* CTestSuite::launchAllCases(TestJobVerbosity verbosity) { return launchCases(cases(), verbosity); } KDevelop::Path CTestSuite::executable() const { return m_executable; } QStringList CTestSuite::cases() const { return m_cases; } QString CTestSuite::name() const { return m_name; } KDevelop::IProject* CTestSuite::project() const { return m_project; } QStringList CTestSuite::arguments() const { return m_args; } IndexedDeclaration CTestSuite::declaration() const { return m_suiteDeclaration; } IndexedDeclaration CTestSuite::caseDeclaration(const QString& testCase) const { return m_declarations.value(testCase, IndexedDeclaration(nullptr)); } void CTestSuite::setTestCases(const QStringList& cases) { m_cases = cases; } QList CTestSuite::sourceFiles() const { return m_files; } QHash CTestSuite::properties() const { return m_properties; } diff --git a/plugins/cmake/testing/ctestutils.cpp b/plugins/cmake/testing/ctestutils.cpp index 7d44202a1e..8a3c8b1776 100644 --- a/plugins/cmake/testing/ctestutils.cpp +++ b/plugins/cmake/testing/ctestutils.cpp @@ -1,68 +1,85 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula CTestTestfile.cmake parsing uses code from the xUnit plugin Copyright 2008 Manuel Breugelmans Copyright 2010 Daniel Calviño Sánchez 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestutils.h" #include "ctestsuite.h" #include "ctestfindjob.h" #include #include #include #include #include #include #include #include using namespace KDevelop; -// TODO: we are lacking introspection into targets, to see what files belong to each target. static CMakeTarget targetByName(const QHash< KDevelop::Path, QVector>& targets, const QString& name) { for (const auto &subdir: targets.values()) { for (const auto &target: subdir) { if (target.name == name) return target; } } + + return {}; +} + +static CMakeTarget targetByExe(const QHash< KDevelop::Path, QVector>& targets, const KDevelop::Path& exe) +{ + for (const auto &subdir: targets.values()) { + for (const auto &target: subdir) { + if (target.artifacts.contains(exe)) + return target; + } + } + return {}; } void CTestUtils::createTestSuites(const QVector& testSuites, const QHash< KDevelop::Path, QVector>& targets, KDevelop::IProject* project) { foreach (const Test& test, testSuites) { KDevelop::Path executablePath; + CMakeTarget target; + if (QDir::isAbsolutePath(test.executable)) { executablePath = KDevelop::Path(test.executable); + target = targetByExe(targets, executablePath); } else { - const auto target = targetByName(targets, test.executable); + target = targetByName(targets, test.executable); if (target.artifacts.isEmpty()) { continue; } executablePath = target.artifacts.first(); } - CTestSuite* suite = new CTestSuite(test.name, executablePath, {}, project, test.arguments, test.properties); + qCDebug(CMAKE) << "looking for tests in test" << test.name << "target" << target.name << "with sources" << target.sources; + + CTestSuite* suite = new CTestSuite(test.name, executablePath, target.sources.toList(), project, test.arguments, test.properties); ICore::self()->runController()->registerJob(new CTestFindJob(suite)); } } diff --git a/plugins/cmake/tests/test_ctestfindsuites.cpp b/plugins/cmake/tests/test_ctestfindsuites.cpp index 144ecb2fee..d63587243d 100644 --- a/plugins/cmake/tests/test_ctestfindsuites.cpp +++ b/plugins/cmake/tests/test_ctestfindsuites.cpp @@ -1,116 +1,155 @@ /* KDevelop CMake Support * * Copyright 2012 Miha Čančula * Copyright 2017 Kevin Funk * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "test_ctestfindsuites.h" #include "testhelpers.h" #include "cmakeutils.h" #include "cmake-test-paths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +Q_DECLARE_METATYPE(KDevelop::ITestSuite*) + using namespace KDevelop; void waitForSuites(IProject* project, int count, int max) { auto testController = ICore::self()->testController(); for(int i = 0; testController->testSuitesForProject(project).size() < count && i < max * 10; ++i) { QSignalSpy spy(testController, &ITestController::testSuiteAdded); QVERIFY(spy.wait(1000)); } } void TestCTestFindSuites::initTestCase() { AutoTestShell::init({"KDevCMakeManager", "KDevCMakeBuilder", "KDevMakeBuilder", "KDevStandardOutputView"}); TestCore::initialize(); + qRegisterMetaType(); + cleanup(); } void TestCTestFindSuites::cleanup() { foreach(IProject* p, ICore::self()->projectController()->projects()) { ICore::self()->projectController()->closeProject(p); } QVERIFY(ICore::self()->projectController()->projects().isEmpty()); } void TestCTestFindSuites::cleanupTestCase() { TestCore::shutdown(); } void TestCTestFindSuites::testCTestSuite() { IProject* project = loadProject( "unit_tests" ); QVERIFY2(project, "Project was not opened"); waitForSuites(project, 5, 10); QList suites = ICore::self()->testController()->testSuitesForProject(project); QCOMPARE(suites.size(), 5); DUChainReadLocker locker(DUChain::lock()); foreach (auto suite, suites) { - qDebug() << "checking suite" << suite->name(); QCOMPARE(suite->cases(), QStringList()); QVERIFY(!suite->declaration().isValid()); CTestSuite* ctestSuite = static_cast(suite); const auto buildDir = Path(CMake::allBuildDirs(project).at(0)); QString exeSubdir = buildDir.relativePath(ctestSuite->executable().parent()); QCOMPARE(exeSubdir, ctestSuite->name() == "fail" ? QStringLiteral("bin") : QString() ); QString willFail; const QString workingDirectory = ctestSuite->properties().value(QLatin1String("WORKING_DIRECTORY"), QString()); if (ctestSuite->name() == QLatin1String("fail")) { willFail = QLatin1String("TRUE"); QCOMPARE(workingDirectory, QLatin1String("/bar/baz")); QCOMPARE(ctestSuite->properties().value(QLatin1String("FOO"), QString()), QLatin1String("foo")); QCOMPARE(ctestSuite->properties().value(QLatin1String("BAR"), QString()), QLatin1String("TRUE")); QCOMPARE(ctestSuite->properties().value(QLatin1String("MULTILINE"), QString()), QLatin1String("this is \na multi\nline property")); QCOMPARE(ctestSuite->properties().value(QLatin1String("QUOTES"), QString()), QLatin1String("\"\\\\\"\\\\\\")); } else if (ctestSuite->name() == QLatin1String("test_three")) QCOMPARE(workingDirectory, QLatin1String("/foo")); else if (ctestSuite->name() == QLatin1String("test_three")) QCOMPARE(workingDirectory, QLatin1String("/foo")); else if (ctestSuite->name() == QLatin1String("test_five")) QCOMPARE(workingDirectory, QString(buildDir.path() + QLatin1String("/bin"))); else QCOMPARE(workingDirectory, QString()); QCOMPARE(ctestSuite->properties().value(QLatin1String("WILL_FAIL")), willFail); } } +void TestCTestFindSuites::testQtTestCases() +{ + IProject* project = loadProject( "unit_tests_kde" ); + QVERIFY2(project, "Project was not opened"); + + QSignalSpy spy(ICore::self()->testController(), &ITestController::testSuiteAdded); + QVERIFY(spy.isValid()); + + // Background parsing can take a long time, so we need a long timeout + QVERIFY(spy.wait(30 * 1000)); + + QList suites = ICore::self()->testController()->testSuitesForProject(project); + QCOMPARE(suites.size(), 1); + + QStringList cases = { + QStringLiteral("passingTestCase"), + QStringLiteral("failingTestCase"), + QStringLiteral("expectedFailTestCase"), + QStringLiteral("unexpectedPassTestCase"), + QStringLiteral("skippedTestCase"), + }; + + DUChainReadLocker locker(DUChain::lock()); + + foreach (auto suite, suites) + { + QCOMPARE(suite->cases(), cases); + QVERIFY(suite->declaration().isValid()); + + foreach (const auto& caseName, suite->cases()) + { + QVERIFY(suite->caseDeclaration(caseName).isValid()); + } + } +} + QTEST_MAIN(TestCTestFindSuites) diff --git a/plugins/cmake/tests/test_ctestfindsuites.h b/plugins/cmake/tests/test_ctestfindsuites.h index fbd6085839..423b868ac2 100644 --- a/plugins/cmake/tests/test_ctestfindsuites.h +++ b/plugins/cmake/tests/test_ctestfindsuites.h @@ -1,40 +1,41 @@ /* KDevelop CMake Support * * Copyright 2012 Miha Čančula * Copyright 2017 Kevin Funk * * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef TEST_CTESTFINDSUITES_H #define TEST_CTESTFINDSUITES_H #include class TestCTestFindSuites : public QObject { Q_OBJECT private Q_SLOTS: void cleanup(); void initTestCase(); void cleanupTestCase(); void testCTestSuite(); + void testQtTestCases(); }; #endif