() << m_filterButton << m_expandAll << m_collapseAll;
+}
+
+QString SquishOutputPane::displayName() const
+{
+ return tr("Squish");
+}
+
+int SquishOutputPane::priorityInStatusBar() const
+{
+ return -777;
+}
+
+void SquishOutputPane::clearContents()
+{
+ if (m_outputPane->currentIndex() == 0)
+ clearOldResults();
+ else if (m_outputPane->currentIndex() == 1)
+ m_runnerServerLog->clear();
+}
+
+void SquishOutputPane::visibilityChanged(bool visible)
+{
+ Q_UNUSED(visible)
+}
+
+void SquishOutputPane::setFocus()
+{
+ if (m_outputPane->currentIndex() == 0)
+ m_treeView->setFocus();
+ else if (m_outputPane->currentIndex() == 1)
+ m_runnerServerLog->setFocus();
+}
+
+bool SquishOutputPane::hasFocus() const
+{
+ return m_treeView->hasFocus() || m_runnerServerLog->hasFocus();
+}
+
+bool SquishOutputPane::canFocus() const
+{
+ return true;
+}
+
+bool SquishOutputPane::canNavigate() const
+{
+ return m_outputPane->currentIndex() == 0; // only support navigation for test results
+}
+
+bool SquishOutputPane::canNext() const
+{
+ return m_filterModel->hasResults();
+}
+
+bool SquishOutputPane::canPrevious() const
+{
+ return m_filterModel->hasResults();
+}
+
+void SquishOutputPane::goToNext()
+{
+ if (!canNext())
+ return;
+
+ const QModelIndex currentIndex = m_treeView->currentIndex();
+ QModelIndex nextCurrentIndex;
+
+ if (currentIndex.isValid()) {
+ // try to set next to first child or next sibling
+ if (m_filterModel->rowCount(currentIndex)) {
+ nextCurrentIndex = m_filterModel->index(0, 0, currentIndex);
+ } else {
+ nextCurrentIndex = currentIndex.sibling(currentIndex.row() + 1, 0);
+ // if it had no sibling check siblings of parent (and grandparents if necessary)
+ if (!nextCurrentIndex.isValid()) {
+ QModelIndex parent = currentIndex.parent();
+ do {
+ if (!parent.isValid())
+ break;
+ nextCurrentIndex = parent.sibling(parent.row() + 1, 0);
+ parent = parent.parent();
+ } while (!nextCurrentIndex.isValid());
+ }
+ }
+ }
+
+ // if we have no current or could not find a next one, use the first item of the whole tree
+ if (!nextCurrentIndex.isValid()) {
+ Utils::TreeItem *rootItem = m_model->itemForIndex(QModelIndex());
+ // if the tree does not contain any item - don't do anything
+ if (!rootItem || !rootItem->childCount())
+ return;
+
+ nextCurrentIndex = m_filterModel->mapFromSource(m_model->indexForItem(rootItem->childAt(0)));
+ }
+
+ m_treeView->setCurrentIndex(nextCurrentIndex);
+ onItemActivated(nextCurrentIndex);
+}
+
+void SquishOutputPane::goToPrev()
+{
+ if (!canPrevious())
+ return;
+
+ const QModelIndex currentIndex = m_treeView->currentIndex();
+ QModelIndex nextCurrentIndex;
+
+ if (currentIndex.isValid()) {
+ // try to set next to prior sibling or parent
+ if (currentIndex.row() > 0) {
+ nextCurrentIndex = currentIndex.sibling(currentIndex.row() - 1, 0);
+ // if the sibling has children, use the last one
+ while (int rowCount = m_filterModel->rowCount(nextCurrentIndex))
+ nextCurrentIndex = m_filterModel->index(rowCount - 1, 0, nextCurrentIndex);
+ } else {
+ nextCurrentIndex = currentIndex.parent();
+ }
+ }
+
+ // if we have no current or didn't find a sibling/parent use the last item of the whole tree
+ if (!nextCurrentIndex.isValid()) {
+ const QModelIndex rootIdx = m_filterModel->index(0, 0);
+ // if the tree does not contain any item - don't do anything
+ if (!rootIdx.isValid())
+ return;
+
+ // get the last (visible) top level index
+ nextCurrentIndex = m_filterModel->index(m_filterModel->rowCount(QModelIndex()) - 1, 0);
+ // step through until end
+ while (int rowCount = m_filterModel->rowCount(nextCurrentIndex))
+ nextCurrentIndex = m_filterModel->index(rowCount - 1, 0, nextCurrentIndex);
+ }
+
+ m_treeView->setCurrentIndex(nextCurrentIndex);
+ onItemActivated(nextCurrentIndex);
+}
+
+void SquishOutputPane::addResultItem(SquishResultItem *item)
+{
+ m_model->addResultItem(item);
+ m_treeView->setHeaderHidden(false);
+ if (!m_treeView->isVisible())
+ popup(Core::IOutputPane::NoModeSwitch);
+ flash();
+ navigateStateChanged();
+}
+
+void SquishOutputPane::addLogOutput(const QString &output)
+{
+ m_runnerServerLog->appendPlainText(output);
+}
+
+void SquishOutputPane::onTestRunFinished()
+{
+ m_model->expandVisibleRootItems();
+ m_summaryWidget->setVisible(true);
+ updateSummaryLabel();
+}
+
+void SquishOutputPane::updateSummaryLabel()
+{
+ if (m_summaryWidget->isVisible()) {
+ const int passes = m_model->resultTypeCount(Result::Pass)
+ + m_model->resultTypeCount(Result::ExpectedFail);
+ const int fails = m_model->resultTypeCount(Result::Fail)
+ + m_model->resultTypeCount(Result::UnexpectedPass);
+ const QString labelText = tr("Test summary: %1 passes, %2 fails, "
+ "%3 fatals, %4 errors, %5 warnings.
")
+ .arg(passes)
+ .arg(fails)
+ .arg(m_model->resultTypeCount(Result::Fatal))
+ .arg(m_model->resultTypeCount(Result::Error))
+ .arg(m_model->resultTypeCount(Result::Warn));
+
+ m_summaryLabel->setText(labelText);
+ }
+}
+
+void SquishOutputPane::clearOldResults()
+{
+ m_treeView->setHeaderHidden(true);
+ m_summaryWidget->setVisible(false);
+ m_filterModel->clearResults();
+ navigateStateChanged();
+}
+
+void SquishOutputPane::createToolButtons()
+{
+ m_expandAll = new QToolButton(m_treeView);
+ m_expandAll->setIcon(Utils::Icons::EXPAND_TOOLBAR.icon());
+ m_expandAll->setToolTip(tr("Expand All"));
+
+ m_collapseAll = new QToolButton(m_treeView);
+ m_collapseAll->setIcon(Utils::Icons::COLLAPSE_TOOLBAR.icon());
+ m_collapseAll->setToolTip(tr("Collapse All"));
+
+ m_filterButton = new QToolButton(m_treeView);
+ m_filterButton->setIcon(Utils::Icons::FILTER.icon());
+ m_filterButton->setToolTip(tr("Filter Test Results"));
+ m_filterButton->setProperty("noArrow", true);
+ m_filterButton->setAutoRaise(true);
+ m_filterButton->setPopupMode(QToolButton::InstantPopup);
+ m_filterMenu = new QMenu(m_filterButton);
+ initializeFilterMenu();
+ m_filterButton->setMenu(m_filterMenu);
+
+ connect(m_expandAll, &QToolButton::clicked, m_treeView, &Utils::TreeView::expandAll);
+ connect(m_collapseAll, &QToolButton::clicked, m_treeView, &Utils::TreeView::collapseAll);
+ connect(m_filterMenu, &QMenu::triggered, this, &SquishOutputPane::onFilterMenuTriggered);
+}
+
+void SquishOutputPane::initializeFilterMenu()
+{
+ QMap textAndType;
+ textAndType.insert(Result::Pass, tr("Pass"));
+ textAndType.insert(Result::Fail, tr("Fail"));
+ textAndType.insert(Result::ExpectedFail, tr("Expected Fail"));
+ textAndType.insert(Result::UnexpectedPass, tr("Unexpected Pass"));
+ textAndType.insert(Result::Warn, tr("Warning Messages"));
+ textAndType.insert(Result::Log, tr("Log Messages"));
+
+ const QList types = textAndType.keys();
+ for (Result::Type type : types) {
+ QAction *action = new QAction(m_filterMenu);
+ action->setText(textAndType.value(type));
+ action->setCheckable(true);
+ action->setChecked(true);
+ action->setData(type);
+ m_filterMenu->addAction(action);
+ }
+ m_filterMenu->addSeparator();
+ QAction *action = new QAction(m_filterMenu);
+ action->setText(tr("Check All Filters"));
+ action->setCheckable(false);
+ m_filterMenu->addAction(action);
+ connect(action, &QAction::triggered, this, &SquishOutputPane::enableAllFiltersTriggered);
+}
+
+void SquishOutputPane::onItemActivated(const QModelIndex &idx)
+{
+ if (!idx.isValid())
+ return;
+
+ const TestResult result = m_filterModel->testResult(idx);
+ if (!result.file().isEmpty())
+ Core::EditorManager::openEditorAt(
+ Utils::Link(Utils::FilePath::fromString(result.file()), result.line(), 0));
+}
+
+// TODO: this is currently a workaround - might vanish if a item delegate will be implemented
+void SquishOutputPane::onSectionResized(int logicalIndex, int /*oldSize*/, int /*newSize*/)
+{
+ // details column should have been modified by user, so no action, time stamp column is fixed
+ if (logicalIndex != 1) {
+ QHeaderView *header = m_treeView->header();
+ const int minimum = m_outputPane->width() - header->sectionSize(0) - header->sectionSize(2);
+ header->resizeSection(1, qMax(minimum, header->sectionSize(1)));
+ }
+}
+
+void SquishOutputPane::onFilterMenuTriggered(QAction *action)
+{
+ m_filterModel->toggleResultType(Result::Type(action->data().toInt()));
+ navigateStateChanged();
+}
+
+void SquishOutputPane::enableAllFiltersTriggered()
+{
+ const QList actions = m_filterMenu->actions();
+ for (QAction *action : actions)
+ action->setChecked(true);
+
+ m_filterModel->enableAllResultTypes();
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishoutputpane.h b/src/plugins/squish/squishoutputpane.h
new file mode 100644
index 00000000000..92750156659
--- /dev/null
+++ b/src/plugins/squish/squishoutputpane.h
@@ -0,0 +1,107 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include
+
+QT_BEGIN_NAMESPACE
+class QAction;
+class QFrame;
+class QLabel;
+class QMenu;
+class QModelIndex;
+class QPlainTextEdit;
+class QTabWidget;
+class QToolButton;
+QT_END_NAMESPACE
+
+namespace Core { class IContext; }
+
+namespace Utils { class TreeView; }
+
+namespace Squish {
+namespace Internal {
+
+class TestResult;
+class SquishResultItem;
+class SquishResultModel;
+class SquishResultFilterModel;
+
+class SquishOutputPane : public Core::IOutputPane
+{
+ Q_OBJECT
+public:
+ static SquishOutputPane *instance();
+
+ // IOutputPane interface
+ QWidget *outputWidget(QWidget *parent) override;
+ QList toolBarWidgets() const override;
+ QString displayName() const override;
+ int priorityInStatusBar() const override;
+ void clearContents() override;
+ void visibilityChanged(bool visible) override;
+ void setFocus() override;
+ bool hasFocus() const override;
+ bool canFocus() const override;
+ bool canNavigate() const override;
+ bool canNext() const override;
+ bool canPrevious() const override;
+ void goToNext() override;
+ void goToPrev() override;
+
+public slots:
+ void addResultItem(SquishResultItem *item);
+ void addLogOutput(const QString &output);
+ void onTestRunFinished();
+ void clearOldResults();
+
+private:
+ SquishOutputPane(QObject *parent = nullptr);
+ void createToolButtons();
+ void initializeFilterMenu();
+ void onItemActivated(const QModelIndex &idx);
+ void onSectionResized(int logicalIndex, int oldSize, int newSize);
+ void onFilterMenuTriggered(QAction *action);
+ void enableAllFiltersTriggered();
+ void updateSummaryLabel();
+
+ QTabWidget *m_outputPane;
+ Core::IContext *m_context;
+ QWidget *m_outputWidget;
+ QFrame *m_summaryWidget;
+ QLabel *m_summaryLabel;
+ Utils::TreeView *m_treeView;
+ SquishResultModel *m_model;
+ SquishResultFilterModel *m_filterModel;
+ QPlainTextEdit *m_runnerServerLog;
+ QToolButton *m_expandAll;
+ QToolButton *m_collapseAll;
+ QToolButton *m_filterButton;
+ QMenu *m_filterMenu;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishplugin.cpp b/src/plugins/squish/squishplugin.cpp
new file mode 100644
index 00000000000..4d1ea84ee5b
--- /dev/null
+++ b/src/plugins/squish/squishplugin.cpp
@@ -0,0 +1,95 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishplugin.h"
+#include "objectsmapeditor.h"
+#include "squishnavigationwidget.h"
+#include "squishoutputpane.h"
+#include "squishsettings.h"
+#include "squishsettingspage.h"
+#include "squishtesttreemodel.h"
+
+#include
+
+#include
+
+#include
+
+#include
+
+using namespace Squish::Internal;
+using namespace Core;
+
+static SquishPlugin *m_instance = nullptr;
+
+SquishPlugin::SquishPlugin()
+ : m_squishSettings(new SquishSettings)
+{
+ m_instance = this;
+}
+
+SquishPlugin::~SquishPlugin()
+{
+ delete m_objectsMapEditorFactory;
+ delete m_navigationWidgetFactory;
+ delete m_settingsPage;
+ delete m_outputPane;
+}
+
+SquishPlugin *SquishPlugin::instance()
+{
+ return m_instance;
+}
+
+QSharedPointer SquishPlugin::squishSettings() const
+{
+ return m_squishSettings;
+}
+
+void SquishPlugin::initializeMenuEntries() {}
+
+bool SquishPlugin::initialize(const QStringList &arguments, QString *errorString)
+{
+ Q_UNUSED(arguments)
+ Q_UNUSED(errorString)
+
+ initializeMenuEntries();
+
+ m_squishSettings->fromSettings(ICore::settings());
+ m_treeModel = new SquishTestTreeModel(this);
+
+ m_settingsPage = new SquishSettingsPage(m_squishSettings);
+ m_navigationWidgetFactory = new SquishNavigationWidgetFactory;
+ m_outputPane = SquishOutputPane::instance();
+ m_objectsMapEditorFactory = new ObjectsMapEditorFactory;
+ return true;
+}
+
+void SquishPlugin::extensionsInitialized() {}
+
+ExtensionSystem::IPlugin::ShutdownFlag SquishPlugin::aboutToShutdown()
+{
+ return SynchronousShutdown;
+}
diff --git a/src/plugins/squish/squishplugin.h b/src/plugins/squish/squishplugin.h
new file mode 100644
index 00000000000..e4b35dace97
--- /dev/null
+++ b/src/plugins/squish/squishplugin.h
@@ -0,0 +1,74 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "squishplugin_global.h"
+
+#include
+
+#include
+
+namespace Squish {
+namespace Internal {
+
+class ObjectsMapEditorFactory;
+class SquishNavigationWidgetFactory;
+class SquishOutputPane;
+struct SquishSettings;
+class SquishSettingsPage;
+class SquishTestTreeModel;
+
+class SquishPlugin : public ExtensionSystem::IPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Squish.json")
+
+public:
+ SquishPlugin();
+ ~SquishPlugin() override;
+
+ static SquishPlugin *instance();
+
+ QSharedPointer squishSettings() const;
+
+ bool initialize(const QStringList &arguments, QString *errorString) override;
+ void extensionsInitialized() override;
+ ShutdownFlag aboutToShutdown() override;
+
+private:
+ void initializeMenuEntries();
+
+ SquishTestTreeModel *m_treeModel;
+ QSharedPointer m_squishSettings;
+
+ SquishSettingsPage *m_settingsPage = nullptr;
+ SquishNavigationWidgetFactory *m_navigationWidgetFactory = nullptr;
+ SquishOutputPane *m_outputPane = nullptr;
+ ObjectsMapEditorFactory *m_objectsMapEditorFactory = nullptr;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishplugin_global.h b/src/plugins/squish/squishplugin_global.h
new file mode 100644
index 00000000000..3284fca64f0
--- /dev/null
+++ b/src/plugins/squish/squishplugin_global.h
@@ -0,0 +1,38 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#ifndef SQUISHPLUGIN_GLOBAL_H
+#define SQUISHPLUGIN_GLOBAL_H
+
+#include
+
+#if defined(SQUISH_LIBRARY)
+# define SQUISHSHARED_EXPORT Q_DECL_EXPORT
+#else
+# define SQUISHSHARED_EXPORT Q_DECL_IMPORT
+#endif
+
+#endif // SQUISHPLUGIN_GLOBAL_H
+
diff --git a/src/plugins/squish/squishresultmodel.cpp b/src/plugins/squish/squishresultmodel.cpp
new file mode 100644
index 00000000000..c22a34d795e
--- /dev/null
+++ b/src/plugins/squish/squishresultmodel.cpp
@@ -0,0 +1,197 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishresultmodel.h"
+
+namespace Squish {
+namespace Internal {
+
+enum Role { Type = Qt::UserRole };
+
+/************************** SquishResultItem ********************************/
+
+SquishResultItem::SquishResultItem(const TestResult &result)
+ : m_testResult(result)
+{}
+
+QVariant SquishResultItem::data(int column, int role) const
+{
+ switch (role) {
+ case Qt::ToolTipRole:
+ return m_testResult.text();
+ case Qt::DisplayRole:
+ switch (column) {
+ case 0:
+ return TestResult::typeToString(m_testResult.type());
+ case 1:
+ return m_testResult.text();
+ case 2:
+ return m_testResult.timeStamp();
+ }
+ break;
+ case Qt::ForegroundRole:
+ if (column == 0)
+ return TestResult::colorForType(m_testResult.type());
+ break;
+ case Type:
+ return m_testResult.type();
+ }
+ return QVariant();
+}
+
+/************************** SquishResultModel *******************************/
+
+SquishResultModel::SquishResultModel(QObject *parent)
+ : Utils::TreeModel<>(parent)
+ , m_rootItem(new Utils::TreeItem)
+{
+ setRootItem(m_rootItem);
+ setHeader(QStringList({tr("Result"), tr("Message"), tr("Time")}));
+
+ connect(this,
+ &QAbstractItemModel::rowsInserted,
+ this,
+ &SquishResultModel::updateResultTypeCount);
+}
+
+int SquishResultModel::resultTypeCount(Result::Type type)
+{
+ return m_resultsCounter.value(type, 0);
+}
+
+void SquishResultModel::clearResults()
+{
+ clear();
+ m_resultsCounter.clear();
+ emit resultTypeCountUpdated();
+}
+
+void SquishResultModel::expandVisibleRootItems()
+{
+ m_rootItem->forChildrenAtLevel(1, [](Utils::TreeItem *item) { item->expand(); });
+}
+
+void SquishResultModel::updateResultTypeCount(const QModelIndex &parent, int first, int last)
+{
+ bool countUpdated = false;
+ for (int i = first; i <= last; ++i) {
+ SquishResultItem *resultItem = static_cast(
+ parent.isValid() ? itemForIndex(parent)->childAt(i) : m_rootItem->childAt(i));
+ QHash results;
+ ++results[resultItem->result().type()];
+
+ resultItem->forAllChildren([&results](Utils::TreeItem *it) {
+ SquishResultItem *item = static_cast(it);
+
+ Result::Type type = item->result().type();
+ ++results[type];
+ });
+
+ auto cend = results.constEnd();
+ for (auto pair = results.constBegin(); pair != cend; ++pair) {
+ Result::Type type = pair.key();
+ switch (type) {
+ case Result::Pass:
+ case Result::Fail:
+ case Result::ExpectedFail:
+ case Result::UnexpectedPass:
+ case Result::Warn:
+ case Result::Error:
+ case Result::Fatal:
+ if (int value = pair.value()) {
+ m_resultsCounter.insert(type, m_resultsCounter.value(type, 0) + value);
+ countUpdated = true;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ if (countUpdated)
+ emit resultTypeCountUpdated();
+}
+
+void SquishResultModel::addResultItem(SquishResultItem *item)
+{
+ m_rootItem->appendChild(item);
+}
+
+/*********************** SquishResultFilerModel *****************************/
+
+SquishResultFilterModel::SquishResultFilterModel(SquishResultModel *sourceModel, QObject *parent)
+ : QSortFilterProxyModel(parent)
+ , m_sourceModel(sourceModel)
+{
+ setSourceModel(sourceModel);
+ enableAllResultTypes();
+}
+
+void SquishResultFilterModel::enableAllResultTypes()
+{
+ m_enabled << Result::Log << Result::Pass << Result::Fail << Result::ExpectedFail
+ << Result::UnexpectedPass << Result::Warn << Result::Error << Result::Fatal
+ << Result::Start << Result::End << Result::Detail;
+ invalidateFilter();
+}
+
+void SquishResultFilterModel::toggleResultType(Result::Type type)
+{
+ if (m_enabled.contains(type))
+ m_enabled.remove(type);
+ else
+ m_enabled.insert(type);
+ invalidateFilter();
+}
+
+void SquishResultFilterModel::clearResults()
+{
+ m_sourceModel->clearResults();
+}
+
+bool SquishResultFilterModel::hasResults()
+{
+ return m_sourceModel->hasResults();
+}
+
+TestResult SquishResultFilterModel::testResult(const QModelIndex &idx) const
+{
+ if (auto item = static_cast(m_sourceModel->itemForIndex(mapToSource(idx))))
+ return item->result();
+
+ return TestResult();
+}
+
+bool SquishResultFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ const QModelIndex idx = m_sourceModel->index(sourceRow, 0, sourceParent);
+ if (!idx.isValid())
+ return false;
+
+ return m_enabled.contains(Result::Type(idx.data(Type).toInt()));
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishresultmodel.h b/src/plugins/squish/squishresultmodel.h
new file mode 100644
index 00000000000..f59cb93dada
--- /dev/null
+++ b/src/plugins/squish/squishresultmodel.h
@@ -0,0 +1,93 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "testresult.h"
+
+#include
+
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+class SquishResultItem : public Utils::TreeItem
+{
+public:
+ SquishResultItem(const TestResult &result);
+ QVariant data(int column, int role) const override;
+ TestResult result() const { return m_testResult; }
+
+private:
+ TestResult m_testResult;
+};
+
+class SquishResultModel : public Utils::TreeModel<>
+{
+ Q_OBJECT
+
+public:
+ SquishResultModel(QObject *parent = nullptr);
+ bool hasResults() const { return m_rootItem ? m_rootItem->hasChildren() : false; }
+ int resultTypeCount(Result::Type type);
+ void clearResults();
+ void expandVisibleRootItems();
+ void updateResultTypeCount(const QModelIndex &parent, int first, int last);
+
+ void addResultItem(SquishResultItem *item);
+
+signals:
+ void resultTypeCountUpdated();
+
+private:
+ Utils::TreeItem *m_rootItem;
+ QHash m_resultsCounter;
+};
+
+class SquishResultFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ SquishResultFilterModel(SquishResultModel *sourceModel, QObject *parent = nullptr);
+
+ void enableAllResultTypes();
+ void toggleResultType(Result::Type type);
+ void clearResults();
+ bool hasResults();
+ TestResult testResult(const QModelIndex &idx) const;
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+
+private:
+ SquishResultModel *m_sourceModel;
+ QSet m_enabled;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishsettings.cpp b/src/plugins/squish/squishsettings.cpp
new file mode 100644
index 00000000000..30b4f619b09
--- /dev/null
+++ b/src/plugins/squish/squishsettings.cpp
@@ -0,0 +1,78 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishsettings.h"
+
+#include
+
+namespace Squish {
+namespace Internal {
+
+static const char group[] = "Squish";
+static const char squishPathKey[] = "SquishPath";
+static const char licensePathKey[] = "LicensePath";
+static const char localKey[] = "Local";
+static const char serverHostKey[] = "ServerHost";
+static const char serverPortKey[] = "ServerPort";
+static const char verboseKey[] = "Verbose";
+
+void SquishSettings::toSettings(QSettings *s) const
+{
+ s->beginGroup(group);
+ s->setValue(squishPathKey, squishPath.toString());
+ s->setValue(licensePathKey, licensePath.toString());
+ s->setValue(localKey, local);
+ s->setValue(serverHostKey, serverHost);
+ s->setValue(serverPortKey, serverPort);
+ s->setValue(verboseKey, verbose);
+ s->endGroup();
+}
+
+void SquishSettings::fromSettings(QSettings *s)
+{
+ s->beginGroup(group);
+ squishPath = Utils::FilePath::fromVariant(s->value(squishPathKey));
+ licensePath = Utils::FilePath::fromVariant(s->value(licensePathKey));
+ local = s->value(localKey, true).toBool();
+ serverHost = s->value(serverHostKey, "localhost").toString();
+ serverPort = s->value(serverPortKey, 9999).toUInt();
+ verbose = s->value(verboseKey, false).toBool();
+ s->endGroup();
+}
+
+bool SquishSettings::operator==(const SquishSettings &other) const
+{
+ return local == other.local && verbose == other.verbose && serverPort == other.serverPort
+ && squishPath == other.squishPath && licensePath == other.licensePath
+ && serverHost == other.serverHost;
+}
+
+bool SquishSettings::operator!=(const SquishSettings &other) const
+{
+ return !(*this == other);
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishsettings.h b/src/plugins/squish/squishsettings.h
new file mode 100644
index 00000000000..cb9d15104e9
--- /dev/null
+++ b/src/plugins/squish/squishsettings.h
@@ -0,0 +1,57 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include
+
+#include
+#include
+
+QT_BEGIN_NAMESPACE
+class QSettings;
+QT_END_NAMESPACE
+
+namespace Squish {
+namespace Internal {
+
+struct SquishSettings
+{
+ void toSettings(QSettings *s) const;
+ void fromSettings(QSettings *s);
+
+ bool operator==(const SquishSettings &other) const;
+ bool operator!=(const SquishSettings &other) const;
+
+ Utils::FilePath squishPath;
+ Utils::FilePath licensePath;
+ QString serverHost;
+ quint16 serverPort;
+ bool local;
+ bool verbose;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishsettingspage.cpp b/src/plugins/squish/squishsettingspage.cpp
new file mode 100644
index 00000000000..505e315aec6
--- /dev/null
+++ b/src/plugins/squish/squishsettingspage.cpp
@@ -0,0 +1,108 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishsettingspage.h"
+#include "squishconstants.h"
+#include "squishsettings.h"
+
+#include
+
+#include
+
+namespace Squish {
+namespace Internal {
+
+SquishSettingsWidget::SquishSettingsWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ m_ui.setupUi(this);
+
+ connect(m_ui.localCheckBox, &QCheckBox::toggled, this, &SquishSettingsWidget::onLocalToggled);
+}
+
+void SquishSettingsWidget::setSettings(const SquishSettings &settings)
+{
+ m_ui.squishPathChooser->setFilePath(settings.squishPath);
+ m_ui.licensePathChooser->setFilePath(settings.licensePath);
+ m_ui.localCheckBox->setChecked(settings.local);
+ m_ui.serverHostLineEdit->setText(settings.serverHost);
+ m_ui.serverPortSpinBox->setValue(settings.serverPort);
+ m_ui.verboseCheckBox->setChecked(settings.verbose);
+}
+
+SquishSettings SquishSettingsWidget::settings() const
+{
+ SquishSettings result;
+ result.squishPath = m_ui.squishPathChooser->filePath();
+ result.licensePath = m_ui.licensePathChooser->filePath();
+ result.local = m_ui.localCheckBox->checkState() == Qt::Checked;
+ result.serverHost = m_ui.serverHostLineEdit->text();
+ result.serverPort = m_ui.serverPortSpinBox->value();
+ result.verbose = m_ui.verboseCheckBox->checkState() == Qt::Checked;
+ return result;
+}
+
+void SquishSettingsWidget::onLocalToggled(bool checked)
+{
+ m_ui.serverHostLineEdit->setEnabled(!checked);
+ m_ui.serverPortSpinBox->setEnabled(!checked);
+}
+
+SquishSettingsPage::SquishSettingsPage(const QSharedPointer &settings)
+ : m_settings(settings)
+ , m_widget(nullptr)
+{
+ setId("A.Squish.General");
+ setDisplayName(tr("General"));
+ setCategory(Constants::SQUISH_SETTINGS_CATEGORY);
+ setDisplayCategory(tr("Squish"));
+ setCategoryIcon(Utils::Icon({{":/squish/images/settingscategory_squish.png",
+ Utils::Theme::PanelTextColorDark}},
+ Utils::Icon::Tint));
+}
+
+QWidget *SquishSettingsPage::widget()
+{
+ if (!m_widget) {
+ m_widget = new SquishSettingsWidget;
+ m_widget->setSettings(*m_settings);
+ }
+ return m_widget;
+}
+
+void SquishSettingsPage::apply()
+{
+ if (!m_widget) // page was not shown at all
+ return;
+
+ const SquishSettings newSettings = m_widget->settings();
+ if (newSettings != *m_settings) {
+ *m_settings = newSettings;
+ m_settings->toSettings(Core::ICore::settings());
+ }
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishsettingspage.h b/src/plugins/squish/squishsettingspage.h
new file mode 100644
index 00000000000..9e480776820
--- /dev/null
+++ b/src/plugins/squish/squishsettingspage.h
@@ -0,0 +1,72 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "ui_squishsettingspage.h"
+
+#include
+
+#include
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+struct SquishSettings;
+
+class SquishSettingsWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit SquishSettingsWidget(QWidget *parent = nullptr);
+
+ void setSettings(const SquishSettings &settings);
+ SquishSettings settings() const;
+
+private:
+ void onLocalToggled(bool checked);
+
+ Ui::SquishSettingsPage m_ui;
+};
+
+class SquishSettingsPage : public Core::IOptionsPage
+{
+ Q_OBJECT
+public:
+ explicit SquishSettingsPage(const QSharedPointer &settings);
+
+ QWidget *widget() override;
+ void apply() override;
+ void finish() override {}
+
+private:
+ QSharedPointer m_settings;
+ QPointer m_widget;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishsettingspage.ui b/src/plugins/squish/squishsettingspage.ui
new file mode 100644
index 00000000000..fa12dcf9d72
--- /dev/null
+++ b/src/plugins/squish/squishsettingspage.ui
@@ -0,0 +1,127 @@
+
+
+ Squish::Internal::SquishSettingsPage
+
+
+
+ 0
+ 0
+ 463
+ 338
+
+
+
+ Form
+
+
+ -
+
+
+ Squish path:
+
+
+
+ -
+
+
+ Path to Squish installation directory.
+
+
+
+ -
+
+
+ License path:
+
+
+
+ -
+
+
+ Path to directory containing Squish license file. You will only need this in special configurations like if you did not run the Squish setup tool.
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Local server
+
+
+
+ -
+
+
+ Server host:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Port:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 65535
+
+
+
+ -
+
+
+ Verbose log
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 289
+
+
+
+
+
+
+
+
+ Utils::PathChooser
+ QLineEdit
+
+ 1
+
+
+
+
+
diff --git a/src/plugins/squish/squishtesttreemodel.cpp b/src/plugins/squish/squishtesttreemodel.cpp
new file mode 100644
index 00000000000..17e5e9ee960
--- /dev/null
+++ b/src/plugins/squish/squishtesttreemodel.cpp
@@ -0,0 +1,433 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishtesttreemodel.h"
+#include "squishfilehandler.h"
+
+#include
+#include
+
+#include
+
+namespace Squish {
+namespace Internal {
+
+/**************************** SquishTestTreeItem ***************************************/
+
+SquishTestTreeItem::SquishTestTreeItem(const QString &displayName, Type type)
+ : m_displayName(displayName)
+ , m_type(type)
+ , m_checked(Qt::Checked)
+{
+ switch (type) {
+ case Root:
+ m_flags = Qt::NoItemFlags;
+ break;
+ case SquishSuite:
+ m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserTristate
+ | Qt::ItemIsUserCheckable;
+ break;
+ case SquishTestCase:
+ m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
+ break;
+ case SquishSharedFile:
+ case SquishSharedFolder:
+ m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ break;
+ }
+}
+
+Qt::ItemFlags SquishTestTreeItem::flags(int /*column*/) const
+{
+ return m_flags;
+}
+
+void SquishTestTreeItem::setFilePath(const QString &filePath)
+{
+ m_filePath = filePath;
+}
+
+void SquishTestTreeItem::setParentName(const QString &parentName)
+{
+ m_parentName = parentName;
+}
+
+void SquishTestTreeItem::setCheckState(Qt::CheckState state)
+{
+ switch (m_type) {
+ case SquishTestCase:
+ m_checked = (state == Qt::Unchecked ? Qt::Unchecked : Qt::Checked);
+ static_cast(parent())->revalidateCheckState();
+ break;
+ case SquishSuite:
+ m_checked = (state == Qt::Unchecked ? Qt::Unchecked : Qt::Checked);
+ forChildrenAtLevel(1, [this](TreeItem *item) {
+ static_cast(item)->m_checked = m_checked;
+ });
+ break;
+ default:
+ break;
+ }
+}
+
+bool SquishTestTreeItem::modifyContent(const SquishTestTreeItem &other)
+{
+ // modification applies only for items of the same type
+ if (m_type != other.m_type)
+ return false;
+
+ const bool modified = m_displayName != other.m_displayName || m_filePath != other.m_filePath
+ || m_parentName != other.m_parentName;
+
+ m_displayName = other.m_displayName;
+ m_filePath = other.m_filePath;
+ m_parentName = other.m_parentName;
+ return modified;
+}
+
+void SquishTestTreeItem::revalidateCheckState()
+{
+ if (childCount() == 0)
+ return;
+
+ bool foundChecked = false;
+ bool foundUnchecked = false;
+
+ forChildrenAtLevel(1, [&foundChecked, &foundUnchecked](const TreeItem *item) {
+ const SquishTestTreeItem *squishItem = static_cast(item);
+ foundChecked |= (squishItem->checkState() != Qt::Unchecked);
+ foundUnchecked |= (squishItem->checkState() == Qt::Unchecked);
+ });
+ if (foundChecked && foundUnchecked) {
+ m_checked = Qt::PartiallyChecked;
+ return;
+ }
+
+ m_checked = (foundUnchecked ? Qt::Unchecked : Qt::Checked);
+}
+
+/**************************** SquishTestTreeModel **************************************/
+
+static SquishTestTreeModel *m_instance = nullptr;
+
+SquishTestTreeModel::SquishTestTreeModel(QObject *parent)
+ : TreeModel(new SquishTestTreeItem(QString(), SquishTestTreeItem::Root),
+ parent)
+ , m_squishSharedFolders(new SquishTestTreeItem(tr("Shared Folders"), SquishTestTreeItem::Root))
+ , m_squishSuitesRoot(new SquishTestTreeItem(tr("Test Suites"), SquishTestTreeItem::Root))
+ , m_squishFileHandler(new SquishFileHandler(this))
+{
+ rootItem()->appendChild(m_squishSharedFolders);
+ rootItem()->appendChild(m_squishSuitesRoot);
+
+ connect(m_squishFileHandler,
+ &SquishFileHandler::testTreeItemCreated,
+ this,
+ &SquishTestTreeModel::addTreeItem);
+ connect(m_squishFileHandler,
+ &SquishFileHandler::suiteTreeItemModified,
+ this,
+ &SquishTestTreeModel::onSuiteTreeItemModified);
+ connect(m_squishFileHandler,
+ &SquishFileHandler::suiteTreeItemRemoved,
+ this,
+ &SquishTestTreeModel::onSuiteTreeItemRemoved);
+
+ m_instance = this;
+}
+
+SquishTestTreeModel::~SquishTestTreeModel() {}
+
+SquishTestTreeModel *SquishTestTreeModel::instance()
+{
+ if (!m_instance)
+ m_instance = new SquishTestTreeModel;
+ return m_instance;
+}
+
+static QIcon treeIcon(SquishTestTreeItem::Type type, int column)
+{
+ static QIcon icons[5] = {QIcon(),
+ Utils::Icons::OPENFILE.icon(),
+ QIcon(":/fancyactionbar/images/mode_Edit.png"),
+ Utils::Icons::OPENFILE.icon(),
+ QIcon(":/fancyactionbar/images/mode_Edit.png")};
+ if (column == 0)
+ return icons[type];
+
+ switch (type) {
+ case SquishTestTreeItem::SquishSuite:
+ if (column == 1)
+ return QIcon(":/squish/mages/play.png");
+ else if (column == 2)
+ return QIcon(":/squish/images/objectsmap.png");
+ break;
+ case SquishTestTreeItem::SquishTestCase:
+ if (column == 1)
+ return QIcon(":/squish/images/play.png");
+ else if (column == 2)
+ return QIcon(":/squish/images/record.png");
+ break;
+ default: // avoid warning of unhandled enum values
+ break;
+ }
+ return icons[0];
+}
+
+QVariant SquishTestTreeModel::data(const QModelIndex &idx, int role) const
+{
+ if (!idx.isValid())
+ return QVariant();
+
+ if (SquishTestTreeItem *item = static_cast(itemForIndex(idx))) {
+ const SquishTestTreeItem::Type type = item->type();
+ switch (role) {
+ case Qt::DisplayRole:
+ if (idx.column() > 0)
+ return QVariant();
+ switch (type) {
+ case SquishTestTreeItem::Root:
+ if (!item->hasChildren())
+ return tr("%1 (none)").arg(item->displayName());
+ return item->displayName();
+ case SquishTestTreeItem::SquishSharedFile:
+ case SquishTestTreeItem::SquishSharedFolder:
+ return item->displayName();
+ default: {
+ } // avoid warning regarding unhandled enum values
+ return item->displayName();
+ }
+ break;
+ case Qt::DecorationRole:
+ return treeIcon(type, idx.column());
+ case Qt::CheckStateRole:
+ if (idx.column() > 0)
+ return QVariant();
+ if (type == SquishTestTreeItem::SquishSuite
+ || type == SquishTestTreeItem::SquishTestCase)
+ return item->checkState();
+ return QVariant();
+ case Qt::ToolTipRole:
+ if (type == SquishTestTreeItem::Root)
+ return QVariant();
+ if (item->displayName() == item->filePath())
+ return item->displayName();
+
+ return item->displayName().append('\n').append(item->filePath());
+ case LinkRole:
+ return item->filePath();
+ case TypeRole:
+ return type;
+ case DisplayNameRole:
+ return item->displayName();
+ }
+ }
+ return TreeModel::data(idx, role);
+}
+
+bool SquishTestTreeModel::setData(const QModelIndex &idx, const QVariant &data, int role)
+{
+ if (!idx.isValid())
+ return false;
+
+ if (role == Qt::CheckStateRole) {
+ SquishTestTreeItem *item = static_cast(itemForIndex(idx));
+ const SquishTestTreeItem::Type type = item->type();
+ if (type == SquishTestTreeItem::SquishSharedFolder
+ || type == SquishTestTreeItem::SquishSharedFile)
+ return false;
+ Qt::CheckState old = item->checkState();
+ item->setCheckState((Qt::CheckState) data.toInt());
+ if (item->checkState() != old) {
+ switch (type) {
+ case SquishTestTreeItem::SquishSuite:
+ emit dataChanged(idx, idx);
+ if (rowCount(idx) > 0)
+ emit dataChanged(index(0, 0, idx), index(rowCount(idx), 0, idx));
+ break;
+ case SquishTestTreeItem::SquishTestCase:
+ emit dataChanged(idx, idx);
+ emit dataChanged(idx.parent(), idx.parent());
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+int SquishTestTreeModel::columnCount(const QModelIndex & /*idx*/) const
+{
+ return COLUMN_COUNT;
+}
+
+void SquishTestTreeModel::addTreeItem(SquishTestTreeItem *item)
+{
+ switch (item->type()) {
+ case SquishTestTreeItem::SquishSharedFolder:
+ m_squishSharedFolders->appendChild(item);
+ break;
+ case SquishTestTreeItem::SquishSuite:
+ m_squishSuitesRoot->appendChild(item);
+ break;
+ case SquishTestTreeItem::SquishTestCase: {
+ const QString folderName = item->parentName();
+ Utils::TreeItem *parent
+ = m_squishSuitesRoot->findChildAtLevel(1, [folderName](Utils::TreeItem *it) {
+ SquishTestTreeItem *squishItem = static_cast(it);
+ return squishItem->displayName() == folderName;
+ });
+ if (parent)
+ parent->appendChild(item);
+ break;
+ }
+ case SquishTestTreeItem::SquishSharedFile: {
+ const QString folderName = item->parentName();
+ Utils::TreeItem *parent
+ = m_squishSharedFolders->findChildAtLevel(1, [folderName](Utils::TreeItem *it) {
+ SquishTestTreeItem *squishItem = static_cast(it);
+ return squishItem->displayName() == folderName;
+ });
+ if (parent)
+ parent->appendChild(item);
+ break;
+ }
+ case SquishTestTreeItem::Root:
+ default:
+ qWarning("Not supposed to be used for Root items or unknown items.");
+ delete item;
+ break;
+ }
+}
+
+void SquishTestTreeModel::removeTreeItem(int row, const QModelIndex &parent)
+{
+ if (!parent.isValid() || row >= rowCount(parent))
+ return;
+
+ Utils::TreeItem *toBeRemoved = itemForIndex(index(row, 0, parent));
+ takeItem(toBeRemoved);
+ delete toBeRemoved;
+}
+
+void SquishTestTreeModel::modifyTreeItem(int row,
+ const QModelIndex &parent,
+ const SquishTestTreeItem &modified)
+{
+ if (!parent.isValid() || row >= rowCount(parent))
+ return;
+
+ QModelIndex childIndex = index(row, 0, parent);
+
+ SquishTestTreeItem *toBeModified = static_cast(itemForIndex(childIndex));
+
+ if (toBeModified->modifyContent(modified))
+ emit dataChanged(childIndex, childIndex);
+}
+
+void SquishTestTreeModel::removeAllSharedFolders()
+{
+ m_squishSharedFolders->removeChildren();
+}
+
+QStringList SquishTestTreeModel::getSelectedSquishTestCases(const QString &suiteConfPath) const
+{
+ QStringList result;
+ const int count = m_squishSuitesRoot->childCount();
+
+ if (count) {
+ for (int row = 0; row < count; ++row) {
+ auto suiteItem = static_cast(m_squishSuitesRoot->childAt(row));
+ if (suiteItem->filePath() == suiteConfPath) {
+ const int testCaseCount = suiteItem->childCount();
+ for (int caseRow = 0; caseRow < testCaseCount; ++caseRow) {
+ auto caseItem = static_cast(suiteItem->childAt(caseRow));
+ if (caseItem->checkState() == Qt::Checked)
+ result.append(caseItem->displayName());
+ }
+ break;
+ }
+ }
+ }
+
+ return result;
+}
+
+SquishTestTreeItem *SquishTestTreeModel::findSuite(const QString &displayName) const
+{
+ return findNonRootItem([&displayName](SquishTestTreeItem *item) {
+ return item->type() == SquishTestTreeItem::SquishSuite
+ && item->displayName() == displayName;
+ });
+}
+
+void SquishTestTreeModel::onSuiteTreeItemRemoved(const QString &suiteName)
+{
+ if (SquishTestTreeItem *suite = findSuite(suiteName)) {
+ const QModelIndex idx = suite->index();
+ removeTreeItem(idx.row(), idx.parent());
+ }
+}
+
+void SquishTestTreeModel::onSuiteTreeItemModified(SquishTestTreeItem *item, const QString &display)
+{
+ if (SquishTestTreeItem *suite = findSuite(display)) {
+ const QModelIndex idx = suite->index();
+ modifyTreeItem(idx.row(), idx.parent(), *item);
+ }
+ // avoid leaking item even when it cannot be found
+ delete item;
+}
+
+/************************************** SquishTestTreeSortModel **********************************/
+
+SquishTestTreeSortModel::SquishTestTreeSortModel(SquishTestTreeModel *sourceModel, QObject *parent)
+ : QSortFilterProxyModel(parent)
+{
+ setSourceModel(sourceModel);
+}
+
+Utils::TreeItem *SquishTestTreeSortModel::itemFromIndex(const QModelIndex &idx) const
+{
+ return static_cast(sourceModel())->itemForIndex(mapToSource(idx));
+}
+
+bool SquishTestTreeSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ // root items keep intended order
+ const SquishTestTreeItem *leftItem = static_cast(left.internalPointer());
+ if (leftItem->type() == SquishTestTreeItem::Root)
+ return left.row() > right.row();
+
+ const QString leftVal = left.data().toString();
+ const QString rightVal = right.data().toString();
+
+ return QString::compare(leftVal, rightVal, Qt::CaseInsensitive) > 0;
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishtesttreemodel.h b/src/plugins/squish/squishtesttreemodel.h
new file mode 100644
index 00000000000..052aefa7983
--- /dev/null
+++ b/src/plugins/squish/squishtesttreemodel.h
@@ -0,0 +1,110 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+#pragma once
+
+#include
+
+#include
+
+namespace {
+enum ItemRole { LinkRole = Qt::UserRole + 2, TypeRole, DisplayNameRole };
+}
+
+namespace Squish {
+namespace Internal {
+
+class SquishFileHandler;
+
+class SquishTestTreeItem : public Utils::TreeItem
+{
+public:
+ enum Type { Root, SquishSuite, SquishTestCase, SquishSharedFolder, SquishSharedFile };
+
+ SquishTestTreeItem(const QString &displayName, Type type);
+ ~SquishTestTreeItem() override {}
+
+ Qt::ItemFlags flags(int column) const override;
+ QString displayName() const { return m_displayName; }
+ void setFilePath(const QString &filePath);
+ QString filePath() const { return m_filePath; }
+ void setParentName(const QString &parentName);
+ QString parentName() const { return m_parentName; }
+ Type type() const { return m_type; }
+ void setCheckState(Qt::CheckState state);
+ Qt::CheckState checkState() const { return m_checked; }
+
+ bool modifyContent(const SquishTestTreeItem &other);
+
+private:
+ void revalidateCheckState();
+
+ QString m_displayName; // holds suite or test case name
+ QString m_filePath; // holds suite.conf path for suites, test.* for test cases
+ Type m_type;
+ QString m_parentName; // holds suite name for test cases, folder path for shared files
+ Qt::CheckState m_checked; // suites and test cases can have a check state
+ Qt::ItemFlags m_flags = Qt::NoItemFlags;
+};
+
+class SquishTestTreeModel : public Utils::TreeModel
+{
+public:
+ SquishTestTreeModel(QObject *parent = nullptr);
+ ~SquishTestTreeModel() override;
+
+ static SquishTestTreeModel *instance();
+ static const int COLUMN_COUNT = 3;
+
+ QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex &idx, const QVariant &data, int role) override;
+ int columnCount(const QModelIndex &idx) const override;
+ void addTreeItem(SquishTestTreeItem *item);
+ void removeTreeItem(int row, const QModelIndex &parent);
+ void modifyTreeItem(int row, const QModelIndex &parent, const SquishTestTreeItem &modified);
+ void removeAllSharedFolders();
+ QStringList getSelectedSquishTestCases(const QString &suiteConfPath) const;
+
+private:
+ SquishTestTreeItem *findSuite(const QString &displayName) const;
+ void onSuiteTreeItemRemoved(const QString &suiteName);
+ void onSuiteTreeItemModified(SquishTestTreeItem *item, const QString &display);
+ Utils::TreeItem *m_squishSharedFolders;
+ Utils::TreeItem *m_squishSuitesRoot;
+ SquishFileHandler *m_squishFileHandler;
+};
+
+class SquishTestTreeSortModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ SquishTestTreeSortModel(SquishTestTreeModel *sourceModel, QObject *parent = nullptr);
+ Utils::TreeItem *itemFromIndex(const QModelIndex &idx) const;
+
+protected:
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishtesttreeview.cpp b/src/plugins/squish/squishtesttreeview.cpp
new file mode 100644
index 00000000000..a1f4b995b7e
--- /dev/null
+++ b/src/plugins/squish/squishtesttreeview.cpp
@@ -0,0 +1,132 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishtesttreeview.h"
+#include "squishconstants.h"
+#include "squishtesttreemodel.h"
+
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+SquishTestTreeView::SquishTestTreeView(QWidget *parent)
+ : Utils::NavigationTreeView(parent)
+ , m_context(new Core::IContext(this))
+{
+ setExpandsOnDoubleClick(false);
+ m_context->setWidget(this);
+ m_context->setContext(Core::Context(Constants::SQUISH_CONTEXT));
+ Core::ICore::addContextObject(m_context);
+}
+
+void SquishTestTreeView::resizeEvent(QResizeEvent *event)
+{
+ // override derived behavior of Utils::NavigationTreeView as we have more than 1 column
+ Utils::NavigationTreeView::resizeEvent(event);
+}
+
+void SquishTestTreeView::mousePressEvent(QMouseEvent *event)
+{
+ if (event->button() == Qt::LeftButton) {
+ const QModelIndex index = indexAt(event->pos());
+ if (index.isValid() && index.column() > 0 && index.column() < 3) {
+ int type = index.data(TypeRole).toInt();
+ if (type == SquishTestTreeItem::SquishSuite
+ || type == SquishTestTreeItem::SquishTestCase) {
+ m_lastMousePressedIndex = index;
+ }
+ }
+ }
+ QTreeView::mousePressEvent(event);
+}
+
+void SquishTestTreeView::mouseReleaseEvent(QMouseEvent *event)
+{
+ if (event->button() == Qt::LeftButton) {
+ const QModelIndex index = indexAt(event->pos());
+ if (index.isValid() && index == m_lastMousePressedIndex) {
+ int type = index.data(TypeRole).toInt();
+ if (type == SquishTestTreeItem::SquishSuite) {
+ if (index.column() == 1)
+ emit runTestSuite(index.data(DisplayNameRole).toString());
+ else if (index.column() == 2)
+ emit openObjectsMap(index.data(DisplayNameRole).toString());
+ } else {
+ const QModelIndex &suiteIndex = index.parent();
+ if (suiteIndex.isValid()) {
+ if (index.column() == 1) {
+ emit runTestCase(suiteIndex.data(DisplayNameRole).toString(),
+ index.data(DisplayNameRole).toString());
+ } else if (index.column() == 2) {
+ emit recordTestCase(suiteIndex.data(DisplayNameRole).toString(),
+ index.data(DisplayNameRole).toString());
+ }
+ }
+ }
+ }
+ }
+ QTreeView::mouseReleaseEvent(event);
+}
+
+/****************************** SquishTestTreeItemDelegate *************************************/
+
+SquishTestTreeItemDelegate::SquishTestTreeItemDelegate(QObject *parent)
+ : QStyledItemDelegate(parent)
+{}
+
+void SquishTestTreeItemDelegate::paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &idx) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, idx);
+
+ // elide first column if necessary
+ if (idx.column() == 0)
+ opt.textElideMode = Qt::ElideMiddle;
+
+ // display disabled items as enabled
+ if (idx.flags() == Qt::NoItemFlags)
+ opt.palette.setColor(QPalette::Text, opt.palette.color(QPalette::Active, QPalette::Text));
+
+ QStyledItemDelegate::paint(painter, opt, idx);
+}
+
+QSize SquishTestTreeItemDelegate::sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &idx) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, idx);
+
+ // elide first column if necessary
+ if (idx.column() == 0)
+ opt.textElideMode = Qt::ElideMiddle;
+ return QStyledItemDelegate::sizeHint(opt, idx);
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishtesttreeview.h b/src/plugins/squish/squishtesttreeview.h
new file mode 100644
index 00000000000..4375e17c66a
--- /dev/null
+++ b/src/plugins/squish/squishtesttreeview.h
@@ -0,0 +1,71 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace Core { class IContext; }
+
+namespace Squish {
+namespace Internal {
+
+class SquishTestTreeView : public Utils::NavigationTreeView
+{
+ Q_OBJECT
+public:
+ SquishTestTreeView(QWidget *parent = nullptr);
+ void resizeEvent(QResizeEvent *event) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+
+signals:
+ void runTestSuite(const QString &suiteName);
+ void runTestCase(const QString &suiteName, const QString &testCaseName);
+ void openObjectsMap(const QString &suiteName);
+ void recordTestCase(const QString &suiteName, const QString &testCaseName);
+
+private:
+ Core::IContext *m_context;
+ QModelIndex m_lastMousePressedIndex;
+};
+
+class SquishTestTreeItemDelegate : public QStyledItemDelegate
+{
+public:
+ SquishTestTreeItemDelegate(QObject *parent = nullptr);
+ ~SquishTestTreeItemDelegate() override {}
+
+ void paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &idx) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishtools.cpp b/src/plugins/squish/squishtools.cpp
new file mode 100644
index 00000000000..a4b83b9e247
--- /dev/null
+++ b/src/plugins/squish/squishtools.cpp
@@ -0,0 +1,743 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishtools.h"
+#include "squishoutputpane.h"
+#include "squishplugin.h"
+#include "squishsettings.h"
+#include "squishxmloutputhandler.h"
+
+#include // TODO remove
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+// make this configurable?
+static const QString resultsDirectory = QFileInfo(QDir::home(), ".squishQC/Test Results")
+ .absoluteFilePath();
+
+SquishTools::SquishTools(QObject *parent)
+ : QObject(parent)
+ , m_serverProcess(nullptr)
+ , m_runnerProcess(nullptr)
+ , m_serverPort(-1)
+ , m_request(None)
+ , m_state(Idle)
+ , m_currentResultsXML(nullptr)
+ , m_resultsFileWatcher(nullptr)
+ , m_testRunning(false)
+ , m_xmlOutputHandler(nullptr)
+{
+ SquishOutputPane *outputPane = SquishOutputPane::instance();
+ connect(this,
+ &SquishTools::logOutputReceived,
+ outputPane,
+ &SquishOutputPane::addLogOutput,
+ Qt::QueuedConnection);
+ connect(this,
+ &SquishTools::squishTestRunStarted,
+ outputPane,
+ &SquishOutputPane::clearOldResults);
+ connect(this,
+ &SquishTools::squishTestRunFinished,
+ outputPane,
+ &SquishOutputPane::onTestRunFinished);
+}
+
+SquishTools::~SquishTools()
+{
+ // TODO add confirmation dialog somewhere
+ if (m_runnerProcess) {
+ m_runnerProcess->terminate();
+ if (!m_runnerProcess->waitForFinished(5000))
+ m_runnerProcess->kill();
+ delete m_runnerProcess;
+ m_runnerProcess = nullptr;
+ }
+
+ if (m_serverProcess) {
+ m_serverProcess->terminate();
+ if (!m_serverProcess->waitForFinished(5000))
+ m_serverProcess->kill();
+ delete m_serverProcess;
+ m_serverProcess = nullptr;
+ }
+
+ delete m_xmlOutputHandler;
+}
+
+struct SquishToolsSettings
+{
+ SquishToolsSettings() {}
+
+ QString squishPath;
+ QString serverPath = "squishserver";
+ QString runnerPath = "squishrunner";
+ bool isLocalServer = true;
+ bool verboseLog = false;
+ QString serverHost = "localhost";
+ int serverPort = 9999;
+ QString licenseKeyPath;
+
+ // populate members using current settings
+ void setup()
+ {
+ QSharedPointer squishSettings = SquishPlugin::instance()->squishSettings();
+ squishPath = squishSettings->squishPath.toString();
+ serverPath = Utils::HostOsInfo::withExecutableSuffix("squishserver");
+ runnerPath = Utils::HostOsInfo::withExecutableSuffix("squishrunner");
+
+ if (!squishPath.isEmpty()) {
+ const QDir squishBin(squishPath + QDir::separator() + "bin");
+ serverPath = QFileInfo(squishBin, serverPath).absoluteFilePath();
+ runnerPath = QFileInfo(squishBin, runnerPath).absoluteFilePath();
+ }
+
+ isLocalServer = squishSettings->local;
+ serverHost = squishSettings->serverHost;
+ serverPort = squishSettings->serverPort;
+ verboseLog = squishSettings->verbose;
+ licenseKeyPath = squishSettings->licensePath.toString();
+ }
+};
+
+void SquishTools::runTestCases(const QString &suitePath,
+ const QStringList &testCases,
+ const QStringList &additionalServerArgs,
+ const QStringList &additionalRunnerArgs)
+{
+ if (m_state != Idle) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Error"),
+ tr("Squish Tools in unexpected state (%1).\n"
+ "Refusing to run a test case.")
+ .arg(m_state));
+ return;
+ }
+ // create test results directory (if necessary) and return on fail
+ if (!QDir().mkpath(resultsDirectory)) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Error"),
+ tr("Could not create test results folder. Canceling test run."));
+ return;
+ }
+
+ m_suitePath = suitePath;
+ m_testCases = testCases;
+ m_reportFiles.clear();
+ m_additionalServerArguments = additionalServerArgs;
+
+ const QString dateTimeString = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH-mm-ss");
+ m_currentResultsDirectory = QFileInfo(QDir(resultsDirectory), dateTimeString).absoluteFilePath();
+
+ m_additionalRunnerArguments = additionalRunnerArgs;
+ m_additionalRunnerArguments << "--interactive" << "--resultdir"
+ << QDir::toNativeSeparators(m_currentResultsDirectory);
+
+ delete m_xmlOutputHandler;
+ m_xmlOutputHandler = new SquishXmlOutputHandler(this);
+ connect(this,
+ &SquishTools::resultOutputCreated,
+ m_xmlOutputHandler,
+ &SquishXmlOutputHandler::outputAvailable,
+ Qt::QueuedConnection);
+
+ m_testRunning = true;
+ emit squishTestRunStarted();
+ startSquishServer(RunTestRequested);
+}
+
+void SquishTools::setState(SquishTools::State state)
+{
+ // TODO check whether state transition is legal
+ m_state = state;
+
+ switch (m_state) {
+ case Idle:
+ m_request = None;
+ m_suitePath = QString();
+ m_testCases.clear();
+ m_reportFiles.clear();
+ m_additionalRunnerArguments.clear();
+ m_additionalServerArguments.clear();
+ m_testRunning = false;
+ m_currentResultsDirectory.clear();
+ m_lastTopLevelWindows.clear();
+ break;
+ case ServerStarted:
+ if (m_request == RunTestRequested) {
+ startSquishRunner();
+ } else if (m_request == RecordTestRequested) {
+ } else if (m_request == RunnerQueryRequested) {
+ } else {
+ QTC_ASSERT(false, qDebug() << m_state << m_request);
+ }
+ break;
+ case ServerStartFailed:
+ m_state = Idle;
+ m_request = None;
+ if (m_testRunning) {
+ emit squishTestRunFinished();
+ m_testRunning = false;
+ }
+ restoreQtCreatorWindows();
+ break;
+ case ServerStopped:
+ m_state = Idle;
+ if (m_request == ServerStopRequested) {
+ m_request = None;
+ if (m_testRunning) {
+ emit squishTestRunFinished();
+ m_testRunning = false;
+ }
+ restoreQtCreatorWindows();
+ } else if (m_request == KillOldBeforeRunRunner) {
+ startSquishServer(RunTestRequested);
+ } else if (m_request == KillOldBeforeRecordRunner) {
+ startSquishServer(RecordTestRequested);
+ } else if (m_request == KillOldBeforeQueryRunner) {
+ startSquishServer(RunnerQueryRequested);
+ } else {
+ QTC_ASSERT(false, qDebug() << m_state << m_request);
+ }
+ break;
+ case ServerStopFailed:
+ if (m_serverProcess && m_serverProcess->state() != QProcess::NotRunning) {
+ m_serverProcess->terminate();
+ if (!m_serverProcess->waitForFinished(5000)) {
+ m_serverProcess->kill();
+ delete m_serverProcess;
+ m_serverProcess = nullptr;
+ }
+ }
+ m_state = Idle;
+ break;
+ case RunnerStartFailed:
+ case RunnerStopped:
+ if (m_testCases.isEmpty()) {
+ m_request = ServerStopRequested;
+ stopSquishServer();
+ QString error;
+ SquishXmlOutputHandler::mergeResultFiles(m_reportFiles,
+ m_currentResultsDirectory,
+ QDir(m_suitePath).dirName(),
+ &error);
+ if (!error.isEmpty())
+ QMessageBox::critical(Core::ICore::dialogParent(), tr("Error"), error);
+ logrotateTestResults();
+ } else {
+ m_xmlOutputHandler->clearForNextRun();
+ startSquishRunner();
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+// make sure to execute setup() to populate with current settings before using it
+static SquishToolsSettings toolsSettings;
+
+void SquishTools::startSquishServer(Request request)
+{
+ m_request = request;
+ if (m_serverProcess) {
+ if (QMessageBox::question(Core::ICore::dialogParent(),
+ tr("Squish Server Already Running"),
+ tr("There is still an old Squish server instance running.\n"
+ "This will cause problems later on.\n\n"
+ "If you continue, the old instance will be terminated.\n"
+ "Do you want to continue?"))
+ == QMessageBox::Yes) {
+ switch (m_request) {
+ case RunTestRequested:
+ m_request = KillOldBeforeRunRunner;
+ break;
+ case RecordTestRequested:
+ m_request = KillOldBeforeRecordRunner;
+ break;
+ case RunnerQueryRequested:
+ m_request = KillOldBeforeQueryRunner;
+ break;
+ default:
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Error"),
+ tr("Unexpected state or request while starting Squish "
+ "server. (state: %1, request: %2)")
+ .arg(m_state)
+ .arg(m_request));
+ }
+ stopSquishServer();
+ }
+ return;
+ }
+
+ toolsSettings.setup();
+ m_serverPort = -1;
+
+ const Utils::FilePath squishServer = Utils::Environment::systemEnvironment().searchInPath(
+ toolsSettings.serverPath);
+ if (squishServer.isEmpty()) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Squish Server Error"),
+ tr("\"%1\" could not be found or is not executable.\n"
+ "Check the settings.")
+ .arg(QDir::toNativeSeparators(toolsSettings.serverPath)));
+ setState(Idle);
+ return;
+ }
+ toolsSettings.serverPath = squishServer.toString();
+
+ if (true) // TODO squish setting of minimize QC on squish run/record
+ minimizeQtCreatorWindows();
+ else
+ m_lastTopLevelWindows.clear();
+
+ m_serverProcess = new QProcess;
+ m_serverProcess->setProgram(toolsSettings.serverPath);
+ QStringList arguments;
+ // TODO if isLocalServer is false we should start a squishserver on remote device
+ if (toolsSettings.isLocalServer)
+ arguments << "--local"; // for now - although Squish Docs say "don't use it"
+ else
+ arguments << "--port" << QString::number(toolsSettings.serverPort);
+ if (toolsSettings.verboseLog)
+ arguments << "--verbose";
+
+ m_serverProcess->setArguments(arguments);
+ m_serverProcess->setProcessEnvironment(squishEnvironment());
+
+ connect(m_serverProcess, &QProcess::readyReadStandardOutput, this, &SquishTools::onServerOutput);
+ connect(m_serverProcess,
+ &QProcess::readyReadStandardError,
+ this,
+ &SquishTools::onServerErrorOutput);
+ connect(m_serverProcess,
+ QOverload::of(&QProcess::finished),
+ this,
+ &SquishTools::onServerFinished);
+
+ setState(ServerStarting);
+ m_serverProcess->start();
+ if (!m_serverProcess->waitForStarted()) {
+ setState(ServerStartFailed);
+ qWarning() << "squishserver did not start within 30s";
+ }
+}
+
+void SquishTools::stopSquishServer()
+{
+ if (m_serverProcess && m_serverPort > 0) {
+ QProcess serverKiller;
+ serverKiller.setProgram(m_serverProcess->program());
+ QStringList args;
+ args << "--stop" << "--port" << QString::number(m_serverPort);
+ serverKiller.setArguments(args);
+ serverKiller.setProcessEnvironment(m_serverProcess->processEnvironment());
+ serverKiller.start();
+ if (serverKiller.waitForStarted()) {
+ if (!serverKiller.waitForFinished()) {
+ qWarning() << "Could not shutdown server within 30s";
+ setState(ServerStopFailed);
+ }
+ } else {
+ qWarning() << "Could not shutdown server within 30s";
+ setState(ServerStopFailed);
+ }
+ } else {
+ qWarning() << "either no process running or port < 1?" << m_serverProcess << m_serverPort;
+ setState(ServerStopFailed);
+ }
+}
+
+void SquishTools::startSquishRunner()
+{
+ if (!m_serverProcess) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("No Squish Server"),
+ tr("Squish server does not seem to be running.\n"
+ "(state: %1, request: %2)\n"
+ "Try again.")
+ .arg(m_state)
+ .arg(m_request));
+ setState(Idle);
+ return;
+ }
+ if (m_serverPort == -1) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("No Squish Server Port"),
+ tr("Failed to get the server port.\n"
+ "(state: %1, request: %2)\n"
+ "Try again.")
+ .arg(m_state)
+ .arg(m_request));
+ // setting state to ServerStartFailed will terminate/kill the current unusable server
+ setState(ServerStartFailed);
+ return;
+ }
+
+ if (m_runnerProcess) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Squish Runner Running"),
+ tr("Squish runner seems to be running already.\n"
+ "(state: %1, request: %2)\n"
+ "Wait until it has finished and try again.")
+ .arg(m_state)
+ .arg(m_request));
+ return;
+ }
+
+ const Utils::FilePath squishRunner = Utils::Environment::systemEnvironment().searchInPath(
+ toolsSettings.runnerPath);
+ if (squishRunner.isEmpty()) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Squish Runner Error"),
+ tr("\"%1\" could not be found or is not executable.\n"
+ "Check the settings.")
+ .arg(QDir::toNativeSeparators(toolsSettings.runnerPath)));
+ setState(RunnerStopped);
+ return;
+ }
+ toolsSettings.runnerPath = squishRunner.toString();
+
+ m_runnerProcess = new QProcess;
+
+ QStringList args;
+ args << m_additionalServerArguments;
+ if (!toolsSettings.isLocalServer)
+ args << "--host" << toolsSettings.serverHost;
+ 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();
+ args << "--suitedir" << m_suitePath;
+
+ args << m_additionalRunnerArguments;
+
+ const QString caseReportFilePath = QFileInfo(QString::fromLatin1("%1/%2/%3/results.xml")
+ .arg(m_currentResultsDirectory,
+ QDir(m_suitePath).dirName(),
+ testCasePath.baseName()))
+ .absoluteFilePath();
+ m_reportFiles.append(caseReportFilePath);
+
+ args << "--reportgen"
+ << QString::fromLatin1("xml2.2,%1").arg(caseReportFilePath);
+
+ m_runnerProcess->setProgram(toolsSettings.runnerPath);
+ m_runnerProcess->setArguments(args);
+ m_runnerProcess->setProcessEnvironment(squishEnvironment());
+
+ connect(m_runnerProcess,
+ &QProcess::readyReadStandardError,
+ this,
+ &SquishTools::onRunnerErrorOutput);
+ connect(m_runnerProcess,
+ QOverload::of(&QProcess::finished),
+ this,
+ &SquishTools::onRunnerFinished);
+
+ setState(RunnerStarting);
+
+ // set up the file system watcher for being able to read the results.xml file
+ m_resultsFileWatcher = new QFileSystemWatcher;
+ // on second run this directory exists and won't emit changes, so use the current subdirectory
+ if (QDir(m_currentResultsDirectory).exists())
+ m_resultsFileWatcher->addPath(m_currentResultsDirectory + QDir::separator()
+ + QDir(m_suitePath).dirName());
+ else
+ m_resultsFileWatcher->addPath(QFileInfo(m_currentResultsDirectory).absolutePath());
+
+ connect(m_resultsFileWatcher,
+ &QFileSystemWatcher::directoryChanged,
+ this,
+ &SquishTools::onResultsDirChanged);
+
+ m_runnerProcess->start();
+ if (!m_runnerProcess->waitForStarted()) {
+ QMessageBox::critical(Core::ICore::dialogParent(),
+ tr("Squish Runner Error"),
+ tr("Squish runner failed to start within given timeframe."));
+ delete m_resultsFileWatcher;
+ m_resultsFileWatcher = nullptr;
+ setState(RunnerStartFailed);
+ return;
+ }
+ setState(RunnerStarted);
+ m_currentResultsXML = new QFile(caseReportFilePath);
+}
+
+QProcessEnvironment SquishTools::squishEnvironment()
+{
+ Utils::Environment environment = Utils::Environment::systemEnvironment();
+ if (!toolsSettings.licenseKeyPath.isEmpty())
+ environment.prependOrSet("SQUISH_LICENSEKEY_DIR", toolsSettings.licenseKeyPath);
+ environment.prependOrSet("SQUISH_PREFIX", toolsSettings.squishPath);
+ return environment.toProcessEnvironment();
+}
+
+void SquishTools::onServerFinished(int, QProcess::ExitStatus)
+{
+ delete m_serverProcess;
+ m_serverProcess = nullptr;
+ m_serverPort = -1;
+ setState(ServerStopped);
+}
+
+void SquishTools::onRunnerFinished(int, QProcess::ExitStatus)
+{
+ delete m_runnerProcess;
+ m_runnerProcess = nullptr;
+
+ if (m_resultsFileWatcher) {
+ delete m_resultsFileWatcher;
+ m_resultsFileWatcher = nullptr;
+ }
+ if (m_currentResultsXML) {
+ // make sure results are being read if not done yet
+ if (m_currentResultsXML->exists() && !m_currentResultsXML->isOpen())
+ onResultsDirChanged(m_currentResultsXML->fileName());
+ if (m_currentResultsXML->isOpen())
+ m_currentResultsXML->close();
+ delete m_currentResultsXML;
+ m_currentResultsXML = nullptr;
+ }
+ setState(RunnerStopped);
+}
+
+void SquishTools::onServerOutput()
+{
+ // output used for getting the port information of the current squishserver
+ const QByteArray output = m_serverProcess->readAllStandardOutput();
+ const QList lines = output.split('\n');
+ for (const QByteArray &line : lines) {
+ const QByteArray trimmed = line.trimmed();
+ if (trimmed.isEmpty())
+ continue;
+ if (trimmed.startsWith("Port:")) {
+ if (m_serverPort == -1) {
+ bool ok;
+ int port = trimmed.mid(6).toInt(&ok);
+ if (ok) {
+ m_serverPort = port;
+ setState(ServerStarted);
+ } else {
+ qWarning() << "could not get port number" << trimmed.mid(6);
+ setState(ServerStartFailed);
+ }
+ } else {
+ qWarning() << "got a Port output - don't know why...";
+ }
+ }
+ emit logOutputReceived(QString("Server: ") + QLatin1String(trimmed));
+ }
+}
+
+void SquishTools::onServerErrorOutput()
+{
+ // output that must be send to the Runner/Server Log
+ const QByteArray output = m_serverProcess->readAllStandardError();
+ const QList lines = output.split('\n');
+ for (const QByteArray &line : lines) {
+ const QByteArray trimmed = line.trimmed();
+ if (!trimmed.isEmpty())
+ emit logOutputReceived(QString("Server: ") + QLatin1String(trimmed));
+ }
+}
+
+static char firstNonWhitespace(const QByteArray &text)
+{
+ for (int i = 0, limit = text.size(); i < limit; ++i)
+ if (isspace(text.at(i)))
+ continue;
+ else
+ return text.at(i);
+ return 0;
+}
+
+static int positionAfterLastClosingTag(const QByteArray &text)
+{
+ QList possibleEndTags;
+ possibleEndTags << ""
+ << ""
+ << ""
+ << ""
+ << ""
+ << ""
+ << ""
+ << "";
+
+ int positionStart = text.lastIndexOf("");
+ if (positionStart == -1)
+ return -1;
+
+ int positionEnd = text.indexOf('>', positionStart);
+ if (positionEnd == -1)
+ return -1;
+
+ QByteArray endTag = text.mid(positionStart, positionEnd + 1 - positionStart);
+ if (possibleEndTags.contains(endTag))
+ return positionEnd + 1;
+
+ return positionAfterLastClosingTag(text.mid(0, positionStart));
+}
+
+void SquishTools::onRunnerOutput()
+{
+ // buffer for already read, but not processed content
+ static QByteArray buffer;
+ const qint64 currentSize = m_currentResultsXML->size();
+
+ if (currentSize <= m_readResultsCount)
+ return;
+
+ QByteArray output = m_currentResultsXML->read(currentSize - m_readResultsCount);
+ if (output.isEmpty())
+ return;
+
+ if (!buffer.isEmpty())
+ output.prepend(buffer);
+ // we might read only partial written stuff - so we have to figure out how much we can
+ // pass on for further processing and buffer the rest for the next reading
+ const int endTag = positionAfterLastClosingTag(output);
+ if (endTag < output.size()) {
+ buffer = output.mid(endTag);
+ output.truncate(endTag);
+ } else {
+ buffer.clear();
+ }
+
+ m_readResultsCount += output.size();
+
+ if (firstNonWhitespace(output) == '<') {
+ // output that must be used for the TestResultsPane
+ emit resultOutputCreated(output);
+ } else {
+ const QList lines = output.split('\n');
+ for (const QByteArray &line : lines) {
+ const QByteArray trimmed = line.trimmed();
+ if (!trimmed.isEmpty())
+ emit logOutputReceived("Runner: " + QLatin1String(trimmed));
+ }
+ }
+}
+
+void SquishTools::onRunnerErrorOutput()
+{
+ // output that must be send to the Runner/Server Log
+ const QByteArray output = m_runnerProcess->readAllStandardError();
+ const QList lines = output.split('\n');
+ for (const QByteArray &line : lines) {
+ const QByteArray trimmed = line.trimmed();
+ if (!trimmed.isEmpty())
+ emit logOutputReceived("Runner: " + QLatin1String(trimmed));
+ }
+}
+
+void SquishTools::onResultsDirChanged(const QString &filePath)
+{
+ if (!m_currentResultsXML)
+ return; // runner finished before, m_currentResultsXML deleted
+
+ if (m_currentResultsXML->exists()) {
+ delete m_resultsFileWatcher;
+ m_resultsFileWatcher = nullptr;
+ m_readResultsCount = 0;
+ if (m_currentResultsXML->open(QFile::ReadOnly)) {
+ m_resultsFileWatcher = new QFileSystemWatcher;
+ m_resultsFileWatcher->addPath(m_currentResultsXML->fileName());
+ connect(m_resultsFileWatcher,
+ &QFileSystemWatcher::fileChanged,
+ this,
+ &SquishTools::onRunnerOutput);
+ // squishrunner might have finished already, call once at least
+ onRunnerOutput();
+ } else {
+ // TODO set a flag to process results.xml as soon the complete test run has finished
+ qWarning() << "could not open results.xml although it exists" << filePath
+ << m_currentResultsXML->error() << m_currentResultsXML->errorString();
+ }
+ } else {
+ disconnect(m_resultsFileWatcher);
+ // results.xml is created as soon some output has been opened - so try again in a second
+ QTimer::singleShot(1000, this, [this, filePath]() { onResultsDirChanged(filePath); });
+ }
+}
+
+void SquishTools::logrotateTestResults()
+{
+ // make this configurable?
+ const int maxNumberOfTestResults = 10;
+ const QStringList existing = QDir(resultsDirectory)
+ .entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
+
+ for (int i = 0, limit = existing.size() - maxNumberOfTestResults; i < limit; ++i) {
+ QDir current(resultsDirectory + QDir::separator() + existing.at(i));
+ if (!current.removeRecursively())
+ qWarning() << "could not remove" << current.absolutePath();
+ }
+}
+
+void SquishTools::minimizeQtCreatorWindows()
+{
+ m_lastTopLevelWindows = QApplication::topLevelWindows();
+ QWindowList toBeRemoved;
+ for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
+ if (window->isVisible())
+ window->showMinimized();
+ else
+ toBeRemoved.append(window);
+ }
+
+ for (QWindow *window : qAsConst(toBeRemoved))
+ m_lastTopLevelWindows.removeOne(window);
+}
+
+void SquishTools::restoreQtCreatorWindows()
+{
+ for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
+ window->requestActivate();
+ window->showNormal();
+ }
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishtools.h b/src/plugins/squish/squishtools.h
new file mode 100644
index 00000000000..1cbc394a6f3
--- /dev/null
+++ b/src/plugins/squish/squishtools.h
@@ -0,0 +1,124 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+QT_BEGIN_NAMESPACE
+class QFile;
+class QFileSystemWatcher;
+QT_END_NAMESPACE
+
+namespace Squish {
+namespace Internal {
+
+class SquishXmlOutputHandler;
+
+class SquishTools : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SquishTools(QObject *parent = nullptr);
+ ~SquishTools() override;
+
+ enum State {
+ Idle,
+ ServerStarting,
+ ServerStarted,
+ ServerStartFailed,
+ ServerStopped,
+ ServerStopFailed,
+ RunnerStarting,
+ RunnerStarted,
+ RunnerStartFailed,
+ RunnerStopped
+ };
+
+ State state() const { return m_state; }
+ void runTestCases(const QString &suitePath,
+ const QStringList &testCases = QStringList(),
+ const QStringList &additionalServerArgs = QStringList(),
+ const QStringList &additionalRunnerArgs = QStringList());
+signals:
+ void logOutputReceived(const QString &output);
+ void squishTestRunStarted();
+ void squishTestRunFinished();
+ void resultOutputCreated(const QByteArray &output);
+
+private:
+ enum Request {
+ None,
+ ServerStopRequested,
+ ServerQueryRequested,
+ RunnerQueryRequested,
+ RunTestRequested,
+ RecordTestRequested,
+ KillOldBeforeRunRunner,
+ KillOldBeforeRecordRunner,
+ KillOldBeforeQueryRunner
+ };
+
+ void setState(State state);
+ void startSquishServer(Request request);
+ void stopSquishServer();
+ void startSquishRunner();
+ static QProcessEnvironment squishEnvironment();
+ Q_SLOT void onServerFinished(int exitCode, QProcess::ExitStatus status = QProcess::NormalExit);
+ Q_SLOT void onRunnerFinished(int exitCode, QProcess::ExitStatus status = QProcess::NormalExit);
+ void onServerOutput();
+ void onServerErrorOutput();
+ void onRunnerOutput();
+ void onRunnerErrorOutput();
+ void onResultsDirChanged(const QString &filePath);
+ static void logrotateTestResults();
+ void minimizeQtCreatorWindows();
+ void restoreQtCreatorWindows();
+
+ QProcess *m_serverProcess;
+ QProcess *m_runnerProcess;
+ int m_serverPort;
+ QString m_serverHost;
+ Request m_request;
+ State m_state;
+ QString m_suitePath;
+ QStringList m_testCases;
+ QStringList m_reportFiles;
+ QString m_currentResultsDirectory;
+ QFile *m_currentResultsXML;
+ QFileSystemWatcher *m_resultsFileWatcher;
+ QStringList m_additionalServerArguments;
+ QStringList m_additionalRunnerArguments;
+ QWindowList m_lastTopLevelWindows;
+ bool m_testRunning;
+ qint64 m_readResultsCount;
+ SquishXmlOutputHandler *m_xmlOutputHandler;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishutils.cpp b/src/plugins/squish/squishutils.cpp
new file mode 100644
index 00000000000..ddd6a7b8e2f
--- /dev/null
+++ b/src/plugins/squish/squishutils.cpp
@@ -0,0 +1,100 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishutils.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+const static char squishLanguageKey[] = "LANGUAGE";
+const static char squishTestCasesKey[] = "TEST_CASES";
+const static char objectsMapKey[] = "OBJECTMAP";
+
+QStringList SquishUtils::validTestCases(const QString &baseDirectory)
+{
+ QStringList validCases;
+ QDir subDir(baseDirectory);
+ QFileInfo suiteConf(subDir, "suite.conf");
+ if (suiteConf.exists()) {
+ QVariantMap conf = readSuiteConf(suiteConf.absoluteFilePath());
+ QString extension = extensionForLanguage(conf.value(squishLanguageKey).toString());
+ QStringList cases = conf.value(squishTestCasesKey)
+ .toString()
+ .split(QRegularExpression("\\s+"));
+
+ for (const QString &testCase : qAsConst(cases)) {
+ QFileInfo testCaseDirInfo(subDir, testCase);
+ if (testCaseDirInfo.isDir()) {
+ QFileInfo testCaseTestInfo(testCaseDirInfo.filePath(), "test" + extension);
+ if (testCaseTestInfo.isFile())
+ validCases.append(testCaseTestInfo.absoluteFilePath());
+ }
+ }
+ }
+
+ return validCases;
+}
+
+QVariantMap SquishUtils::readSuiteConf(const QString &suiteConfPath)
+{
+ const QSettings suiteConf(suiteConfPath, QSettings::IniFormat);
+ QVariantMap result;
+ // TODO get all information - actually only the information needed now is fetched
+ result.insert(squishLanguageKey, suiteConf.value(squishLanguageKey));
+ result.insert(squishTestCasesKey, suiteConf.value(squishTestCasesKey));
+ return result;
+}
+
+QString SquishUtils::objectsMapPath(const QString &suitePath)
+{
+ const QString suiteDir = QFileInfo(suitePath).absolutePath();
+ const QSettings suiteConf(suitePath, QSettings::IniFormat);
+ const QString objMapPath = suiteConf.value(objectsMapKey).toString();
+ return QFileInfo(suiteDir, objMapPath).canonicalFilePath();
+}
+
+QString SquishUtils::extensionForLanguage(const QString &language)
+{
+ if (language == "Python")
+ return ".py";
+ if (language == "Perl")
+ return ".pl";
+ if (language == "JavaScript")
+ return ".js";
+ if (language == "Ruby")
+ return ".rb";
+ if (language == "Tcl")
+ return ".tcl";
+ return QString(); // better return an invalid extension?
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishutils.h b/src/plugins/squish/squishutils.h
new file mode 100644
index 00000000000..d52ddf43532
--- /dev/null
+++ b/src/plugins/squish/squishutils.h
@@ -0,0 +1,47 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+class SquishUtils
+{
+public:
+ static QStringList validTestCases(const QString &baseDirectory);
+ static QVariantMap readSuiteConf(const QString &suiteConfPath);
+ static QString objectsMapPath(const QString &suitePath);
+ static QString extensionForLanguage(const QString &language);
+
+private:
+ SquishUtils() {}
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishxmloutputhandler.cpp b/src/plugins/squish/squishxmloutputhandler.cpp
new file mode 100644
index 00000000000..974eb832ef1
--- /dev/null
+++ b/src/plugins/squish/squishxmloutputhandler.cpp
@@ -0,0 +1,323 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "squishxmloutputhandler.h"
+#include "squishoutputpane.h"
+#include "squishresultmodel.h"
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+SquishXmlOutputHandler::SquishXmlOutputHandler(QObject *parent)
+ : QObject(parent)
+{
+ connect(this,
+ &SquishXmlOutputHandler::resultItemCreated,
+ SquishOutputPane::instance(),
+ &SquishOutputPane::addResultItem,
+ Qt::QueuedConnection);
+}
+
+void SquishXmlOutputHandler::clearForNextRun()
+{
+ m_xmlReader.clear();
+}
+
+void SquishXmlOutputHandler::mergeResultFiles(const QStringList &reportFiles,
+ const QString &resultsDirectory,
+ const QString &suiteName,
+ QString *error)
+{
+ QFile resultsXML(QString::fromLatin1("%1/results.xml").arg(resultsDirectory));
+ if (resultsXML.exists()) {
+ if (error)
+ *error = tr("Could not merge results into single results.xml.\n"
+ "Destination file \"%1\" already exists.")
+ .arg(resultsXML.fileName());
+ return;
+ }
+
+ if (!resultsXML.open(QFile::WriteOnly)) {
+ if (error)
+ *error = tr("Could not merge results into single results.xml.\n"
+ "Failed to open file \"%1\"")
+ .arg(resultsXML.fileName());
+ return;
+ }
+
+ QXmlStreamWriter xmlWriter(&resultsXML);
+ xmlWriter.writeStartDocument("1.0");
+ bool isFirstReport = true;
+ bool isFirstTest = true;
+ QString lastEpilogTime;
+ for (const QString &caseResult : reportFiles) {
+ QFile currentResultsFile(caseResult);
+ if (!currentResultsFile.exists())
+ continue;
+ if (!currentResultsFile.open(QFile::ReadOnly))
+ continue;
+ QXmlStreamReader reader(¤tResultsFile);
+ while (!reader.atEnd()) {
+ QXmlStreamReader::TokenType type = reader.readNext();
+ switch (type) {
+ case QXmlStreamReader::StartElement: {
+ const QString tagName = reader.name().toString();
+ // SquishReport of the first results.xml will be taken as is and as this is a
+ // merged results.xml we add another test tag holding the suite's name
+ if (tagName == "SquishReport") {
+ if (isFirstReport) {
+ xmlWriter.writeStartElement(tagName);
+ xmlWriter.writeAttributes(reader.attributes());
+ xmlWriter.writeStartElement("test");
+ xmlWriter.writeAttribute("name", suiteName);
+ isFirstReport = false;
+ }
+ break;
+ }
+ if (isFirstTest && tagName == "test") {
+ // the prolog tag of the first results.xml holds the start time of the suite
+ // we already wrote the test tag for the suite, but haven't added the start
+ // time as we didn't know about it, so store information of the current test
+ // tag (case name), read ahead (prolog tag), write prolog (suite's test tag)
+ // and finally write test tag (case name) - the prolog tag (for test case)
+ // will be written outside the if
+ const QXmlStreamAttributes testAttributes = reader.attributes();
+ QXmlStreamReader::TokenType token;
+ while (!reader.atEnd()) {
+ token = reader.readNext();
+ if (token != QXmlStreamReader::Characters)
+ break;
+ }
+ const QString prolog = reader.name().toString();
+ QTC_ASSERT(token == QXmlStreamReader::StartElement
+ && prolog == "prolog",
+ if (error) *error = tr("Error while parsing first test result.");
+ return );
+ xmlWriter.writeStartElement(prolog);
+ xmlWriter.writeAttributes(reader.attributes());
+ xmlWriter.writeEndElement();
+ xmlWriter.writeStartElement("test");
+ xmlWriter.writeAttributes(testAttributes);
+ isFirstTest = false;
+ } else if (tagName == "epilog") {
+ lastEpilogTime = reader.attributes().value("time").toString();
+ }
+ xmlWriter.writeCurrentToken(reader);
+ break;
+ }
+ case QXmlStreamReader::EndElement:
+ if (reader.name() != QLatin1String("SquishReport"))
+ xmlWriter.writeCurrentToken(reader);
+ break;
+ case QXmlStreamReader::Characters:
+ xmlWriter.writeCurrentToken(reader);
+ break;
+ // ignore the rest
+ default:
+ break;
+ }
+ }
+ currentResultsFile.close();
+ }
+ if (!lastEpilogTime.isEmpty()) {
+ xmlWriter.writeStartElement("epilog");
+ xmlWriter.writeAttribute("time", lastEpilogTime);
+ xmlWriter.writeEndElement();
+ }
+ xmlWriter.writeEndDocument();
+}
+
+Result::Type resultFromString(const QString &type)
+{
+ if (type == "DETAILED")
+ return Result::Detail;
+ if (type == "LOG")
+ return Result::Log;
+ if (type == "PASS")
+ return Result::Pass;
+ if (type == "FAIL")
+ return Result::Fail;
+ if (type == "WARNING")
+ return Result::Warn;
+ if (type == "XFAIL")
+ return Result::ExpectedFail;
+ if (type == "XPASS")
+ return Result::UnexpectedPass;
+ if (type == "FATAL")
+ return Result::Fatal;
+ if (type == "ERROR")
+ return Result::Error;
+ return Result::Log;
+}
+
+// this method uses the XML reader to parse output of the Squish results.xml and put it into an
+// item that can be used to display inside the test results pane
+// TODO: support Squish report 3.x as well
+void SquishXmlOutputHandler::outputAvailable(const QByteArray &output)
+{
+ static SquishResultItem *testCaseRootItem;
+ static QString name;
+ static QString details;
+ static QString logDetails;
+ static QStringList logDetailsList;
+ static QString time;
+ static QString file;
+ static Result::Type type;
+ static int line = 0;
+ static bool prepend = false;
+
+ m_xmlReader.addData(output);
+
+ while (!m_xmlReader.atEnd()) {
+ QXmlStreamReader::TokenType tokenType = m_xmlReader.readNext();
+ switch (tokenType) {
+ case QXmlStreamReader::StartDocument:
+ case QXmlStreamReader::EndDocument:
+ break;
+ case QXmlStreamReader::StartElement: {
+ const QString currentName = m_xmlReader.name().toString();
+ // tags verification, message, epilog and test will start a new entry, so, reset values
+ if (currentName == "verification"
+ || currentName == "message"
+ || currentName == "epilog"
+ || currentName == "test") {
+ name = currentName;
+ details.clear();
+ logDetails.clear();
+ logDetailsList.clear();
+ time.clear();
+ file.clear();
+ line = 0;
+ type = Result::Log;
+ if (currentName == "test")
+ testCaseRootItem = nullptr;
+ } else if (currentName == "result") {
+ // result tag won't add another entry, but gives more information on enclosing tag
+ name = currentName;
+ }
+
+ // description tag could provide information that must be prepended to the former entry
+ if (currentName == "description") {
+ prepend = (name == "result" && m_xmlReader.attributes().isEmpty());
+ } else {
+ const QXmlStreamAttributes attributes = m_xmlReader.attributes();
+ for (const QXmlStreamAttribute &att : attributes) {
+ const QString attributeName = att.name().toString();
+ if (attributeName == "time")
+ time = QDateTime::fromString(att.value().toString(), Qt::ISODate)
+ .toString("MMM dd, yyyy h:mm:ss AP");
+ else if (attributeName == "file")
+ file = att.value().toString();
+ else if (attributeName == "line")
+ line = att.value().toInt();
+ else if (attributeName == "type")
+ type = resultFromString(att.value().toString());
+ else if (attributeName == "name")
+ logDetails = att.value().toString();
+ }
+ }
+ // handle prolog (test) elements already within the start tag
+ if (currentName == "prolog") {
+ TestResult result(Result::Start, logDetails, time);
+ result.setFile(file);
+ result.setLine(line);
+ testCaseRootItem = new SquishResultItem(result);
+ emit resultItemCreated(testCaseRootItem);
+ }
+ break;
+ }
+ case QXmlStreamReader::EndElement: {
+ const QString currentName = m_xmlReader.name().toString();
+ // description and result tags are handled differently, test tags are handled by
+ // prolog tag (which is handled in QXmlStreamReader::StartElement already),
+ // SquishReport tags will be ignored completely
+ if (currentName == "epilog") {
+ QTC_ASSERT(testCaseRootItem, break);
+ TestResult result(Result::End, QString(), time);
+ SquishResultItem *item = new SquishResultItem(result);
+ testCaseRootItem->appendChild(item);
+ } else if (currentName == "description") {
+ if (!prepend && !details.trimmed().isEmpty()) {
+ logDetailsList.append(details.trimmed());
+ details.clear();
+ }
+ } else if (currentName != "prolog"
+ && currentName != "test"
+ && currentName != "result"
+ && currentName != "SquishReport") {
+ 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);
+ if (!logDetailsList.isEmpty()) {
+ for (const QString &detail : qAsConst(logDetailsList)) {
+ TestResult childResult(Result::Detail, detail);
+ SquishResultItem *childItem = new SquishResultItem(childResult);
+ item->appendChild(childItem);
+ }
+ }
+ testCaseRootItem->appendChild(item);
+ }
+ break;
+ }
+ case QXmlStreamReader::Characters: {
+ QString text = m_xmlReader.text().toString();
+ if (m_xmlReader.isCDATA() || !text.trimmed().isEmpty()) {
+ if (!m_xmlReader.isCDATA())
+ text = text.trimmed();
+ if (prepend) {
+ if (!logDetails.isEmpty() && (text == "Verified" || text == "Not Verified"))
+ logDetails.prepend(text + ": ");
+ else
+ logDetails = text;
+ } else {
+ details.append(text).append('\n');
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (m_xmlReader.hasError()) {
+ // kind of expected as we get the output piece by piece
+ if (m_xmlReader.error() != QXmlStreamReader::PrematureEndOfDocumentError)
+ qWarning() << m_xmlReader.error() << m_xmlReader.errorString();
+ }
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/squishxmloutputhandler.h b/src/plugins/squish/squishxmloutputhandler.h
new file mode 100644
index 00000000000..6311787b44b
--- /dev/null
+++ b/src/plugins/squish/squishxmloutputhandler.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "testresult.h"
+
+#include
+#include
+
+namespace Squish {
+namespace Internal {
+
+class SquishResultItem;
+
+class SquishXmlOutputHandler : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SquishXmlOutputHandler(QObject *parent = nullptr);
+ void clearForNextRun();
+
+ static void mergeResultFiles(const QStringList &reportFiles,
+ const QString &resultsDirectory,
+ const QString &suiteName,
+ QString *error = nullptr);
+
+signals:
+ void resultItemCreated(SquishResultItem *resultItem);
+
+public slots:
+ void outputAvailable(const QByteArray &output);
+
+private:
+ QXmlStreamReader m_xmlReader;
+};
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/symbolnameitemdelegate.cpp b/src/plugins/squish/symbolnameitemdelegate.cpp
new file mode 100644
index 00000000000..5ab571f6491
--- /dev/null
+++ b/src/plugins/squish/symbolnameitemdelegate.cpp
@@ -0,0 +1,90 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "symbolnameitemdelegate.h"
+#include "objectsmaptreeitem.h"
+
+#include
+
+namespace Squish {
+namespace Internal {
+
+/********************************** SymbolNameItemDelegate ************************************/
+
+SymbolNameItemDelegate::SymbolNameItemDelegate(QObject *parent)
+ : QStyledItemDelegate(parent)
+{}
+
+QWidget *SymbolNameItemDelegate::createEditor(QWidget *parent,
+ const QStyleOptionViewItem &,
+ const QModelIndex &index) const
+{
+ if (auto filterModel = qobject_cast(index.model()))
+ if (auto treeModel = qobject_cast(filterModel->sourceModel()))
+ return new ValidatingContainerNameLineEdit(treeModel->allSymbolicNames(), parent);
+
+ return new ValidatingContainerNameLineEdit(QStringList(), parent);
+}
+
+void SymbolNameItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+{
+ if (auto lineEdit = qobject_cast(editor))
+ lineEdit->setText(index.data().toString());
+}
+
+void SymbolNameItemDelegate::setModelData(QWidget *editor,
+ QAbstractItemModel *model,
+ const QModelIndex &index) const
+{
+ if (auto edit = qobject_cast(editor)) {
+ if (!edit->isValid())
+ return;
+ }
+
+ QStyledItemDelegate::setModelData(editor, model, index);
+}
+
+/******************************* ValidatingContainerNameEdit **********************************/
+
+ValidatingContainerNameLineEdit::ValidatingContainerNameLineEdit(const QStringList &forbidden,
+ QWidget *parent)
+ : Utils::FancyLineEdit(parent)
+ , m_forbidden(forbidden)
+{
+ setValidationFunction([this](FancyLineEdit *edit, QString * /*errorMessage*/) {
+ if (!edit)
+ return false;
+ const QString &value = edit->text();
+ if (value.isEmpty())
+ return false;
+ const QString realName = value.at(0) == ObjectsMapTreeItem::COLON
+ ? value
+ : ObjectsMapTreeItem::COLON + value;
+ return !m_forbidden.contains(realName);
+ });
+}
+
+} // namespace Internal
+} // namespace Squish
diff --git a/src/plugins/squish/symbolnameitemdelegate.h b/src/plugins/squish/symbolnameitemdelegate.h
new file mode 100644
index 00000000000..85920e35c92
--- /dev/null
+++ b/src/plugins/squish/symbolnameitemdelegate.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator Squish plugin.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include