mirror of
https://bitbucket.org/smil3y/kde-extraapps.git
synced 2025-02-25 11:22:55 +00:00
428 lines
14 KiB
C++
428 lines
14 KiB
C++
/* 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;
|
|
}
|
|
|