From d284cd99d4143ebe21844347e9a0bc4627494c4c Mon Sep 17 00:00:00 2001 From: Christian Stenger Date: Tue, 4 Aug 2015 15:53:09 +0200 Subject: [PATCH] Add basic support for data tags This enables displaying data tags for data functions inside the test tree. Clicking on the data tag opens the editor at the location the respective QTest::newRow() call is done. Change-Id: Ia91bf87437c2608a05bae88ed715711217685fdf Reviewed-by: Nikolai Kosjar --- plugins/autotest/autotest.qrc | 1 + plugins/autotest/images/data.png | Bin 0 -> 646 bytes plugins/autotest/testcodeparser.cpp | 54 ++++++++++- plugins/autotest/testtreeitem.h | 5 +- plugins/autotest/testtreemodel.cpp | 25 ++++- plugins/autotest/testvisitor.cpp | 137 +++++++++++++++++++++++++++- plugins/autotest/testvisitor.h | 27 ++++++ 7 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 plugins/autotest/images/data.png diff --git a/plugins/autotest/autotest.qrc b/plugins/autotest/autotest.qrc index 2770aff5fe2..8c70e3ae3f5 100644 --- a/plugins/autotest/autotest.qrc +++ b/plugins/autotest/autotest.qrc @@ -21,5 +21,6 @@ images/run.png images/runselected.png images/stop.png + images/data.png diff --git a/plugins/autotest/images/data.png b/plugins/autotest/images/data.png new file mode 100644 index 0000000000000000000000000000000000000000..1f510c4042474f3ed76bbcafbc3ece42d12a03cb GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!tod_7$pLo9liPTt!$#ayIK z{`Zah=XrMj5%y3JVpL)35)e}LEn-}G$xWn0L_;WbokD4zg1Ce4bRka8Rwf}97J&si zJa0B^u-D$@8kNt<9y;S{~(~nloKQiMe%TkR)o2q%{tvzX` zSn~dt$h|eY>)+W)JYTsv>Lk;GlCz(~XXn(+xx9t{<7e0J4`kQ7%YWEdeUX`O-8FTm zkgVca25O6a-E!1Y3(h@$Hg6sG5|NWTnp>a%@t;NQW1`~M|@H?taUh#Bf0+xF*g zMz6YmtGD>RBfBN &testFunctions) +{ + foreach (const QString &functionName, testFunctions.keys()) { + if (functionName.endsWith(QLatin1String("_data")) && + testFunctions.contains(functionName.left(functionName.size() - 5))) { + return true; + } + } + return false; +} + +static QMap checkForDataTags(const QString &fileName, + const QMap &testFunctions) +{ + if (hasFunctionWithDataTagUsage(testFunctions)) { + const CPlusPlus::Snapshot snapshot = CPlusPlus::CppModelManagerBase::instance()->snapshot(); + const QByteArray fileContent = getFileContent(fileName); + CPlusPlus::Document::Ptr document = snapshot.preprocessedDocument(fileContent, fileName); + document->check(); + CPlusPlus::AST *ast = document->translationUnit()->ast(); + TestDataFunctionVisitor visitor(document); + visitor.accept(ast); + return visitor.dataTags(); + } + return QMap(); +} + + static TestTreeItem constructTestTreeItem(const QString &fileName, const QString &mainFile, // used for Quick Tests only const QString &testCaseName, int line, int column, - const QMap functions) + const QMap functions, + const QMap dataTags = QMap()) { TestTreeItem treeItem(testCaseName, fileName, TestTreeItem::TEST_CLASS); treeItem.setMainFile(mainFile); // used for Quick Tests only @@ -368,10 +397,24 @@ static TestTreeItem constructTestTreeItem(const QString &fileName, foreach (const QString &functionName, functions.keys()) { const TestCodeLocationAndType locationAndType = functions.value(functionName); - TestTreeItem *treeItemChild = new TestTreeItem(functionName, locationAndType.m_fileName, + TestTreeItem *treeItemChild = new TestTreeItem(functionName, locationAndType.m_name, locationAndType.m_type, &treeItem); treeItemChild->setLine(locationAndType.m_line); treeItemChild->setColumn(locationAndType.m_column); + // check for data tags and if there are any for this function add them + const QString qualifiedFunctionName = testCaseName + QLatin1String("::") + functionName; + if (dataTags.contains(qualifiedFunctionName)) { + const TestCodeLocationList &tags = dataTags.value(qualifiedFunctionName); + foreach (const TestCodeLocationAndType &tagLocation, tags) { + TestTreeItem *tagTreeItem = new TestTreeItem(tagLocation.m_name, + locationAndType.m_name, + tagLocation.m_type, treeItemChild); + tagTreeItem->setLine(tagLocation.m_line); + tagTreeItem->setColumn(tagLocation.m_column); + treeItemChild->appendChild(tagTreeItem); + } + } + treeItem.appendChild(treeItemChild); } return treeItem; @@ -437,8 +480,11 @@ void TestCodeParser::checkDocumentForTestCode(CPlusPlus::Document::Ptr document) visitor.accept(declaringDoc->globalNamespace()); const QMap testFunctions = visitor.privateSlots(); + const QMap dataTags = + checkForDataTags(declaringDoc->fileName(), testFunctions); TestTreeItem item = constructTestTreeItem(declaringDoc->fileName(), QString(), - testCaseName, line, column, testFunctions); + testCaseName, line, column, testFunctions, + dataTags); updateModelAndCppDocMap(document, declaringDoc->fileName(), item); return; } @@ -487,7 +533,7 @@ void TestCodeParser::handleQtQuickTest(CPlusPlus::Document::Ptr document) // construct new/modified TestTreeItem TestTreeItem testTreeItem - = constructTestTreeItem(tcLocationAndType.m_fileName, cppFileName, testCaseName, + = constructTestTreeItem(tcLocationAndType.m_name, cppFileName, testCaseName, tcLocationAndType.m_line, tcLocationAndType.m_column, testFunctions); diff --git a/plugins/autotest/testtreeitem.h b/plugins/autotest/testtreeitem.h index 0cfa1aead11..8c7bf14307f 100644 --- a/plugins/autotest/testtreeitem.h +++ b/plugins/autotest/testtreeitem.h @@ -35,6 +35,7 @@ public: ROOT, TEST_CLASS, TEST_FUNCTION, + TEST_DATATAG, TEST_DATAFUNCTION, TEST_SPECIALFUNCTION }; @@ -84,12 +85,14 @@ private: }; struct TestCodeLocationAndType { - QString m_fileName; + QString m_name; // tag name for m_type == TEST_DATATAG, file name for other values unsigned m_line; unsigned m_column; TestTreeItem::Type m_type; }; +typedef QVector TestCodeLocationList; + } // namespace Internal } // namespace Autotest diff --git a/plugins/autotest/testtreemodel.cpp b/plugins/autotest/testtreemodel.cpp index 5eea7b7d982..e30b7f651cb 100644 --- a/plugins/autotest/testtreemodel.cpp +++ b/plugins/autotest/testtreemodel.cpp @@ -180,12 +180,13 @@ int TestTreeModel::columnCount(const QModelIndex &) const static QIcon testTreeIcon(TestTreeItem::Type type) { - static QIcon icons[3] = { + static QIcon icons[] = { QIcon(), QIcon(QLatin1String(":/images/class.png")), - QIcon(QLatin1String(":/images/func.png")) + QIcon(QLatin1String(":/images/func.png")), + QIcon(QLatin1String(":/images/data.png")) }; - if (type >= 3) + if (type >= sizeof(icons) / sizeof(icons[0])) return icons[2]; return icons[type]; } @@ -204,7 +205,7 @@ QVariant TestTreeModel::data(const QModelIndex &index, int role) const || (item == m_quickTestRootItem && m_quickTestRootItem->childCount() == 0)) { return QString(item->name() + tr(" (none)")); } else { - if (item->name().isEmpty()) + if (item->name().isEmpty() && item->type() == TestTreeItem::TEST_CLASS) return tr(Constants::UNNAMED_QUICKTESTS); return item->name(); } @@ -222,6 +223,7 @@ QVariant TestTreeModel::data(const QModelIndex &index, int role) const case Qt::CheckStateRole: switch (item->type()) { case TestTreeItem::ROOT: + case TestTreeItem::TEST_DATATAG: case TestTreeItem::TEST_DATAFUNCTION: case TestTreeItem::TEST_SPECIALFUNCTION: return QVariant(); @@ -682,7 +684,7 @@ void TestTreeModel::updateUnnamedQuickTest(const QString &fileName, const QStrin foreach (const QString &functionName, functions.keys()) { const TestCodeLocationAndType locationAndType = functions.value(functionName); - TestTreeItem *testFunction = new TestTreeItem(functionName, locationAndType.m_fileName, + TestTreeItem *testFunction = new TestTreeItem(functionName, locationAndType.m_name, locationAndType.m_type, &unnamed); testFunction->setLine(locationAndType.m_line); testFunction->setColumn(locationAndType.m_column); @@ -819,6 +821,19 @@ void TestTreeModel::processChildren(QModelIndex &parentIndex, const TestTreeItem TestTreeItem *modifiedChild = newItem.child(row); if (toBeModifiedChild->modifyContent(modifiedChild)) emit dataChanged(child, child, modificationRoles); + + // handle data tags - just remove old and add them + if (modifiedChild->childCount() || toBeModifiedChild->childCount()) { + beginRemoveRows(child, 0, toBeModifiedChild->childCount()); + toBeModifiedChild->removeChildren(); + endRemoveRows(); + const int count = modifiedChild->childCount(); + beginInsertRows(child, 0, count); + for (int childRow = 0; childRow < count; ++childRow) + toBeModifiedChild->appendChild(new TestTreeItem(*modifiedChild->child(childRow))); + endInsertRows(); + } + if (checkStates.contains(toBeModifiedChild->name())) { Qt::CheckState state = checkStates.value(toBeModifiedChild->name()); if (state != toBeModifiedChild->checked()) { diff --git a/plugins/autotest/testvisitor.cpp b/plugins/autotest/testvisitor.cpp index d0179c1ca72..3fe84cb0776 100644 --- a/plugins/autotest/testvisitor.cpp +++ b/plugins/autotest/testvisitor.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include @@ -29,6 +28,8 @@ #include +#include + #include namespace Autotest { @@ -73,11 +74,11 @@ bool TestVisitor::visit(CPlusPlus::Class *symbol) CPlusPlus::Function *functionDefinition = m_symbolFinder.findMatchingDefinition( func, CppTools::CppModelManager::instance()->snapshot(), true); if (functionDefinition) { - locationAndType.m_fileName = QString::fromUtf8(functionDefinition->fileName()); + locationAndType.m_name = QString::fromUtf8(functionDefinition->fileName()); locationAndType.m_line = functionDefinition->line(); locationAndType.m_column = functionDefinition->column() - 1; } else { // if we cannot find the definition use declaration as fallback - locationAndType.m_fileName = QString::fromUtf8(member->fileName()); + locationAndType.m_name = QString::fromUtf8(member->fileName()); locationAndType.m_line = member->line(); locationAndType.m_column = member->column() - 1; } @@ -145,6 +146,132 @@ bool TestAstVisitor::visit(CPlusPlus::CompoundStatementAST *ast) return true; } +/********************** Test Data Function AST Visitor ************************/ + +TestDataFunctionVisitor::TestDataFunctionVisitor(CPlusPlus::Document::Ptr doc) + : CPlusPlus::ASTVisitor(doc->translationUnit()), + m_currentDoc(doc), + m_currentAstDepth(0), + m_insideUsingQTestDepth(0), + m_insideUsingQTest(false) +{ +} + +TestDataFunctionVisitor::~TestDataFunctionVisitor() +{ +} + +bool TestDataFunctionVisitor::visit(CPlusPlus::UsingDirectiveAST *ast) +{ + if (auto nameAST = ast->name) { + if (m_overview.prettyName(nameAST->name) == QLatin1String("QTest")) { + m_insideUsingQTest = true; + // we need the surrounding AST depth as using directive is an AST itself + m_insideUsingQTestDepth = m_currentAstDepth - 1; + } + } + return true; +} + +bool TestDataFunctionVisitor::visit(CPlusPlus::FunctionDefinitionAST *ast) +{ + if (ast->declarator) { + CPlusPlus::DeclaratorIdAST *id = ast->declarator->core_declarator->asDeclaratorId(); + if (!id) + return false; + + const QString prettyName = m_overview.prettyName(id->name->name); + // do not handle functions that aren't real test data functions + if (!prettyName.endsWith(QLatin1String("_data")) || !ast->symbol + || ast->symbol->argumentCount() != 0) { + return false; + } + + m_currentFunction = prettyName.left(prettyName.size() - 5); + m_currentTags.clear(); + return true; + } + + return false; +} + +bool TestDataFunctionVisitor::visit(CPlusPlus::CallAST *ast) +{ + if (m_currentFunction.isEmpty()) + return true; + + unsigned firstToken; + if (newRowCallFound(ast, &firstToken)) { + if (const auto expressionListAST = ast->expression_list) { + // first argument is the one we need + if (const auto argumentExpressionAST = expressionListAST->value) { + if (const auto stringLiteral = argumentExpressionAST->asStringLiteral()) { + auto token = m_currentDoc->translationUnit()->tokenAt( + stringLiteral->literal_token); + if (token.isStringLiteral()) { + unsigned line = 0; + unsigned column = 0; + m_currentDoc->translationUnit()->getTokenStartPosition( + firstToken, &line, &column); + TestCodeLocationAndType locationAndType; + locationAndType.m_name = QString::fromUtf8(token.spell()); + locationAndType.m_column = column - 1; + locationAndType.m_line = line; + locationAndType.m_type = TestTreeItem::TEST_DATATAG; + m_currentTags.append(locationAndType); + } + } + } + } + } + return true; +} + +bool TestDataFunctionVisitor::preVisit(CPlusPlus::AST *) +{ + ++m_currentAstDepth; + return true; +} + +void TestDataFunctionVisitor::postVisit(CPlusPlus::AST *ast) +{ + --m_currentAstDepth; + m_insideUsingQTest &= m_currentAstDepth >= m_insideUsingQTestDepth; + + if (!ast->asFunctionDefinition()) + return; + + if (!m_currentFunction.isEmpty() && !m_currentTags.isEmpty()) + m_dataTags.insert(m_currentFunction, m_currentTags); + + m_currentFunction.clear(); + m_currentTags.clear(); +} + +bool TestDataFunctionVisitor::newRowCallFound(CPlusPlus::CallAST *ast, unsigned *firstToken) const +{ + QTC_ASSERT(firstToken, return false); + + if (!ast->base_expression) + return false; + + bool found = false; + + if (const CPlusPlus::IdExpressionAST *exp = ast->base_expression->asIdExpression()) { + if (!exp->name) + return false; + + if (const auto qualifiedNameAST = exp->name->asQualifiedName()) { + found = m_overview.prettyName(qualifiedNameAST->name) == QLatin1String("QTest::newRow"); + *firstToken = qualifiedNameAST->firstToken(); + } else if (m_insideUsingQTest) { + found = m_overview.prettyName(exp->name->name) == QLatin1String("newRow"); + *firstToken = exp->name->firstToken(); + } + } + return found; +} + /*************************** Quick Test AST Visitor ***************************/ TestQmlVisitor::TestQmlVisitor(QmlJS::Document::Ptr doc) @@ -164,7 +291,7 @@ bool TestQmlVisitor::visit(QmlJS::AST::UiObjectDefinition *ast) m_currentTestCaseName.clear(); const auto sourceLocation = ast->firstSourceLocation(); - m_testCaseLocation.m_fileName = m_currentDoc->fileName(); + m_testCaseLocation.m_name = m_currentDoc->fileName(); m_testCaseLocation.m_line = sourceLocation.startLine; m_testCaseLocation.m_column = sourceLocation.startColumn - 1; m_testCaseLocation.m_type = TestTreeItem::TEST_CLASS; @@ -192,7 +319,7 @@ bool TestQmlVisitor::visit(QmlJS::AST::FunctionDeclaration *ast) || specialFunctions.contains(name.toString())) { const auto sourceLocation = ast->firstSourceLocation(); TestCodeLocationAndType locationAndType; - locationAndType.m_fileName = m_currentDoc->fileName(); + locationAndType.m_name = m_currentDoc->fileName(); locationAndType.m_line = sourceLocation.startLine; locationAndType.m_column = sourceLocation.startColumn - 1; if (specialFunctions.contains(name.toString())) diff --git a/plugins/autotest/testvisitor.h b/plugins/autotest/testvisitor.h index 07d58352930..4441eb26f8a 100644 --- a/plugins/autotest/testvisitor.h +++ b/plugins/autotest/testvisitor.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -72,6 +73,32 @@ private: }; +class TestDataFunctionVisitor : public CPlusPlus::ASTVisitor +{ +public: + TestDataFunctionVisitor(CPlusPlus::Document::Ptr doc); + virtual ~TestDataFunctionVisitor(); + + bool visit(CPlusPlus::UsingDirectiveAST *ast); + bool visit(CPlusPlus::FunctionDefinitionAST *ast); + bool visit(CPlusPlus::CallAST *ast); + bool preVisit(CPlusPlus::AST *ast); + void postVisit(CPlusPlus::AST *ast); + QMap dataTags() const { return m_dataTags; } + +private: + bool newRowCallFound(CPlusPlus::CallAST *ast, unsigned *firstToken) const; + + CPlusPlus::Document::Ptr m_currentDoc; + CPlusPlus::Overview m_overview; + QString m_currentFunction; + QMap m_dataTags; + TestCodeLocationList m_currentTags; + unsigned m_currentAstDepth; + unsigned m_insideUsingQTestDepth; + bool m_insideUsingQTest; +}; + class TestQmlVisitor : public QmlJS::AST::Visitor { public: