diff --git a/src/plugins/squish/CMakeLists.txt b/src/plugins/squish/CMakeLists.txt index 153b5c94636..7687956624d 100644 --- a/src/plugins/squish/CMakeLists.txt +++ b/src/plugins/squish/CMakeLists.txt @@ -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 diff --git a/src/plugins/squish/squish.qbs b/src/plugins/squish/squish.qbs index 8b952e31d7c..819f417908c 100644 --- a/src/plugins/squish/squish.qbs +++ b/src/plugins/squish/squish.qbs @@ -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", ] } diff --git a/src/plugins/squish/squishperspective.cpp b/src/plugins/squish/squishperspective.cpp new file mode 100644 index 00000000000..c8a4338cc18 --- /dev/null +++ b/src/plugins/squish/squishperspective.cpp @@ -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 +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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("\\+(?.+):\\{(?.*)\\}"); + static const QRegularExpression inner("(?.+)#(?\\+*+)(?[^=]+)(=(?.+))?"); + 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 diff --git a/src/plugins/squish/squishperspective.h b/src/plugins/squish/squishperspective.h new file mode 100644 index 00000000000..e432413fe0f --- /dev/null +++ b/src/plugins/squish/squishperspective.h @@ -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 + +#include + +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 m_localsModel; + State m_state = State::None; + + friend class SquishControlBar; +}; + +} // namespace Internal +} // namespace Squish + diff --git a/src/plugins/squish/squishplugin.cpp b/src/plugins/squish/squishplugin.cpp index de3d6295633..3fe6e1b84b8 100644 --- a/src/plugins/squish/squishplugin.cpp +++ b/src/plugins/squish/squishplugin.cpp @@ -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; } diff --git a/src/plugins/squish/squishsettings.cpp b/src/plugins/squish/squishsettings.cpp index 6ee11a50f19..71e94c3ca61 100644 --- a/src/plugins/squish/squishsettings.cpp +++ b/src/plugins/squish/squishsettings.cpp @@ -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); }); diff --git a/src/plugins/squish/squishsettings.h b/src/plugins/squish/squishsettings.h index 575b9ab8ea5..f95f6f8a1ca 100644 --- a/src/plugins/squish/squishsettings.h +++ b/src/plugins/squish/squishsettings.h @@ -28,6 +28,7 @@ public: Utils::IntegerAspect serverPort; Utils::BoolAspect local; Utils::BoolAspect verbose; + Utils::BoolAspect minimizeIDE; }; class SquishSettingsPage final : public Core::IOptionsPage diff --git a/src/plugins/squish/squishtools.cpp b/src/plugins/squish/squishtools.cpp index 68b8603d29d..41b1d690b00 100644 --- a/src/plugins/squish/squishtools.cpp +++ b/src/plugins/squish/squishtools.cpp @@ -11,10 +11,16 @@ #include // TODO remove +#include #include +#include +#include +#include +#include #include #include +#include #include #include @@ -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 &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 &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; } - restoreQtCreatorWindows(); + 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; } - restoreQtCreatorWindows(); + 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,11 +650,181 @@ void SquishTools::onRunnerErrorOutput() const QList 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) { if (!m_currentResultsXML) @@ -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); + if (!m_lastTopLevelWindows.contains(window)) { + m_lastTopLevelWindows.append(window); + connect(window, &QWindow::destroyed, this, [this, window]() { + m_lastTopLevelWindows.removeOne(window); + }); + } + } } - - for (QWindow *window : qAsConst(toBeRemoved)) - 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()) { diff --git a/src/plugins/squish/squishtools.h b/src/plugins/squish/squishtools.h index 09eeb4f42ed..b6f0ef6de08 100644 --- a/src/plugins/squish/squishtools.h +++ b/src/plugins/squish/squishtools.h @@ -3,12 +3,13 @@ #pragma once +#include "squishperspective.h" + #include #include #include #include -#include #include @@ -51,6 +52,9 @@ public: const QStringList &additionalRunnerArgs = QStringList()); void queryServerSettings(); void writeServerSettingsChanges(const QList &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 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 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 diff --git a/src/plugins/squish/squishxmloutputhandler.cpp b/src/plugins/squish/squishxmloutputhandler.cpp index 8a0f011c527..fbdb7b3e48d 100644 --- a/src/plugins/squish/squishxmloutputhandler.cpp +++ b/src/plugins/squish/squishxmloutputhandler.cpp @@ -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); diff --git a/src/plugins/squish/squishxmloutputhandler.h b/src/plugins/squish/squishxmloutputhandler.h index 416fb56440e..3c2ebe3c559 100644 --- a/src/plugins/squish/squishxmloutputhandler.h +++ b/src/plugins/squish/squishxmloutputhandler.h @@ -3,8 +3,6 @@ #pragma once -#include "testresult.h" - #include #include @@ -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);