forked from qt-creator/qt-creator
AutoTest: Widen scope for run under cursor
..and debug under cursor for QtTest and GTest. Depending on the information coming from the code model we may be able to get a correct scope even when standing inside the function that is defining the test. This makes more sense than the original behavior which made it a hard requirement to stand on a literal to run or debug a test under cursor. But as the codemodel does not provide usable information for all frameworks keep the original behavior as fallback. Task-number: QTCREATORBUG-28752 Change-Id: I13ea7b0ad1e8207da6cb884d212667c4c657957c Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -387,9 +387,44 @@ static QList<ITestConfiguration *> testItemsToTestConfigurations(const QList<ITe
|
|||||||
void AutotestPluginPrivate::onRunUnderCursorTriggered(TestRunMode mode)
|
void AutotestPluginPrivate::onRunUnderCursorTriggered(TestRunMode mode)
|
||||||
{
|
{
|
||||||
TextEditor::BaseTextEditor *currentEditor = TextEditor::BaseTextEditor::currentTextEditor();
|
TextEditor::BaseTextEditor *currentEditor = TextEditor::BaseTextEditor::currentTextEditor();
|
||||||
|
QTC_ASSERT(currentEditor && currentEditor->textDocument(), return);
|
||||||
|
const int line = currentEditor->currentLine();
|
||||||
|
const FilePath filePath = currentEditor->textDocument()->filePath();
|
||||||
|
|
||||||
|
const CPlusPlus::Snapshot snapshot = CppEditor::CppModelManager::instance()->snapshot();
|
||||||
|
const CPlusPlus::Document::Ptr doc = snapshot.document(filePath);
|
||||||
|
if (doc.isNull()) // not part of C++ snapshot
|
||||||
|
return;
|
||||||
|
|
||||||
|
CPlusPlus::Scope *scope = doc->scopeAt(line, currentEditor->currentColumn());
|
||||||
QTextCursor cursor = currentEditor->editorWidget()->textCursor();
|
QTextCursor cursor = currentEditor->editorWidget()->textCursor();
|
||||||
cursor.select(QTextCursor::WordUnderCursor);
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
const QString text = cursor.selectedText();
|
const QString text = cursor.selectedText();
|
||||||
|
|
||||||
|
while (scope && scope->asBlock())
|
||||||
|
scope = scope->enclosingScope();
|
||||||
|
if (scope && scope->asFunction()) { // class, namespace for further stuff?
|
||||||
|
const QList<const CPlusPlus::Name *> fullName
|
||||||
|
= CPlusPlus::LookupContext::fullyQualifiedName(scope);
|
||||||
|
const QString funcName = CPlusPlus::Overview().prettyName(fullName);
|
||||||
|
const TestFrameworks active = AutotestPlugin::activeTestFrameworks();
|
||||||
|
for (auto framework : active) {
|
||||||
|
const QStringList testName = framework->testNameForSymbolName(funcName);
|
||||||
|
if (!testName.size())
|
||||||
|
continue;
|
||||||
|
TestTreeItem *it = framework->rootNode()->findTestByNameAndFile(testName, filePath);
|
||||||
|
if (it) {
|
||||||
|
const QList<ITestConfiguration *> testsToRun
|
||||||
|
= testItemsToTestConfigurations({ it }, mode);
|
||||||
|
if (!testsToRun.isEmpty()) {
|
||||||
|
m_testRunner.runTests(mode, testsToRun);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// general approach
|
||||||
if (text.isEmpty())
|
if (text.isEmpty())
|
||||||
return; // Do not trigger when no name under cursor
|
return; // Do not trigger when no name under cursor
|
||||||
|
|
||||||
@@ -398,28 +433,22 @@ void AutotestPluginPrivate::onRunUnderCursorTriggered(TestRunMode mode)
|
|||||||
return; // Wrong location triggered
|
return; // Wrong location triggered
|
||||||
|
|
||||||
// check whether we have been triggered on a test function definition
|
// check whether we have been triggered on a test function definition
|
||||||
const int line = currentEditor->currentLine();
|
|
||||||
const FilePath &filePath = currentEditor->textDocument()->filePath();
|
|
||||||
QList<ITestTreeItem *> filteredItems = Utils::filtered(testsItems, [&](ITestTreeItem *it){
|
QList<ITestTreeItem *> filteredItems = Utils::filtered(testsItems, [&](ITestTreeItem *it){
|
||||||
return it->line() == line && it->filePath() == filePath;
|
return it->line() == line && it->filePath() == filePath;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filteredItems.size() == 0 && testsItems.size() > 1) {
|
if (filteredItems.size() == 0 && testsItems.size() > 1) {
|
||||||
const CPlusPlus::Snapshot snapshot = CppEditor::CppModelManager::instance()->snapshot();
|
CPlusPlus::Scope *scope = doc->scopeAt(line, currentEditor->currentColumn());
|
||||||
const CPlusPlus::Document::Ptr doc = snapshot.document(filePath);
|
if (scope->asClass()) {
|
||||||
if (!doc.isNull()) {
|
const QList<const CPlusPlus::Name *> fullName
|
||||||
CPlusPlus::Scope *scope = doc->scopeAt(line, currentEditor->currentColumn());
|
= CPlusPlus::LookupContext::fullyQualifiedName(scope);
|
||||||
if (scope->asClass()) {
|
const QString className = CPlusPlus::Overview().prettyName(fullName);
|
||||||
const QList<const CPlusPlus::Name *> fullName
|
|
||||||
= CPlusPlus::LookupContext::fullyQualifiedName(scope);
|
|
||||||
const QString className = CPlusPlus::Overview().prettyName(fullName);
|
|
||||||
|
|
||||||
filteredItems = Utils::filtered(testsItems,
|
filteredItems = Utils::filtered(testsItems,
|
||||||
[&text, &className](ITestTreeItem *it){
|
[&text, &className](ITestTreeItem *it){
|
||||||
return it->name() == text
|
return it->name() == text
|
||||||
&& static_cast<ITestTreeItem *>(it->parent())->name() == className;
|
&& static_cast<ITestTreeItem *>(it->parent())->name() == className;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((filteredItems.size() != 1 && testsItems.size() > 1)
|
if ((filteredItems.size() != 1 && testsItems.size() > 1)
|
||||||
|
@@ -7,6 +7,8 @@
|
|||||||
#include "gtesttreeitem.h"
|
#include "gtesttreeitem.h"
|
||||||
#include "gtestparser.h"
|
#include "gtestparser.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
namespace Autotest {
|
namespace Autotest {
|
||||||
namespace Internal {
|
namespace Internal {
|
||||||
|
|
||||||
@@ -59,5 +61,15 @@ GTest::Constants::GroupMode GTestFramework::groupMode()
|
|||||||
return GTest::Constants::GroupMode(g_settings->groupMode.itemValue().toInt());
|
return GTest::Constants::GroupMode(g_settings->groupMode.itemValue().toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList GTestFramework::testNameForSymbolName(const QString &symbolName) const
|
||||||
|
{
|
||||||
|
static const QRegularExpression r("^(.+::)?((DISABLED_)?.+?)_((DISABLED_)?.+)_Test::TestBody$");
|
||||||
|
const QRegularExpressionMatch match = r.match(symbolName);
|
||||||
|
if (!match.hasMatch())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return { match.captured(2), match.captured(4) };
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Internal
|
} // namespace Internal
|
||||||
} // namespace Autotest
|
} // namespace Autotest
|
||||||
|
@@ -18,6 +18,7 @@ public:
|
|||||||
static GTest::Constants::GroupMode groupMode();
|
static GTest::Constants::GroupMode groupMode();
|
||||||
static QString currentGTestFilter();
|
static QString currentGTestFilter();
|
||||||
|
|
||||||
|
QStringList testNameForSymbolName(const QString &symbolName) const override;
|
||||||
private:
|
private:
|
||||||
const char *name() const override;
|
const char *name() const override;
|
||||||
QString displayName() const override;
|
QString displayName() const override;
|
||||||
|
@@ -63,6 +63,11 @@ ITestParser *ITestFramework::testParser()
|
|||||||
return m_testParser;
|
return m_testParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList ITestFramework::testNameForSymbolName(const QString &) const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
ITestTool::ITestTool(bool activeByDefault)
|
ITestTool::ITestTool(bool activeByDefault)
|
||||||
: ITestBase(activeByDefault, ITestBase::Tool)
|
: ITestBase(activeByDefault, ITestBase::Tool)
|
||||||
{}
|
{}
|
||||||
|
@@ -75,6 +75,8 @@ public:
|
|||||||
virtual QString groupingToolTip() const { return {}; }
|
virtual QString groupingToolTip() const { return {}; }
|
||||||
|
|
||||||
ITestFramework *asFramework() final { return this; }
|
ITestFramework *asFramework() final { return this; }
|
||||||
|
// helper for matching a function symbol's name to a test of this framework
|
||||||
|
virtual QStringList testNameForSymbolName(const QString &symbolName) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual ITestParser *createTestParser() = 0;
|
virtual ITestParser *createTestParser() = 0;
|
||||||
|
@@ -36,5 +36,13 @@ unsigned QtTestFramework::priority() const
|
|||||||
return QtTest::Constants::FRAMEWORK_PRIORITY;
|
return QtTest::Constants::FRAMEWORK_PRIORITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList QtTestFramework::testNameForSymbolName(const QString &symbolName) const
|
||||||
|
{
|
||||||
|
int index = symbolName.lastIndexOf("::");
|
||||||
|
if (index == -1)
|
||||||
|
return {};
|
||||||
|
return { symbolName.left(index), symbolName.mid(index + 2) };
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Internal
|
} // namespace Internal
|
||||||
} // namespace Autotest
|
} // namespace Autotest
|
||||||
|
@@ -15,6 +15,7 @@ class QtTestFramework : public ITestFramework
|
|||||||
public:
|
public:
|
||||||
QtTestFramework() : ITestFramework(true) {}
|
QtTestFramework() : ITestFramework(true) {}
|
||||||
|
|
||||||
|
QStringList testNameForSymbolName(const QString &symbolName) const override;
|
||||||
private:
|
private:
|
||||||
const char *name() const override;
|
const char *name() const override;
|
||||||
QString displayName() const override;
|
QString displayName() const override;
|
||||||
|
@@ -271,6 +271,48 @@ TestTreeItem *TestTreeItem::findChildByNameAndFile(const QString &name, const Fi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static TestTreeItem *findMatchingTestAt(TestTreeItem *parent, const QStringList &testName,
|
||||||
|
const FilePath &filePath)
|
||||||
|
{
|
||||||
|
const QString &first = testName.first();
|
||||||
|
const QString &last = testName.last();
|
||||||
|
for (int i = 0, iEnd = parent->childCount(); i < iEnd; ++i) {
|
||||||
|
auto it = parent->childItem(i);
|
||||||
|
if (it->name() != first)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (int j = 0, jEnd = it->childCount(); j < jEnd; ++j) {
|
||||||
|
auto grandchild = it->childItem(j);
|
||||||
|
if (grandchild->name() != last)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (it->filePath() == filePath || grandchild->filePath() == filePath)
|
||||||
|
return grandchild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTreeItem *TestTreeItem::findTestByNameAndFile(const QStringList &testName,
|
||||||
|
const FilePath &filePath)
|
||||||
|
{
|
||||||
|
QTC_ASSERT(type() == Root, return nullptr);
|
||||||
|
QTC_ASSERT(testName.size() == 2, return nullptr);
|
||||||
|
|
||||||
|
if (!childCount())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (childAt(0)->type() != GroupNode) // process root's children directly
|
||||||
|
return findMatchingTestAt(this, testName, filePath);
|
||||||
|
|
||||||
|
// process children of groups instead of root
|
||||||
|
for (int i = 0, end = childCount(); i < end; ++i) {
|
||||||
|
if (TestTreeItem *found = findMatchingTestAt(childItem(i), testName, filePath))
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
ITestConfiguration *TestTreeItem::asConfiguration(TestRunMode mode) const
|
ITestConfiguration *TestTreeItem::asConfiguration(TestRunMode mode) const
|
||||||
{
|
{
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
@@ -127,6 +127,15 @@ public:
|
|||||||
TestTreeItem *findChildByFileAndType(const Utils::FilePath &filePath, Type type);
|
TestTreeItem *findChildByFileAndType(const Utils::FilePath &filePath, Type type);
|
||||||
TestTreeItem *findChildByNameAndFile(const QString &name, const Utils::FilePath &filePath);
|
TestTreeItem *findChildByNameAndFile(const QString &name, const Utils::FilePath &filePath);
|
||||||
|
|
||||||
|
// search children for a test (function, ...) that matches the given testName
|
||||||
|
// e.g. testName = { "Foo", "tst_bar" } will return a (grand)child item where name() matches
|
||||||
|
// "tst_bar" and its parent matches "Foo"
|
||||||
|
// filePath is used to distinguish this even more - if any of the items (item and parent) has
|
||||||
|
// its file path set to filePath the found result is considered valid
|
||||||
|
// function is expected to be called on a root node of a test framework
|
||||||
|
TestTreeItem *findTestByNameAndFile(const QStringList &testName,
|
||||||
|
const Utils::FilePath &filePath); // make virtual for other frameworks?
|
||||||
|
|
||||||
virtual ITestConfiguration *debugConfiguration() const { return nullptr; }
|
virtual ITestConfiguration *debugConfiguration() const { return nullptr; }
|
||||||
virtual bool canProvideDebugConfiguration() const { return false; }
|
virtual bool canProvideDebugConfiguration() const { return false; }
|
||||||
ITestConfiguration *asConfiguration(TestRunMode mode) const final;
|
ITestConfiguration *asConfiguration(TestRunMode mode) const final;
|
||||||
|
Reference in New Issue
Block a user