1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "autotestplugin.h"
27 
28 #include "autotestconstants.h"
29 #include "autotesticons.h"
30 #include "projectsettingswidget.h"
31 #include "testcodeparser.h"
32 #include "testframeworkmanager.h"
33 #include "testnavigationwidget.h"
34 #include "testprojectsettings.h"
35 #include "testresultspane.h"
36 #include "testrunner.h"
37 #include "testsettings.h"
38 #include "testsettingspage.h"
39 #include "testtreeitem.h"
40 #include "testtreemodel.h"
41 #include "testtreeview.h"
42 
43 #include "boost/boosttestframework.h"
44 #include "catch/catchframework.h"
45 #include "ctest/ctesttool.h"
46 #include "gtest/gtestframework.h"
47 #include "qtest/qttestframework.h"
48 #include "quick/quicktestframework.h"
49 
50 #include <coreplugin/actionmanager/actioncontainer.h>
51 #include <coreplugin/actionmanager/actionmanager.h>
52 #include <coreplugin/actionmanager/command.h>
53 #include <coreplugin/coreconstants.h>
54 #include <coreplugin/icontext.h>
55 #include <coreplugin/icore.h>
56 #include <coreplugin/messagemanager.h>
57 #include <cppeditor/cppeditorconstants.h>
58 #include <extensionsystem/pluginmanager.h>
59 #include <projectexplorer/buildmanager.h>
60 #include <projectexplorer/project.h>
61 #include <projectexplorer/projectexplorer.h>
62 #include <projectexplorer/projectexplorericons.h>
63 #include <projectexplorer/projectpanelfactory.h>
64 #include <projectexplorer/runcontrol.h>
65 #include <projectexplorer/session.h>
66 #include <projectexplorer/target.h>
67 #include <texteditor/textdocument.h>
68 #include <texteditor/texteditor.h>
69 #include <utils/textutils.h>
70 #include <utils/utilsicons.h>
71 
72 #include <QAction>
73 #include <QList>
74 #include <QMainWindow>
75 #include <QMap>
76 #include <QMenu>
77 #include <QMessageBox>
78 #include <QTextCursor>
79 
80 #ifdef WITH_TESTS
81 #include "autotestunittests.h"
82 #include "loadprojectscenario.h"
83 #endif
84 
85 using namespace Core;
86 
87 namespace Autotest {
88 namespace Internal {
89 
90 class AutotestPluginPrivate : public QObject
91 {
92     Q_OBJECT
93 public:
94     AutotestPluginPrivate();
95     ~AutotestPluginPrivate() override;
96 
97     TestNavigationWidgetFactory m_navigationWidgetFactory;
98     TestResultsPane *m_resultsPane = nullptr;
99     QMap<QString, ChoicePair> m_runconfigCache;
100 
101     void initializeMenuEntries();
102     void onRunAllTriggered();
103     void onRunSelectedTriggered();
104     void onRunFailedTriggered();
105     void onRunFileTriggered();
106     void onRunUnderCursorTriggered(TestRunMode mode);
107 
108     TestSettings m_settings;
109     TestSettingsPage m_testSettingPage{&m_settings};
110 
111     TestCodeParser m_testCodeParser;
112     TestTreeModel m_testTreeModel{&m_testCodeParser};
113     TestRunner m_testRunner;
114     TestFrameworkManager m_frameworkManager;
115 #ifdef WITH_TESTS
116     LoadProjectScenario m_loadProjectScenario{&m_testTreeModel};
117 #endif
118 };
119 
120 static AutotestPluginPrivate *dd = nullptr;
121 static QHash<ProjectExplorer::Project *, TestProjectSettings *> s_projectSettings;
122 
AutotestPlugin()123 AutotestPlugin::AutotestPlugin()
124 {
125     // needed to be used in QueuedConnection connects
126     qRegisterMetaType<TestResult>();
127     qRegisterMetaType<TestTreeItem *>();
128     qRegisterMetaType<TestCodeLocationAndType>();
129     // warm up meta type system to be able to read Qt::CheckState with persistent settings
130     qRegisterMetaType<Qt::CheckState>();
131 }
132 
~AutotestPlugin()133 AutotestPlugin::~AutotestPlugin()
134 {
135     delete dd;
136     dd = nullptr;
137 }
138 
AutotestPluginPrivate()139 AutotestPluginPrivate::AutotestPluginPrivate()
140 {
141     dd = this; // Needed as the code below access it via the static plugin interface
142     initializeMenuEntries();
143     m_frameworkManager.registerTestFramework(new QtTestFramework);
144     m_frameworkManager.registerTestFramework(new QuickTestFramework);
145     m_frameworkManager.registerTestFramework(new GTestFramework);
146     m_frameworkManager.registerTestFramework(new BoostTestFramework);
147     m_frameworkManager.registerTestFramework(new CatchFramework);
148 
149     m_frameworkManager.registerTestTool(new CTestTool);
150 
151     m_frameworkManager.synchronizeSettings(ICore::settings());
152     m_resultsPane = TestResultsPane::instance();
153 
154     auto panelFactory = new ProjectExplorer::ProjectPanelFactory();
155     panelFactory->setPriority(666);
156 //    panelFactory->setIcon();  // TODO ?
157     panelFactory->setDisplayName(tr("Testing"));
158     panelFactory->setCreateWidgetFunction([](ProjectExplorer::Project *project) {
159         return new ProjectTestSettingsWidget(project);
160     });
161     ProjectExplorer::ProjectPanelFactory::registerFactory(panelFactory);
162 
163     TestFrameworkManager::activateFrameworksAndToolsFromSettings(&m_settings);
164     m_testTreeModel.synchronizeTestFrameworks();
165     m_testTreeModel.synchronizeTestTools();
166 
167     auto sessionManager = ProjectExplorer::SessionManager::instance();
168     connect(sessionManager, &ProjectExplorer::SessionManager::startupProjectChanged,
169             this, [this] { m_runconfigCache.clear(); });
170 
171     connect(sessionManager, &ProjectExplorer::SessionManager::aboutToRemoveProject,
172             this, [] (ProjectExplorer::Project *project) {
173         auto it = s_projectSettings.find(project);
174         if (it != s_projectSettings.end()) {
175             delete it.value();
176             s_projectSettings.erase(it);
177         }
178     });
179 }
180 
~AutotestPluginPrivate()181 AutotestPluginPrivate::~AutotestPluginPrivate()
182 {
183     if (!s_projectSettings.isEmpty()) {
184         qDeleteAll(s_projectSettings);
185         s_projectSettings.clear();
186     }
187 
188     delete m_resultsPane;
189 }
190 
settings()191 TestSettings *AutotestPlugin::settings()
192 {
193     return &dd->m_settings;
194 }
195 
projectSettings(ProjectExplorer::Project * project)196 TestProjectSettings *AutotestPlugin::projectSettings(ProjectExplorer::Project *project)
197 {
198     auto &settings = s_projectSettings[project];
199     if (!settings)
200         settings = new TestProjectSettings(project);
201 
202     return settings;
203 }
204 
initializeMenuEntries()205 void AutotestPluginPrivate::initializeMenuEntries()
206 {
207     ActionContainer *menu = ActionManager::createMenu(Constants::MENU_ID);
208     menu->menu()->setTitle(tr("&Tests"));
209     menu->setOnAllDisabledBehavior(ActionContainer::Show);
210 
211     QAction *action = new QAction(tr("Run &All Tests"), this);
212     action->setIcon(Utils::Icons::RUN_SMALL.icon());
213     action->setToolTip(tr("Run All Tests"));
214     Command *command = ActionManager::registerAction(action, Constants::ACTION_RUN_ALL_ID);
215     command->setDefaultKeySequence(
216         QKeySequence(useMacShortcuts ? tr("Ctrl+Meta+T, Ctrl+Meta+A") : tr("Alt+Shift+T,Alt+A")));
217     connect(action, &QAction::triggered, this, &AutotestPluginPrivate::onRunAllTriggered);
218     action->setEnabled(false);
219     menu->addAction(command);
220 
221     action = new QAction(tr("&Run Selected Tests"), this);
222     action->setIcon(Utils::Icons::RUN_SELECTED.icon());
223     action->setToolTip(tr("Run Selected Tests"));
224     command = ActionManager::registerAction(action, Constants::ACTION_RUN_SELECTED_ID);
225     command->setDefaultKeySequence(
226         QKeySequence(useMacShortcuts ? tr("Ctrl+Meta+T, Ctrl+Meta+R") : tr("Alt+Shift+T,Alt+R")));
227     connect(action, &QAction::triggered, this, &AutotestPluginPrivate::onRunSelectedTriggered);
228     action->setEnabled(false);
229     menu->addAction(command);
230 
231     action = new QAction(tr("Run &Failed Tests"),  this);
232     action->setIcon(Icons::RUN_FAILED.icon());
233     action->setToolTip(tr("Run Failed Tests"));
234     command = ActionManager::registerAction(action, Constants::ACTION_RUN_FAILED_ID);
235     command->setDefaultKeySequence(
236                 useMacShortcuts ? tr("Ctrl+Meta+T, Ctrl+Meta+F") : tr("Alt+Shift+T,Alt+F"));
237     connect(action, &QAction::triggered, this, &AutotestPluginPrivate::onRunFailedTriggered);
238     action->setEnabled(false);
239     menu->addAction(command);
240 
241     action = new QAction(tr("Run Tests for &Current File"), this);
242     action->setIcon(Utils::Icons::RUN_FILE.icon());
243     action->setToolTip(tr("Run Tests for Current File"));
244     command = ActionManager::registerAction(action, Constants::ACTION_RUN_FILE_ID);
245     command->setDefaultKeySequence(
246         QKeySequence(useMacShortcuts ? tr("Ctrl+Meta+T, Ctrl+Meta+C") : tr("Alt+Shift+T,Alt+C")));
247     connect(action, &QAction::triggered, this, &AutotestPluginPrivate::onRunFileTriggered);
248     action->setEnabled(false);
249     menu->addAction(command);
250 
251     action = new QAction(tr("Re&scan Tests"), this);
252     command = ActionManager::registerAction(action, Constants::ACTION_SCAN_ID);
253     command->setDefaultKeySequence(
254         QKeySequence(useMacShortcuts ? tr("Ctrl+Meta+T, Ctrl+Meta+S") : tr("Alt+Shift+T,Alt+S")));
255 
256     connect(action, &QAction::triggered, this, [] { dd->m_testCodeParser.updateTestTree(); });
257     menu->addAction(command);
258 
259     ActionContainer *toolsMenu = ActionManager::actionContainer(Core::Constants::M_TOOLS);
260     toolsMenu->addMenu(menu);
261     using namespace ProjectExplorer;
262     connect(BuildManager::instance(), &BuildManager::buildStateChanged,
263             this, &AutotestPlugin::updateMenuItemsEnabledState);
264     connect(BuildManager::instance(), &BuildManager::buildQueueFinished,
265             this, &AutotestPlugin::updateMenuItemsEnabledState);
266     connect(ProjectExplorerPlugin::instance(), &ProjectExplorerPlugin::runActionsUpdated,
267             this, &AutotestPlugin::updateMenuItemsEnabledState);
268     connect(&dd->m_testTreeModel, &TestTreeModel::testTreeModelChanged,
269             this, &AutotestPlugin::updateMenuItemsEnabledState);
270 }
271 
initialize(const QStringList & arguments,QString * errorString)272 bool AutotestPlugin::initialize(const QStringList &arguments, QString *errorString)
273 {
274     Q_UNUSED(arguments)
275     Q_UNUSED(errorString)
276 
277     dd = new AutotestPluginPrivate;
278 #ifdef WITH_TESTS
279     ExtensionSystem::PluginManager::registerScenario("TestStringTable",
280                    [] { return dd->m_loadProjectScenario(); });
281     ExtensionSystem::PluginManager::registerScenario("TestModelManagerInterface",
282                    [] { return dd->m_loadProjectScenario(); });
283 #endif
284     return true;
285 }
286 
extensionsInitialized()287 void AutotestPlugin::extensionsInitialized()
288 {
289     ActionContainer *contextMenu = ActionManager::actionContainer(CppEditor::Constants::M_CONTEXT);
290     if (!contextMenu) // if QC is started without CppEditor plugin
291         return;
292 
293     QAction *action = new QAction(tr("&Run Test Under Cursor"), this);
294     action->setEnabled(false);
295     action->setIcon(Utils::Icons::RUN_SMALL.icon());
296 
297     Command *command = ActionManager::registerAction(action, Constants::ACTION_RUN_UCURSOR);
298     connect(action, &QAction::triggered,
299             std::bind(&AutotestPluginPrivate::onRunUnderCursorTriggered, dd, TestRunMode::Run));
300     contextMenu->addSeparator();
301     contextMenu->addAction(command);
302 
303     action = new QAction(tr("&Debug Test Under Cursor"), this);
304     action->setEnabled(false);
305     action->setIcon(ProjectExplorer::Icons::DEBUG_START_SMALL.icon());
306 
307     command = ActionManager::registerAction(action, Constants::ACTION_RUN_DBG_UCURSOR);
308     connect(action, &QAction::triggered,
309             std::bind(&AutotestPluginPrivate::onRunUnderCursorTriggered, dd, TestRunMode::Debug));
310     contextMenu->addAction(command);
311     contextMenu->addSeparator();
312 }
313 
aboutToShutdown()314 ExtensionSystem::IPlugin::ShutdownFlag AutotestPlugin::aboutToShutdown()
315 {
316     dd->m_testCodeParser.aboutToShutdown();
317     dd->m_testTreeModel.disconnect();
318     return SynchronousShutdown;
319 }
320 
onRunAllTriggered()321 void AutotestPluginPrivate::onRunAllTriggered()
322 {
323     m_testRunner.setSelectedTests(m_testTreeModel.getAllTestCases());
324     m_testRunner.prepareToRunTests(TestRunMode::Run);
325 }
326 
onRunSelectedTriggered()327 void AutotestPluginPrivate::onRunSelectedTriggered()
328 {
329     m_testRunner.setSelectedTests(m_testTreeModel.getSelectedTests());
330     m_testRunner.prepareToRunTests(TestRunMode::Run);
331 }
332 
onRunFailedTriggered()333 void AutotestPluginPrivate::onRunFailedTriggered()
334 {
335     const QList<ITestConfiguration *> failed = m_testTreeModel.getFailedTests();
336     if (failed.isEmpty()) // the framework might not be able to provide them
337         return;
338     m_testRunner.setSelectedTests(failed);
339     m_testRunner.prepareToRunTests(TestRunMode::Run);
340 }
341 
onRunFileTriggered()342 void AutotestPluginPrivate::onRunFileTriggered()
343 {
344     const IDocument *document = EditorManager::currentDocument();
345     if (!document)
346         return;
347 
348     const Utils::FilePath &fileName = document->filePath();
349     if (fileName.isEmpty())
350         return;
351 
352     const QList<ITestConfiguration *> tests = m_testTreeModel.getTestsForFile(fileName);
353     if (tests.isEmpty())
354         return;
355 
356     m_testRunner.setSelectedTests(tests);
357     m_testRunner.prepareToRunTests(TestRunMode::Run);
358 }
359 
testItemsToTestConfigurations(const QList<ITestTreeItem * > & items,TestRunMode mode)360 static QList<ITestConfiguration *> testItemsToTestConfigurations(const QList<ITestTreeItem *> &items,
361                                                                 TestRunMode mode)
362 {
363     QList<ITestConfiguration *> configs;
364     for (const ITestTreeItem * item : items) {
365         if (ITestConfiguration *currentConfig = item->asConfiguration(mode))
366             configs << currentConfig;
367     }
368     return configs;
369 }
370 
onRunUnderCursorTriggered(TestRunMode mode)371 void AutotestPluginPrivate::onRunUnderCursorTriggered(TestRunMode mode)
372 {
373     TextEditor::BaseTextEditor *currentEditor = TextEditor::BaseTextEditor::currentTextEditor();
374     QTextCursor cursor = currentEditor->editorWidget()->textCursor();
375     cursor.select(QTextCursor::WordUnderCursor);
376     const QString text = cursor.selectedText();
377     if (text.isEmpty())
378         return; // Do not trigger when no name under cursor
379 
380     const QList<ITestTreeItem *> testsItems = m_testTreeModel.testItemsByName(text);
381     if (testsItems.isEmpty())
382         return; // Wrong location triggered
383 
384     // check whether we have been triggered on a test function definition
385     const int line = currentEditor->currentLine();
386     const Utils::FilePath &filePath = currentEditor->textDocument()->filePath();
387     const QList<ITestTreeItem *> filteredItems = Utils::filtered(testsItems, [&](ITestTreeItem *it){
388         return it->line() == line && it->filePath() == filePath;
389     });
390 
391     const QList<ITestConfiguration *> testsToRun = testItemsToTestConfigurations(
392                 filteredItems.size() == 1 ? filteredItems : testsItems, mode);
393 
394     if (testsToRun.isEmpty()) {
395         MessageManager::writeFlashing(tr("Selected test was not found (%1).").arg(text));
396         return;
397     }
398 
399     m_testRunner.setSelectedTests(testsToRun);
400     m_testRunner.prepareToRunTests(mode);
401 }
402 
updateMenuItemsEnabledState()403 void AutotestPlugin::updateMenuItemsEnabledState()
404 {
405     const ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject();
406     const ProjectExplorer::Target *target = project ? project->activeTarget() : nullptr;
407     const bool canScan = !dd->m_testRunner.isTestRunning()
408             && dd->m_testCodeParser.state() == TestCodeParser::Idle;
409     const bool hasTests = dd->m_testTreeModel.hasTests();
410     // avoid expensive call to PE::canRunStartupProject() - limit to minimum necessary checks
411     const bool canRun = hasTests && canScan
412             && project && !project->needsConfiguration()
413             && target && target->activeRunConfiguration()
414             && !ProjectExplorer::BuildManager::isBuilding();
415     const bool canRunFailed = canRun && dd->m_testTreeModel.hasFailedTests();
416 
417     ActionManager::command(Constants::ACTION_RUN_ALL_ID)->action()->setEnabled(canRun);
418     ActionManager::command(Constants::ACTION_RUN_SELECTED_ID)->action()->setEnabled(canRun);
419     ActionManager::command(Constants::ACTION_RUN_FAILED_ID)->action()->setEnabled(canRunFailed);
420     ActionManager::command(Constants::ACTION_RUN_FILE_ID)->action()->setEnabled(canRun);
421     ActionManager::command(Constants::ACTION_SCAN_ID)->action()->setEnabled(canScan);
422 
423     ActionContainer *contextMenu = ActionManager::actionContainer(CppEditor::Constants::M_CONTEXT);
424     if (!contextMenu)
425         return; // When no context menu, actions do not exists
426 
427     ActionManager::command(Constants::ACTION_RUN_UCURSOR)->action()->setEnabled(canRun);
428     ActionManager::command(Constants::ACTION_RUN_DBG_UCURSOR)->action()->setEnabled(canRun);
429 }
430 
cacheRunConfigChoice(const QString & buildTargetKey,const ChoicePair & choice)431 void AutotestPlugin::cacheRunConfigChoice(const QString &buildTargetKey, const ChoicePair &choice)
432 {
433     if (dd)
434         dd->m_runconfigCache.insert(buildTargetKey, choice);
435 }
436 
cachedChoiceFor(const QString & buildTargetKey)437 ChoicePair AutotestPlugin::cachedChoiceFor(const QString &buildTargetKey)
438 {
439     return dd ? dd->m_runconfigCache.value(buildTargetKey) : ChoicePair();
440 }
441 
clearChoiceCache()442 void AutotestPlugin::clearChoiceCache()
443 {
444     if (dd)
445         dd->m_runconfigCache.clear();
446 }
447 
popupResultsPane()448 void AutotestPlugin::popupResultsPane()
449 {
450     if (dd)
451         dd->m_resultsPane->popup(Core::IOutputPane::NoModeSwitch);
452 }
453 
createTestObjects() const454 QVector<QObject *> AutotestPlugin::createTestObjects() const
455 {
456     QVector<QObject *> tests;
457 #ifdef WITH_TESTS
458     tests << new AutoTestUnitTests(&dd->m_testTreeModel);
459 #endif
460     return tests;
461 }
462 
matches(const ProjectExplorer::RunConfiguration * rc) const463 bool ChoicePair::matches(const ProjectExplorer::RunConfiguration *rc) const
464 {
465     return rc && rc->displayName() == displayName && rc->runnable().executable.toString() == executable;
466 }
467 
468 } // Internal
469 } // Autotest
470 
471 #include "autotestplugin.moc"
472