AutoTest: Test execution from result output pane

Enable test execution via context menu in the result output pane.
At the moment only gtest is supported.

Task-number: QTCREATORBUG-16695
Change-Id: Ib39164c3cb44d249647b11e25dc51c9ac5db89c5
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Claus Steuer
2017-09-09 16:46:43 +02:00
parent 8b80442131
commit 4c04cff7c1
11 changed files with 153 additions and 102 deletions

View File

@@ -74,13 +74,13 @@ void GTestOutputReader::processOutput(const QByteArray &outputLine)
m_description = line; m_description = line;
if (m_iteration > 1) if (m_iteration > 1)
m_description.append(' ' + tr("(iteration %1)").arg(m_iteration)); m_description.append(' ' + tr("(iteration %1)").arg(m_iteration));
TestResultPtr testResult = TestResultPtr(new GTestResult); TestResultPtr testResult = TestResultPtr(new GTestResult(m_projectFile));
testResult->setResult(Result::MessageInternal); testResult->setResult(Result::MessageInternal);
testResult->setDescription(m_description); testResult->setDescription(m_description);
m_futureInterface.reportResult(testResult); m_futureInterface.reportResult(testResult);
m_description.clear(); m_description.clear();
} else if (disabledTests.exactMatch(line)) { } else if (disabledTests.exactMatch(line)) {
TestResultPtr testResult = TestResultPtr(new GTestResult); TestResultPtr testResult = TestResultPtr(new GTestResult(m_projectFile));
testResult->setResult(Result::MessageDisabledTests); testResult->setResult(Result::MessageDisabledTests);
int disabled = disabledTests.cap(1).toInt(); int disabled = disabledTests.cap(1).toInt();
testResult->setDescription(tr("You have %n disabled test(s).", 0, disabled)); testResult->setDescription(tr("You have %n disabled test(s).", 0, disabled));
@@ -98,7 +98,6 @@ void GTestOutputReader::processOutput(const QByteArray &outputLine)
m_futureInterface.reportResult(TestResultPtr(testResult)); m_futureInterface.reportResult(TestResultPtr(testResult));
m_currentTestName.clear(); m_currentTestName.clear();
m_currentTestSet.clear(); m_currentTestSet.clear();
m_normalizedCurrentTestSet.clear();
} else if (newTestStarts.exactMatch(line)) { } else if (newTestStarts.exactMatch(line)) {
setCurrentTestName(newTestStarts.cap(1)); setCurrentTestName(newTestStarts.cap(1));
TestResultPtr testResult = TestResultPtr(createDefaultResult()); TestResultPtr testResult = TestResultPtr(createDefaultResult());
@@ -112,7 +111,7 @@ void GTestOutputReader::processOutput(const QByteArray &outputLine)
m_futureInterface.reportResult(testResult); m_futureInterface.reportResult(testResult);
} else if (newTestSetStarts.exactMatch(line)) { } else if (newTestSetStarts.exactMatch(line)) {
setCurrentTestSet(newTestSetStarts.cap(1)); setCurrentTestSet(newTestSetStarts.cap(1));
TestResultPtr testResult = TestResultPtr(new GTestResult); TestResultPtr testResult = TestResultPtr(new GTestResult(m_projectFile));
testResult->setResult(Result::MessageCurrentTest); testResult->setResult(Result::MessageCurrentTest);
testResult->setDescription(tr("Entering test set %1").arg(m_currentTestSet)); testResult->setDescription(tr("Entering test set %1").arg(m_currentTestSet));
m_futureInterface.reportResult(testResult); m_futureInterface.reportResult(testResult);
@@ -163,39 +162,21 @@ void GTestOutputReader::processOutput(const QByteArray &outputLine)
void GTestOutputReader::setCurrentTestSet(const QString &testSet) void GTestOutputReader::setCurrentTestSet(const QString &testSet)
{ {
m_currentTestSet = testSet; m_currentTestSet = testSet;
m_normalizedCurrentTestSet = normalizeName(testSet);
} }
void GTestOutputReader::setCurrentTestName(const QString &testName) void GTestOutputReader::setCurrentTestName(const QString &testName)
{ {
m_currentTestName = testName; m_currentTestName = testName;
m_normalizedTestName = normalizeTestName(testName);
}
QString GTestOutputReader::normalizeName(const QString &name) const
{
static QRegExp parameterIndex("/\\d+");
QString nameWithoutParameterIndices = name;
nameWithoutParameterIndices.remove(parameterIndex);
return nameWithoutParameterIndices.split('/').last();
}
QString GTestOutputReader::normalizeTestName(const QString &testname) const
{
QString nameWithoutTypeParam = testname.split(',').first();
return normalizeName(nameWithoutTypeParam);
} }
GTestResult *GTestOutputReader::createDefaultResult() const GTestResult *GTestOutputReader::createDefaultResult() const
{ {
GTestResult *result = new GTestResult(m_executable, m_currentTestName); GTestResult *result = new GTestResult(m_executable, m_projectFile, m_currentTestName);
result->setTestSetName(m_currentTestSet); result->setTestSetName(m_currentTestSet);
result->setIteration(m_iteration); result->setIteration(m_iteration);
const TestTreeItem *testItem = findTestTreeItemForCurrentLine(); const TestTreeItem *testItem = result->findTestTreeItem();
if (testItem && testItem->line()) { if (testItem && testItem->line()) {
result->setFileName(testItem->filePath()); result->setFileName(testItem->filePath());
result->setLine(static_cast<int>(testItem->line())); result->setLine(static_cast<int>(testItem->line()));
@@ -204,43 +185,5 @@ GTestResult *GTestOutputReader::createDefaultResult() const
return result; return result;
} }
const TestTreeItem *GTestOutputReader::findTestTreeItemForCurrentLine() const
{
const auto item = TestTreeModel::instance()->findNonRootItem([&](const Utils::TreeItem *item) {
const TestTreeItem &treeItem = static_cast<const TestTreeItem &>(*item);
return matches(treeItem);
});
return static_cast<const TestTreeItem *>(item);
}
bool GTestOutputReader::matches(const TestTreeItem &treeItem) const
{
if (treeItem.proFile() != m_projectFile)
return false;
if (m_currentTestSet.isEmpty())
return matchesTestCase(treeItem);
return matchesTestFunctionOrSet(treeItem);
}
bool GTestOutputReader::matchesTestFunctionOrSet(const TestTreeItem &treeItem) const
{
if (treeItem.type() != TestTreeItem::TestFunctionOrSet)
return false;
const QString testItemTestSet = treeItem.parentItem()->name() + '.' + treeItem.name();
return testItemTestSet == m_normalizedCurrentTestSet;
}
bool GTestOutputReader::matchesTestCase(const TestTreeItem &treeItem) const
{
if (treeItem.type() != TestTreeItem::TestCase)
return false;
return treeItem.name() == m_normalizedTestName;
}
} // namespace Internal } // namespace Internal
} // namespace Autotest } // namespace Autotest

View File

@@ -50,20 +50,12 @@ protected:
private: private:
void setCurrentTestSet(const QString &testSet); void setCurrentTestSet(const QString &testSet);
void setCurrentTestName(const QString &testName); void setCurrentTestName(const QString &testName);
QString normalizeName(const QString &name) const;
QString normalizeTestName(const QString &testname) const;
GTestResult *createDefaultResult() const; GTestResult *createDefaultResult() const;
const TestTreeItem *findTestTreeItemForCurrentLine() const;
bool matches(const TestTreeItem &treeItem) const;
bool matchesTestFunctionOrSet(const TestTreeItem &treeItem) const;
bool matchesTestCase(const TestTreeItem &treeItem) const;
QString m_executable; QString m_executable;
QString m_projectFile; QString m_projectFile;
QString m_currentTestName; QString m_currentTestName;
QString m_normalizedTestName;
QString m_currentTestSet; QString m_currentTestSet;
QString m_normalizedCurrentTestSet;
QString m_description; QString m_description;
int m_iteration = 1; int m_iteration = 1;
}; };

View File

@@ -24,17 +24,20 @@
****************************************************************************/ ****************************************************************************/
#include "gtestresult.h" #include "gtestresult.h"
#include "../testtreemodel.h"
#include "../testtreeitem.h"
namespace Autotest { namespace Autotest {
namespace Internal { namespace Internal {
GTestResult::GTestResult(const QString &name) GTestResult::GTestResult(const QString &projectFile, const QString &name)
: TestResult(name) : TestResult(name), m_projectFile(projectFile)
{ {
} }
GTestResult::GTestResult(const QString &executable, const QString &name) GTestResult::GTestResult(const QString &executable, const QString &projectFile,
: TestResult(executable, name) const QString &name)
: TestResult(executable, name), m_projectFile(projectFile)
{ {
} }
@@ -68,5 +71,60 @@ bool GTestResult::isDirectParentOf(const TestResult *other, bool *needsIntermedi
return isTest() && gtOther->isTestSet(); return isTest() && gtOther->isTestSet();
} }
static QString normalizeName(const QString &name)
{
static QRegExp parameterIndex("/\\d+");
QString nameWithoutParameterIndices = name;
nameWithoutParameterIndices.remove(parameterIndex);
return nameWithoutParameterIndices.split('/').last();
}
static QString normalizeTestName(const QString &testname)
{
QString nameWithoutTypeParam = testname.split(',').first();
return normalizeName(nameWithoutTypeParam);
}
const TestTreeItem *GTestResult::findTestTreeItem() const
{
const auto item = TestTreeModel::instance()->findNonRootItem([this](const Utils::TreeItem *item) {
const TestTreeItem &treeItem = static_cast<const TestTreeItem &>(*item);
return matches(treeItem);
});
return static_cast<const TestTreeItem *>(item);
}
bool GTestResult::matches(const TestTreeItem &treeItem) const
{
if (treeItem.proFile() != m_projectFile)
return false;
if (isTest())
return matchesTestCase(treeItem);
return matchesTestFunctionOrSet(treeItem);
}
bool GTestResult::matchesTestFunctionOrSet(const TestTreeItem &treeItem) const
{
if (treeItem.type() != TestTreeItem::TestFunctionOrSet)
return false;
const QString testItemTestSet = treeItem.parentItem()->name() + '.' + treeItem.name();
return testItemTestSet == normalizeName(m_testSetName);
}
bool GTestResult::matchesTestCase(const TestTreeItem &treeItem) const
{
if (treeItem.type() != TestTreeItem::TestCase)
return false;
return treeItem.name() == normalizeTestName(name());
}
} // namespace Internal } // namespace Internal
} // namespace Autotest } // namespace Autotest

View File

@@ -33,17 +33,25 @@ namespace Internal {
class GTestResult : public TestResult class GTestResult : public TestResult
{ {
public: public:
explicit GTestResult(const QString &name = QString()); GTestResult(const QString &projectFile, const QString &name = QString());
GTestResult(const QString &executable, const QString &name); GTestResult(const QString &executable, const QString &projectFile, const QString &name);
const QString outputString(bool selected) const override; const QString outputString(bool selected) const override;
void setTestSetName(const QString &testSetName) { m_testSetName = testSetName; } void setTestSetName(const QString &testSetName) { m_testSetName = testSetName; }
void setIteration(int iteration) { m_iteration = iteration; } void setIteration(int iteration) { m_iteration = iteration; }
bool isDirectParentOf(const TestResult *other, bool *needsIntermediate) const override; bool isDirectParentOf(const TestResult *other, bool *needsIntermediate) const override;
virtual const TestTreeItem *findTestTreeItem() const override;
private: private:
bool isTest() const { return m_testSetName.isEmpty(); } bool isTest() const { return m_testSetName.isEmpty(); }
bool isTestSet() const { return !m_testSetName.isEmpty(); } bool isTestSet() const { return !m_testSetName.isEmpty(); }
bool matches(const TestTreeItem &item) const;
bool matchesTestFunctionOrSet(const TestTreeItem &treeItem) const;
bool matchesTestCase(const TestTreeItem &treeItem) const;
QString m_testSetName; QString m_testSetName;
QString m_projectFile;
int m_iteration = 1; int m_iteration = 1;
}; };

View File

@@ -301,25 +301,7 @@ void TestNavigationWidget::onRunThisTestTriggered(TestRunMode runMode)
return; return;
TestTreeItem *item = static_cast<TestTreeItem *>(sourceIndex.internalPointer()); TestTreeItem *item = static_cast<TestTreeItem *>(sourceIndex.internalPointer());
TestConfiguration *configuration; TestRunner::instance()->runTest(runMode, item);
switch (runMode) {
case TestRunMode::Run:
case TestRunMode::RunWithoutDeploy:
configuration = item->testConfiguration();
break;
case TestRunMode::Debug:
case TestRunMode::DebugWithoutDeploy:
configuration = item->debugConfiguration();
break;
default:
configuration = nullptr;
}
if (configuration) {
TestRunner *runner = TestRunner::instance();
runner->setSelectedTests({configuration});
runner->prepareToRunTests(runMode);
}
} }
TestNavigationWidgetFactory::TestNavigationWidgetFactory() TestNavigationWidgetFactory::TestNavigationWidgetFactory()

View File

@@ -58,6 +58,11 @@ const QString TestResult::outputString(bool selected) const
return selected ? m_description : m_description.split('\n').first(); return selected ? m_description : m_description.split('\n').first();
} }
const TestTreeItem *TestResult::findTestTreeItem() const
{
return nullptr;
}
Result::Type TestResult::resultFromString(const QString &resultString) Result::Type TestResult::resultFromString(const QString &resultString)
{ {
if (resultString == "pass") if (resultString == "pass")

View File

@@ -35,6 +35,8 @@
namespace Autotest { namespace Autotest {
namespace Internal { namespace Internal {
class TestTreeItem;
namespace Result{ namespace Result{
enum Type { enum Type {
Pass, FIRST_TYPE = Pass, Pass, FIRST_TYPE = Pass,
@@ -76,6 +78,7 @@ public:
virtual ~TestResult() {} virtual ~TestResult() {}
virtual const QString outputString(bool selected) const; virtual const QString outputString(bool selected) const;
virtual const TestTreeItem *findTestTreeItem() const;
QString executable() const { return m_executable; } QString executable() const { return m_executable; }
QString name() const { return m_name; } QString name() const { return m_name; }

View File

@@ -144,7 +144,7 @@ TestResultsPane::TestResultsPane(QObject *parent) :
connect(m_treeView, &Utils::TreeView::customContextMenuRequested, connect(m_treeView, &Utils::TreeView::customContextMenuRequested,
this, &TestResultsPane::onCustomContextMenuRequested); this, &TestResultsPane::onCustomContextMenuRequested);
connect(m_treeView, &ResultsTreeView::copyShortcutTriggered, [this] () { connect(m_treeView, &ResultsTreeView::copyShortcutTriggered, [this] () {
onCopyItemTriggered(m_treeView->currentIndex()); onCopyItemTriggered(getTestResult(m_treeView->currentIndex()));
}); });
connect(m_model, &TestResultModel::requestExpansion, [this] (QModelIndex idx) { connect(m_model, &TestResultModel::requestExpansion, [this] (QModelIndex idx) {
m_treeView->expand(m_filterModel->mapFromSource(idx)); m_treeView->expand(m_filterModel->mapFromSource(idx));
@@ -562,11 +562,12 @@ void TestResultsPane::onCustomContextMenuRequested(const QPoint &pos)
{ {
const bool resultsAvailable = m_filterModel->hasResults(); const bool resultsAvailable = m_filterModel->hasResults();
const bool enabled = !m_testRunning && resultsAvailable; const bool enabled = !m_testRunning && resultsAvailable;
const QModelIndex clicked = m_treeView->indexAt(pos); const TestResult *clicked = getTestResult(m_treeView->indexAt(pos));
QMenu menu; QMenu menu;
QAction *action = new QAction(tr("Copy"), &menu); QAction *action = new QAction(tr("Copy"), &menu);
action->setShortcut(QKeySequence(QKeySequence::Copy)); action->setShortcut(QKeySequence(QKeySequence::Copy));
action->setEnabled(resultsAvailable); action->setEnabled(resultsAvailable && clicked);
connect(action, &QAction::triggered, [this, clicked] () { connect(action, &QAction::triggered, [this, clicked] () {
onCopyItemTriggered(clicked); onCopyItemTriggered(clicked);
}); });
@@ -582,14 +583,36 @@ void TestResultsPane::onCustomContextMenuRequested(const QPoint &pos)
connect(action, &QAction::triggered, this, &TestResultsPane::onSaveWholeTriggered); connect(action, &QAction::triggered, this, &TestResultsPane::onSaveWholeTriggered);
menu.addAction(action); menu.addAction(action);
action = new QAction(tr("Run This Test"), &menu);
action->setEnabled(clicked && clicked->findTestTreeItem());
connect(action, &QAction::triggered, this, [this, clicked] {
onRunThisTestTriggered(TestRunMode::Run, clicked);
});
menu.addAction(action);
action = new QAction(tr("Debug This Test"), &menu);
action->setEnabled(clicked && clicked->findTestTreeItem());
connect(action, &QAction::triggered, this, [this, clicked] {
onRunThisTestTriggered(TestRunMode::Debug, clicked);
});
menu.addAction(action);
menu.exec(m_treeView->mapToGlobal(pos)); menu.exec(m_treeView->mapToGlobal(pos));
} }
void TestResultsPane::onCopyItemTriggered(const QModelIndex &idx) const TestResult *TestResultsPane::getTestResult(const QModelIndex &idx)
{ {
if (!idx.isValid()) if (!idx.isValid())
return; return nullptr;
const TestResult *result = m_filterModel->testResult(idx); const TestResult *result = m_filterModel->testResult(idx);
QTC_CHECK(result);
return result;
}
void TestResultsPane::onCopyItemTriggered(const TestResult *result)
{
QTC_ASSERT(result, return); QTC_ASSERT(result, return);
QApplication::clipboard()->setText(result->outputString(true)); QApplication::clipboard()->setText(result->outputString(true));
} }
@@ -614,6 +637,16 @@ void TestResultsPane::onSaveWholeTriggered()
} }
} }
void TestResultsPane::onRunThisTestTriggered(TestRunMode runMode, const TestResult *result)
{
QTC_ASSERT(result, return);
const TestTreeItem *item = result->findTestTreeItem();
if (item)
TestRunner::instance()->runTest(runMode, item);
}
void TestResultsPane::toggleOutputStyle() void TestResultsPane::toggleOutputStyle()
{ {
const bool displayText = m_outputWidget->currentIndex() == 0; const bool displayText = m_outputWidget->currentIndex() == 0;

View File

@@ -52,6 +52,7 @@ namespace Internal {
class TestResultModel; class TestResultModel;
class TestResultFilterModel; class TestResultFilterModel;
class TestResult;
class ResultsTreeView : public Utils::TreeView class ResultsTreeView : public Utils::TreeView
{ {
@@ -109,9 +110,11 @@ private:
void onScrollBarRangeChanged(int, int max); void onScrollBarRangeChanged(int, int max);
void updateRunActions(); void updateRunActions();
void onCustomContextMenuRequested(const QPoint &pos); void onCustomContextMenuRequested(const QPoint &pos);
void onCopyItemTriggered(const QModelIndex &idx); const TestResult *getTestResult(const QModelIndex &idx);
void onCopyItemTriggered(const TestResult *result);
void onCopyWholeTriggered(); void onCopyWholeTriggered();
void onSaveWholeTriggered(); void onSaveWholeTriggered();
void onRunThisTestTriggered(TestRunMode runMode, const TestResult *result);
void toggleOutputStyle(); void toggleOutputStyle();
QString getWholeOutput(const QModelIndex &parent = QModelIndex()); QString getWholeOutput(const QModelIndex &parent = QModelIndex());

View File

@@ -31,6 +31,7 @@
#include "testrunconfiguration.h" #include "testrunconfiguration.h"
#include "testsettings.h" #include "testsettings.h"
#include "testoutputreader.h" #include "testoutputreader.h"
#include "testtreeitem.h"
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/progressmanager/futureprogress.h> #include <coreplugin/progressmanager/futureprogress.h>
@@ -106,6 +107,28 @@ void TestRunner::setSelectedTests(const QList<TestConfiguration *> &selected)
m_selectedTests = selected; m_selectedTests = selected;
} }
void TestRunner::runTest(TestRunMode mode, const TestTreeItem *item)
{
TestConfiguration *configuration;
switch (mode) {
case TestRunMode::Run:
case TestRunMode::RunWithoutDeploy:
configuration = item->testConfiguration();
break;
case TestRunMode::Debug:
case TestRunMode::DebugWithoutDeploy:
configuration = item->debugConfiguration();
break;
default:
configuration = nullptr;
}
if (configuration) {
setSelectedTests({configuration});
prepareToRunTests(mode);
}
}
static QString processInformation(const QProcess &proc) static QString processInformation(const QProcess &proc)
{ {
QString information("\nCommand line: " + proc.program() + ' ' + proc.arguments().join(' ')); QString information("\nCommand line: " + proc.program() + ' ' + proc.arguments().join(' '));

View File

@@ -55,6 +55,7 @@ public:
~TestRunner(); ~TestRunner();
void setSelectedTests(const QList<TestConfiguration *> &selected); void setSelectedTests(const QList<TestConfiguration *> &selected);
void runTest(TestRunMode mode, const TestTreeItem *item);
bool isTestRunning() const { return m_executingTests; } bool isTestRunning() const { return m_executingTests; }
void prepareToRunTests(TestRunMode mode); void prepareToRunTests(TestRunMode mode);