diff --git a/src/plugins/autotest/CMakeLists.txt b/src/plugins/autotest/CMakeLists.txt index 55adc4a923e..1d8e6dde6bd 100644 --- a/src/plugins/autotest/CMakeLists.txt +++ b/src/plugins/autotest/CMakeLists.txt @@ -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 diff --git a/src/plugins/autotest/autotest.pro b/src/plugins/autotest/autotest.pro index e6c0fb980ee..2633883b09b 100644 --- a/src/plugins/autotest/autotest.pro +++ b/src/plugins/autotest/autotest.pro @@ -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 \ diff --git a/src/plugins/autotest/catch/catchcodeparser.cpp b/src/plugins/autotest/catch/catchcodeparser.cpp new file mode 100644 index 00000000000..44f3632b83c --- /dev/null +++ b/src/plugins/autotest/catch/catchcodeparser.cpp @@ -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 + +#include + +#include + +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 diff --git a/src/plugins/autotest/catch/catchcodeparser.h b/src/plugins/autotest/catch/catchcodeparser.h new file mode 100644 index 00000000000..f8f20529271 --- /dev/null +++ b/src/plugins/autotest/catch/catchcodeparser.h @@ -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 +#include + +#include + +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 diff --git a/src/plugins/autotest/catch/catchtestparser.cpp b/src/plugins/autotest/catch/catchtestparser.cpp index 7b8772d6d35..18740ac4ad1 100644 --- a/src/plugins/autotest/catch/catchtestparser.cpp +++ b/src/plugins/autotest/catch/catchtestparser.cpp @@ -24,12 +24,11 @@ #include "catchtestparser.h" +#include "catchcodeparser.h" #include "catchtreeitem.h" #include #include -#include -#include #include #include @@ -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 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 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 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 result = visitor.testFunctions(); - QString proFile; - const QList &ppList = modelManager->projectPart(filePath); - if (ppList.size()) - proFile = ppList.first()->projectFile; - else + const QList 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 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 futureInter futureInterface.reportResult(TestParseResultPtr(parseResult)); - return !result.keys().isEmpty(); + return !foundTests.isEmpty(); } bool CatchTestParser::processDocument(QFutureInterface futureInterface, const QString &fileName)