forked from qt-creator/qt-creator
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:
@@ -21,7 +21,8 @@ add_qtc_plugin(AutoTest
|
||||
boost/boosttestsettings.cpp boost/boosttestsettings.h
|
||||
boost/boosttestsettingspage.cpp boost/boosttestsettingspage.h boost/boosttestsettingspage.ui
|
||||
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/catchoutputreader.cpp catch/catchresult.h catch/catchresult.cpp catch/catchtestparser.h
|
||||
catch/catchtestparser.cpp catch/catchtreeitem.h catch/catchtreeitem.cpp
|
||||
|
@@ -27,6 +27,7 @@ SOURCES += \
|
||||
testtreeitemdelegate.cpp \
|
||||
testtreemodel.cpp \
|
||||
testtreeview.cpp \
|
||||
catch/catchcodeparser.cpp \
|
||||
catch/catchconfiguration.cpp \
|
||||
catch/catchframework.cpp \
|
||||
catch/catchoutputreader.cpp \
|
||||
@@ -97,6 +98,7 @@ HEADERS += \
|
||||
testtreeitemdelegate.h \
|
||||
testtreemodel.h \
|
||||
testtreeview.h \
|
||||
catch/catchcodeparser.h \
|
||||
catch/catchconfiguration.h \
|
||||
catch/catchframework.h \
|
||||
catch/catchoutputreader.h \
|
||||
|
170
src/plugins/autotest/catch/catchcodeparser.cpp
Normal file
170
src/plugins/autotest/catch/catchcodeparser.cpp
Normal 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
|
63
src/plugins/autotest/catch/catchcodeparser.h
Normal file
63
src/plugins/autotest/catch/catchcodeparser.h
Normal 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
|
@@ -24,12 +24,11 @@
|
||||
|
||||
#include "catchtestparser.h"
|
||||
|
||||
#include "catchcodeparser.h"
|
||||
#include "catchtreeitem.h"
|
||||
|
||||
#include <cpptools/cppmodelmanager.h>
|
||||
#include <cpptools/projectpart.h>
|
||||
#include <cplusplus/LookupContext.h>
|
||||
#include <cplusplus/Overview.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QRegularExpression>
|
||||
@@ -37,12 +36,6 @@
|
||||
namespace Autotest {
|
||||
namespace Internal {
|
||||
|
||||
struct CatchTestCaseSpec
|
||||
{
|
||||
QString name;
|
||||
QStringList tags;
|
||||
};
|
||||
|
||||
static bool isCatchTestCaseMacro(const QString ¯oName)
|
||||
{
|
||||
const QStringList validTestCaseMacros = {
|
||||
@@ -59,123 +52,6 @@ static bool isCatchMacro(const QString ¯oName)
|
||||
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,
|
||||
const CPlusPlus::Snapshot &snapshot)
|
||||
{
|
||||
@@ -217,37 +93,30 @@ static bool handleCatchDocument(QFutureInterface<TestParseResultPtr> futureInter
|
||||
const CppTools::CppModelManager *modelManager = CppTools::CppModelManager::instance();
|
||||
const QString &filePath = doc->fileName();
|
||||
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();
|
||||
QString proFile;
|
||||
const QList<CppTools::ProjectPart::Ptr> &ppList = modelManager->projectPart(filePath);
|
||||
if (ppList.size())
|
||||
proFile = ppList.first()->projectFile;
|
||||
else
|
||||
const QList<CppTools::ProjectPart::Ptr> projectParts = modelManager->projectPart(filePath);
|
||||
if (projectParts.isEmpty()) // happens if shutting down while parsing
|
||||
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);
|
||||
parseResult->itemType = TestTreeItem::TestCase;
|
||||
parseResult->fileName = filePath;
|
||||
parseResult->name = 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;
|
||||
|
||||
for (const auto &testSpec : result.keys()) {
|
||||
const TestCodeLocationAndType &testLocation = result.value(testSpec);
|
||||
for (const TestCodeLocationAndType & testLocation : foundTests) {
|
||||
CatchParseResult *testCase = new CatchParseResult(framework);
|
||||
testCase->fileName = filePath;
|
||||
testCase->name = testSpec.name;
|
||||
testCase->name = testLocation.m_name;
|
||||
testCase->proFile = proFile;
|
||||
testCase->itemType = TestTreeItem::TestFunction;
|
||||
testCase->itemType = testLocation.m_type;
|
||||
testCase->line = testLocation.m_line;
|
||||
testCase->column = testLocation.m_column;
|
||||
|
||||
@@ -256,7 +125,7 @@ static bool handleCatchDocument(QFutureInterface<TestParseResultPtr> futureInter
|
||||
|
||||
futureInterface.reportResult(TestParseResultPtr(parseResult));
|
||||
|
||||
return !result.keys().isEmpty();
|
||||
return !foundTests.isEmpty();
|
||||
}
|
||||
|
||||
bool CatchTestParser::processDocument(QFutureInterface<TestParseResultPtr> futureInterface, const QString &fileName)
|
||||
|
Reference in New Issue
Block a user