AutoTest: Re-implement catch parser

The AST handling is limited and fails for other macros
that will be added later on.
Replace the parser by using a lexer.

Change-Id: Ia11f0a05eec770c703180935a64615e5090b314c
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Stenger
2020-04-09 13:59:04 +02:00
parent d7b1adae78
commit d04597f2aa
5 changed files with 250 additions and 145 deletions

View File

@@ -21,7 +21,8 @@ add_qtc_plugin(AutoTest
boost/boosttestsettings.cpp boost/boosttestsettings.h boost/boosttestsettings.cpp boost/boosttestsettings.h
boost/boosttestsettingspage.cpp boost/boosttestsettingspage.h boost/boosttestsettingspage.ui boost/boosttestsettingspage.cpp boost/boosttestsettingspage.h boost/boosttestsettingspage.ui
boost/boosttesttreeitem.cpp boost/boosttesttreeitem.h boost/boosttesttreeitem.cpp boost/boosttesttreeitem.h
catch/catchconfiguration.h catch/catchconfiguration.cpp catch/catchcodeparser.cpp catch/catchcodeparser.h
catch/catchconfiguration.cpp catch/catchconfiguration.h
catch/catchframework.h catch/catchframework.cpp catch/catchoutputreader.h catch/catchframework.h catch/catchframework.cpp catch/catchoutputreader.h
catch/catchoutputreader.cpp catch/catchresult.h catch/catchresult.cpp catch/catchtestparser.h catch/catchoutputreader.cpp catch/catchresult.h catch/catchresult.cpp catch/catchtestparser.h
catch/catchtestparser.cpp catch/catchtreeitem.h catch/catchtreeitem.cpp catch/catchtestparser.cpp catch/catchtreeitem.h catch/catchtreeitem.cpp

View File

@@ -27,6 +27,7 @@ SOURCES += \
testtreeitemdelegate.cpp \ testtreeitemdelegate.cpp \
testtreemodel.cpp \ testtreemodel.cpp \
testtreeview.cpp \ testtreeview.cpp \
catch/catchcodeparser.cpp \
catch/catchconfiguration.cpp \ catch/catchconfiguration.cpp \
catch/catchframework.cpp \ catch/catchframework.cpp \
catch/catchoutputreader.cpp \ catch/catchoutputreader.cpp \
@@ -97,6 +98,7 @@ HEADERS += \
testtreeitemdelegate.h \ testtreeitemdelegate.h \
testtreemodel.h \ testtreemodel.h \
testtreeview.h \ testtreeview.h \
catch/catchcodeparser.h \
catch/catchconfiguration.h \ catch/catchconfiguration.h \
catch/catchframework.h \ catch/catchframework.h \
catch/catchoutputreader.h \ catch/catchoutputreader.h \

View File

@@ -0,0 +1,170 @@
/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "catchcodeparser.h"
#include <cplusplus/Token.h>
#include <utils/qtcassert.h>
#include <QRegularExpression>
namespace Autotest {
namespace Internal {
using namespace CPlusPlus;
CatchCodeParser::CatchCodeParser(const QByteArray &source, const LanguageFeatures &features,
const Document::Ptr &doc, const Snapshot &snapshot)
: m_source(source)
, m_features(features)
, m_doc(doc)
, m_snapshot(snapshot)
{
}
static TestCodeLocationAndType locationAndTypeFromToken(const Token &tkn)
{
TestCodeLocationAndType locationAndType;
locationAndType.m_type = TestTreeItem::TestFunction;
locationAndType.m_line = tkn.lineno;
locationAndType.m_column = 0;
return locationAndType;
}
static Tokens tokensForSource(const QByteArray &source, const LanguageFeatures &features)
{
SimpleLexer lexer;
lexer.setPreprocessorMode(false); // or true? does not make a difference so far..
lexer.setLanguageFeatures(features);
return lexer(QString::fromUtf8(source));
}
static QStringList parseTags(const QString &tagsString)
{
QStringList tagsList;
const QRegularExpression tagRegEx("\\[(.*?)\\]",QRegularExpression::CaseInsensitiveOption);
int pos = 0;
QRegularExpressionMatch it = tagRegEx.match(tagsString, pos);
while (it.hasMatch()) {
tagsList.append(it.captured(1));
pos += it.capturedLength();
it = tagRegEx.match(tagsString, pos);
}
return tagsList;
}
TestCodeLocationList CatchCodeParser::findTests()
{
m_tokens = tokensForSource(m_source, m_features);
m_currentIndex = 0;
for ( ; m_currentIndex < m_tokens.size(); ++m_currentIndex) {
const Token &token = m_tokens.at(m_currentIndex);
if (token.kind() == T_IDENTIFIER)
handleIdentifier();
}
return m_testCases;
}
void CatchCodeParser::handleIdentifier()
{
QTC_ASSERT(m_currentIndex < m_tokens.size(), return);
const Token &token = m_tokens.at(m_currentIndex);
const QByteArray &identifier = m_source.mid(int(token.bytesBegin()), int(token.bytes()));
if (identifier == "TEST_CASE") {
handleTestCase(false);
} else if (identifier == "SCENARIO") {
handleTestCase(true);
}
}
void CatchCodeParser::handleTestCase(bool isScenario)
{
if (!skipCommentsUntil(T_LPAREN))
return;
Token token = m_tokens.at(m_currentIndex);
TestCodeLocationAndType locationAndType = locationAndTypeFromToken(token);
Kind stoppedAt;
++m_currentIndex;
QString testCaseName = getStringLiteral(stoppedAt);
QString tagsString; // TODO: use them
if (stoppedAt == T_COMMA) {
++m_currentIndex;
tagsString = getStringLiteral(stoppedAt);
}
if (stoppedAt != T_RPAREN)
return;
if (isScenario)
testCaseName.prepend("Scenario: "); // use a flag?
locationAndType.m_name = testCaseName;
m_testCases.append(locationAndType);
}
QString CatchCodeParser::getStringLiteral(Kind &stoppedAtKind)
{
QByteArray captured;
int end = m_tokens.size();
while (m_currentIndex < end) {
const Token token = m_tokens.at(m_currentIndex);
Kind kind = token.kind();
if (kind == T_STRING_LITERAL) {
// store the string without its quotes
captured.append(m_source.mid(token.bytesBegin() + 1, token.bytes() - 2));
} else if (kind == T_RPAREN || kind == T_COMMA) {
stoppedAtKind = kind;
return QString::fromUtf8(captured);
} else if (!token.isComment()) { // comments are okay - but anything else will cancel
stoppedAtKind = kind;
return {};
}
++m_currentIndex;
}
stoppedAtKind = T_ERROR;
return {};
}
bool CatchCodeParser::skipCommentsUntil(Kind nextExpectedKind)
{
for (int index = m_currentIndex + 1, end = m_tokens.size(); index < end; ++index) {
const Token &token = m_tokens.at(index);
if (token.isComment())
continue;
if (token.kind() != nextExpectedKind)
break;
m_currentIndex = index;
return true;
}
return false;
}
} // namespace Internal
} // namespace Autotest

View File

@@ -0,0 +1,63 @@
/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include "catchtreeitem.h"
#include <cplusplus/CppDocument.h>
#include <cplusplus/SimpleLexer.h>
#include <QByteArray>
namespace Autotest {
namespace Internal {
class CatchCodeParser
{
public:
CatchCodeParser(const QByteArray &source, const CPlusPlus::LanguageFeatures &features,
const CPlusPlus::Document::Ptr &doc, const CPlusPlus::Snapshot &snapshot);
virtual ~CatchCodeParser() = default;
TestCodeLocationList findTests();
private:
void handleIdentifier();
void handleTestCase(bool isScenario);
QString getStringLiteral(CPlusPlus::Kind &stoppedAtKind);
bool skipCommentsUntil(CPlusPlus::Kind nextExpectedKind); // moves currentIndex if succeeds
const QByteArray &m_source;
const CPlusPlus::LanguageFeatures &m_features;
const CPlusPlus::Document::Ptr &m_doc;
const CPlusPlus::Snapshot &m_snapshot;
CPlusPlus::Tokens m_tokens;
int m_currentIndex = 0;
TestCodeLocationList m_testCases;
int m_lineNo = 0;
};
} // namespace Internal
} // namespace Autotest

View File

@@ -24,12 +24,11 @@
#include "catchtestparser.h" #include "catchtestparser.h"
#include "catchcodeparser.h"
#include "catchtreeitem.h" #include "catchtreeitem.h"
#include <cpptools/cppmodelmanager.h> #include <cpptools/cppmodelmanager.h>
#include <cpptools/projectpart.h> #include <cpptools/projectpart.h>
#include <cplusplus/LookupContext.h>
#include <cplusplus/Overview.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <QRegularExpression> #include <QRegularExpression>
@@ -37,12 +36,6 @@
namespace Autotest { namespace Autotest {
namespace Internal { namespace Internal {
struct CatchTestCaseSpec
{
QString name;
QStringList tags;
};
static bool isCatchTestCaseMacro(const QString &macroName) static bool isCatchTestCaseMacro(const QString &macroName)
{ {
const QStringList validTestCaseMacros = { const QStringList validTestCaseMacros = {
@@ -59,123 +52,6 @@ static bool isCatchMacro(const QString &macroName)
return isCatchTestCaseMacro(macroName) || validSectionMacros.contains(macroName); return isCatchTestCaseMacro(macroName) || validSectionMacros.contains(macroName);
} }
static inline QString stringLiteralToQString(const CPlusPlus::StringLiteral *literal)
{
return QString::fromLatin1(literal->chars(), int(literal->size()));
}
static QStringList parseTags(const QString &tagsString)
{
QStringList tagsList;
const QRegularExpression tagRegEx("\\[(.*?)\\]",QRegularExpression::CaseInsensitiveOption);
int pos = 0;
QRegularExpressionMatch it = tagRegEx.match(tagsString, pos);
while (it.hasMatch()) {
tagsList.append(it.captured(1));
pos += it.capturedLength();
it = tagRegEx.match(tagsString, pos);
}
return tagsList;
}
class CatchTestVisitor : public CPlusPlus::ASTVisitor
{
public:
explicit CatchTestVisitor(CPlusPlus::Document::Ptr doc)
: CPlusPlus::ASTVisitor(doc->translationUnit()) , m_document(doc) {}
bool visit(CPlusPlus::FunctionDefinitionAST *ast) override;
QMap<CatchTestCaseSpec, TestCodeLocationAndType> testFunctions() const { return m_testFunctions; }
private:
bool isValidTestCase(unsigned int macroTokenIndex, QString &testCaseName, QStringList &tags);
CPlusPlus::Document::Ptr m_document;
CPlusPlus::Overview m_overview;
QMap<CatchTestCaseSpec, TestCodeLocationAndType> m_testFunctions;
};
bool CatchTestVisitor::visit(CPlusPlus::FunctionDefinitionAST *ast)
{
if (!ast || !ast->declarator || !ast->declarator->core_declarator)
return false;
CPlusPlus::DeclaratorIdAST *id = ast->declarator->core_declarator->asDeclaratorId();
if (!id || !id->name)
return false;
const QString prettyName = m_overview.prettyName(id->name->name);
if (!isCatchTestCaseMacro(prettyName))
return false;
QString testName;
QStringList tags;
if (!isValidTestCase(ast->firstToken(), testName, tags))
return false;
CatchTestCaseSpec spec;
spec.name = testName;
if (prettyName == "SCENARIO")
spec.name.prepend("Scenario: "); // TODO maybe better as a flag?
spec.tags = tags;
TestCodeLocationAndType location;
location.m_type = TestTreeItem::TestCase;
location.m_name = m_document->fileName();
getTokenStartPosition(ast->firstToken(), &location.m_line, &location.m_column);
m_testFunctions.insert(spec, location);
return true;
}
bool CatchTestVisitor::isValidTestCase(unsigned int macroTokenIndex, QString &testCaseName, QStringList &tags)
{
QTC_ASSERT(testCaseName.isEmpty(), return false);
QTC_ASSERT(tags.isEmpty(), return false);
unsigned int end = tokenCount();
++macroTokenIndex;
enum ParseMode { TestCaseMode, TagsMode, Ignored } mode = TestCaseMode;
QString captured;
while (macroTokenIndex < end) {
CPlusPlus::Token token = tokenAt(macroTokenIndex);
if (token.kind() == CPlusPlus::T_RPAREN) {
if (mode == TagsMode)
tags = parseTags(captured);
else
testCaseName = captured;
break;
} else if (token.kind() == CPlusPlus::T_COMMA) {
if (mode == TestCaseMode) {
mode = TagsMode;
testCaseName = captured;
captured = QString();
} else if (mode == TagsMode) {
mode = Ignored;
}
} else if (token.kind() == CPlusPlus::T_STRING_LITERAL) {
if (mode != Ignored)
captured += stringLiteralToQString(stringLiteral(macroTokenIndex));
}
++macroTokenIndex;
}
return !testCaseName.isEmpty();
}
inline bool operator<(const CatchTestCaseSpec &spec1, const CatchTestCaseSpec &spec2)
{
if (spec1.name != spec2.name)
return spec1.name < spec2.name;
if (spec1.tags != spec2.tags)
return spec1.tags < spec2.tags;
return false;
}
static bool includesCatchHeader(const CPlusPlus::Document::Ptr &doc, static bool includesCatchHeader(const CPlusPlus::Document::Ptr &doc,
const CPlusPlus::Snapshot &snapshot) const CPlusPlus::Snapshot &snapshot)
{ {
@@ -217,37 +93,30 @@ static bool handleCatchDocument(QFutureInterface<TestParseResultPtr> futureInter
const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance(); const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
const QString &filePath = doc->fileName(); const QString &filePath = doc->fileName();
const QByteArray &fileContent = CppParser::getFileContent(filePath); const QByteArray &fileContent = CppParser::getFileContent(filePath);
CPlusPlus::Document::Ptr document = snapshot.preprocessedDocument(fileContent, filePath);
document->check();
CPlusPlus::AST *ast = document->translationUnit()->ast();
CatchTestVisitor visitor(document);
visitor.accept(ast);
QMap<CatchTestCaseSpec, TestCodeLocationAndType> result = visitor.testFunctions(); const QList<CppTools::ProjectPart::Ptr> projectParts = modelManager->projectPart(filePath);
QString proFile; if (projectParts.isEmpty()) // happens if shutting down while parsing
const QList<CppTools::ProjectPart::Ptr> &ppList = modelManager->projectPart(filePath);
if (ppList.size())
proFile = ppList.first()->projectFile;
else
return false; return false;
QString proFile;
const CppTools::ProjectPart::Ptr projectPart = projectParts.first();
proFile = projectPart->projectFile;
CatchCodeParser codeParser(fileContent, projectPart->languageFeatures, doc, snapshot);
const TestCodeLocationList foundTests = codeParser.findTests();
CatchParseResult *parseResult = new CatchParseResult(framework); CatchParseResult *parseResult = new CatchParseResult(framework);
parseResult->itemType = TestTreeItem::TestCase; parseResult->itemType = TestTreeItem::TestCase;
parseResult->fileName = filePath; parseResult->fileName = filePath;
parseResult->name = filePath; parseResult->name = filePath;
parseResult->displayName = filePath; parseResult->displayName = filePath;
QList<CppTools::ProjectPart::Ptr> projectParts = modelManager->projectPart(doc->fileName());
if (projectParts.isEmpty()) // happens if shutting down while parsing
return false;
parseResult->proFile = projectParts.first()->projectFile; parseResult->proFile = projectParts.first()->projectFile;
for (const auto &testSpec : result.keys()) { for (const TestCodeLocationAndType & testLocation : foundTests) {
const TestCodeLocationAndType &testLocation = result.value(testSpec);
CatchParseResult *testCase = new CatchParseResult(framework); CatchParseResult *testCase = new CatchParseResult(framework);
testCase->fileName = filePath; testCase->fileName = filePath;
testCase->name = testSpec.name; testCase->name = testLocation.m_name;
testCase->proFile = proFile; testCase->proFile = proFile;
testCase->itemType = TestTreeItem::TestFunction; testCase->itemType = testLocation.m_type;
testCase->line = testLocation.m_line; testCase->line = testLocation.m_line;
testCase->column = testLocation.m_column; testCase->column = testLocation.m_column;
@@ -256,7 +125,7 @@ static bool handleCatchDocument(QFutureInterface<TestParseResultPtr> futureInter
futureInterface.reportResult(TestParseResultPtr(parseResult)); futureInterface.reportResult(TestParseResultPtr(parseResult));
return !result.keys().isEmpty(); return !foundTests.isEmpty();
} }
bool CatchTestParser::processDocument(QFutureInterface<TestParseResultPtr> futureInterface, const QString &fileName) bool CatchTestParser::processDocument(QFutureInterface<TestParseResultPtr> futureInterface, const QString &fileName)