Commit d284cd99 authored by Christian Stenger's avatar Christian Stenger
Browse files

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: default avatarNikolai Kosjar <nikolai.kosjar@theqtcompany.com>
parent afcb9283
......@@ -21,5 +21,6 @@
<file>images/run.png</file>
<file>images/runselected.png</file>
<file>images/stop.png</file>
<file>images/data.png</file>
</qresource>
</RCC>
......@@ -355,11 +355,40 @@ static CPlusPlus::Document::Ptr declaringDocument(CPlusPlus::Document::Ptr doc,
return declaringDoc;
}
static bool hasFunctionWithDataTagUsage(const QMap<QString, TestCodeLocationAndType> &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<QString, TestCodeLocationList> checkForDataTags(const QString &fileName,
const QMap<QString, TestCodeLocationAndType> &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<QString, TestCodeLocationList>();
}
static TestTreeItem constructTestTreeItem(const QString &fileName,
const QString &mainFile, // used for Quick Tests only
const QString &testCaseName,
int line, int column,
const QMap<QString, TestCodeLocationAndType> functions)
const QMap<QString, TestCodeLocationAndType> functions,
const QMap<QString, TestCodeLocationList> dataTags = QMap<QString, TestCodeLocationList>())
{
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<QString, TestCodeLocationAndType> testFunctions = visitor.privateSlots();
const QMap<QString, TestCodeLocationList> 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);
......
......@@ -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<TestCodeLocationAndType> TestCodeLocationList;
} // namespace Internal
} // namespace Autotest
......
......@@ -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()) {
......
......@@ -21,7 +21,6 @@
#include <cplusplus/FullySpecifiedType.h>
#include <cplusplus/LookupContext.h>
#include <cplusplus/Overview.h>
#include <cplusplus/Symbols.h>
#include <cplusplus/TypeOfExpression.h>
......@@ -29,6 +28,8 @@
#include <qmljs/parser/qmljsast_p.h>
#include <utils/qtcassert.h>
#include <QList>
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()))
......
......@@ -24,6 +24,7 @@
#include <cplusplus/ASTVisitor.h>
#include <cplusplus/CppDocument.h>
#include <cplusplus/Overview.h>
#include <cplusplus/Scope.h>
#include <cplusplus/SymbolVisitor.h>
......@@ -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<QString, TestCodeLocationList> 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<QString, TestCodeLocationList> m_dataTags;
TestCodeLocationList m_currentTags;
unsigned m_currentAstDepth;
unsigned m_insideUsingQTestDepth;
bool m_insideUsingQTest;
};
class TestQmlVisitor : public QmlJS::AST::Visitor
{
public:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment