diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index 109051fe0ea..58de3bc167d 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -62,6 +62,9 @@ #include #include #include +#include +#include +#include #include #include @@ -543,8 +546,11 @@ void CMakeBuildSystem::combineScanAndParse() m_reader.resetData(); m_currentGuard = {}; + m_testNames.clear(); emitBuildSystemUpdated(); + + QTimer::singleShot(0, this, &CMakeBuildSystem::runCTest); } void CMakeBuildSystem::checkAndReportError(QString &errorMessage) @@ -715,6 +721,8 @@ void CMakeBuildSystem::handleParsingSucceeded() checkAndReportError(errorMessage); } + m_ctestPath = m_reader.ctestPath(); + setApplicationTargets(appTargets()); setDeploymentData(deploymentData()); @@ -735,6 +743,8 @@ void CMakeBuildSystem::handleParsingFailed(const QString &msg) cmakeBuildConfiguration()->setConfigurationFromCMake(cmakeConfig); // ignore errorMessage here, we already got one. + m_ctestPath.clear(); + QTC_CHECK(m_waitingForParse); m_waitingForParse = false; m_combinedScanAndParseResult = false; @@ -871,6 +881,49 @@ int CMakeBuildSystem::takeReparseParameters() return result; } +void CMakeBuildSystem::runCTest() +{ + if (!cmakeBuildConfiguration()->error().isEmpty() || m_ctestPath.isEmpty()) { + qCDebug(cmakeBuildSystemLog) << "Cancel ctest run after failed cmake run"; + emit testInformationUpdated(); + return; + } + qCDebug(cmakeBuildSystemLog) << "Requesting ctest run after cmake run"; + + CommandLine cmd{m_ctestPath, {"-N", "--show-only=json-v1"}}; + SynchronousProcess ctest; + ctest.setTimeoutS(1); + ctest.setEnvironment(cmakeBuildConfiguration()->environment().toStringList()); + ctest.setWorkingDirectory(cmakeBuildConfiguration()->buildDirectory().toString()); + + const SynchronousProcessResponse response = ctest.run(cmd); + if (response.result == SynchronousProcessResponse::Finished) { + const QJsonDocument json = QJsonDocument::fromJson(response.allRawOutput()); + if (!json.isEmpty() && json.isObject()) { + const QJsonObject jsonObj = json.object(); + const QJsonObject btGraph = jsonObj.value("backtraceGraph").toObject(); + const QJsonArray cmakelists = btGraph.value("files").toArray(); + const QJsonArray nodes = btGraph.value("nodes").toArray(); + const QJsonArray tests = jsonObj.value("tests").toArray(); + for (const QJsonValue &testVal : tests) { + const QJsonObject test = testVal.toObject(); + QTC_ASSERT(!test.isEmpty(), continue); + const int bt = test.value("backtrace").toInt(-1); + QTC_ASSERT(bt != -1, continue); + const QJsonObject btRef = nodes.at(bt).toObject(); + int file = btRef.value("file").toInt(-1); + int line = btRef.value("line").toInt(-1); + QTC_ASSERT(file != -1 && line != -1, continue); + m_testNames.append({test.value("name").toString(), + FilePath::fromString(cmakelists.at(file).toString()), + line + }); + } + } + } + emit testInformationUpdated(); +} + CMakeBuildConfiguration *CMakeBuildSystem::cmakeBuildConfiguration() const { return static_cast(BuildSystem::buildConfiguration()); @@ -943,6 +996,26 @@ CMakeConfig CMakeBuildSystem::parseCMakeCacheDotTxt(const Utils::FilePath &cache return result; } +const QList CMakeBuildSystem::testcasesInfo() const +{ + return m_testNames; +} + +CommandLine CMakeBuildSystem::commandLineForTests(const QList &tests, + const QStringList &options) const +{ + QStringList args = options; + auto current = Utils::transform>(m_testNames, &TestCaseInfo::name); + if (tests.isEmpty() || current == Utils::toSet(tests)) + return {m_ctestPath, args}; + + const QString regex = Utils::transform(tests, [](const QString ¤t) { + return QRegularExpression::escape(current); + }).join('|'); + args << "-R" << QString('(' + regex + ')'); + return {m_ctestPath, args}; +} + DeploymentData CMakeBuildSystem::deploymentData() const { DeploymentData result; diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index ccc0a2e9342..a496d9ba351 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -89,6 +89,10 @@ public: CMakeBuildConfiguration *cmakeBuildConfiguration() const; + QList const testcasesInfo() const final; + Utils::CommandLine commandLineForTests(const QList &tests, + const QStringList &options) const final; + // Generic CMake helper functions: static CMakeConfig parseCMakeCacheDotTxt(const Utils::FilePath &cacheFile, QString *errorMessage); @@ -143,6 +147,8 @@ private: void updateReparseParameters(const int parameters); int takeReparseParameters(); + void runCTest(); + ProjectExplorer::TreeScanner m_treeScanner; QHash m_mimeBinaryCache; QList m_allFiles; @@ -164,6 +170,10 @@ private: m_buildDirToTempDir; FileApiReader m_reader; mutable bool m_isHandlingError = false; + + // CTest integration + QString m_ctestPath; + QList m_testNames; }; } // namespace Internal diff --git a/src/plugins/cmakeprojectmanager/cmakeprocess.cpp b/src/plugins/cmakeprojectmanager/cmakeprocess.cpp index ba6b582896a..664c3852610 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprocess.cpp +++ b/src/plugins/cmakeprojectmanager/cmakeprocess.cpp @@ -203,6 +203,7 @@ void CMakeProcess::handleProcessFinished(int code, QProcess::ExitStatus status) } else if (code != 0) { msg = tr("CMake process exited with exit code %1.").arg(code); } + m_lastExitCode = code; if (!msg.isEmpty()) { Core::MessageManager::write(msg); diff --git a/src/plugins/cmakeprojectmanager/cmakeprocess.h b/src/plugins/cmakeprojectmanager/cmakeprocess.h index c63f54952ad..9dcc8bfa9a0 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprocess.h +++ b/src/plugins/cmakeprojectmanager/cmakeprocess.h @@ -62,6 +62,7 @@ public: void processStandardOutput(); void processStandardError(); + int lastExitCode() const { return m_lastExitCode; } signals: void started(); void finished(int exitCode, QProcess::ExitStatus exitStatus); @@ -76,6 +77,7 @@ private: bool m_processWasCanceled = false; QTimer m_cancelTimer; QElapsedTimer m_elapsed; + int m_lastExitCode = 0; }; } // namespace Internal diff --git a/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp b/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp index eea97ed04fd..ec0422e2b87 100644 --- a/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp +++ b/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp @@ -689,6 +689,8 @@ FileApiQtcData extractData(FileApiData &input, setupLocationInfoForTargets(result.rootProjectNode.get(), result.buildTargets); + result.ctestPath = input.replyFile.ctestExecutable; + return result; } diff --git a/src/plugins/cmakeprojectmanager/fileapidataextractor.h b/src/plugins/cmakeprojectmanager/fileapidataextractor.h index 9db433765d8..41e116df273 100644 --- a/src/plugins/cmakeprojectmanager/fileapidataextractor.h +++ b/src/plugins/cmakeprojectmanager/fileapidataextractor.h @@ -54,6 +54,7 @@ public: ProjectExplorer::RawProjectParts projectParts; std::unique_ptr rootProjectNode; QSet knownHeaders; + QString ctestPath; }; FileApiQtcData extractData(FileApiData &data, diff --git a/src/plugins/cmakeprojectmanager/fileapiparser.cpp b/src/plugins/cmakeprojectmanager/fileapiparser.cpp index c4767289a15..5c3c3e92665 100644 --- a/src/plugins/cmakeprojectmanager/fileapiparser.cpp +++ b/src/plugins/cmakeprojectmanager/fileapiparser.cpp @@ -135,6 +135,7 @@ static ReplyFileContents readReplyFile(const QFileInfo &fi, QString &errorMessag const QJsonObject paths = cmakeObject.value("paths").toObject(); { result.cmakeExecutable = paths.value("cmake").toString(); + result.ctestExecutable = paths.value("ctest").toString(); result.cmakeRoot = paths.value("root").toString(); } const QJsonObject generator = cmakeObject.value("generator").toObject(); diff --git a/src/plugins/cmakeprojectmanager/fileapiparser.h b/src/plugins/cmakeprojectmanager/fileapiparser.h index eb096b8a73b..b277b0c3a41 100644 --- a/src/plugins/cmakeprojectmanager/fileapiparser.h +++ b/src/plugins/cmakeprojectmanager/fileapiparser.h @@ -57,6 +57,7 @@ class ReplyFileContents public: QString generator; QString cmakeExecutable; + QString ctestExecutable; QString cmakeRoot; QVector replies; diff --git a/src/plugins/cmakeprojectmanager/fileapireader.cpp b/src/plugins/cmakeprojectmanager/fileapireader.cpp index 75cf51a9868..56c9806a112 100644 --- a/src/plugins/cmakeprojectmanager/fileapireader.cpp +++ b/src/plugins/cmakeprojectmanager/fileapireader.cpp @@ -192,6 +192,12 @@ CMakeConfig FileApiReader::takeParsedConfiguration(QString &errorMessage) return cache; } +QString FileApiReader::ctestPath() const +{ + // if we failed to run cmake we should not offer ctest information either + return m_lastCMakeExitCode == 0 ? m_ctestPath : QString(); +} + std::unique_ptr FileApiReader::generateProjectTree( const QList &allFiles, QString &errorMessage, bool includeHeaderNodes) { @@ -267,6 +273,7 @@ void FileApiReader::endState(const QFileInfo &replyFi) m_projectParts = std::move(value->projectParts); m_rootProjectNode = std::move(value->rootProjectNode); m_knownHeaders = std::move(value->knownHeaders); + m_ctestPath = std::move(value->ctestPath); if (value->errorMessage.isEmpty()) { emit this->dataAvailable(); @@ -296,6 +303,7 @@ void FileApiReader::cmakeFinishedState(int code, QProcess::ExitStatus status) Q_UNUSED(code) Q_UNUSED(status) + m_lastCMakeExitCode = m_cmakeProcess->lastExitCode(); m_cmakeProcess.release()->deleteLater(); endState(FileApiParser::scanForCMakeReplyFile(m_parameters.workDirectory)); diff --git a/src/plugins/cmakeprojectmanager/fileapireader.h b/src/plugins/cmakeprojectmanager/fileapireader.h index d8dc9eb0c0c..97bd6232699 100644 --- a/src/plugins/cmakeprojectmanager/fileapireader.h +++ b/src/plugins/cmakeprojectmanager/fileapireader.h @@ -66,6 +66,7 @@ public: QSet projectFilesToWatch() const; QList takeBuildTargets(QString &errorMessage); CMakeConfig takeParsedConfiguration(QString &errorMessage); + QString ctestPath() const; std::unique_ptr generateProjectTree( const QList &allFiles, QString &errorMessage, @@ -95,6 +96,8 @@ private: ProjectExplorer::RawProjectParts m_projectParts; std::unique_ptr m_rootProjectNode; QSet m_knownHeaders; + QString m_ctestPath; + int m_lastCMakeExitCode = 0; Utils::optional> m_future;