Files
qt-creator/plugins/autotest/testtreemodel.cpp
2014-12-04 13:52:16 +01:00

633 lines
24 KiB
C++

/****************************************************************************
**
** Copyright (C) 2014 Digia Plc
** All rights reserved.
** For any questions to Digia, please use contact form at http://qt.digia.com
**
** This file is part of the Qt Creator Enterprise Auto Test Add-on.
**
** Licensees holding valid Qt Enterprise licenses may use this file in
** accordance with the Qt Enterprise License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.
**
** If you have questions regarding the use of this file, please use
** contact form at http://qt.digia.com
**
****************************************************************************/
#include "testcodeparser.h"
#include "testtreeitem.h"
#include "testtreemodel.h"
#include <cpptools/cppmodelmanager.h>
#include <projectexplorer/buildtargetinfo.h>
#include <projectexplorer/environmentaspect.h>
#include <projectexplorer/localapplicationrunconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/runconfiguration.h>
#include <projectexplorer/session.h>
#include <projectexplorer/target.h>
#include <texteditor/texteditor.h>
#include <utils/fileutils.h>
#include <QIcon>
namespace Autotest {
namespace Internal {
TestTreeModel::TestTreeModel(QObject *parent) :
QAbstractItemModel(parent),
m_rootItem(new TestTreeItem(QString(), QString(), TestTreeItem::ROOT)),
m_autoTestRootItem(new TestTreeItem(tr("Auto Tests"), QString(), TestTreeItem::ROOT, m_rootItem)),
m_quickTestRootItem(new TestTreeItem(tr("Qt Quick Tests"), QString(), TestTreeItem::ROOT, m_rootItem)),
m_parser(new TestCodeParser(this))
{
m_rootItem->appendChild(m_autoTestRootItem);
m_rootItem->appendChild(m_quickTestRootItem);
m_parser->updateTestTree();
// CppTools::CppModelManagerInterface *cppMM = CppTools::CppModelManagerInterface::instance();
// if (cppMM) {
// // replace later on by
// // cppMM->registerAstProcessor([this](const CplusPlus::Document::Ptr &doc,
// // const CPlusPlus::Snapshot &snapshot) {
// // checkForQtTestStuff(doc, snapshot);
// // });
// connect(cppMM, SIGNAL(documentUpdated(CPlusPlus::Document::Ptr)),
// this, SLOT(checkForQtTestStuff(CPlusPlus::Document::Ptr)),
// Qt::DirectConnection);
// }
}
static TestTreeModel *m_instance = 0;
TestTreeModel *TestTreeModel::instance()
{
if (!m_instance)
m_instance = new TestTreeModel;
return m_instance;
}
TestTreeModel::~TestTreeModel()
{
delete m_rootItem;
m_instance = 0;
}
QModelIndex TestTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
TestTreeItem *parentItem = parent.isValid()
? static_cast<TestTreeItem *>(parent.internalPointer())
: m_rootItem;
TestTreeItem *childItem = parentItem->child(row);
return childItem ? createIndex(row, column, childItem) : QModelIndex();
}
QModelIndex TestTreeModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
TestTreeItem *childItem = static_cast<TestTreeItem *>(index.internalPointer());
TestTreeItem *parentItem = childItem->parent();
if (parentItem == m_rootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
bool TestTreeModel::hasChildren(const QModelIndex &parent) const
{
if (!parent.isValid())
return true;
TestTreeItem *item = static_cast<TestTreeItem *>(parent.internalPointer());
return item->childCount() > 0;
}
int TestTreeModel::rowCount(const QModelIndex &parent) const
{
TestTreeItem *parentItem = parent.isValid()
? static_cast<TestTreeItem *>(parent.internalPointer()) : m_rootItem;
return parentItem->childCount();
}
int TestTreeModel::columnCount(const QModelIndex &) const
{
return 1;
}
static QIcon testTreeIcon(TestTreeItem::Type type)
{
static QIcon icons[3] = {
QIcon(),
QIcon(QLatin1String(":/images/class.png")),
QIcon(QLatin1String(":/images/func.png"))
};
return icons[type];
}
QVariant TestTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
TestTreeItem *item = static_cast<TestTreeItem *>(index.internalPointer());
if (!item)
return QVariant();
if (role == Qt::DisplayRole) {
if ((item == m_autoTestRootItem && m_autoTestRootItem->childCount() == 0)
|| (item == m_quickTestRootItem && m_quickTestRootItem->childCount() == 0)) {
return QString(item->name() + tr(" (none)"));
} else {
if (item->name().isEmpty())
return tr("<unnamed>");
return item->name();
}
return QVariant(); // TODO ?
}
switch(role) {
case Qt::ToolTipRole:
if (item->type() == TestTreeItem::TEST_CLASS && item->name().isEmpty())
return tr("<p>Unnamed test cases can't be (un)checked - avoid this by assigning a name."
"<br/>Having unnamed test cases invalidates the check state of named test "
"cases with the same main.cpp when executing selected tests.</p>");
return item->filePath();
case Qt::DecorationRole:
return testTreeIcon(item->type());
case Qt::CheckStateRole:
if (item->type() == TestTreeItem::ROOT)
return QVariant();
return item->checked();
case LinkRole:
QVariant itemLink;
TextEditor::TextEditorWidget::Link link(item->filePath(), item->line(), item->column());
itemLink.setValue(link);
return itemLink;
}
// TODO ?
return QVariant();
}
bool TestTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid())
return false;
if (role == Qt::CheckStateRole) {
TestTreeItem *item = static_cast<TestTreeItem *>(index.internalPointer());
Qt::CheckState old = item->checked();
item->setChecked((Qt::CheckState)value.toInt());
if (item->checked() != old) {
switch(item->type()) {
case TestTreeItem::TEST_CLASS:
emit dataChanged(index, index);
if (item->childCount() > 0) {
emit dataChanged(index.child(0, 0), index.child(item->childCount() - 1, 0));
}
break;
case TestTreeItem::TEST_FUNCTION:
emit dataChanged(index, index);
emit dataChanged(index.parent(), index.parent());
break;
default: // avoid warning regarding unhandled enum member
break;
}
return true;
}
}
return false;
}
Qt::ItemFlags TestTreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
TestTreeItem *item = static_cast<TestTreeItem *>(index.internalPointer());
switch(item->type()) {
case TestTreeItem::TEST_CLASS:
if (item->name().isEmpty())
return Qt::ItemIsSelectable | Qt::ItemIsTristate;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsTristate | Qt::ItemIsUserCheckable;
case TestTreeItem::TEST_FUNCTION:
if (item->parent()->name().isEmpty())
return Qt::ItemIsSelectable;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
case TestTreeItem::ROOT:
default:
return Qt::ItemIsEnabled;
}
}
bool TestTreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
if (!parent.isValid())
return false;
TestTreeItem *parentItem = static_cast<TestTreeItem *>(parent.internalPointer());
if (!parentItem)
return false;
bool subItemsSuccess = true;
bool itemSuccess = true;
for (int i = row + count - 1; i >= row; --i) {
QModelIndex child = index(i, 0, parent);
subItemsSuccess &= removeRows(0, rowCount(child), child);
beginRemoveRows(parent, i, i);
itemSuccess &= parentItem->removeChild(i);
endRemoveRows();
}
return subItemsSuccess && itemSuccess;
}
bool TestTreeModel::hasTests() const
{
return m_autoTestRootItem->childCount() > 0 || m_quickTestRootItem->childCount() > 0;
}
static void addProjectInformation(TestConfiguration *config, const QString &filePath)
{
QString targetFile;
QString targetName;
QString workDir;
QString proFile;
Utils::Environment env;
ProjectExplorer::Project *project = 0;
CppTools::CppModelManager *cppMM = CppTools::CppModelManager::instance();
QList<CppTools::ProjectPart::Ptr> projParts = cppMM->projectPart(filePath);
if (!projParts.empty()) {
proFile = projParts.at(0)->projectFile;
project = projParts.at(0)->project; // necessary to grab this here? or should this be the current active startup project anyway?
}
if (project) {
if (auto target = project->activeTarget()) {
ProjectExplorer::BuildTargetInfoList appTargets = target->applicationTargets();
foreach (const ProjectExplorer::BuildTargetInfo &bti, appTargets.list) {
if (bti.isValid() && bti.projectFilePath.toString() == proFile) {
targetFile = bti.targetFilePath.toString();
targetName = bti.targetName;
break;
}
}
QList<ProjectExplorer::RunConfiguration *> rcs = target->runConfigurations();
foreach (ProjectExplorer::RunConfiguration *rc, rcs) {
if (ProjectExplorer::LocalApplicationRunConfiguration *localRunConfiguration
= qobject_cast<ProjectExplorer::LocalApplicationRunConfiguration *>(rc)) {
if (localRunConfiguration->executable() == targetFile) {
workDir = Utils::FileUtils::normalizePathName(
localRunConfiguration->workingDirectory());
ProjectExplorer::EnvironmentAspect *envAsp
= localRunConfiguration->extraAspect<ProjectExplorer::EnvironmentAspect>();
env = envAsp->environment();
break;
}
}
}
}
}
config->setTargetFile(targetFile);
config->setTargetName(targetName);
config->setWorkingDirectory(workDir);
config->setProFile(proFile);
config->setEnvironment(env);
config->setProject(project);
}
QList<TestConfiguration *> TestTreeModel::getAllTestCases() const
{
QList<TestConfiguration *> result;
// get all Auto Tests
for (int row = 0, count = m_autoTestRootItem->childCount(); row < count; ++row) {
const TestTreeItem *child = m_autoTestRootItem->child(row);
TestConfiguration *tc = new TestConfiguration(child->name(), QStringList(),
child->childCount());
addProjectInformation(tc, child->filePath());
result << tc;
}
// get all Quick Tests
QMap<QString, int> foundMains;
for (int row = 0, count = m_quickTestRootItem->childCount(); row < count; ++row) {
TestTreeItem *child = m_quickTestRootItem->child(row);
// unnamed Quick Tests must be handled separately
if (child->name().isEmpty()) {
for (int childRow = 0, ccount = child->childCount(); childRow < ccount; ++ childRow) {
const TestTreeItem *grandChild = child->child(childRow);
const QString mainFile = grandChild->mainFile();
foundMains.insert(mainFile, foundMains.contains(mainFile)
? foundMains.value(mainFile) + 1 : 1);
}
continue;
}
// named Quick Test
const QString mainFile = child->mainFile();
foundMains.insert(mainFile, foundMains.contains(mainFile)
? foundMains.value(mainFile) + child->childCount()
: child->childCount());
}
// create TestConfiguration for each main
foreach (const QString &mainFile, foundMains.keys()) {
TestConfiguration *tc = new TestConfiguration(QString(), QStringList(),
foundMains.value(mainFile));
addProjectInformation(tc, mainFile);
result << tc;
}
return result;
}
QList<TestConfiguration *> TestTreeModel::getSelectedTests() const
{
QList<TestConfiguration *> result;
TestConfiguration *tc;
for (int row = 0, count = m_autoTestRootItem->childCount(); row < count; ++row) {
TestTreeItem *child = m_autoTestRootItem->child(row);
switch (child->checked()) {
case Qt::Unchecked:
continue;
case Qt::Checked:
tc = new TestConfiguration(child->name(), QStringList(), child->childCount());
addProjectInformation(tc, child->filePath());
result << tc;
continue;
case Qt::PartiallyChecked:
default:
const QString childName = child->name();
int grandChildCount = child->childCount();
QStringList testCases;
for (int grandChildRow = 0; grandChildRow < grandChildCount; ++grandChildRow) {
const TestTreeItem *grandChild = child->child(grandChildRow);
if (grandChild->checked() == Qt::Checked)
testCases << grandChild->name();
}
tc = new TestConfiguration(childName, testCases);
addProjectInformation(tc, child->filePath());
result << tc;
}
}
// Quick Tests must be handled differently - need the calling cpp file to use this in
// addProjectInformation() - additionally this must be unique to not execute the same executable
// on and on and on...
// TODO: do this later on for Auto Tests as well to support strange setups? or redo the model
QMap<QString, TestConfiguration *> foundMains;
TestTreeItem *unnamed = unnamedQuickTests();
for (int childRow = 0, ccount = unnamed->childCount(); childRow < ccount; ++ childRow) {
const TestTreeItem *grandChild = unnamed->child(childRow);
const QString mainFile = grandChild->mainFile();
if (foundMains.contains(mainFile)) {
foundMains[mainFile]->setTestCaseCount(tc->testCaseCount() + 1);
} else {
TestConfiguration *tc = new TestConfiguration(QString(), QStringList());
tc->setTestCaseCount(1);
addProjectInformation(tc, mainFile);
foundMains.insert(mainFile, tc);
}
}
for (int row = 0, count = m_quickTestRootItem->childCount(); row < count; ++row) {
TestTreeItem *child = m_quickTestRootItem->child(row);
// unnamed Quick Tests have been handled separately already
if (child->name().isEmpty())
continue;
// named Quick Tests
switch (child->checked()) {
case Qt::Unchecked:
continue;
case Qt::Checked:
case Qt::PartiallyChecked:
default:
QStringList testFunctions;
int grandChildCount = child->childCount();
for (int grandChildRow = 0; grandChildRow < grandChildCount; ++grandChildRow) {
const TestTreeItem *grandChild = child->child(grandChildRow);
if (grandChild->checked() == Qt::Checked)
testFunctions << child->name() + QLatin1String("::") + grandChild->name();
}
TestConfiguration *tc;
if (foundMains.contains(child->mainFile())) {
tc = foundMains[child->mainFile()];
QStringList oldFunctions(tc->testCases());
// if oldFunctions.size() is 0 this test configuration is used for at least one
// unnamed test case
if (oldFunctions.size() == 0) {
tc->setTestCaseCount(tc->testCaseCount() + testFunctions.size());
} else {
oldFunctions << testFunctions;
tc->setTestCases(oldFunctions);
}
} else {
tc = new TestConfiguration(QString(), testFunctions);
addProjectInformation(tc, child->mainFile());
foundMains.insert(child->mainFile(), tc);
}
break;
}
}
foreach (TestConfiguration *config, foundMains.values())
result << config;
return result;
}
TestTreeItem *TestTreeModel::unnamedQuickTests() const
{
for (int row = 0, count = m_quickTestRootItem->childCount(); row < count; ++row) {
TestTreeItem *child = m_quickTestRootItem->child(row);
if (child->name().isEmpty())
return child;
}
return 0;
}
void TestTreeModel::modifyAutoTestSubtree(int row, TestTreeItem *newItem)
{
QModelIndex toBeModifiedIndex = index(0, 0).child(row, 0);
modifyTestSubtree(toBeModifiedIndex, newItem);
}
void TestTreeModel::removeAutoTestSubtreeByFilePath(const QString &file)
{
const QModelIndex atRootIndex = index(0, 0);
const int count = rowCount(atRootIndex);
for (int row = 0; row < count; ++row) {
const QModelIndex childIndex = atRootIndex.child(row, 0);
TestTreeItem *childItem = static_cast<TestTreeItem *>(childIndex.internalPointer());
if (file == childItem->filePath()) {
removeRow(row, atRootIndex);
break;
}
}
emit testTreeModelChanged();
}
void TestTreeModel::addAutoTest(TestTreeItem *newItem)
{
beginInsertRows(index(0, 0), m_autoTestRootItem->childCount(), m_autoTestRootItem->childCount());
m_autoTestRootItem->appendChild(newItem);
endInsertRows();
emit testTreeModelChanged();
}
void TestTreeModel::removeAllAutoTests()
{
beginResetModel();
m_autoTestRootItem->removeChildren();
endResetModel();
emit testTreeModelChanged();
}
void TestTreeModel::modifyQuickTestSubtree(int row, TestTreeItem *newItem)
{
QModelIndex toBeModifiedIndex = index(1, 0).child(row, 0);
modifyTestSubtree(toBeModifiedIndex, newItem);
}
void TestTreeModel::removeQuickTestSubtreeByFilePath(const QString &file)
{
const QModelIndex qtRootIndex = index(1, 0);
for (int row = 0, count = rowCount(qtRootIndex); row < count; ++row) {
const QModelIndex childIndex = qtRootIndex.child(row, 0);
const TestTreeItem *childItem = static_cast<TestTreeItem *>(childIndex.internalPointer());
if (file == childItem->filePath()) {
removeRow(row, qtRootIndex);
break;
}
}
emit testTreeModelChanged();
}
void TestTreeModel::addQuickTest(TestTreeItem *newItem)
{
beginInsertRows(index(1, 0), m_quickTestRootItem->childCount(), m_quickTestRootItem->childCount());
m_quickTestRootItem->appendChild(newItem);
endInsertRows();
emit testTreeModelChanged();
}
void TestTreeModel::removeAllQuickTests()
{
beginResetModel();
m_quickTestRootItem->removeChildren();
endResetModel();
emit testTreeModelChanged();
}
void TestTreeModel::removeUnnamedQuickTest(const QString &filePath)
{
TestTreeItem *unnamedQT = unnamedQuickTests();
if (!unnamedQT)
return;
const QModelIndex unnamedQTIndex = index(1, 0).child(unnamedQT->row(), 0);
for (int childRow = unnamedQT->childCount() - 1; childRow >= 0; --childRow) {
const TestTreeItem *child = unnamedQT->child(childRow);
if (filePath == child->filePath())
removeRow(childRow, unnamedQTIndex);
}
emit testTreeModelChanged();
}
void TestTreeModel::modifyTestSubtree(QModelIndex &toBeModifiedIndex, TestTreeItem *newItem)
{
if (!toBeModifiedIndex.isValid())
return;
static QVector<int> modificationRoles = QVector<int>() << Qt::DisplayRole
<< Qt::ToolTipRole << LinkRole;
TestTreeItem *toBeModifiedItem = static_cast<TestTreeItem *>(toBeModifiedIndex.internalPointer());
if (toBeModifiedItem->modifyContent(newItem))
emit dataChanged(toBeModifiedIndex, toBeModifiedIndex, modificationRoles);
// process sub-items as well...
const int childCount = toBeModifiedItem->childCount();
const int newChildCount = newItem->childCount();
// for keeping the CheckState on modifications
QHash<QString, Qt::CheckState> originalItems;
for (int row = 0; row < childCount; ++row) {
const TestTreeItem *child = toBeModifiedItem->child(row);
originalItems.insert(child->name(), child->checked());
}
if (childCount <= newChildCount) {
for (int row = 0; row < childCount; ++row) {
QModelIndex child = toBeModifiedIndex.child(row, 0);
TestTreeItem *toBeModifiedChild = toBeModifiedItem->child(row);
TestTreeItem *modifiedChild = newItem->child(row);
if (toBeModifiedChild->modifyContent(modifiedChild)) {
emit dataChanged(child, child, modificationRoles);
}
if (originalItems.contains(toBeModifiedChild->name())) {
Qt::CheckState state = originalItems.value(toBeModifiedChild->name());
if (state != toBeModifiedChild->checked()) {
toBeModifiedChild->setChecked(state);
emit dataChanged(child, child, QVector<int>() << Qt::CheckStateRole);
}
} else { // newly added (BAD: happens for renaming as well)
toBeModifiedChild->setChecked(Qt::Checked);
emit dataChanged(child, child, QVector<int>() << Qt::CheckStateRole);
}
}
if (childCount < newChildCount) { // add aditional items
for (int row = childCount; row < newChildCount; ++row) {
TestTreeItem *newChild = newItem->child(row);
TestTreeItem *toBeAdded = new TestTreeItem(newChild->name(), newChild->filePath(),
newChild->type(), toBeModifiedItem);
toBeAdded->setLine(newChild->line());
beginInsertRows(toBeModifiedIndex, row, row);
toBeModifiedItem->appendChild(toBeAdded);
endInsertRows();
if (originalItems.contains(toBeAdded->name())
&& originalItems.value(toBeAdded->name()) != Qt::Checked)
toBeAdded->setChecked(originalItems.value(toBeAdded->name()));
}
}
} else {
for (int row = 0; row < newChildCount; ++row) {
QModelIndex child = toBeModifiedIndex.child(row, 0);
TestTreeItem *toBeModifiedChild = toBeModifiedItem->child(row);
TestTreeItem *modifiedChild = newItem->child(row);
if (toBeModifiedChild->modifyContent(modifiedChild))
emit dataChanged(child, child, modificationRoles);
if (originalItems.contains(toBeModifiedChild->name())) {
Qt::CheckState state = originalItems.value(toBeModifiedChild->name());
if (state != toBeModifiedChild->checked()) {
toBeModifiedChild->setChecked(state);
emit dataChanged(child, child, QVector<int>() << Qt::CheckStateRole);
}
} else { // newly added (BAD: happens for renaming as well)
toBeModifiedChild->setChecked(Qt::Checked);
emit dataChanged(child, child, QVector<int>() << Qt::CheckStateRole);
}
} // remove rest of the items
removeRows(newChildCount, childCount - newChildCount, toBeModifiedIndex);
}
emit testTreeModelChanged();
}
} // namespace Internal
} // namespace Autotest