CppEditor: Tweak test infrastructure

... so we can re-use the "follow symbol" test cases with clangd.

Call Creator like this (clangd needs to be version 12 or later):
$ QTC_CLANGD=<path to clangd> qtcreator -test
'CppEditor,*Follow*,*Switch*' -test 'ClangCodeModel,*dummy*'

During testing, some invalid code in the test cases was uncovered and
fixed.

Change-Id: I9dc650fdba2a27600e6a550420ee873f6fb31d23
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Kandeler
2021-05-19 13:22:49 +02:00
parent 003ab51c3d
commit ff0301635e
16 changed files with 207 additions and 40 deletions

View File

@@ -36,6 +36,7 @@
#include <cpptools/cppvirtualfunctionassistprovider.h>
#include <cpptools/cppvirtualfunctionproposalitem.h>
#include <languageclient/languageclientinterface.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projecttree.h>
#include <projectexplorer/session.h>
#include <texteditor/basefilefind.h>
@@ -513,11 +514,14 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir)
setLocatorsEnabled(false);
setProgressTitleForToken(indexingToken(), tr("Parsing C/C++ Files (clangd)"));
setCurrentProject(project);
connect(this, &Client::workDone, this, [this](const ProgressToken &token) {
connect(this, &Client::workDone, this, [this, project](const ProgressToken &token) {
const QString * const val = Utils::get_if<QString>(&token);
if (val && *val == indexingToken()) {
d->isFullyIndexed = true;
emit indexingFinished();
#ifdef WITH_TESTS
emit project->indexingFinished("Indexer.Clangd");
#endif
}
});
@@ -848,6 +852,8 @@ void ClangdClient::followSymbol(
return;
}
qCDebug(clangdLog) << "follow symbol requested" << document->filePath()
<< cursor.blockNumber() << cursor.positionInBlock();
d->followSymbolData.emplace(this, ++d->nextFollowSymbolId, cursor, editorWidget,
DocumentUri::fromFilePath(document->filePath()),
std::move(callback), openInSplit);
@@ -856,6 +862,7 @@ void ClangdClient::followSymbol(
// AST node corresponding to the cursor position, so we can find out whether
// we have to look for overrides.
const auto gotoDefCallback = [this, id = d->followSymbolData->id](const Utils::Link &link) {
qCDebug(clangdLog) << "received go to definition response";
if (!link.hasValidTarget()) {
d->followSymbolData.reset();
return;
@@ -872,6 +879,7 @@ void ClangdClient::followSymbol(
Range(cursor)));
astRequest.setResponseCallback([this, id = d->followSymbolData->id](
const AstRequest::Response &response) {
qCDebug(clangdLog) << "received ast response";
if (!d->followSymbolData || d->followSymbolData->id != id)
return;
const auto result = response.result();
@@ -891,6 +899,8 @@ void ClangdClient::Private::handleGotoDefinitionResult()
QTC_ASSERT(followSymbolData->defLink.hasValidTarget(), return);
QTC_ASSERT(followSymbolData->cursorNode.isValid(), return);
qCDebug(clangdLog) << "handling go to definition result";
// No dis-ambiguation necessary. Call back with the link and finish.
if (!followSymbolData->cursorNode.isMemberFunctionCall()
&& !followSymbolData->cursorNode.isPureVirtualDeclaration()) {

View File

@@ -49,6 +49,7 @@
using namespace CPlusPlus;
using namespace Core;
using namespace CppTools::Tests;
using namespace ProjectExplorer;
namespace ClangCodeModel {
@@ -67,20 +68,6 @@ void ClangdTests::initTestCase()
settings->setUseClangd(true);
}
template <typename Signal> static bool waitForSignalOrTimeout(
const typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal)
{
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeOutInMs());
QEventLoop loop;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(sender, signal, &loop, &QEventLoop::quit);
timer.start();
loop.exec();
return timer.isActive();
}
// The main point here is to test our access type categorization.
// We do not try to stress-test clangd's "Find References" functionality; such tests belong
// into LLVM.
@@ -110,7 +97,7 @@ void ClangdTests::testFindReferences()
ClangdClient *client = modelManagerSupport->clientForProject(openProjectResult.project());
if (!client) {
QVERIFY(waitForSignalOrTimeout(modelManagerSupport,
&ClangModelManagerSupport::createdClient));
&ClangModelManagerSupport::createdClient, timeOutInMs()));
client = modelManagerSupport->clientForProject(openProjectResult.project());
}
QVERIFY(client);
@@ -119,7 +106,7 @@ void ClangdTests::testFindReferences()
// Wait until the client is fully initialized, i.e. it's completed the handshake
// with the server.
if (!client->reachable())
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::initialized));
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::initialized, timeOutInMs()));
QVERIFY(client->reachable());
// The kind of AST support we need was introduced in LLVM 13.
@@ -128,7 +115,7 @@ void ClangdTests::testFindReferences()
// Wait for index to build.
if (!client->isFullyIndexed())
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::indexingFinished));
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::indexingFinished, timeOutInMs()));
QVERIFY(client->isFullyIndexed());
// Open cpp documents.
@@ -167,7 +154,7 @@ void ClangdTests::testFindReferences()
cursor.setPosition((pos)); \
searchResults.clear(); \
client->findUsages((doc), cursor, {}); \
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::findUsagesDone)); \
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::findUsagesDone, timeOutInMs())); \
} while (false)
#define EXPECT_RESULT(index, lne, col, type) \

View File

@@ -192,6 +192,10 @@ signals:
void autoSaved();
void currentEditorAboutToChange(Core::IEditor *editor);
#ifdef WITH_TESTS
void linkOpened();
#endif
public slots:
static void saveDocument();
static void saveDocumentAs();

View File

@@ -1,7 +1,7 @@
add_qtc_plugin(CppEditor
DEFINES CPPEDITOR_LIBRARY
PLUGIN_DEPENDS Core CppTools ProjectExplorer TextEditor
PLUGIN_TEST_DEPENDS QmakeProjectManager
PLUGIN_TEST_DEPENDS QbsProjectManager QmakeProjectManager
SOURCES
cppautocompleter.cpp cppautocompleter.h
cppcodemodelinspectordialog.cpp cppcodemodelinspectordialog.h cppcodemodelinspectordialog.ui

View File

@@ -17,6 +17,7 @@ QtcPlugin {
pluginTestDepends: [
"QmakeProjectManager",
"QbsProjectManager",
]
files: [

View File

@@ -9,4 +9,5 @@ QTC_PLUGIN_DEPENDS += \
cpptools \
projectexplorer
QTC_TEST_DEPENDS += \
qbsprojectmanager \
qmakeprojectmanager

View File

@@ -27,6 +27,10 @@
#include <extensionsystem/iplugin.h>
#ifdef WITH_TESTS
namespace ProjectExplorer { class Kit; }
#endif
namespace CppEditor {
namespace Internal {
@@ -49,6 +53,10 @@ public:
CppQuickFixAssistProvider *quickFixProvider() const;
#ifdef WITH_TESTS
ProjectExplorer::Kit *m_testKit = nullptr;
#endif
signals:
void outlineSortingChanged(bool sort);
void typeHierarchyRequested();
@@ -67,6 +75,8 @@ private:
QVector<QObject *> createTestObjects() const override;
private slots:
void initTestCase();
// The following tests expect that no projects are loaded on start-up.
void test_SwitchMethodDeclarationDefinition_data();
void test_SwitchMethodDeclarationDefinition();

View File

@@ -28,9 +28,12 @@
#include "cppeditor.h"
#include "cppeditorwidget.h"
#include "cppeditordocument.h"
#include "cppeditorplugin.h"
#include <coreplugin/editormanager/editormanager.h>
#include <cpptools/cppcodemodelsettings.h>
#include <cpptools/cppsemanticinfo.h>
#include <cpptools/cpptoolsreuse.h>
#include <cplusplus/CppDocument.h>
#include <QDir>
@@ -74,12 +77,20 @@ bool TestDocument::hasCursorMarker() const { return m_cursorPosition != -1; }
bool TestDocument::hasAnchorMarker() const { return m_anchorPosition != -1; }
TestCase::TestCase(bool runGarbageCollector)
: CppTools::Tests::TestCase(runGarbageCollector)
: CppTools::Tests::TestCase(runGarbageCollector),
m_prevUseClangd(CppTools::codeModelSettings()->useClangd())
{
}
TestCase::~TestCase()
{
CppTools::codeModelSettings()->setUseClangd(m_prevUseClangd);
}
void TestCase::setUseClangd()
{
if (CppEditorPlugin::instance()->m_testKit)
CppTools::codeModelSettings()->setUseClangd(true);
}
bool TestCase::openCppEditor(const QString &fileName, CppEditor **editor, CppEditorWidget **editorWidget)

View File

@@ -62,12 +62,17 @@ public:
TestCase(bool runGarbageCollector = true);
~TestCase();
void setUseClangd();
static bool openCppEditor(const QString &fileName,
CppEditor **editor,
CppEditorWidget **editorWidget = 0);
static CPlusPlus::Document::Ptr waitForRehighlightedSemanticDocument(
CppEditorWidget *editorWidget);
private:
const bool m_prevUseClangd;
};
} // namespace Tests

View File

@@ -7185,7 +7185,7 @@ void CppEditorPlugin::test_quickfix_ExtractFunction_data()
"}\n"
"void NS::C::f(NS::C &c)\n"
"{\n"
" @{start}C *c = &c;@{end}\n"
" @{start}C *c2 = &c;@{end}\n"
"}\n")
<< _("namespace NS {\n"
"class C {\n"
@@ -7197,7 +7197,7 @@ void CppEditorPlugin::test_quickfix_ExtractFunction_data()
"}\n"
"inline void NS::C::extracted(NS::C &c)\n"
"{\n"
" C *c = &c;\n"
" C *c2 = &c;\n"
"}\n"
"\n"
"void NS::C::f(NS::C &c)\n"

View File

@@ -28,6 +28,8 @@
#include "cppeditorplugin.h"
#include "cppeditortestcase.h"
#include <cpptools/cppmodelmanager.h>
#include <QElapsedTimer>
#include <QtTest>
@@ -100,11 +102,18 @@ UseSelectionsTestCase::UseSelectionsTestCase(TestDocument &testFile,
bool hasTimedOut;
const SelectionList selections = waitForUseSelections(&hasTimedOut);
QEXPECT_FAIL("non-local use as macro argument - argument expanded 1", "TODO", Abort);
const bool clangCodeModel = CppTools::CppModelManager::instance()->isClangCodeModelActive();
if (clangCodeModel) {
QEXPECT_FAIL("local use as macro argument - argument eaten", "fails with CCM, find out why",
Abort);
} else {
QEXPECT_FAIL("non-local use as macro argument - argument expanded 1", "TODO", Abort);
}
QVERIFY(!hasTimedOut);
// foreach (const Selection &selection, selections)
// qDebug() << QTest::toString(selection);
QEXPECT_FAIL("non-local use as macro argument - argument expanded 2", "TODO", Abort);
if (!clangCodeModel)
QEXPECT_FAIL("non-local use as macro argument - argument expanded 2", "TODO", Abort);
QCOMPARE(selections, expectedSelections);
}

View File

@@ -28,13 +28,18 @@
#include "cppeditorplugin.h"
#include "cppeditortestcase.h"
#include <cpptools/cppcodemodelsettings.h>
#include <cpptools/cppelementevaluator.h>
#include <cpptools/cppfollowsymbolundercursor.h>
#include <cpptools/cppvirtualfunctionassistprovider.h>
#include <cpptools/cppvirtualfunctionproposalitem.h>
#include <cpptools/cpptoolsreuse.h>
#include <cpptools/cpptoolstestcase.h>
#include <cpptools/cppmodelmanager.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectexplorer.h>
#include <texteditor/codeassist/genericproposalmodel.h>
#include <texteditor/codeassist/iassistprocessor.h>
#include <texteditor/codeassist/iassistproposal.h>
@@ -76,6 +81,7 @@ using namespace CPlusPlus;
using namespace CppTools;
using namespace TextEditor;
using namespace Core;
using namespace ProjectExplorer;
class OverrideItem {
public:
@@ -267,6 +273,8 @@ F2TestCase::F2TestCase(CppEditorAction action,
{
QVERIFY(succeededSoFar());
setUseClangd();
// Check if there are initial and target position markers
TestDocumentPtr initialTestFile = testFileWithInitialCursorMarker(testFiles);
QVERIFY2(initialTestFile,
@@ -275,13 +283,68 @@ F2TestCase::F2TestCase(CppEditorAction action,
QVERIFY2(targetTestFile,
"No test file with target cursor marker is provided.");
const QString curTestName = QLatin1String(QTest::currentTestFunction());
const QString tag = QLatin1String(QTest::currentDataTag());
const bool useClangd = CppTools::codeModelSettings()->useClangd();
if (useClangd) {
if (curTestName == "test_FollowSymbolUnderCursor_virtualFunctionCall"
|| curTestName == "test_FollowSymbolUnderCursor_virtualFunctionCall_multipleDocuments") {
QSKIP("TODO: Add test infrastructure for this");
}
if (curTestName == "test_FollowSymbolUnderCursor_QObject_connect"
|| curTestName == "test_FollowSymbolUnderCursor_QObject_oldStyleConnect") {
QSKIP("TODO: Implement fall-back");
}
if (curTestName == "test_FollowSymbolUnderCursor_classOperator" && tag == "backward")
QSKIP("clangd goes to operator name first");
if (tag.toLower().contains("fuzzy"))
QSKIP("fuzzy matching is not supposed to work with clangd"); // TODO: Implement fallback as we do with libclang
if (tag == "baseClassFunctionIntroducedByUsingDeclaration")
QSKIP("clangd points to the using declaration");
if (tag == "classDestructor")
QSKIP("clangd wants the cursor before the ~ character");
if (curTestName == "test_FollowSymbolUnderCursor_classOperator_inOp")
QSKIP("clangd goes to operator name first");
if (tag == "fromFunctionBody" || tag == "fromReturnType"
|| tag == "conversionOperatorDecl2Def") {
QSKIP("TODO: explicit decl/def switch not yet supported with clangd");
}
}
// Write files to disk
CppTools::Tests::TemporaryDir temporaryDir;
QVERIFY(temporaryDir.isValid());
QString projectFileContent = "CppApplication { files: [";
foreach (TestDocumentPtr testFile, testFiles) {
QVERIFY(testFile->baseDirectory().isEmpty());
testFile->setBaseDirectory(temporaryDir.path());
QVERIFY(testFile->writeToDisk());
projectFileContent += QString::fromLatin1("\"%1\",").arg(testFile->filePath());
}
projectFileContent += "]}\n";
class ProjectCloser {
public:
void setProject(Project *p) { m_p = p; }
~ProjectCloser() { if (m_p) ProjectExplorerPlugin::unloadProject(m_p); }
private:
Project * m_p = nullptr;
} projectCloser;
if (useClangd) {
TestDocument projectFile(projectFileContent.toUtf8(), "project.qbs");
projectFile.setBaseDirectory(temporaryDir.path());
QVERIFY(projectFile.writeToDisk());
const auto openProjectResult = ProjectExplorerPlugin::openProject(projectFile.filePath());
QVERIFY2(openProjectResult && openProjectResult.project(),
qPrintable(openProjectResult.errorMessage()));
projectCloser.setProject(openProjectResult.project());
openProjectResult.project()->configureAsExampleProject(
CppEditorPlugin::instance()->m_testKit);
// Wait until project is fully indexed.
QVERIFY(CppTools::Tests::waitForSignalOrTimeout(openProjectResult.project(),
&Project::indexingFinished, CppTools::Tests::clangdIndexingTimeout()));
}
// Update Code Model
@@ -294,7 +357,8 @@ F2TestCase::F2TestCase(CppEditorAction action,
foreach (TestDocumentPtr testFile, testFiles) {
QVERIFY(openCppEditor(testFile->filePath(), &testFile->m_editor,
&testFile->m_editorWidget));
closeEditorAtEndOfTestCase(testFile->m_editor);
if (!useClangd) // Editors get closed when unloading project.
closeEditorAtEndOfTestCase(testFile->m_editor);
// Wait until the indexer processed the just opened file.
// The file is "Full Checked" since it is in the working copy now,
@@ -303,13 +367,16 @@ F2TestCase::F2TestCase(CppEditorAction action,
const Document::Ptr document = waitForFileInGlobalSnapshot(testFile->filePath());
QVERIFY(document);
if (document->checkMode() == Document::FullCheck) {
if (!document->diagnosticMessages().isEmpty())
qDebug() << document->diagnosticMessages().first().text();
QVERIFY(document->diagnosticMessages().isEmpty());
break;
}
}
// Rehighlight
waitForRehighlightedSemanticDocument(testFile->m_editorWidget);
if (!useClangd)
waitForRehighlightedSemanticDocument(testFile->m_editorWidget);
}
// Activate editor of initial test file
@@ -329,14 +396,8 @@ F2TestCase::F2TestCase(CppEditorAction action,
FollowSymbolInterface &delegate = CppModelManager::instance()->followSymbolInterface();
auto* builtinFollowSymbol = dynamic_cast<FollowSymbolUnderCursor *>(&delegate);
if (!builtinFollowSymbol) {
if (filePaths.size() > 1)
QSKIP("Clang FollowSymbol does not currently support multiple files (except cpp+header)");
const QString curTestName = QLatin1String(QTest::currentTestFunction());
if (curTestName == "test_FollowSymbolUnderCursor_QObject_connect"
|| curTestName == "test_FollowSymbolUnderCursor_virtualFunctionCall"
|| curTestName == "test_FollowSymbolUnderCursor_QTCREATORBUG7903") {
if (curTestName == "test_FollowSymbolUnderCursor_QTCREATORBUG7903")
QSKIP((curTestName + " is not supported by Clang FollowSymbol").toLatin1());
}
widget->openLinkUnderCursor();
break;
@@ -358,14 +419,24 @@ F2TestCase::F2TestCase(CppEditorAction action,
break;
}
case SwitchBetweenMethodDeclarationDefinitionAction:
CppEditorPlugin::instance()->switchDeclarationDefinition();
if (CppTools::codeModelSettings()->useClangd())
initialTestFile->m_editorWidget->openLinkUnderCursor();
else
CppEditorPlugin::instance()->switchDeclarationDefinition();
break;
default:
QFAIL("Unknown test action");
break;
}
QCoreApplication::processEvents();
if (useClangd) {
QEXPECT_FAIL("infiniteLoopLocalTypedef_QTCREATORBUG-11999",
"clangd bug: Go to definition does not return", Abort);
QVERIFY(CppTools::Tests::waitForSignalOrTimeout(EditorManager::instance(),
&EditorManager::linkOpened, 10000));
} else {
QCoreApplication::processEvents();
}
// Compare
IEditor *currentEditor = EditorManager::currentEditor();
@@ -379,8 +450,11 @@ F2TestCase::F2TestCase(CppEditorAction action,
// qDebug() << "Expected line:" << expectedLine;
// qDebug() << "Expected column:" << expectedColumn;
QEXPECT_FAIL("globalVarFromEnum", "Contributor works on a fix.", Abort);
QEXPECT_FAIL("matchFunctionSignature_Follow_5", "foo(int) resolved as CallAST", Abort);
if (!CppTools::codeModelSettings()->useClangd()) {
QEXPECT_FAIL("globalVarFromEnum", "Contributor works on a fix.", Abort);
QEXPECT_FAIL("matchFunctionSignature_Follow_5", "foo(int) resolved as CallAST", Abort);
}
QCOMPARE(currentTextEditor->currentLine(), expectedLine);
QCOMPARE(currentTextEditor->currentColumn(), expectedColumn);
@@ -421,6 +495,25 @@ Q_DECLARE_METATYPE(QList<CppEditor::Internal::TestDocumentPtr>)
namespace CppEditor {
namespace Internal {
void CppEditorPlugin::initTestCase()
{
const auto settings = CppTools::codeModelSettings();
const QString clangdFromEnv = qEnvironmentVariable("QTC_CLANGD");
if (clangdFromEnv.isEmpty())
return;
settings->setClangdFilePath(Utils::FilePath::fromString(clangdFromEnv));
const auto clangd = settings->clangdFilePath();
if (clangd.isEmpty() || !clangd.exists())
return;
// Find suitable kit.
m_testKit = Utils::findOr(KitManager::kits(), nullptr, [](const Kit *k) {
return k->isValid();
});
if (!m_testKit)
QSKIP("This test requires at least one kit to be present");
}
void CppEditorPlugin::test_SwitchMethodDeclarationDefinition_data()
{
QTest::addColumn<QByteArray>("header");
@@ -576,7 +669,6 @@ void CppEditorPlugin::test_SwitchMethodDeclarationDefinition_data()
"int OtherClass::var;\n"
"namespace NS {\n"
"int OtherClass::var;\n"
"float Test::var;\n"
"int Test::$var;\n"
"}\n");
@@ -1276,7 +1368,6 @@ void CppEditorPlugin::test_FollowSymbolUnderCursor_multipleDocuments_data()
"int OtherClass::var;\n"
"namespace NS {\n"
"int OtherClass::var;\n"
"float Test::var;\n"
"int Test::$var;\n"
"}\n", "file.cpp")};
}

View File

@@ -44,6 +44,7 @@
#include <cplusplus/CppDocument.h>
#include <utils/executeondestruction.h>
#include <utils/fileutils.h>
#include <utils/hostosinfo.h>
#include <utils/temporarydirectory.h>
#include <QtTest>
@@ -421,5 +422,15 @@ bool VerifyCleanCppModelManager::isClean(bool testOnlyForCleanedProjects)
#undef RETURN_FALSE_IF_NOT
int clangdIndexingTimeout()
{
const QByteArray timeoutAsByteArray = qgetenv("QTC_CLANGD_INDEXING_TIMEOUT");
bool isConversionOk = false;
const int intervalAsInt = timeoutAsByteArray.toInt(&isConversionOk);
if (!isConversionOk)
return Utils::HostOsInfo::isWindowsHost() ? 20000 : 10000;
return intervalAsInt;
}
} // namespace Tests
} // namespace CppTools

View File

@@ -30,7 +30,9 @@
#include <cplusplus/CppDocument.h>
#include <utils/temporarydirectory.h>
#include <QEventLoop>
#include <QStringList>
#include <QTimer>
namespace CPlusPlus {
class Document;
@@ -54,6 +56,23 @@ class ProjectInfo;
namespace Tests {
int CPPTOOLS_EXPORT clangdIndexingTimeout();
template <typename Signal> inline bool waitForSignalOrTimeout(
const typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal,
int timeoutInMs)
{
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutInMs);
QEventLoop loop;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(sender, signal, &loop, &QEventLoop::quit);
timer.start();
loop.exec();
return timer.isActive();
}
class CPPTOOLS_EXPORT TestDocument
{
public:

View File

@@ -212,6 +212,10 @@ signals:
void rootProjectDirectoryChanged();
#ifdef WITH_TESTS
void indexingFinished(Utils::Id indexer);
#endif
protected:
virtual RestoreResult fromMap(const QVariantMap &map, QString *errorMessage);
void createTargetFromMap(const QVariantMap &map, int index);

View File

@@ -6257,6 +6257,10 @@ void TextEditorWidget::findLinkAt(const QTextCursor &cursor,
bool TextEditorWidget::openLink(const Utils::Link &link, bool inNextSplit)
{
#ifdef WITH_TESTS
struct Signaller { ~Signaller() { emit EditorManager::instance()->linkOpened(); } } s;
#endif
if (!link.hasValidTarget())
return false;