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"