File indexing completed on 2024-04-28 04:39:52
0001 /* 0002 SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "testview.h" 0008 #include "testviewplugin.h" 0009 #include <debug.h> 0010 0011 #include <interfaces/icore.h> 0012 #include <interfaces/iproject.h> 0013 #include <interfaces/iprojectcontroller.h> 0014 #include <interfaces/itestcontroller.h> 0015 #include <interfaces/itestsuite.h> 0016 #include <interfaces/iruncontroller.h> 0017 #include <interfaces/idocumentcontroller.h> 0018 #include <interfaces/isession.h> 0019 0020 #include <util/executecompositejob.h> 0021 0022 #include <language/duchain/indexeddeclaration.h> 0023 #include <language/duchain/duchainlock.h> 0024 #include <language/duchain/duchain.h> 0025 #include <language/duchain/declaration.h> 0026 0027 #include <KActionCollection> 0028 #include <KJob> 0029 #include <KLocalizedString> 0030 0031 #include <QAction> 0032 #include <QHeaderView> 0033 #include <QIcon> 0034 #include <QLineEdit> 0035 #include <QStandardItem> 0036 #include <QStandardItemModel> 0037 #include <QVBoxLayout> 0038 #include <QWidgetAction> 0039 #include <QSortFilterProxyModel> 0040 0041 using namespace KDevelop; 0042 0043 enum CustomRoles { 0044 ProjectRole = Qt::UserRole + 1, 0045 SuiteRole, 0046 CaseRole 0047 }; 0048 0049 TestView::TestView(TestViewPlugin* plugin, QWidget* parent) 0050 : QWidget(parent) 0051 , m_plugin(plugin) 0052 , m_tree(new QTreeView(this)) 0053 , m_filter(new QSortFilterProxyModel(this)) 0054 { 0055 setWindowIcon(QIcon::fromTheme(QStringLiteral("preflight-verifier"), windowIcon())); 0056 setWindowTitle(i18nc("@title:window", "Unit Tests")); 0057 0058 auto* layout = new QVBoxLayout(this); 0059 layout->setContentsMargins(0, 0, 0, 0); 0060 setLayout(layout); 0061 layout->addWidget(m_tree); 0062 0063 m_tree->setSortingEnabled(true); 0064 m_tree->header()->hide(); 0065 m_tree->setIndentation(10); 0066 m_tree->setEditTriggers(QTreeView::NoEditTriggers); 0067 m_tree->setSelectionBehavior(QTreeView::SelectRows); 0068 m_tree->setSelectionMode(QTreeView::ExtendedSelection); 0069 m_tree->setExpandsOnDoubleClick(false); 0070 m_tree->sortByColumn(0, Qt::AscendingOrder); 0071 connect(m_tree, &QTreeView::doubleClicked, this, &TestView::doubleClicked); 0072 0073 m_model = new QStandardItemModel(this); 0074 m_filter->setRecursiveFilteringEnabled(true); 0075 m_filter->setSourceModel(m_model); 0076 m_tree->setModel(m_filter); 0077 0078 auto* showSource = new QAction( QIcon::fromTheme(QStringLiteral("code-context")), i18nc("@action:inmenu", "Show Source"), this ); 0079 connect (showSource, &QAction::triggered, this, &TestView::showSource); 0080 m_contextMenuActions << showSource; 0081 0082 addAction(plugin->actionCollection()->action(QStringLiteral("run_all_tests"))); 0083 addAction(plugin->actionCollection()->action(QStringLiteral("stop_running_tests"))); 0084 0085 auto* runSelected = new QAction( QIcon::fromTheme(QStringLiteral("system-run")), i18nc("@action", "Run Selected Tests"), this ); 0086 connect (runSelected, &QAction::triggered, this, &TestView::runSelectedTests); 0087 addAction(runSelected); 0088 0089 auto* edit = new QLineEdit(parent); 0090 edit->setPlaceholderText(i18nc("@info:placeholder", "Filter...")); 0091 edit->setClearButtonEnabled(true); 0092 auto* widgetAction = new QWidgetAction(this); 0093 widgetAction->setDefaultWidget(edit); 0094 connect(edit, &QLineEdit::textChanged, this, &TestView::changeFilter); 0095 addAction(widgetAction); 0096 0097 setFocusProxy(edit); 0098 0099 IProjectController* pc = ICore::self()->projectController(); 0100 connect (pc, &IProjectController::projectClosed, this, &TestView::removeProject); 0101 0102 ITestController* tc = ICore::self()->testController(); 0103 connect (tc, &ITestController::testSuiteAdded, 0104 this, &TestView::addTestSuite); 0105 connect (tc, &ITestController::testSuiteRemoved, 0106 this, &TestView::removeTestSuite); 0107 connect (tc, &ITestController::testRunFinished, 0108 this, &TestView::updateTestSuite); 0109 connect (tc, &ITestController::testRunStarted, 0110 this, &TestView::notifyTestCaseStarted); 0111 0112 const auto suites = tc->testSuites(); 0113 for (ITestSuite* suite : suites) { 0114 addTestSuite(suite); 0115 } 0116 } 0117 0118 TestView::~TestView() 0119 { 0120 0121 } 0122 0123 void TestView::updateTestSuite(ITestSuite* suite, const TestResult& result) 0124 { 0125 QStandardItem* item = itemForSuite(suite); 0126 if (!item) 0127 { 0128 return; 0129 } 0130 0131 qCDebug(PLUGIN_TESTVIEW) << "Updating test suite" << suite->name(); 0132 0133 item->setIcon(iconForTestResult(result.suiteResult)); 0134 0135 for (int i = 0; i < item->rowCount(); ++i) 0136 { 0137 qCDebug(PLUGIN_TESTVIEW) << "Found a test case" << item->child(i)->text(); 0138 QStandardItem* caseItem = item->child(i); 0139 const auto resultIt = result.testCaseResults.constFind(caseItem->text()); 0140 if (resultIt != result.testCaseResults.constEnd()) { 0141 caseItem->setIcon(iconForTestResult(*resultIt)); 0142 } 0143 } 0144 } 0145 0146 void TestView::changeFilter(const QString &newFilter) 0147 { 0148 m_filter->setFilterWildcard(newFilter); 0149 if (newFilter.isEmpty()) { 0150 m_tree->collapseAll(); 0151 } else { 0152 m_tree->expandAll(); 0153 } 0154 } 0155 0156 void TestView::notifyTestCaseStarted(ITestSuite* suite, const QStringList& test_cases) 0157 { 0158 QStandardItem* item = itemForSuite(suite); 0159 if (!item) 0160 { 0161 return; 0162 } 0163 0164 qCDebug(PLUGIN_TESTVIEW) << "Notify a test of the suite " << suite->name() << " has started"; 0165 0166 // Global test suite icon 0167 item->setIcon(QIcon::fromTheme(QStringLiteral("process-idle"))); 0168 0169 for (int i = 0; i < item->rowCount(); ++i) 0170 { 0171 qCDebug(PLUGIN_TESTVIEW) << "Found a test case" << item->child(i)->text(); 0172 QStandardItem* caseItem = item->child(i); 0173 if (test_cases.contains(caseItem->text())) 0174 { 0175 // Each test case icon 0176 caseItem->setIcon(QIcon::fromTheme(QStringLiteral("process-idle"))); 0177 } 0178 } 0179 } 0180 0181 0182 QIcon TestView::iconForTestResult(TestResult::TestCaseResult result) 0183 { 0184 switch (result) 0185 { 0186 case TestResult::NotRun: 0187 return QIcon::fromTheme(QStringLiteral("code-function")); 0188 0189 case TestResult::Skipped: 0190 return QIcon::fromTheme(QStringLiteral("task-delegate")); 0191 0192 case TestResult::Passed: 0193 return QIcon::fromTheme(QStringLiteral("dialog-ok-apply")); 0194 0195 case TestResult::UnexpectedPass: 0196 // This is a very rare occurrence, so the icon should stand out 0197 return QIcon::fromTheme(QStringLiteral("dialog-warning")); 0198 0199 case TestResult::Failed: 0200 return QIcon::fromTheme(QStringLiteral("edit-delete")); 0201 0202 case TestResult::ExpectedFail: 0203 return QIcon::fromTheme(QStringLiteral("dialog-ok")); 0204 0205 case TestResult::Error: 0206 return QIcon::fromTheme(QStringLiteral("dialog-cancel")); 0207 } 0208 Q_UNREACHABLE(); 0209 } 0210 0211 QStandardItem* TestView::itemForSuite(ITestSuite* suite) 0212 { 0213 const auto items = m_model->findItems(suite->name(), Qt::MatchRecursive); 0214 auto it = std::find_if(items.begin(), items.end(), [&](QStandardItem* item) { 0215 return (item->parent() && item->parent()->text() == suite->project()->name() 0216 && !item->parent()->parent()); 0217 }); 0218 return (it != items.end()) ? *it : nullptr; 0219 } 0220 0221 QStandardItem* TestView::itemForProject(IProject* project) 0222 { 0223 QList<QStandardItem*> itemsForProject = m_model->findItems(project->name()); 0224 if (!itemsForProject.isEmpty()) { 0225 return itemsForProject.first(); 0226 } 0227 return addProject(project); 0228 } 0229 0230 0231 void TestView::runSelectedTests() 0232 { 0233 QModelIndexList indexes = m_tree->selectionModel()->selectedIndexes(); 0234 if (indexes.isEmpty()) 0235 { 0236 //if there's no selection we'll run all of them (or only the filtered) 0237 //in case there's a filter. 0238 const int rc = m_filter->rowCount(); 0239 indexes.reserve(rc); 0240 for(int i=0; i<rc; ++i) { 0241 indexes << m_filter->index(i, 0); 0242 } 0243 } 0244 0245 QList<KJob*> jobs; 0246 ITestController* tc = ICore::self()->testController(); 0247 0248 /* 0249 * NOTE: If a test suite or a single test case was selected, 0250 * the job is launched in Verbose mode with raised output window. 0251 * If a project is selected, it is launched silently. 0252 * 0253 * This is the somewhat-intuitive approach. Maybe a configuration should be offered. 0254 */ 0255 0256 for (const QModelIndex& idx : qAsConst(indexes)) { 0257 QModelIndex index = m_filter->mapToSource(idx); 0258 if (index.parent().isValid() && indexes.contains(index.parent())) 0259 { 0260 continue; 0261 } 0262 QStandardItem* item = m_model->itemFromIndex(index); 0263 if (item->parent() == nullptr) 0264 { 0265 // A project was selected 0266 IProject* project = ICore::self()->projectController()->findProjectByName(item->data(ProjectRole).toString()); 0267 const auto suites = tc->testSuitesForProject(project); 0268 for (ITestSuite* suite : suites) { 0269 jobs << suite->launchAllCases(ITestSuite::Silent); 0270 } 0271 } 0272 else if (item->parent()->parent() == nullptr) 0273 { 0274 // A suite was selected 0275 IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->data(ProjectRole).toString()); 0276 ITestSuite* suite = tc->findTestSuite(project, item->data(SuiteRole).toString()); 0277 jobs << suite->launchAllCases(ITestSuite::Verbose); 0278 } 0279 else 0280 { 0281 // This was a single test case 0282 IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->parent()->data(ProjectRole).toString()); 0283 ITestSuite* suite = tc->findTestSuite(project, item->parent()->data(SuiteRole).toString()); 0284 const QString testCase = item->data(CaseRole).toString(); 0285 jobs << suite->launchCase(testCase, ITestSuite::Verbose); 0286 } 0287 } 0288 0289 if (!jobs.isEmpty()) 0290 { 0291 auto* compositeJob = new KDevelop::ExecuteCompositeJob(this, jobs); 0292 compositeJob->setObjectName(i18np("Run 1 test", "Run %1 tests", jobs.size())); 0293 compositeJob->setProperty("test_job", true); 0294 ICore::self()->runController()->registerJob(compositeJob); 0295 } 0296 } 0297 0298 void TestView::showSource() 0299 { 0300 QModelIndexList indexes = m_tree->selectionModel()->selectedIndexes(); 0301 if (indexes.isEmpty()) 0302 { 0303 return; 0304 } 0305 0306 IndexedDeclaration declaration; 0307 ITestController* tc = ICore::self()->testController(); 0308 0309 QModelIndex index = m_filter->mapToSource(indexes.first()); 0310 QStandardItem* item = m_model->itemFromIndex(index); 0311 if (item->parent() == nullptr) 0312 { 0313 // No sense in finding source code for projects. 0314 return; 0315 } 0316 else if (item->parent()->parent() == nullptr) 0317 { 0318 IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->data(ProjectRole).toString()); 0319 ITestSuite* suite = tc->findTestSuite(project, item->data(SuiteRole).toString()); 0320 declaration = suite->declaration(); 0321 } 0322 else 0323 { 0324 IProject* project = ICore::self()->projectController()->findProjectByName(item->parent()->parent()->data(ProjectRole).toString()); 0325 ITestSuite* suite = tc->findTestSuite(project, item->parent()->data(SuiteRole).toString()); 0326 declaration = suite->caseDeclaration(item->data(CaseRole).toString()); 0327 } 0328 0329 DUChainReadLocker locker; 0330 Declaration* d = declaration.data(); 0331 if (!d) 0332 { 0333 return; 0334 } 0335 0336 QUrl url = d->url().toUrl(); 0337 KTextEditor::Cursor cursor = d->rangeInCurrentRevision().start(); 0338 locker.unlock(); 0339 0340 IDocumentController* dc = ICore::self()->documentController(); 0341 qCDebug(PLUGIN_TESTVIEW) << "Activating declaration in" << url; 0342 dc->openDocument(url, cursor); 0343 } 0344 0345 void TestView::addTestSuite(ITestSuite* suite) 0346 { 0347 QStandardItem* projectItem = itemForProject(suite->project()); 0348 Q_ASSERT(projectItem); 0349 0350 auto* suiteItem = new QStandardItem(QIcon::fromTheme(QStringLiteral("view-list-tree")), suite->name()); 0351 0352 suiteItem->setData(suite->name(), SuiteRole); 0353 const auto caseNames = suite->cases(); 0354 for (const QString& caseName : caseNames) { 0355 auto* caseItem = new QStandardItem(iconForTestResult(TestResult::NotRun), caseName); 0356 caseItem->setData(caseName, CaseRole); 0357 suiteItem->appendRow(caseItem); 0358 } 0359 projectItem->appendRow(suiteItem); 0360 } 0361 0362 void TestView::removeTestSuite(ITestSuite* suite) 0363 { 0364 QStandardItem* item = itemForSuite(suite); 0365 item->parent()->removeRow(item->row()); 0366 } 0367 0368 QStandardItem* TestView::addProject(IProject* project) 0369 { 0370 auto* projectItem = new QStandardItem(QIcon::fromTheme(QStringLiteral("project-development")), project->name()); 0371 projectItem->setData(project->name(), ProjectRole); 0372 m_model->appendRow(projectItem); 0373 return projectItem; 0374 } 0375 0376 void TestView::removeProject(IProject* project) 0377 { 0378 QStandardItem* projectItem = itemForProject(project); 0379 m_model->removeRow(projectItem->row()); 0380 } 0381 0382 void TestView::doubleClicked(const QModelIndex& index) 0383 { 0384 m_tree->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); 0385 runSelectedTests(); 0386 } 0387 0388 QList< QAction* > TestView::contextMenuActions() 0389 { 0390 return m_contextMenuActions; 0391 } 0392 0393 #include "moc_testview.cpp"