diff --git a/plugins/cmake/cmakeprojectdata.h b/plugins/cmake/cmakeprojectdata.h --- a/plugins/cmake/cmakeprojectdata.h +++ b/plugins/cmake/cmakeprojectdata.h @@ -64,6 +64,7 @@ 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) { diff --git a/plugins/cmake/cmakeserverimportjob.cpp b/plugins/cmake/cmakeserverimportjob.cpp --- a/plugins/cmake/cmakeserverimportjob.cpp +++ b/plugins/cmake/cmakeserverimportjob.cpp @@ -108,19 +108,7 @@ 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(); @@ -139,9 +127,26 @@ 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; } } } diff --git a/plugins/cmake/testing/ctestfindjob.cpp b/plugins/cmake/testing/ctestfindjob.cpp --- a/plugins/cmake/testing/ctestfindjob.cpp +++ b/plugins/cmake/testing/ctestfindjob.cpp @@ -46,16 +46,21 @@ 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()) @@ -73,7 +78,7 @@ 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())); diff --git a/plugins/cmake/testing/ctestrunjob.h b/plugins/cmake/testing/ctestrunjob.h --- a/plugins/cmake/testing/ctestrunjob.h +++ b/plugins/cmake/testing/ctestrunjob.h @@ -26,6 +26,10 @@ class CTestSuite; +namespace KDevelop { + class OutputModel; +} + class CTestRunJob : public KJob { Q_OBJECT @@ -45,7 +49,7 @@ QStringList m_cases; QHash m_caseResults; KJob* m_job; - KDevelop::OutputJob* m_outputJob; + KDevelop::OutputModel* m_outputModel; KDevelop::OutputJob::OutputJobVerbosity m_verbosity; }; diff --git a/plugins/cmake/testing/ctestrunjob.cpp b/plugins/cmake/testing/ctestrunjob.cpp --- a/plugins/cmake/testing/ctestrunjob.cpp +++ b/plugins/cmake/testing/ctestrunjob.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -41,7 +42,7 @@ , m_suite(suite) , m_cases(cases) , m_job(nullptr) -, m_outputJob(nullptr) +, m_outputModel(nullptr) , m_verbosity(verbosity) { foreach (const QString& testCase, cases) @@ -119,20 +120,21 @@ 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); @@ -150,25 +152,38 @@ 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) @@ -180,7 +195,7 @@ 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) { diff --git a/plugins/cmake/testing/ctestsuite.cpp b/plugins/cmake/testing/ctestsuite.cpp --- a/plugins/cmake/testing/ctestsuite.cpp +++ b/plugins/cmake/testing/ctestsuite.cpp @@ -35,6 +35,39 @@ #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; @@ -67,22 +100,57 @@ 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; } } } diff --git a/plugins/cmake/testing/ctestutils.cpp b/plugins/cmake/testing/ctestutils.cpp --- a/plugins/cmake/testing/ctestutils.cpp +++ b/plugins/cmake/testing/ctestutils.cpp @@ -36,33 +36,50 @@ 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.h b/plugins/cmake/tests/test_ctestfindsuites.h --- a/plugins/cmake/tests/test_ctestfindsuites.h +++ b/plugins/cmake/tests/test_ctestfindsuites.h @@ -35,6 +35,7 @@ void cleanupTestCase(); void testCTestSuite(); + void testQtTestCases(); }; #endif diff --git a/plugins/cmake/tests/test_ctestfindsuites.cpp b/plugins/cmake/tests/test_ctestfindsuites.cpp --- a/plugins/cmake/tests/test_ctestfindsuites.cpp +++ b/plugins/cmake/tests/test_ctestfindsuites.cpp @@ -40,6 +40,8 @@ #include #include +Q_DECLARE_METATYPE(KDevelop::ITestSuite*) + using namespace KDevelop; void waitForSuites(IProject* project, int count, int max) @@ -56,6 +58,8 @@ AutoTestShell::init({"KDevCMakeManager", "KDevCMakeBuilder", "KDevMakeBuilder", "KDevStandardOutputView"}); TestCore::initialize(); + qRegisterMetaType(); + cleanup(); } @@ -85,7 +89,6 @@ foreach (auto suite, suites) { - qDebug() << "checking suite" << suite->name(); QCOMPARE(suite->cases(), QStringList()); QVERIFY(!suite->declaration().isValid()); CTestSuite* ctestSuite = static_cast(suite); @@ -113,4 +116,40 @@ } } +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)