AutoTest: Fix parsing of multiple test cases in single qml file

Quick tests allow definition of more than one TestCase inside a
qml file and even nesting is possible, so support this correctly.

Fixes: QTCREATORBUG-22761
Change-Id: I65fcc7cd6063d976d798c3e900d3299a12e2d73f
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Stenger
2019-07-26 08:28:17 +02:00
parent 6c3be76c8d
commit c020fb6e3e
7 changed files with 104 additions and 56 deletions

View File

@@ -131,13 +131,13 @@ void AutoTestUnitTests::testCodeParser_data()
<< 1 << 0 << 0 << 0; << 1 << 0 << 0 << 0;
QTest::newRow("mixedAutoTestAndQuickTests") QTest::newRow("mixedAutoTestAndQuickTests")
<< QString(m_tmpDir->path() + "/mixed_atp/mixed_atp.pro") << QString(m_tmpDir->path() + "/mixed_atp/mixed_atp.pro")
<< 4 << 7 << 3 << 10; << 4 << 10 << 4 << 10;
QTest::newRow("plainAutoTestQbs") QTest::newRow("plainAutoTestQbs")
<< QString(m_tmpDir->path() + "/plain/plain.qbs") << QString(m_tmpDir->path() + "/plain/plain.qbs")
<< 1 << 0 << 0 << 0; << 1 << 0 << 0 << 0;
QTest::newRow("mixedAutoTestAndQuickTestsQbs") QTest::newRow("mixedAutoTestAndQuickTestsQbs")
<< QString(m_tmpDir->path() + "/mixed_atp/mixed_atp.qbs") << QString(m_tmpDir->path() + "/mixed_atp/mixed_atp.qbs")
<< 4 << 7 << 3 << 10; << 4 << 10 << 4 << 10;
} }
void AutoTestUnitTests::testCodeParserSwitchStartup() void AutoTestUnitTests::testCodeParserSwitchStartup()
@@ -183,8 +183,8 @@ void AutoTestUnitTests::testCodeParserSwitchStartup_data()
m_tmpDir->path() + "/mixed_atp/mixed_atp.qbs"}); m_tmpDir->path() + "/mixed_atp/mixed_atp.qbs"});
QList<int> expectedAutoTests = QList<int>() << 1 << 4 << 1 << 4; QList<int> expectedAutoTests = QList<int>() << 1 << 4 << 1 << 4;
QList<int> expectedNamedQuickTests = QList<int>() << 0 << 7 << 0 << 7; QList<int> expectedNamedQuickTests = QList<int>() << 0 << 10 << 0 << 10;
QList<int> expectedUnnamedQuickTests = QList<int>() << 0 << 3 << 0 << 3; QList<int> expectedUnnamedQuickTests = QList<int>() << 0 << 4 << 0 << 4;
QList<int> expectedDataTagsCount = QList<int>() << 0 << 10 << 0 << 10; QList<int> expectedDataTagsCount = QList<int>() << 0 << 10 << 0 << 10;
QTest::newRow("loadMultipleProjects") QTest::newRow("loadMultipleProjects")

View File

@@ -190,20 +190,26 @@ static bool checkQmlDocumentForQuickTestCode(QFutureInterface<TestParseResultPtr
if (!qmlVisitor.isValid()) if (!qmlVisitor.isValid())
return false; return false;
const QString testCaseName = qmlVisitor.testCaseName(); const QVector<QuickTestCaseSpec> &testFunctions = qmlVisitor.testFunctions();
const TestCodeLocationAndType tcLocationAndType = qmlVisitor.testCaseLocation();
const QMap<QString, TestCodeLocationAndType> &testFunctions = qmlVisitor.testFunctions(); for (const QuickTestCaseSpec &it : testFunctions) {
const QString testCaseName = it.m_caseName;
const QString functionName = it.m_functionName;
const TestCodeLocationAndType &loc = it.m_functionLocationAndType;
QuickTestParseResult *parseResult = new QuickTestParseResult(id);
parseResult->proFile = proFile;
parseResult->itemType = TestTreeItem::TestCase;
if (!testCaseName.isEmpty()) {
parseResult->fileName = it.m_name;
parseResult->name = testCaseName;
parseResult->line = it.m_line;
parseResult->column = it.m_column;
}
QuickTestParseResult *parseResult = new QuickTestParseResult(id);
parseResult->proFile = proFile;
parseResult->itemType = TestTreeItem::TestCase;
QMap<QString, TestCodeLocationAndType>::ConstIterator it = testFunctions.begin();
const QMap<QString, TestCodeLocationAndType>::ConstIterator end = testFunctions.end();
for ( ; it != end; ++it) {
const TestCodeLocationAndType &loc = it.value();
QuickTestParseResult *funcResult = new QuickTestParseResult(id); QuickTestParseResult *funcResult = new QuickTestParseResult(id);
funcResult->name = it.key(); funcResult->name = functionName;
funcResult->displayName = it.key(); funcResult->displayName = functionName;
funcResult->itemType = loc.m_type; funcResult->itemType = loc.m_type;
funcResult->fileName = loc.m_name; funcResult->fileName = loc.m_name;
funcResult->line = loc.m_line; funcResult->line = loc.m_line;
@@ -211,14 +217,9 @@ static bool checkQmlDocumentForQuickTestCode(QFutureInterface<TestParseResultPtr
funcResult->proFile = proFile; funcResult->proFile = proFile;
parseResult->children.append(funcResult); parseResult->children.append(funcResult);
futureInterface.reportResult(TestParseResultPtr(parseResult));
} }
if (!testCaseName.isEmpty()) {
parseResult->fileName = tcLocationAndType.m_name;
parseResult->name = testCaseName;
parseResult->line = tcLocationAndType.m_line;
parseResult->column = tcLocationAndType.m_column;
}
futureInterface.reportResult(TestParseResultPtr(parseResult));
return true; return true;
} }

View File

@@ -323,11 +323,11 @@ TestTreeItem *QuickTestTreeItem::find(const TestParseResult *result)
TestTreeItem *group = findFirstLevelChild([path](TestTreeItem *group) { TestTreeItem *group = findFirstLevelChild([path](TestTreeItem *group) {
return group->filePath() == path; return group->filePath() == path;
}); });
return group ? group->findChildByFile(result->fileName) : nullptr; return group ? group->findChildByNameAndFile(result->name, result->fileName) : nullptr;
} }
return findChildByFile(result->fileName); return findChildByNameAndFile(result->name, result->fileName);
case GroupNode: case GroupNode:
return findChildByFile(result->fileName); return findChildByNameAndFile(result->name, result->fileName);
case TestCase: case TestCase:
return name().isEmpty() ? findChildByNameAndFile(result->name, result->fileName) return name().isEmpty() ? findChildByNameAndFile(result->name, result->fileName)
: findChildByName(result->name); : findChildByName(result->name);
@@ -345,9 +345,9 @@ TestTreeItem *QuickTestTreeItem::findChild(const TestTreeItem *other)
case Root: case Root:
if (otherType == TestCase && other->name().isEmpty()) if (otherType == TestCase && other->name().isEmpty())
return unnamedQuickTests(); return unnamedQuickTests();
return findChildByFileAndType(other->filePath(), otherType); return findChildByFileNameAndType(other->filePath(), other->name(), otherType);
case GroupNode: case GroupNode:
return findChildByFileAndType(other->filePath(), otherType); return findChildByFileNameAndType(other->filePath(), other->name(), otherType);
case TestCase: case TestCase:
if (otherType != TestFunction && otherType != TestDataFunction && otherType != TestSpecialFunction) if (otherType != TestFunction && otherType != TestDataFunction && otherType != TestSpecialFunction)
return nullptr; return nullptr;
@@ -444,6 +444,16 @@ void QuickTestTreeItem::markForRemovalRecursively(const QString &filePath)
} }
} }
TestTreeItem *QuickTestTreeItem::findChildByFileNameAndType(const QString &filePath,
const QString &name,
TestTreeItem::Type tType)
{
return findFirstLevelChild([filePath, name, tType](const TestTreeItem *other) {
return other->type() == tType && other->name() == name && other->filePath() == filePath;
});
}
TestTreeItem *QuickTestTreeItem::unnamedQuickTests() const TestTreeItem *QuickTestTreeItem::unnamedQuickTests() const
{ {
if (type() != Root) if (type() != Root)

View File

@@ -57,6 +57,8 @@ public:
QSet<QString> internalTargets() const override; QSet<QString> internalTargets() const override;
void markForRemovalRecursively(const QString &filePath) override; void markForRemovalRecursively(const QString &filePath) override;
private: private:
TestTreeItem *findChildByFileNameAndType(const QString &filePath, const QString &name,
Type tType);
TestTreeItem *unnamedQuickTests() const; TestTreeItem *unnamedQuickTests() const;
}; };

View File

@@ -31,6 +31,7 @@
#include <qmljs/qmljslink.h> #include <qmljs/qmljslink.h>
#include <qmljs/qmljsutils.h> #include <qmljs/qmljsutils.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/qtcassert.h>
namespace Autotest { namespace Autotest {
namespace Internal { namespace Internal {
@@ -96,18 +97,23 @@ bool TestQmlVisitor::visit(QmlJS::AST::UiObjectDefinition *ast)
m_typeIsTestCase = true; m_typeIsTestCase = true;
m_insideTestCase = true; m_insideTestCase = true;
m_currentTestCaseName.clear();
const auto sourceLocation = ast->firstSourceLocation(); const auto sourceLocation = ast->firstSourceLocation();
m_testCaseLocation.m_name = m_currentDoc->fileName(); QuickTestCaseSpec currentSpec;
m_testCaseLocation.m_line = sourceLocation.startLine; currentSpec.m_name = m_currentDoc->fileName();
m_testCaseLocation.m_column = sourceLocation.startColumn - 1; currentSpec.m_line = sourceLocation.startLine;
m_testCaseLocation.m_type = TestTreeItem::TestCase; currentSpec.m_column = sourceLocation.startColumn - 1;
currentSpec.m_type = TestTreeItem::TestCase;
m_testCases.push(currentSpec);
return true; return true;
} }
void TestQmlVisitor::endVisit(QmlJS::AST::UiObjectDefinition *) void TestQmlVisitor::endVisit(QmlJS::AST::UiObjectDefinition *)
{ {
m_insideTestCase = m_objectStack.pop() == "TestCase"; if (!m_objectStack.isEmpty() && m_objectStack.pop() == "TestCase") {
if (!m_testCases.isEmpty())
m_testCases.pop();
m_insideTestCase = !m_objectStack.isEmpty() && m_objectStack.top() == "TestCase";
}
} }
bool TestQmlVisitor::visit(QmlJS::AST::ExpressionStatement *ast) bool TestQmlVisitor::visit(QmlJS::AST::ExpressionStatement *ast)
@@ -148,15 +154,22 @@ bool TestQmlVisitor::visit(QmlJS::AST::FunctionDeclaration *ast)
else else
locationAndType.m_type = TestTreeItem::TestFunction; locationAndType.m_type = TestTreeItem::TestFunction;
m_testFunctions.insert(name.toString(), locationAndType); if (m_testCases.isEmpty()) // invalid qml code
return false;
QuickTestCaseSpec testCaseWithFunc = m_testCases.top();
testCaseWithFunc.m_functionName = name.toString();
testCaseWithFunc.m_functionLocationAndType = locationAndType;
m_testFunctions.append(testCaseWithFunc);
} }
return false; return false;
} }
bool TestQmlVisitor::visit(QmlJS::AST::StringLiteral *ast) bool TestQmlVisitor::visit(QmlJS::AST::StringLiteral *ast)
{ {
if (m_expectTestCaseName && m_currentTestCaseName.isEmpty()) { if (m_expectTestCaseName) {
m_currentTestCaseName = ast->value.toString(); QTC_ASSERT(!m_testCases.isEmpty(), return false);
m_testCases.top().m_caseName = ast->value.toString();
m_expectTestCaseName = false; m_expectTestCaseName = false;
} }
return false; return false;

View File

@@ -37,6 +37,14 @@
namespace Autotest { namespace Autotest {
namespace Internal { namespace Internal {
class QuickTestCaseSpec : public TestCodeLocationAndType
{
public:
QString m_caseName;
QString m_functionName;
TestCodeLocationAndType m_functionLocationAndType;
};
class TestQmlVisitor : public QmlJS::AST::Visitor class TestQmlVisitor : public QmlJS::AST::Visitor
{ {
public: public:
@@ -50,17 +58,14 @@ public:
bool visit(QmlJS::AST::FunctionDeclaration *ast) override; bool visit(QmlJS::AST::FunctionDeclaration *ast) override;
bool visit(QmlJS::AST::StringLiteral *ast) override; bool visit(QmlJS::AST::StringLiteral *ast) override;
QString testCaseName() const { return m_currentTestCaseName; } QVector<QuickTestCaseSpec> testFunctions() const { return m_testFunctions; }
TestCodeLocationAndType testCaseLocation() const { return m_testCaseLocation; }
QMap<QString, TestCodeLocationAndType> testFunctions() const { return m_testFunctions; }
bool isValid() const { return m_typeIsTestCase; } bool isValid() const { return m_typeIsTestCase; }
private: private:
QmlJS::Document::Ptr m_currentDoc; QmlJS::Document::Ptr m_currentDoc;
QmlJS::Snapshot m_snapshot; QmlJS::Snapshot m_snapshot;
QString m_currentTestCaseName; QStack<QuickTestCaseSpec> m_testCases;
TestCodeLocationAndType m_testCaseLocation; QVector<QuickTestCaseSpec> m_testFunctions;
QMap<QString, TestCodeLocationAndType> m_testFunctions;
QStack<QString> m_objectStack; QStack<QString> m_objectStack;
bool m_typeIsTestCase = false; bool m_typeIsTestCase = false;
bool m_insideTestCase = false; bool m_insideTestCase = false;

View File

@@ -34,21 +34,38 @@ TestCase {
verify(blubb == bla, "Comparing concat equality") verify(blubb == bla, "Comparing concat equality")
} }
// nested TestCases actually fail TestCase {
// TestCase { name: "boo"
// name: "boo"
// function test_boo() { function test_boo() {
// verify(true); verify(true);
// } }
// TestCase { TestCase {
// name: "far" name: "far"
// function test_far() { function test_far() {
// verify(true); verify(true);
// } }
// } }
// }
function test_boo2() { // should not get added to "far", but to "boo"
verify(false);
}
}
TestCase {
name: "secondBoo"
function test_bar() {
compare(1, 1);
}
}
TestCase { // unnamed
function test_func() {
verify(true);
}
}
} }