kde-extraapps/kdevplatform/plugins/testview/testview.cpp

429 lines
14 KiB
C++
Raw Normal View History

/* This file is part of KDevelop
Copyright 2012 Miha Čančula <miha@noughmad.eu>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "testview.h"
#include "testviewplugin.h"
#include "testviewdebug.h"
#include <interfaces/icore.h>
#include <interfaces/iproject.h>
#include <interfaces/iprojectcontroller.h>
#include <interfaces/itestcontroller.h>
#include <interfaces/itestsuite.h>
#include <interfaces/iruncontroller.h>
#include <interfaces/idocumentcontroller.h>
#include <interfaces/isession.h>
#include <util/executecompositejob.h>
#include <language/duchain/indexeddeclaration.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/duchain.h>
#include <language/duchain/declaration.h>
#include <KIcon>
#include <KActionCollection>
#include <KAction>
#include <KLocalizedString>
#include <KJob>
#include <krecursivefilterproxymodel.h>
#include <KLineEdit>
#include <KConfigGroup>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QHeaderView>
#include <QVBoxLayout>
using namespace KDevelop;
enum CustomRoles {
ProjectRole = Qt::UserRole + 1,
SuiteRole,
CaseRole
};
//BEGIN TestViewFilterAction
TestViewFilterAction::TestViewFilterAction( const QString &initialFilter, QObject* parent )
: KAction( parent )
, m_intialFilter(initialFilter)
{
setIcon(KIcon("view-filter"));
setText(i18n("Filter..."));
setToolTip(i18n("Insert wildcard patterns to filter the test view"
" for matching test suites and cases."));
}
QWidget* TestViewFilterAction::createWidget( QWidget* parent )
{
KLineEdit* edit = new KLineEdit(parent);
edit->setClickMessage(i18n("Filter..."));
edit->setClearButtonShown(true);
connect(edit, SIGNAL(textChanged(QString)), this, SIGNAL(filterChanged(QString)));
if (!m_intialFilter.isEmpty()) {
edit->setText(m_intialFilter);
}
return edit;
}
//END TestViwFilterAction
static const char* sessionConfigGroup = "TestView";
static const char* filterConfigKey = "filter";
TestView::TestView(TestViewPlugin* plugin, QWidget* parent)
: QWidget(parent)
, m_plugin(plugin)
, m_tree(new QTreeView(this))
, m_filter(new KRecursiveFilterProxyModel(this))
{
setWindowIcon(KIcon("preflight-verifier"));
setWindowTitle(i18n("Unit Tests"));
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
layout->addWidget(m_tree);
m_tree->setSortingEnabled(true);
m_tree->header()->hide();
m_tree->setIndentation(10);
m_tree->setEditTriggers(QTreeView::NoEditTriggers);
m_tree->setSelectionBehavior(QTreeView::SelectRows);
m_tree->setSelectionMode(QTreeView::SingleSelection);
m_tree->setExpandsOnDoubleClick(false);
m_tree->sortByColumn(0, Qt::AscendingOrder);
connect(m_tree, SIGNAL(activated(QModelIndex)), SLOT(doubleClicked(QModelIndex)));
m_model = new QStandardItemModel(this);
m_filter->setSourceModel(m_model);
m_tree->setModel(m_filter);
KAction* showSource = new KAction( KIcon("code-context"), i18n("Show Source"), this );
connect (showSource, SIGNAL(triggered(bool)), SLOT(showSource()));
m_contextMenuActions << showSource;
KAction* runSelected = new KAction( KIcon("system-run"), i18n("Run Selected Tests"), this );
connect (runSelected, SIGNAL(triggered(bool)), SLOT(runSelectedTests()));
m_contextMenuActions << runSelected;
addAction(plugin->actionCollection()->action("run_all_tests"));
addAction(plugin->actionCollection()->action("stop_running_tests"));
QString filterText;
KConfigGroup config(ICore::self()->activeSession()->config(), sessionConfigGroup);
if (config.hasKey(filterConfigKey))
{
filterText = config.readEntry(filterConfigKey, QString());
}
TestViewFilterAction* filterAction = new TestViewFilterAction(filterText, this);
connect(filterAction, SIGNAL(filterChanged(QString)),
m_filter, SLOT(setFilterFixedString(QString)));
addAction(filterAction);
IProjectController* pc = ICore::self()->projectController();
connect (pc, SIGNAL(projectClosed(KDevelop::IProject*)), SLOT(removeProject(KDevelop::IProject*)));
ITestController* tc = ICore::self()->testController();
connect (tc, SIGNAL(testSuiteAdded(KDevelop::ITestSuite*)),
SLOT(addTestSuite(KDevelop::ITestSuite*)));
connect (tc, SIGNAL(testSuiteRemoved(KDevelop::ITestSuite*)),
SLOT(removeTestSuite(KDevelop::ITestSuite*)));
connect (tc, SIGNAL(testRunFinished(KDevelop::ITestSuite*, KDevelop::TestResult)),
SLOT(updateTestSuite(KDevelop::ITestSuite*, KDevelop::TestResult)));
connect (tc, SIGNAL(testRunStarted(KDevelop::ITestSuite*, QStringList)),
SLOT(notifyTestCaseStarted(KDevelop::ITestSuite*, QStringList)));
foreach (ITestSuite* suite, tc->testSuites())
{
addTestSuite(suite);
}
}
TestView::~TestView()
{
}
void TestView::updateTestSuite(ITestSuite* suite, const TestResult& result)
{
QStandardItem* item = itemForSuite(suite);
if (!item)
{
return;
}
debug() << "Updating test suite" << suite->name();
item->setIcon(iconForTestResult(result.suiteResult));
for (int i = 0; i < item->rowCount(); ++i)
{
debug() << "Found a test case" << item->child(i)->text();
QStandardItem* caseItem = item->child(i);
if (result.testCaseResults.contains(caseItem->text()))
{
TestResult::TestCaseResult caseResult = result.testCaseResults.value(caseItem->text(), TestResult::NotRun);
caseItem->setIcon(iconForTestResult(caseResult));
}
}
}
void TestView::notifyTestCaseStarted(ITestSuite* suite, const QStringList& test_cases)
{
QStandardItem* item = itemForSuite(suite);
if (!item)
{
return;
}
debug() << "Notify a test of the suite " << suite->name() << " has started";
// Global test suite icon
item->setIcon(KIcon("process-idle"));
for (int i = 0; i < item->rowCount(); ++i)
{
debug() << "Found a test case" << item->child(i)->text();
QStandardItem* caseItem = item->child(i);
if (test_cases.contains(caseItem->text()))
{
// Each test case icon
caseItem->setIcon(KIcon("process-idle"));
}
}
}
KIcon TestView::iconForTestResult(TestResult::TestCaseResult result)
{
switch (result)
{
case TestResult::NotRun:
return KIcon("code-function");
case TestResult::Skipped:
return KIcon("task-delegate");
case TestResult::Passed:
return KIcon("dialog-ok-apply");
case TestResult::UnexpectedPass:
// This is a very rare occurrence, so the icon should stand out
return KIcon("dialog-warning");
case TestResult::Failed:
return KIcon("edit-delete");
case TestResult::ExpectedFail:
return KIcon("dialog-ok");
case TestResult::Error:
return KIcon("dialog-cancel");
default:
return KIcon("");
}
}
QStandardItem* TestView::itemForSuite(ITestSuite* suite)
{
foreach (QStandardItem* item, m_model->findItems(suite->name(), Qt::MatchRecursive))
{
if (item->parent() && item->parent()->text() == suite->project()->name()
&& !item->parent()->parent())
{
return item;
}
}
return 0;
}
QStandardItem* TestView::itemForProject(IProject* project)
{
foreach (QStandardItem* item, m_model->findItems(project->name()))
{
return item;
}
return addProject(project);
}
void TestView::runSelectedTests()
{
QModelIndexList indexes = m_tree->selectionModel()->selectedIndexes();
if (indexes.isEmpty())
{
return;
}
QList<KJob*> jobs;
ITestController* tc = ICore::self()->testController();
/*
* NOTE: If a test suite or a single test case was selected,
* the job is launched in Verbose mode with raised output window.
* If a project is selected, it is launched silently.
*
* This is the somewhat-intuitive approach. Maybe a configuration should be offered.
*/
foreach (const QModelIndex& idx, indexes)
{
QModelIndex index = m_filter->mapToSource(idx);
if (index.parent().isValid() && indexes.contains(index.parent()))
{
continue;
}
QStandardItem* item = m_model->itemFromIndex(index);
if (item->parent() == 0)
{
// A project was selected
IProject* project = ICore::self()->projectController()->findProjectByName(item->data(ProjectRole).toString());
foreach (ITestSuite* suite, tc->testSuitesForProject(project))
{
jobs << suite->launchAllCases(ITestSuite::Silent);
}
}
else if (item->parent()->parent() == 0)
{
// A suite was selected
IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->data(ProjectRole).toString());
ITestSuite* suite = tc->findTestSuite(project, item->data(SuiteRole).toString());
jobs << suite->launchAllCases(ITestSuite::Verbose);
}
else
{
// This was a single test case
IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->parent()->data(ProjectRole).toString());
ITestSuite* suite = tc->findTestSuite(project, item->parent()->data(SuiteRole).toString());
const QString testCase = item->data(CaseRole).toString();
jobs << suite->launchCase(testCase, ITestSuite::Verbose);
}
}
if (!jobs.isEmpty())
{
KDevelop::ExecuteCompositeJob* compositeJob = new KDevelop::ExecuteCompositeJob(this, jobs);
compositeJob->setObjectName(i18np("Run 1 test", "Run %1 tests", jobs.size()));
compositeJob->setProperty("test_job", true);
ICore::self()->runController()->registerJob(compositeJob);
}
}
void TestView::showSource()
{
QModelIndexList indexes = m_tree->selectionModel()->selectedIndexes();
if (indexes.isEmpty())
{
return;
}
IndexedDeclaration declaration;
ITestController* tc = ICore::self()->testController();
QModelIndex index = m_filter->mapToSource(indexes.first());
QStandardItem* item = m_model->itemFromIndex(index);
if (item->parent() == 0)
{
// No sense in finding source code for projects.
return;
}
else if (item->parent()->parent() == 0)
{
IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->data(ProjectRole).toString());
ITestSuite* suite = tc->findTestSuite(project, item->data(SuiteRole).toString());
declaration = suite->declaration();
}
else
{
IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->parent()->data(ProjectRole).toString());
ITestSuite* suite = tc->findTestSuite(project, item->parent()->data(SuiteRole).toString());
declaration = suite->caseDeclaration(item->data(CaseRole).toString());
}
DUChainReadLocker locker(DUChain::lock());
Declaration* d = declaration.data();
if (!d)
{
return;
}
KUrl url = d->url().toUrl();
KTextEditor::Cursor cursor = d->rangeInCurrentRevision().textRange().start();
locker.unlock();
IDocumentController* dc = ICore::self()->documentController();
debug() << "Activating declaration in" << url;
dc->openDocument(url, cursor);
}
void TestView::addTestSuite(ITestSuite* suite)
{
QStandardItem* projectItem = itemForProject(suite->project());
Q_ASSERT(projectItem);
QStandardItem* suiteItem = new QStandardItem(KIcon("view-list-tree"), suite->name());
suiteItem->setData(suite->name(), SuiteRole);
foreach (QString caseName, suite->cases())
{
QStandardItem* caseItem = new QStandardItem(iconForTestResult(TestResult::NotRun), caseName);
caseItem->setData(caseName, CaseRole);
suiteItem->appendRow(caseItem);
}
projectItem->appendRow(suiteItem);
}
void TestView::removeTestSuite(ITestSuite* suite)
{
QStandardItem* item = itemForSuite(suite);
item->parent()->removeRow(item->row());
}
QStandardItem* TestView::addProject(IProject* project)
{
QStandardItem* projectItem = new QStandardItem(KIcon("project-development"), project->name());
projectItem->setData(project->name(), ProjectRole);
m_model->appendRow(projectItem);
return projectItem;
}
void TestView::removeProject(IProject* project)
{
QStandardItem* projectItem = itemForProject(project);
m_model->removeRow(projectItem->row());
}
void TestView::doubleClicked(const QModelIndex& index)
{
m_tree->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
runSelectedTests();
}
QList< QAction* > TestView::contextMenuActions()
{
return m_contextMenuActions;
}