Squish: Redo running test cases

Start allowing interactions with the AUT which enables
interrupting and debugging the test cases.
Use a debugger perspective for interaction and provide
a control bar which is visible while running test cases.

Change-Id: I2c9fde51263516c38e814c91241d3ed3489ecacb
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
Christian Stenger
2022-08-01 15:18:45 +02:00
parent f8b430c0b2
commit 27c8e2a638
11 changed files with 968 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
add_qtc_plugin(Squish
PLUGIN_DEPENDS
Core
Core Debugger TextEditor
DEPENDS ExtensionSystem Utils
SOURCES
deletesymbolicnamedialog.cpp deletesymbolicnamedialog.h
@@ -15,6 +15,7 @@ add_qtc_plugin(Squish
squishfilehandler.cpp squishfilehandler.h
squishnavigationwidget.cpp squishnavigationwidget.h
squishoutputpane.cpp squishoutputpane.h
squishperspective.cpp squishperspective.h
squishplugin.cpp squishplugin.h
squishresultmodel.cpp squishresultmodel.h
squishsettings.cpp squishsettings.h

View File

@@ -4,56 +4,60 @@ QtcPlugin {
name: "Squish"
Depends { name: "Core" }
Depends { name: "Debugger" }
Depends { name: "TextEditor" }
Depends { name: "Utils" }
Depends { name: "Qt.widgets" }
files: [
"squish.qrc",
"squishplugin_global.h",
"squishconstants.h",
"squishplugin.cpp",
"squishplugin.h",
"squishsettings.cpp",
"squishsettings.h",
"squishnavigationwidget.cpp",
"squishnavigationwidget.h",
"squishoutputpane.cpp",
"squishoutputpane.h",
"squishtesttreemodel.cpp",
"squishtesttreemodel.h",
"squishtesttreeview.cpp",
"squishtesttreeview.h",
"squishfilehandler.cpp",
"squishfilehandler.h",
"opensquishsuitesdialog.cpp",
"opensquishsuitesdialog.h",
"squishutils.cpp",
"squishutils.h",
"squishtools.cpp",
"squishtools.h",
"squishtr.h",
"squishxmloutputhandler.cpp",
"squishxmloutputhandler.h",
"testresult.cpp",
"testresult.h",
"squishresultmodel.cpp",
"squishresultmodel.h",
"deletesymbolicnamedialog.cpp",
"deletesymbolicnamedialog.h",
"objectsmapdocument.cpp",
"objectsmapdocument.h",
"objectsmaptreeitem.cpp",
"objectsmaptreeitem.h",
"propertytreeitem.cpp",
"propertytreeitem.h",
"objectsmapeditorwidget.cpp",
"objectsmapeditorwidget.h",
"objectsmapeditor.cpp",
"objectsmapeditor.h",
"objectsmapeditorwidget.cpp",
"objectsmapeditorwidget.h",
"objectsmaptreeitem.cpp",
"objectsmaptreeitem.h",
"opensquishsuitesdialog.cpp",
"opensquishsuitesdialog.h",
"propertyitemdelegate.cpp",
"propertyitemdelegate.h",
"propertytreeitem.cpp",
"propertytreeitem.h",
"squish.qrc",
"squishconstants.h",
"squishfilehandler.cpp",
"squishfilehandler.h",
"squishnavigationwidget.cpp",
"squishnavigationwidget.h",
"squishoutputpane.cpp",
"squishoutputpane.h",
"squishperspective.cpp",
"squishperspective.h",
"squishplugin.cpp",
"squishplugin.h",
"squishplugin_global.h",
"squishresultmodel.cpp",
"squishresultmodel.h",
"squishsettings.cpp",
"squishsettings.h",
"squishtesttreemodel.cpp",
"squishtesttreemodel.h",
"squishtesttreeview.cpp",
"squishtesttreeview.h",
"squishtools.cpp",
"squishtools.h",
"squishtr.h",
"squishutils.cpp",
"squishutils.h",
"squishxmloutputhandler.cpp",
"squishxmloutputhandler.h",
"symbolnameitemdelegate.cpp",
"symbolnameitemdelegate.h"
"symbolnameitemdelegate.h",
"testresult.cpp",
"testresult.h",
]
}

View File

@@ -0,0 +1,457 @@
// Copyright (C) 2022 The Qt Company Ltd
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "squishperspective.h"
#include "squishtools.h"
#include "squishtr.h"
#include "squishxmloutputhandler.h"
#include <debugger/analyzer/analyzermanager.h>
#include <debugger/debuggericons.h>
#include <coreplugin/icore.h>
#include <utils/itemviews.h>
#include <utils/qtcassert.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
#include <QDialog>
#include <QLabel>
#include <QProgressBar>
#include <QScreen>
#include <QToolBar>
#include <QVBoxLayout>
namespace Squish {
namespace Internal {
static QString stateName(SquishPerspective::State state)
{
switch (state) {
case SquishPerspective::State::None: return "None";
case SquishPerspective::State::Starting: return "Starting";
case SquishPerspective::State::Running: return "Running";
case SquishPerspective::State::RunRequested: return "RunRequested";
case SquishPerspective::State::StepInRequested: return "StepInRequested";
case SquishPerspective::State::StepOverRequested: return "StepOverRequested";
case SquishPerspective::State::StepReturnRequested: return "StepReturnRequested";
case SquishPerspective::State::Interrupted: return "Interrupted";
case SquishPerspective::State::InterruptRequested: return "InterruptedRequested";
case SquishPerspective::State::Canceling: return "Canceling";
case SquishPerspective::State::Canceled: return "Canceled";
case SquishPerspective::State::CancelRequested: return "CancelRequested";
case SquishPerspective::State::CancelRequestedWhileInterrupted: return "CancelRequestedWhileInterrupted";
case SquishPerspective::State::Finished: return "Finished";
}
return "ThouShallNotBeHere";
}
enum IconType { StopRecord, Play, Pause, StepIn, StepOver, StepReturn, Stop };
static QIcon iconForType(IconType type)
{
switch (type) {
case StopRecord:
return QIcon();
case Play:
return Debugger::Icons::DEBUG_CONTINUE_SMALL_TOOLBAR.icon();
case Pause:
return Utils::Icons::INTERRUPT_SMALL.icon();
case StepIn:
return Debugger::Icons::STEP_INTO_TOOLBAR.icon();
case StepOver:
return Debugger::Icons::STEP_OVER_TOOLBAR.icon();
case StepReturn:
return Debugger::Icons::STEP_OUT_TOOLBAR.icon();
case Stop:
return Utils::Icons::STOP_SMALL.icon();
}
return QIcon();
}
static QString customStyleSheet(bool extended)
{
static const QString red = Utils::creatorTheme()->color(
Utils::Theme::ProgressBarColorError).name();
static const QString green = Utils::creatorTheme()->color(
Utils::Theme::ProgressBarColorFinished).name();
if (!extended)
return "QProgressBar {text-align:left; border:0px}";
return QString("QProgressBar {background:%1; text-align:left; border:0px}"
"QProgressBar::chunk {background:%2; border:0px}").arg(red, green);
}
static QStringList splitDebugContent(const QString &content)
{
if (content.isEmpty())
return {};
QStringList symbols;
int delimiter = -1;
int start = 0;
do {
delimiter = content.indexOf(',', delimiter + 1);
if (delimiter > 0 && content.at(delimiter - 1) == '\\')
continue;
symbols.append(content.mid(start, delimiter - start));
start = delimiter + 1;
} while (delimiter >= 0);
return symbols;
}
QVariant LocalsItem::data(int column, int role) const
{
if (role == Qt::DisplayRole) {
switch (column) {
case 0: return name;
case 1: return type;
case 2: return value;
}
}
return TreeItem::data(column, role);
}
class SquishControlBar : public QDialog
{
public:
explicit SquishControlBar(SquishPerspective *perspective);
void increaseFailCounter() { ++m_fails; updateProgressBar(); }
void increasePassCounter() { ++m_passes; updateProgressBar(); }
void updateProgressText(const QString &label);
protected:
void closeEvent(QCloseEvent *) override
{
m_perspective->onStopTriggered();
}
void resizeEvent(QResizeEvent *) override
{
updateProgressText(m_labelText);
}
private:
void updateProgressBar();
SquishPerspective *m_perspective = nullptr;
QToolBar *m_toolBar = nullptr;
QProgressBar *m_progress = nullptr;
QString m_labelText;
int m_passes = 0;
int m_fails = 0;
};
SquishControlBar::SquishControlBar(SquishPerspective *perspective)
: QDialog()
, m_perspective(perspective)
{
setWindowTitle(Tr::tr("Control Bar"));
setWindowFlags(Qt::Widget | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->addWidget(m_toolBar = new QToolBar(this));
// for now
m_toolBar->addAction(perspective->m_pausePlayAction);
m_toolBar->addAction(perspective->m_stopAction);
mainLayout->addWidget(m_progress = new QProgressBar(this));
m_progress->setMinimumHeight(48);
m_progress->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
m_progress->setStyleSheet(customStyleSheet(false));
m_progress->setFormat(QString());
m_progress->setValue(0);
QPalette palette = Utils::creatorTheme()->palette();
m_progress->setPalette(palette);
setLayout(mainLayout);
}
void SquishControlBar::updateProgressBar()
{
const int allCounted = m_passes + m_fails;
if (allCounted == 0)
return;
if (allCounted == 1)
m_progress->setStyleSheet(customStyleSheet(true));
m_progress->setRange(0, allCounted);
m_progress->setValue(m_passes);
}
void SquishControlBar::updateProgressText(const QString &label)
{
const QString status = m_progress->fontMetrics().elidedText(label, Qt::ElideMiddle,
m_progress->width());
if (!status.isEmpty()) {
m_labelText = label;
m_progress->setFormat(status);
}
}
SquishPerspective::SquishPerspective()
: Utils::Perspective("Squish.Perspective", Tr::tr("Squish"))
{
Core::ICore::addPreCloseListener([this]{
destroyControlBar();
return true;
});
}
void SquishPerspective::initPerspective()
{
m_pausePlayAction = new QAction(this);
m_pausePlayAction->setIcon(iconForType(Pause));
m_pausePlayAction->setToolTip(Tr::tr("Interrupt"));
m_pausePlayAction->setEnabled(false);
m_stepInAction = new QAction(this);
m_stepInAction->setIcon(iconForType(StepIn));
m_stepInAction->setToolTip(Tr::tr("Step Into"));
m_stepInAction->setEnabled(false);
m_stepOverAction = new QAction(this);
m_stepOverAction->setIcon(iconForType(StepOver));
m_stepOverAction->setToolTip(Tr::tr("Step Over"));
m_stepOverAction->setEnabled(false);
m_stepOutAction = new QAction(this);
m_stepOutAction->setIcon(iconForType(StepReturn));
m_stepOutAction->setToolTip(Tr::tr("Step Out"));
m_stepOutAction->setEnabled(false);
m_stopAction = Debugger::createStopAction();
m_stopAction->setEnabled(false);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(1);
m_localsModel.setHeader({Tr::tr("Name"), Tr::tr("Type"), Tr::tr("Value")});
auto localsView = new Utils::TreeView;
localsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
localsView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
localsView->setModel(&m_localsModel);
localsView->setRootIsDecorated(true);
mainLayout->addWidget(localsView);
QWidget *mainWidget = new QWidget;
mainWidget->setObjectName("SquishLocalsView");
mainWidget->setWindowTitle(Tr::tr("Squish Locals"));
mainWidget->setLayout(mainLayout);
addToolBarAction(m_pausePlayAction);
addToolBarAction(m_stepInAction);
addToolBarAction(m_stepOverAction);
addToolBarAction(m_stepOutAction);
addToolBarAction(m_stopAction);
addToolbarSeparator();
m_status = new QLabel;
addToolBarWidget(m_status);
addWindow(mainWidget, Perspective::AddToTab, nullptr, true, Qt::RightDockWidgetArea);
connect(m_pausePlayAction, &QAction::triggered, this, &SquishPerspective::onPausePlayTriggered);
connect(m_stepInAction, &QAction::triggered, this, [this] {
setState(State::StepInRequested);
});
connect(m_stepOverAction, &QAction::triggered, this, [this] {
setState(State::StepOverRequested);
});
connect(m_stepOutAction, &QAction::triggered, this, [this] {
setState(State::StepReturnRequested);
});
connect(m_stopAction, &QAction::triggered, this, &SquishPerspective::onStopTriggered);
connect(SquishTools::instance(), &SquishTools::localsUpdated,
this, &SquishPerspective::onLocalsUpdated);
connect(SquishTools::instance(), &SquishTools::symbolUpdated,
this, &SquishPerspective::onLocalsUpdated);
connect(localsView, &QTreeView::expanded, this, [this](const QModelIndex &idx) {
LocalsItem *item = m_localsModel.itemForIndex(idx);
if (QTC_GUARD(item)) {
if (item->expanded)
return;
item->expanded = true;
SquishTools::instance()->requestExpansion(item->name);
}
});
}
void SquishPerspective::onStopTriggered()
{
m_pausePlayAction->setEnabled(false);
m_stopAction->setEnabled(false);
setState(m_state == State::Interrupted ? State::CancelRequestedWhileInterrupted
: State::CancelRequested);
}
void SquishPerspective::onPausePlayTriggered()
{
if (m_state == State::Interrupted)
setState(State::RunRequested);
else if (m_state == State::Running)
setState(State::InterruptRequested);
else
qDebug() << "###state: " << stateName(m_state);
}
void SquishPerspective::onLocalsUpdated(const QString &output)
{
static const QRegularExpression regex("\\+(?<name>.+):\\{(?<content>.*)\\}");
static const QRegularExpression inner("(?<type>.+)#(?<exp>\\+*+)(?<name>[^=]+)(=(?<value>.+))?");
const QRegularExpressionMatch match = regex.match(output);
LocalsItem *parent = nullptr;
bool singleSymbol = match.hasMatch();
if (singleSymbol) {
const QString name = match.captured("name");
parent = m_localsModel.findNonRootItem([name](LocalsItem *it) {
return it->name == name;
});
if (!parent)
return;
parent->removeChildren();
} else {
m_localsModel.clear();
parent = m_localsModel.rootItem();
}
const QStringList symbols = splitDebugContent(singleSymbol ? match.captured("content")
: output);
for (const QString &part : symbols) {
const QRegularExpressionMatch iMatch = inner.match(part);
QTC_ASSERT(iMatch.hasMatch(), qDebug() << part; continue);
if (iMatch.captured("value").startsWith('<'))
continue;
LocalsItem *l = new LocalsItem(iMatch.captured("name"),
iMatch.captured("type"),
iMatch.captured("value").replace("\\,", ",")); // TODO
if (!iMatch.captured("exp").isEmpty())
l->appendChild(new LocalsItem); // add pseudo child
parent->appendChild(l);
}
}
void SquishPerspective::updateStatus(const QString &status)
{
m_status->setText(status);
}
void SquishPerspective::showControlBar(SquishXmlOutputHandler *xmlOutputHandler)
{
QTC_ASSERT(!m_controlBar, return);
m_controlBar = new SquishControlBar(this);
connect(xmlOutputHandler, &SquishXmlOutputHandler::increasePassCounter,
m_controlBar, &SquishControlBar::increasePassCounter);
connect(xmlOutputHandler, &SquishXmlOutputHandler::increaseFailCounter,
m_controlBar, &SquishControlBar::increaseFailCounter);
connect (xmlOutputHandler, &SquishXmlOutputHandler::updateStatus,
m_controlBar, &SquishControlBar::updateProgressText);
const QRect rect = Core::ICore::dialogParent()->screen()->availableGeometry();
m_controlBar->move(rect.width() - m_controlBar->width() - 10, 10);
m_controlBar->showNormal();
}
void SquishPerspective::destroyControlBar()
{
if (!m_controlBar)
return;
delete m_controlBar;
m_controlBar = nullptr;
}
void SquishPerspective::setState(State state)
{
if (m_state == state) // ignore triggering the state again
return;
if (!isStateTransitionValid(state)) {
qDebug() << "Illegal state transition" << stateName(m_state) << "->" << stateName(state);
return;
}
m_state = state;
m_localsModel.clear();
emit stateChanged(state);
switch (m_state) {
case State::Running:
case State::Interrupted:
m_pausePlayAction->setEnabled(true);
if (m_state == State::Interrupted) {
m_pausePlayAction->setIcon(iconForType(Play));
m_pausePlayAction->setToolTip(Tr::tr("Continue"));
m_stepInAction->setEnabled(true);
m_stepOverAction->setEnabled(true);
m_stepOutAction->setEnabled(true);
} else {
m_pausePlayAction->setIcon(iconForType(Pause));
m_pausePlayAction->setToolTip(Tr::tr("Interrupt"));
m_stepInAction->setEnabled(false);
m_stepOverAction->setEnabled(false);
m_stepOutAction->setEnabled(false);
}
m_stopAction->setEnabled(true);
break;
case State::RunRequested:
case State::Starting:
case State::StepInRequested:
case State::StepOverRequested:
case State::StepReturnRequested:
case State::InterruptRequested:
case State::CancelRequested:
case State::CancelRequestedWhileInterrupted:
case State::Canceled:
case State::Finished:
m_pausePlayAction->setIcon(iconForType(Pause));
m_pausePlayAction->setToolTip(Tr::tr("Interrupt"));
m_pausePlayAction->setEnabled(false);
m_stepInAction->setEnabled(false);
m_stepOverAction->setEnabled(false);
m_stepOutAction->setEnabled(false);
m_stopAction->setEnabled(false);
break;
default:
break;
}
}
bool SquishPerspective::isStateTransitionValid(State newState) const
{
if (newState == State::Finished || newState == State::CancelRequested)
return true;
switch (m_state) {
case State::None:
return newState == State::Starting;
case State::Starting:
return newState == State::RunRequested;
case State::Running:
return newState == State::Interrupted
|| newState == State::InterruptRequested;
case State::RunRequested:
case State::StepInRequested:
case State::StepOverRequested:
case State::StepReturnRequested:
return newState == State::Running;
case State::Interrupted:
return newState == State::RunRequested
|| newState == State::StepInRequested
|| newState == State::StepOverRequested
|| newState == State::StepReturnRequested
|| newState == State::CancelRequestedWhileInterrupted;
case State::InterruptRequested:
return newState == State::Interrupted;
case State::Canceling:
return newState == State::Canceled;
case State::Canceled:
return newState == State::None;
case State::CancelRequested:
case State::CancelRequestedWhileInterrupted:
return newState == State::Canceling;
case State::Finished:
return newState == State::Starting;
}
return false;
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,82 @@
// Copyright (C) 2022 The Qt Company Ltd
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#pragma once
#include <debugger/debuggermainwindow.h>
#include <utils/treemodel.h>
namespace Squish {
namespace Internal {
class SquishXmlOutputHandler;
class LocalsItem : public Utils::TreeItem
{
public:
LocalsItem() = default;
LocalsItem(const QString &n, const QString &t, const QString &v) : name(n), type(t), value(v) {}
QVariant data(int column, int role) const override;
QString name;
QString type;
QString value;
bool expanded = false;
};
class SquishPerspective : public Utils::Perspective
{
Q_OBJECT
public:
enum class State {
None,
Starting,
Running,
RunRequested,
StepInRequested,
StepOverRequested,
StepReturnRequested,
Interrupted,
InterruptRequested,
Canceling,
Canceled,
CancelRequested,
CancelRequestedWhileInterrupted,
Finished
};
SquishPerspective();
void initPerspective();
State state() const { return m_state; }
void setState(State state);
void updateStatus(const QString &status);
void showControlBar(SquishXmlOutputHandler *xmlOutputHandler);
void destroyControlBar();
signals:
void stateChanged(State state);
private:
void onStopTriggered();
void onPausePlayTriggered();
void onLocalsUpdated(const QString &output);
bool isStateTransitionValid(State newState) const;
QAction *m_pausePlayAction = nullptr;
QAction *m_stepInAction = nullptr;
QAction *m_stepOverAction = nullptr;
QAction *m_stepOutAction = nullptr;
QAction *m_stopAction = nullptr;
QLabel *m_status = nullptr;
class SquishControlBar *m_controlBar = nullptr;
Utils::TreeModel<LocalsItem> m_localsModel;
State m_state = State::None;
friend class SquishControlBar;
};
} // namespace Internal
} // namespace Squish

View File

@@ -102,6 +102,13 @@ bool SquishPlugin::initialize(const QStringList &, QString *)
ExtensionSystem::IPlugin::ShutdownFlag SquishPlugin::aboutToShutdown()
{
if (dd->m_squishTools) {
if (dd->m_squishTools->shutdown())
return SynchronousShutdown;
connect(dd->m_squishTools, &SquishTools::shutdownFinished,
this, &ExtensionSystem::IPlugin::asynchronousShutdownFinished);
return AsynchronousShutdown;
}
return SynchronousShutdown;
}

View File

@@ -73,6 +73,12 @@ SquishSettings::SquishSettings()
verbose.setLabel(Tr::tr("Verbose log"));
verbose.setDefaultValue(false);
registerAspect(&minimizeIDE);
minimizeIDE.setSettingsKey("MinimizeIDE");
minimizeIDE.setLabel(Tr::tr("Minimize IDE"));
minimizeIDE.setToolTip(Tr::tr("Minimize IDE automatically while running or recording test cases."));
minimizeIDE.setDefaultValue(true);
connect(&local, &BoolAspect::volatileValueChanged,
this, [this] (bool checked) {
serverHost.setEnabled(!checked);
@@ -100,6 +106,7 @@ SquishSettingsPage::SquishSettingsPage(SquishSettings *settings)
s.licensePath, br,
Span {2, Row { s.local, s.serverHost, s.serverPort } }, br,
s.verbose, br,
s.minimizeIDE, br,
};
Column { Row { grid }, st }.attachTo(widget);
});

View File

@@ -28,6 +28,7 @@ public:
Utils::IntegerAspect serverPort;
Utils::BoolAspect local;
Utils::BoolAspect verbose;
Utils::BoolAspect minimizeIDE;
};
class SquishSettingsPage final : public Core::IOptionsPage

View File

@@ -11,10 +11,16 @@
#include <QDebug> // TODO remove
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <debugger/breakhandler.h>
#include <debugger/debuggerconstants.h>
#include <debugger/debuggericons.h>
#include <texteditor/textmark.h>
#include <utils/hostosinfo.h>
#include <utils/qtcassert.h>
#include <utils/utilsicons.h>
#include <QApplication>
#include <QDateTime>
@@ -30,6 +36,17 @@ using namespace Utils;
namespace Squish {
namespace Internal {
class SquishLocationMark : public TextEditor::TextMark
{
public:
SquishLocationMark(const FilePath &filePath, int line)
: TextEditor::TextMark(filePath, line, Id("Squish.LocationMark"))
{
setIcon(Debugger::Icons::LOCATION.icon());
setPriority(HighPriority);
}
};
// make this configurable?
static const QString resultsDirectory = QFileInfo(QDir::home(), ".squishQC/Test Results")
.absoluteFilePath();
@@ -47,6 +64,11 @@ SquishTools::SquishTools(QObject *parent)
connect(this, &SquishTools::squishTestRunFinished,
outputPane, &SquishOutputPane::onTestRunFinished);
m_runnerProcess.setProcessMode(ProcessMode::Writer);
m_runnerProcess.setStdOutLineCallback([this](const QString &line) {
onRunnerStdOutput(line);
});
connect(&m_runnerProcess, &QtcProcess::readyReadStandardError,
this, &SquishTools::onRunnerErrorOutput);
connect(&m_runnerProcess, &QtcProcess::done,
@@ -59,9 +81,16 @@ SquishTools::SquishTools(QObject *parent)
connect(&m_serverProcess, &QtcProcess::done,
this, &SquishTools::onServerFinished);
s_instance = this;
m_perspective.initPerspective();
connect(&m_perspective, &SquishPerspective::stateChanged,
this, &SquishTools::onPerspectiveStateChanged);
}
SquishTools::~SquishTools() = default;
SquishTools::~SquishTools()
{
if (m_locationMarker) // happens when QC is closed while Squish is executed
delete m_locationMarker;
}
SquishTools *SquishTools::instance()
{
@@ -76,8 +105,10 @@ struct SquishToolsSettings
FilePath squishPath;
FilePath serverPath;
FilePath runnerPath;
FilePath processComPath;
bool isLocalServer = true;
bool verboseLog = false;
bool minimizeIDE = true;
QString serverHost = "localhost";
int serverPort = 9999;
FilePath licenseKeyPath;
@@ -95,6 +126,8 @@ struct SquishToolsSettings
HostOsInfo::withExecutableSuffix("squishserver")).absoluteFilePath();
runnerPath = squishBin.pathAppended(
HostOsInfo::withExecutableSuffix("squishrunner")).absoluteFilePath();
processComPath = squishBin.pathAppended(
HostOsInfo::withExecutableSuffix("processcomm")).absoluteFilePath();
}
isLocalServer = squishSettings->local.value();
@@ -102,14 +135,20 @@ struct SquishToolsSettings
serverPort = squishSettings->serverPort.value();
verboseLog = squishSettings->verbose.value();
licenseKeyPath = squishSettings->licensePath.filePath();
minimizeIDE = squishSettings->minimizeIDE.value();
}
};
// make sure to execute setup() to populate with current settings before using it
static SquishToolsSettings toolsSettings;
void SquishTools::runTestCases(const QString &suitePath,
const QStringList &testCases,
const QStringList &additionalServerArgs,
const QStringList &additionalRunnerArgs)
{
if (m_shutdownInitiated)
return;
if (m_state != Idle) {
QMessageBox::critical(Core::ICore::dialogParent(),
Tr::tr("Error"),
@@ -142,6 +181,8 @@ void SquishTools::runTestCases(const QString &suitePath,
connect(this, &SquishTools::resultOutputCreated,
m_xmlOutputHandler.get(), &SquishXmlOutputHandler::outputAvailable,
Qt::QueuedConnection);
connect(m_xmlOutputHandler.get(), &SquishXmlOutputHandler::updateStatus,
&m_perspective, &SquishPerspective::updateStatus);
m_squishRunnerMode = TestingMode;
emit squishTestRunStarted();
@@ -150,6 +191,8 @@ void SquishTools::runTestCases(const QString &suitePath,
void SquishTools::queryServerSettings()
{
if (m_shutdownInitiated)
return;
if (m_state != Idle) {
QMessageBox::critical(Core::ICore::dialogParent(),
Tr::tr("Error"),
@@ -164,6 +207,8 @@ void SquishTools::queryServerSettings()
void SquishTools::writeServerSettingsChanges(const QList<QStringList> &changes)
{
if (m_shutdownInitiated)
return;
if (m_state != Idle) {
QMessageBox::critical(Core::ICore::dialogParent(),
Tr::tr("Error"),
@@ -175,7 +220,6 @@ void SquishTools::writeServerSettingsChanges(const QList<QStringList> &changes)
startSquishServer(ServerConfigChangeRequested);
}
void SquishTools::setState(SquishTools::State state)
{
// TODO check whether state transition is legal
@@ -186,6 +230,7 @@ void SquishTools::setState(SquishTools::State state)
m_request = None;
m_suitePath = QString();
m_testCases.clear();
m_currentTestCasePath.clear();
m_reportFiles.clear();
m_additionalRunnerArguments.clear();
m_additionalServerArguments.clear();
@@ -211,10 +256,13 @@ void SquishTools::setState(SquishTools::State state)
emit squishTestRunFinished();
m_squishRunnerMode = NoMode;
}
if (toolsSettings.minimizeIDE)
restoreQtCreatorWindows();
m_perspective.destroyControlBar();
break;
case ServerStopped:
m_state = Idle;
emit shutdownFinished();
if (m_request == ServerConfigChangeRequested) {
if (m_serverProcess.result() == ProcessResult::FinishedWithError) {
emit configChangesFailed(m_serverProcess.error());
@@ -232,7 +280,9 @@ void SquishTools::setState(SquishTools::State state)
emit squishTestRunFinished();
m_squishRunnerMode = NoMode;
}
if (toolsSettings.minimizeIDE)
restoreQtCreatorWindows();
m_perspective.destroyControlBar();
} else if (m_request == KillOldBeforeRunRunner) {
startSquishServer(RunTestRequested);
} else if (m_request == KillOldBeforeRecordRunner) {
@@ -245,6 +295,9 @@ void SquishTools::setState(SquishTools::State state)
break;
case ServerStopFailed:
m_serverProcess.close();
if (toolsSettings.minimizeIDE)
restoreQtCreatorWindows();
m_perspective.destroyControlBar();
m_state = Idle;
break;
case RunnerStartFailed:
@@ -252,7 +305,8 @@ void SquishTools::setState(SquishTools::State state)
if (m_squishRunnerMode == QueryMode) {
m_request = ServerStopRequested;
stopSquishServer();
} else if (m_testCases.isEmpty()) {
} else if (m_testCases.isEmpty()
|| (m_perspective.state() == SquishPerspective::State::Canceled)) {
m_request = ServerStopRequested;
stopSquishServer();
QString error;
@@ -265,6 +319,7 @@ void SquishTools::setState(SquishTools::State state)
logrotateTestResults();
} else {
m_xmlOutputHandler->clearForNextRun();
m_perspective.setState(SquishPerspective::State::Starting);
startSquishRunner();
}
break;
@@ -273,11 +328,10 @@ void SquishTools::setState(SquishTools::State state)
}
}
// make sure to execute setup() to populate with current settings before using it
static SquishToolsSettings toolsSettings;
void SquishTools::startSquishServer(Request request)
{
if (m_shutdownInitiated)
return;
m_request = request;
if (m_serverProcess.state() != QProcess::NotRunning) {
if (QMessageBox::question(Core::ICore::dialogParent(),
@@ -327,10 +381,15 @@ void SquishTools::startSquishServer(Request request)
toolsSettings.serverPath = squishServer;
if (m_squishRunnerMode == TestingMode) {
if (true) // TODO squish setting of minimize QC on squish run/record
if (toolsSettings.minimizeIDE)
minimizeQtCreatorWindows();
else
m_lastTopLevelWindows.clear();
if (QTC_GUARD(m_xmlOutputHandler))
m_perspective.showControlBar(m_xmlOutputHandler.get());
m_perspective.select();
m_perspective.setState(SquishPerspective::State::Starting);
}
QStringList arguments;
@@ -392,16 +451,17 @@ void SquishTools::startSquishRunner()
args << "--port" << QString::number(m_serverPort);
args << "--debugLog" << "alpw"; // TODO make this configurable?
const QFileInfo testCasePath(QDir(m_suitePath), m_testCases.takeFirst());
args << "--testcase" << testCasePath.absoluteFilePath();
m_currentTestCasePath = FilePath::fromString(m_suitePath) / m_testCases.takeFirst();
args << "--testcase" << m_currentTestCasePath.toString();
args << "--suitedir" << m_suitePath;
args << "--debug" << "--ide";
args << m_additionalRunnerArguments;
const QString caseReportFilePath = QFileInfo(QString::fromLatin1("%1/%2/%3/results.xml")
.arg(m_currentResultsDirectory,
QDir(m_suitePath).dirName(),
testCasePath.baseName()))
m_currentTestCasePath.baseName()))
.absoluteFilePath();
m_reportFiles.append(caseReportFilePath);
@@ -442,6 +502,11 @@ void SquishTools::onRunnerFinished()
return;
}
if (!m_shutdownInitiated) {
m_perspective.setState(SquishPerspective::State::Finished);
m_perspective.updateStatus(Tr::tr("Test run finished."));
}
if (m_resultsFileWatcher) {
delete m_resultsFileWatcher;
m_resultsFileWatcher = nullptr;
@@ -585,9 +650,179 @@ void SquishTools::onRunnerErrorOutput()
const QList<QByteArray> lines = output.split('\n');
for (const QByteArray &line : lines) {
const QByteArray trimmed = line.trimmed();
if (!trimmed.isEmpty())
if (!trimmed.isEmpty()) {
emit logOutputReceived("Runner: " + QLatin1String(trimmed));
if (trimmed.startsWith("QSocketNotifier: Invalid socket")) {
// we've lost connection to the AUT - if Interrupted, try to cancel the runner
if (m_perspective.state() == SquishPerspective::State::Interrupted)
m_perspective.setState(SquishPerspective::State::CancelRequestedWhileInterrupted);
}
}
}
}
void SquishTools::onRunnerStdOutput(const QString &lineIn)
{
if (m_request == RunnerQueryRequested) // only handle test runs here
return;
int fileLine = -1;
int fileColumn = -1;
QString fileName;
// we might enter this function by invoking it directly instead of getting signaled
bool isPrompt = false;
QString line = lineIn;
line.chop(1); // line has a newline
if (line.startsWith("SDBG:"))
line = line.mid(5);
if (line.isEmpty()) // we have a prompt
isPrompt = true;
else if (line.startsWith("symb")) { // symbols information (locals)
isPrompt = true;
// paranoia
if (!line.endsWith("}"))
return;
if (line.at(4) == '.') { // single symbol information
line = line.mid(5);
emit symbolUpdated(line);
} else { // lline.at(4) == ':' // all locals
line = line.mid(6);
line.chop(1);
emit localsUpdated(line);
}
} else if (line.startsWith("@line")) { // location information (interrupted)
isPrompt = true;
// paranoia
if (!line.endsWith(":"))
return;
const QStringList locationParts = line.split(',');
QTC_ASSERT(locationParts.size() == 3, return);
fileLine = locationParts[0].mid(6).toInt();
fileColumn = locationParts[1].mid(7).toInt();
fileName = locationParts[2].trimmed();
fileName.chop(1); // remove the colon
const FilePath fp = FilePath::fromString(fileName);
if (fp.isRelativePath())
fileName = m_currentTestCasePath.resolvePath(fileName).toString();
}
if (isPrompt)
handlePrompt(fileName, fileLine, fileColumn);
}
// FIXME: add/removal of breakpoints while debugging not handled yet
// FIXME: enabled state of breakpoints
void SquishTools::setBreakpoints()
{
using namespace Debugger::Internal;
const GlobalBreakpoints globalBPs = BreakpointManager::globalBreakpoints();
for (const GlobalBreakpoint &gb : globalBPs) {
if (!gb->isEnabled())
continue;
auto fileName = Utils::FilePath::fromUserInput(
gb->data(BreakpointFileColumn, Qt::DisplayRole).toString()).toUserOutput();
if (fileName.isEmpty())
continue;
// mask backslashes and spaces
fileName.replace('\\', "\\\\");
fileName.replace(' ', "\\x20");
auto line = gb->data(BreakpointLineColumn, Qt::DisplayRole).toInt();
QString cmd = "break ";
cmd.append(fileName);
cmd.append(':');
cmd.append(QString::number(line));
cmd.append('\n');
m_runnerProcess.write(cmd);
}
}
void SquishTools::handlePrompt(const QString &fileName, int line, int column)
{
const SquishPerspective::State state = m_perspective.state();
switch (state) {
case SquishPerspective::State::Starting:
setBreakpoints();
m_perspective.setState(SquishPerspective::State::RunRequested);
break;
case SquishPerspective::State::RunRequested:
case SquishPerspective::State::StepInRequested:
case SquishPerspective::State::StepOverRequested:
case SquishPerspective::State::StepReturnRequested:
if (m_requestVarsTimer) {
delete m_requestVarsTimer;
m_requestVarsTimer = nullptr;
}
if (state == SquishPerspective::State::RunRequested)
m_runnerProcess.write("continue\n");
else if (state == SquishPerspective::State::StepInRequested)
m_runnerProcess.write("step\n");
else if (state == SquishPerspective::State::StepOverRequested)
m_runnerProcess.write("next\n");
else // SquishPerspective::State::StepReturnRequested
m_runnerProcess.write("return\n");
clearLocationMarker();
if (state == SquishPerspective::State::RunRequested && toolsSettings.minimizeIDE)
minimizeQtCreatorWindows();
m_perspective.setState(SquishPerspective::State::Running);
break;
case SquishPerspective::State::CancelRequested:
case SquishPerspective::State::CancelRequestedWhileInterrupted:
m_runnerProcess.write("exit\n");
clearLocationMarker();
m_perspective.setState(SquishPerspective::State::Canceling);
break;
case SquishPerspective::State::Canceling:
m_runnerProcess.write("quit\n");
m_perspective.setState(SquishPerspective::State::Canceled);
break;
case SquishPerspective::State::Canceled:
QTC_CHECK(false);
break;
default:
if (line != -1 && column != -1) {
m_perspective.setState(SquishPerspective::State::Interrupted);
restoreQtCreatorWindows();
// if we're returning from a function we might end up without a file information
if (fileName.isEmpty()) {
m_runnerProcess.write("next\n");
} else {
// request local variables
m_runnerProcess.write("print variables\n");
const FilePath filePath = FilePath::fromString(fileName);
Core::EditorManager::openEditorAt({filePath, line, column});
updateLocationMarker(filePath, line);
}
} else { // it's just some output coming from the server
if (m_perspective.state() == SquishPerspective::State::Interrupted && !m_requestVarsTimer) {
// FIXME: this should be easier, but when interrupted and AUT is closed
// runner does not get notified until continued/canceled
m_requestVarsTimer = new QTimer(this);
m_requestVarsTimer->setSingleShot(true);
m_requestVarsTimer->setInterval(1000);
connect(m_requestVarsTimer, &QTimer::timeout, this, [this]() {
m_runnerProcess.write("print variables\n");
});
m_requestVarsTimer->start();
}
}
}
}
void SquishTools::requestExpansion(const QString &name)
{
QTC_ASSERT(m_perspective.state() == SquishPerspective::State::Interrupted, return);
m_runnerProcess.write("print variables +" + name + "\n");
}
bool SquishTools::shutdown()
{
QTC_ASSERT(!m_shutdownInitiated, return true);
m_shutdownInitiated = true;
if (m_runnerProcess.isRunning())
terminateRunner();
if (m_serverProcess.isRunning())
m_serverProcess.stop();
return !(m_serverProcess.isRunning() || m_runnerProcess.isRunning());
}
void SquishTools::onResultsDirChanged(const QString &filePath)
@@ -636,27 +871,94 @@ void SquishTools::logrotateTestResults()
void SquishTools::minimizeQtCreatorWindows()
{
m_lastTopLevelWindows = QApplication::topLevelWindows();
QWindowList toBeRemoved;
for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
if (window->isVisible())
const QWindowList topLevelWindows = QApplication::topLevelWindows();
for (QWindow *window : topLevelWindows) {
if (window->flags() & Qt::WindowStaysOnTopHint)
continue;
if (window->isVisible()) {
window->showMinimized();
else
toBeRemoved.append(window);
}
for (QWindow *window : qAsConst(toBeRemoved))
if (!m_lastTopLevelWindows.contains(window)) {
m_lastTopLevelWindows.append(window);
connect(window, &QWindow::destroyed, this, [this, window]() {
m_lastTopLevelWindows.removeOne(window);
});
}
}
}
}
void SquishTools::restoreQtCreatorWindows()
{
for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
window->raise();
window->requestActivate();
window->showNormal();
}
}
void SquishTools::updateLocationMarker(const Utils::FilePath &file, int line)
{
if (QTC_GUARD(!m_locationMarker)) {
m_locationMarker = new SquishLocationMark(file, line);
} else {
m_locationMarker->updateFileName(file);
m_locationMarker->move(line);
}
}
void SquishTools::clearLocationMarker()
{
delete m_locationMarker;
m_locationMarker = nullptr;
}
void SquishTools::onPerspectiveStateChanged(SquishPerspective::State state)
{
switch (state) {
case SquishPerspective::State::InterruptRequested:
if (m_runnerProcess.processId() != -1)
interruptRunner();
break;
case SquishPerspective::State::CancelRequested:
if (m_runnerProcess.processId() != -1)
terminateRunner();
break;
case SquishPerspective::State::RunRequested:
case SquishPerspective::State::StepInRequested:
case SquishPerspective::State::StepOverRequested:
case SquishPerspective::State::StepReturnRequested:
case SquishPerspective::State::CancelRequestedWhileInterrupted:
handlePrompt();
break;
default:
break;
}
}
void SquishTools::interruptRunner()
{
const CommandLine cmd(toolsSettings.processComPath,
{QString::number(m_runnerProcess.processId()), "break"});
QtcProcess process;
process.setCommand(cmd);
process.start();
process.waitForFinished();
}
void SquishTools::terminateRunner()
{
m_testCases.clear();
m_currentTestCasePath.clear();
m_perspective.updateStatus(Tr::tr("User stop initiated."));
// should we terminate the AUT instead of the runner?!?
const CommandLine cmd(toolsSettings.processComPath,
{QString::number(m_runnerProcess.processId()), "terminate"});
QtcProcess process;
process.setCommand(cmd);
process.start();
process.waitForFinished();
}
bool SquishTools::isValidToStartRunner()
{
if (!m_serverProcess.isRunning()) {

View File

@@ -3,12 +3,13 @@
#pragma once
#include "squishperspective.h"
#include <utils/environment.h>
#include <utils/qtcprocess.h>
#include <QObject>
#include <QStringList>
#include <QWindowList>
#include <memory>
@@ -51,6 +52,9 @@ public:
const QStringList &additionalRunnerArgs = QStringList());
void queryServerSettings();
void writeServerSettingsChanges(const QList<QStringList> &changes);
void requestExpansion(const QString &name);
bool shutdown();
signals:
void logOutputReceived(const QString &output);
@@ -60,6 +64,9 @@ signals:
void queryFinished(const QByteArray &output);
void configChangesFailed(QProcess::ProcessError error);
void configChangesWritten();
void localsUpdated(const QString &output);
void symbolUpdated(const QString &output);
void shutdownFinished();
private:
enum Request {
@@ -84,17 +91,26 @@ private:
void onRunnerFinished();
void onServerOutput();
void onServerErrorOutput();
void onRunnerOutput();
void onRunnerErrorOutput();
void onRunnerOutput(); // runner's results file
void onRunnerErrorOutput(); // runner's error stream
void onRunnerStdOutput(const QString &line); // runner's output stream
void setBreakpoints();
void handlePrompt(const QString &fileName = {}, int line = -1, int column = -1);
void onResultsDirChanged(const QString &filePath);
static void logrotateTestResults();
void minimizeQtCreatorWindows();
void restoreQtCreatorWindows();
void updateLocationMarker(const Utils::FilePath &file, int line);
void clearLocationMarker();
void onPerspectiveStateChanged(SquishPerspective::State state);
void interruptRunner();
void terminateRunner();
bool isValidToStartRunner();
bool setupRunnerPath();
void setupAndStartSquishRunnerProcess(const QStringList &arg,
const QString &caseReportFilePath = {});
SquishPerspective m_perspective;
std::unique_ptr<SquishXmlOutputHandler> m_xmlOutputHandler;
Utils::QtcProcess m_serverProcess;
Utils::QtcProcess m_runnerProcess;
@@ -106,14 +122,18 @@ private:
QStringList m_testCases;
QStringList m_reportFiles;
QString m_currentResultsDirectory;
Utils::FilePath m_currentTestCasePath;
QFile *m_currentResultsXML = nullptr;
QFileSystemWatcher *m_resultsFileWatcher = nullptr;
QStringList m_additionalServerArguments;
QStringList m_additionalRunnerArguments;
QList<QStringList> m_serverConfigChanges;
QWindowList m_lastTopLevelWindows;
class SquishLocationMark *m_locationMarker = nullptr;
QTimer *m_requestVarsTimer = nullptr;
enum RunnerMode { NoMode, TestingMode, QueryMode} m_squishRunnerMode = NoMode;
qint64 m_readResultsCount;
bool m_shutdownInitiated = false;
};
} // namespace Internal

View File

@@ -232,6 +232,7 @@ void SquishXmlOutputHandler::outputAvailable(const QByteArray &output)
result.setLine(line);
testCaseRootItem = new SquishResultItem(result);
emit resultItemCreated(testCaseRootItem);
emit updateStatus(result.text());
}
break;
}
@@ -245,6 +246,7 @@ void SquishXmlOutputHandler::outputAvailable(const QByteArray &output)
TestResult result(Result::End, QString(), time);
SquishResultItem *item = new SquishResultItem(result);
testCaseRootItem->appendChild(item);
emit updateStatus(result.text());
} else if (currentName == "description") {
if (!prepend && !details.trimmed().isEmpty()) {
logDetailsList.append(details.trimmed());
@@ -254,17 +256,36 @@ void SquishXmlOutputHandler::outputAvailable(const QByteArray &output)
&& currentName != "test"
&& currentName != "result"
&& currentName != "SquishReport") {
QTC_ASSERT(testCaseRootItem, break);
TestResult result(type, logDetails, time);
if (logDetails.isEmpty() && !logDetailsList.isEmpty())
result.setText(logDetailsList.takeFirst());
result.setFile(file);
result.setLine(line);
SquishResultItem *item = new SquishResultItem(result);
emit updateStatus(result.text());
switch (result.type()) {
case Result::Pass:
case Result::ExpectedFail:
emit increasePassCounter();
break;
case Result::Error:
case Result::Fail:
case Result::Fatal:
case Result::UnexpectedPass:
emit increaseFailCounter();
break;
default:
break;
}
if (!logDetailsList.isEmpty()) {
for (const QString &detail : qAsConst(logDetailsList)) {
TestResult childResult(Result::Detail, detail);
SquishResultItem *childItem = new SquishResultItem(childResult);
item->appendChild(childItem);
emit updateStatus(childResult.text());
}
}
testCaseRootItem->appendChild(item);

View File

@@ -3,8 +3,6 @@
#pragma once
#include "testresult.h"
#include <QObject>
#include <QXmlStreamReader>
@@ -27,6 +25,9 @@ public:
signals:
void resultItemCreated(SquishResultItem *resultItem);
void updateStatus(const QString &text);
void increasePassCounter();
void increaseFailCounter();
public slots:
void outputAvailable(const QByteArray &output);