// Copyright (C) 2022 The Qt Company Ltd // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "squishsettings.h" #include "squishconstants.h" #include "squishtools.h" #include "squishtr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Utils; namespace Squish { namespace Internal { SquishSettings::SquishSettings() { setSettingsGroup("Squish"); setAutoApply(false); registerAspect(&squishPath); squishPath.setSettingsKey("SquishPath"); squishPath.setLabelText(Tr::tr("Squish path:")); squishPath.setDisplayStyle(StringAspect::PathChooserDisplay); squishPath.setExpectedKind(PathChooser::ExistingDirectory); squishPath.setPlaceHolderText(Tr::tr("Path to Squish installation")); squishPath.setValidationFunction([this](FancyLineEdit *edit, QString *error) { QTC_ASSERT(edit, return false); if (!squishPath.pathChooser()->defaultValidationFunction()(edit, error)) return false; const FilePath squishServer = FilePath::fromString(edit->text()) .pathAppended(HostOsInfo::withExecutableSuffix("bin/squishserver")); const bool valid = squishServer.isExecutableFile(); if (!valid && error) *error = Tr::tr("Path does not contain server executable at its default location."); return valid; }); registerAspect(&licensePath); licensePath.setSettingsKey("LicensePath"); licensePath.setLabelText(Tr::tr("License path:")); licensePath.setDisplayStyle(StringAspect::PathChooserDisplay); licensePath.setExpectedKind(PathChooser::ExistingDirectory); registerAspect(&local); local.setSettingsKey("Local"); local.setLabel(Tr::tr("Local Server")); local.setDefaultValue(true); registerAspect(&serverHost); serverHost.setSettingsKey("ServerHost"); serverHost.setLabelText(Tr::tr("Server host:")); serverHost.setDisplayStyle(StringAspect::LineEditDisplay); serverHost.setDefaultValue("localhost"); serverHost.setEnabled(false); registerAspect(&serverPort); serverPort.setSettingsKey("ServerPort"); serverPort.setLabel(Tr::tr("Server Port")); serverPort.setRange(1, 65535); serverPort.setDefaultValue(9999); serverPort.setEnabled(false); registerAspect(&verbose); verbose.setSettingsKey("Verbose"); verbose.setLabel(Tr::tr("Verbose log")); verbose.setDefaultValue(false); registerAspect(&minimizeIDE); minimizeIDE.setSettingsKey("MinimizeIDE"); minimizeIDE.setLabel(Tr::tr("Minimize IDE")); minimizeIDE.setToolTip(Tr::tr("Minimize IDE automatically while running or recording test cases.")); minimizeIDE.setDefaultValue(true); connect(&local, &BoolAspect::volatileValueChanged, this, [this] (bool checked) { serverHost.setEnabled(!checked); serverPort.setEnabled(!checked); }); connect(&squishPath, &Utils::StringAspect::valueChanged, this, &SquishSettings::squishPathChanged); } Utils::FilePath SquishSettings::scriptsPath(Language language) const { Utils::FilePath scripts = squishPath.filePath().pathAppended("scriptmodules"); switch (language) { case Language::Python: scripts = scripts.pathAppended("python"); break; case Language::Perl: scripts = scripts.pathAppended("perl"); break; case Language::JavaScript: scripts = scripts.pathAppended("javascript"); break; case Language::Ruby: scripts = scripts.pathAppended("ruby"); break; case Language::Tcl: scripts = scripts.pathAppended("tcl"); break; } return scripts.isReadableDir() ? scripts : Utils::FilePath(); } SquishSettingsPage::SquishSettingsPage(SquishSettings *settings) { setId("A.Squish.General"); setDisplayName(Tr::tr("General")); setCategory(Constants::SQUISH_SETTINGS_CATEGORY); setDisplayCategory("Squish"); setCategoryIcon(Icon({{":/squish/images/settingscategory_squish.png", Theme::PanelTextColorDark}}, Icon::Tint)); setSettings(settings); setLayouter([settings](QWidget *widget) { SquishSettings &s = *settings; using namespace Layouting; Grid grid { s.squishPath, br, s.licensePath, br, Span {2, Row { s.local, s.serverHost, s.serverPort } }, br, s.verbose, br, s.minimizeIDE, br, }; Column { Row { grid }, st }.attachTo(widget); }); } SquishServerSettings::SquishServerSettings() { registerAspect(&autTimeout); autTimeout.setLabel(Tr::tr("Maximum startup time:")); autTimeout.setToolTip(Tr::tr("Specifies how many seconds Squish should wait for a reply from the " "AUT directly after starting it.")); autTimeout.setRange(1, 65535); autTimeout.setSuffix("s"); autTimeout.setDefaultValue(20); registerAspect(&responseTimeout); responseTimeout.setLabel(Tr::tr("Maximum response time:")); responseTimeout.setToolTip(Tr::tr("Specifies how many seconds Squish should wait for a reply from " "the hooked up AUT before raising a timeout error.")); responseTimeout.setRange(1, 65535); responseTimeout.setDefaultValue(300); responseTimeout.setSuffix("s"); registerAspect(&postMortemWaitTime); postMortemWaitTime.setLabel(Tr::tr("Maximum post-mortem wait time:")); postMortemWaitTime.setToolTip(Tr::tr("Specifies how many seconds Squish should wait after the the " "first AUT process has exited.")); postMortemWaitTime.setRange(1, 65535); postMortemWaitTime.setDefaultValue(1500); postMortemWaitTime.setSuffix("ms"); registerAspect(&animatedCursor); animatedCursor.setLabel(Tr::tr("Animate mouse cursor:")); animatedCursor.setDefaultValue(true); } enum InfoMode {None, Applications, AutPaths, AttachableAuts, AutTimeout, AutPMTimeout, AutResponseTimeout, AnimatedCursor, ToolkitWrappers}; InfoMode infoModeFromType(const QString &type) { if (type == "applications") return Applications; if (type == "autPaths") return AutPaths; if (type == "attachableApplications") return AttachableAuts; if (type == "AUTTimeout") return AutTimeout; if (type == "AUTPMTimeout") return AutPMTimeout; if (type == "responseTimeout") return AutResponseTimeout; if (type == "cursorAnimation") return AnimatedCursor; if (type == "toolkitWrappers") return ToolkitWrappers; return None; } void SquishServerSettings::setFromXmlOutput(const QString &output) { SquishServerSettings newSettings; InfoMode infoMode = None; QXmlStreamReader reader(output); while (!reader.atEnd()) { QXmlStreamReader::TokenType type = reader.readNext(); if (type == QXmlStreamReader::Invalid) { // MessageBox? return; } else if (type == QXmlStreamReader::StartElement) { const QString tagName = reader.name().toString(); if (tagName == "info") { const QString typeString = reader.attributes().value("type").toString(); QTC_ASSERT(!typeString.isEmpty(), infoMode = None; continue); infoMode = infoModeFromType(typeString); } else if (tagName == "item") { const QXmlStreamAttributes attributes = reader.attributes(); switch (infoMode) { case Applications: if (attributes.value("mappedOrViaAUTPaths").toString() == "path") continue; // ignore applications provided by autPaths newSettings.mappedAuts.insert(attributes.value("executableName").toString(), attributes.value("executablePath").toString()); break; case AutPaths: newSettings.autPaths.append(attributes.value("value").toString()); break; case AttachableAuts: newSettings.attachableAuts.insert(attributes.value("name").toString(), attributes.value("hostAndPort").toString()); break; case AutTimeout: newSettings.autTimeout.setValue(attributes.value("value").toInt()); break; case AutPMTimeout: newSettings.postMortemWaitTime.setValue(attributes.value("value").toInt()); break; case AutResponseTimeout: newSettings.responseTimeout.setValue(attributes.value("value").toInt()); break; case AnimatedCursor: newSettings.animatedCursor.setValue(attributes.value("value").toString() == "on"); break; case ToolkitWrappers: newSettings.licensedToolkits.append(attributes.value("value").toString()); break; default: break; } } } } // if we end here, we update the settings with the read settings mappedAuts = newSettings.mappedAuts; autPaths = newSettings.autPaths; attachableAuts = newSettings.attachableAuts; licensedToolkits = newSettings.licensedToolkits; autTimeout.setValue(newSettings.autTimeout.value()); postMortemWaitTime.setValue(newSettings.postMortemWaitTime.value()); responseTimeout.setValue(newSettings.responseTimeout.value()); animatedCursor.setValue(newSettings.animatedCursor.value()); } class SquishServerItem : public TreeItem { public: explicit SquishServerItem(const QString &col1 = {}, const QString &col2 = {}); QVariant data(int column, int role) const override; bool setData(int column, const QVariant &value, int role) override; private: QString m_first; QString m_second; }; SquishServerItem::SquishServerItem(const QString &col1, const QString &col2) : m_first(col1) , m_second(col2) { } QVariant SquishServerItem::data(int column, int role) const { if (role == Qt::DisplayRole) { switch (column) { case 0: return m_first; case 1: return m_second; default: return QVariant(); } } return QVariant(); } bool SquishServerItem::setData(int column, const QVariant &value, int role) { if (column != 1 || role != Qt::EditRole) return TreeItem::setData(column, value, role); m_second = value.toString(); return true; } class AttachableAutDialog : public QDialog { public: AttachableAutDialog() { executable.setLabelText(Tr::tr("Name:")); executable.setDisplayStyle(StringAspect::LineEditDisplay); host.setLabelText(Tr::tr("Host:")); host.setDisplayStyle(StringAspect::LineEditDisplay); host.setDefaultValue("localhost"); port.setLabelText(Tr::tr("Port:")); port.setRange(1, 65535); port.setDefaultValue(12345); QWidget *widget = new QWidget(this); auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, this); using namespace Layouting; Form { executable, host, port, st }.attachTo(widget); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(widget); layout->addWidget(buttons); setLayout(layout); connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &QDialog::accept); connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &QDialog::reject); setWindowTitle(Tr::tr("Add Attachable AUT")); } StringAspect executable; StringAspect host; IntegerAspect port; }; class SquishServerSettingsWidget : public QWidget { public: explicit SquishServerSettingsWidget(QWidget *parent = nullptr); QList toConfigChangeArguments() const; private: void repopulateApplicationView(); void addApplicationOrPath(); void addMappedAut(TreeItem *categoryItem, SquishServerItem *original); void addAutPath(TreeItem *categoryItem, SquishServerItem *original); void addAttachableAut(TreeItem *categoryItem, SquishServerItem *original); void editApplicationOrPath(); void removeApplicationOrPath(); SquishServerSettings m_originalSettings; SquishServerSettings m_serverSettings; BaseTreeView m_applicationsView; TreeModel m_model; }; SquishServerSettingsWidget::SquishServerSettingsWidget(QWidget *parent) : QWidget(parent) { m_model.setHeader({QString(), QString()}); // enforce 2 columns m_applicationsView.setModel(&m_model); m_applicationsView.setHeaderHidden(true); m_applicationsView.setAttribute(Qt::WA_MacShowFocusRect, false); m_applicationsView.setFrameStyle(QFrame::NoFrame); m_applicationsView.setRootIsDecorated(true); m_applicationsView.setSelectionMode(QAbstractItemView::SingleSelection); m_applicationsView.header()->setStretchLastSection(false); m_applicationsView.header()->setSectionResizeMode(QHeaderView::ResizeToContents); m_applicationsView.setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_applicationsView.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); auto add = new QPushButton(Tr::tr("Add"), this); auto edit = new QPushButton(Tr::tr("Edit"), this); auto remove = new QPushButton(Tr::tr("Remove"), this); auto updateButtons = [add, edit, remove] (const QModelIndex &idx) { add->setEnabled(idx.isValid()); bool enabled = idx.isValid() && idx.parent() != QModelIndex(); edit->setEnabled(enabled); remove->setEnabled(enabled); }; updateButtons(QModelIndex()); using namespace Layouting; Form grid { &m_applicationsView, br, &m_serverSettings.autTimeout, &m_serverSettings.responseTimeout, &m_serverSettings.postMortemWaitTime, &m_serverSettings.animatedCursor, }; Column buttonCol { add, edit, remove, st }; Column { Row { grid, buttonCol }, st }.attachTo(this); repopulateApplicationView(); // initial connect(&m_applicationsView, &QAbstractItemView::clicked, this, updateButtons); connect(add, &QPushButton::clicked, this, &SquishServerSettingsWidget::addApplicationOrPath); connect(edit, &QPushButton::clicked, this, &SquishServerSettingsWidget::editApplicationOrPath); connect(remove, &QPushButton::clicked, this, &SquishServerSettingsWidget::removeApplicationOrPath); auto progress = new ProgressIndicator(ProgressIndicatorSize::Large, this); progress->attachToWidget(this); setEnabled(false); progress->show(); // query settings SquishTools *squishTools = SquishTools::instance(); squishTools->queryServerSettings([this, progress] (const QString &out, const QString &) { m_serverSettings.setFromXmlOutput(out); m_originalSettings.setFromXmlOutput(out); repopulateApplicationView(); progress->hide(); setEnabled(true); }); } void SquishServerSettingsWidget::repopulateApplicationView() { m_model.clear(); SquishServerItem *mapped = new SquishServerItem(Tr::tr("Mapped AUTs")); m_model.rootItem()->appendChild(mapped); for (auto it = m_serverSettings.mappedAuts.begin(), end = m_serverSettings.mappedAuts.end(); it != end; ++it) { mapped->appendChild(new SquishServerItem(it.key(), it.value())); } SquishServerItem *autPaths = new SquishServerItem(Tr::tr("AUT Paths")); m_model.rootItem()->appendChild(autPaths); for (const QString &path : std::as_const(m_serverSettings.autPaths)) autPaths->appendChild(new SquishServerItem(path, "")); SquishServerItem *attachable = new SquishServerItem(Tr::tr("Attachable AUTs")); m_model.rootItem()->appendChild(attachable); for (auto it = m_serverSettings.attachableAuts.begin(), end = m_serverSettings.attachableAuts.end(); it != end; ++it) { attachable->appendChild(new SquishServerItem(it.key(), it.value())); } } enum Category { MappedAutCategory, AutPathCategory, AttachableAutCategory }; void SquishServerSettingsWidget::addApplicationOrPath() { const QModelIndex &idx = m_applicationsView.currentIndex(); QTC_ASSERT(idx.isValid(), return); const SquishServerItem *item = m_model.itemForIndex(idx); QTC_ASSERT(item, return); const int category = (item->level() == 2) ? idx.parent().row() : idx.row(); QTC_ASSERT(category >= 0 && category <= 2, return); TreeItem *categoryItem = m_model.rootItem()->childAt(category); switch (category) { case MappedAutCategory: addMappedAut(categoryItem, nullptr); break; case AutPathCategory: addAutPath(categoryItem, nullptr); break; case AttachableAutCategory: addAttachableAut(categoryItem, nullptr); break; } } void SquishServerSettingsWidget::addMappedAut(TreeItem *categoryItem, SquishServerItem *original) { FilePath entry = original ? FilePath::fromString(original->data(1, Qt::DisplayRole).toString()) : FilePath(); const FilePath aut = FileUtils::getOpenFilePath(nullptr, Tr::tr("Select Application to test"), entry); if (aut.isEmpty()) return; const QString fileName = aut.completeBaseName(); if (original) { const QString originalFileName = original->data(0, Qt::DisplayRole).toString(); if (originalFileName != fileName) { m_serverSettings.mappedAuts.remove(originalFileName); m_model.destroyItem(original); } } m_serverSettings.mappedAuts.insert(fileName, aut.parentDir().path()); TreeItem *found = categoryItem->findAnyChild([&fileName](TreeItem *it) { return it->data(0, Qt::DisplayRole).toString() == fileName; }); if (found) found->setData(1, aut.path(), Qt::EditRole); else categoryItem->appendChild(new SquishServerItem(fileName, aut.parentDir().path())); } void SquishServerSettingsWidget::addAutPath(TreeItem *categoryItem, SquishServerItem *original) { const QString originalPathStr = original ? original->data(0, Qt::DisplayRole).toString() : QString(); FilePath entry = FilePath::fromString(originalPathStr); const FilePath path = FileUtils::getExistingDirectory(nullptr, Tr::tr("Select Application Path"), entry); if (path.isEmpty() || path == entry) return; const QString pathStr = path.toString(); if (original) { m_serverSettings.autPaths.removeOne(originalPathStr); m_model.destroyItem(original); } int index = m_serverSettings.autPaths.indexOf(pathStr); if (index != -1) return; m_serverSettings.autPaths.append(pathStr); categoryItem->appendChild(new SquishServerItem(pathStr)); } void SquishServerSettingsWidget::addAttachableAut(TreeItem *categoryItem, SquishServerItem *original) { AttachableAutDialog dialog; const QString originalExecutable = original ? original->data(0, Qt::DisplayRole).toString() : QString(); const QString originalHostAndPort = original ? original->data(1, Qt::DisplayRole).toString() : QString(); if (original) { dialog.executable.setValue(originalExecutable); QStringList hostAndPortList = originalHostAndPort.split(':'); QTC_ASSERT(hostAndPortList.size() == 2, return); dialog.host.setValue(hostAndPortList.first()); dialog.port.setValue(hostAndPortList.last().toInt()); } if (dialog.exec() != QDialog::Accepted) return; const QString executableStr = dialog.executable.value(); const QString hostStr = dialog.host.value(); if (executableStr.isEmpty() || hostStr.isEmpty()) return; if (original && executableStr != originalExecutable) { m_serverSettings.attachableAuts.remove(originalExecutable); m_model.destroyItem(original); } const QString hostAndPort = hostStr + ':' + QString::number(dialog.port.value()); m_serverSettings.attachableAuts.insert(executableStr, hostAndPort); TreeItem *found = categoryItem->findAnyChild([&executableStr](TreeItem *it) { return it->data(0, Qt::DisplayRole).toString() == executableStr; }); if (found) found->setData(1, hostAndPort, Qt::EditRole); else categoryItem->appendChild(new SquishServerItem(executableStr, hostAndPort)); } void SquishServerSettingsWidget::removeApplicationOrPath() { const QModelIndex &idx = m_applicationsView.currentIndex(); QTC_ASSERT(idx.isValid(), return); SquishServerItem *item = m_model.itemForIndex(idx); QTC_ASSERT(item, return); QTC_ASSERT(item->level() == 2, return); int row = idx.parent().row(); QTC_ASSERT(row >= 0 && row <= 2, return); switch (row) { case MappedAutCategory: m_serverSettings.mappedAuts.remove(item->data(0, Qt::DisplayRole).toString()); break; case AutPathCategory: m_serverSettings.autPaths.removeOne(item->data(0, Qt::DisplayRole).toString()); break; case AttachableAutCategory: m_serverSettings.attachableAuts.remove(item->data(0, Qt::DisplayRole).toString()); break; } m_model.destroyItem(item); } QList SquishServerSettingsWidget::toConfigChangeArguments() const { QList result; for (auto it = m_originalSettings.mappedAuts.begin(), end = m_originalSettings.mappedAuts.end(); it != end; ++it) { const QString value = m_serverSettings.mappedAuts.value(it.key()); if (value == it.value()) continue; if (value.isEmpty()) result.append({"removeAUT", it.key(), it.value()}); else result.append({"addAUT", it.key(), value}); } for (auto it = m_serverSettings.mappedAuts.begin(), end = m_serverSettings.mappedAuts.end(); it != end; ++it) { if (!m_originalSettings.mappedAuts.contains(it.key())) result.append({"addAUT", it.key(), it.value()}); } for (auto it = m_originalSettings.attachableAuts.begin(), end = m_originalSettings.attachableAuts.end(); it != end; ++it) { const QString value = m_serverSettings.attachableAuts.value(it.key()); if (value == it.value()) continue; if (value.isEmpty()) result.append({"removeAttachableAUT", it.key(), it.value()}); else result.append({"addAttachableAUT", it.key(), value}); } for (auto it = m_serverSettings.attachableAuts.begin(), end = m_serverSettings.attachableAuts.end(); it != end; ++it) { if (!m_originalSettings.attachableAuts.contains(it.key())) result.append({"addAttachableAUT", it.key(), it.value()}); } for (auto &path : std::as_const(m_originalSettings.autPaths)) { if (!m_serverSettings.autPaths.contains(path)) result.append({"removeAppPath", path}); } for (auto &path : std::as_const(m_serverSettings.autPaths)) { if (!m_originalSettings.autPaths.contains(path)) result.append({"addAppPath", path}); } if (m_originalSettings.autTimeout.value() != m_serverSettings.autTimeout.value()) result.append({"setAUTTimeout", QString::number(m_serverSettings.autTimeout.value())}); if (m_originalSettings.responseTimeout.value() != m_serverSettings.responseTimeout.value()) result.append({"setResponseTimeout", QString::number(m_serverSettings.responseTimeout.value())}); if (m_originalSettings.postMortemWaitTime.value() != m_serverSettings.postMortemWaitTime.value()) result.append({"setAUTPostMortemTimeout", QString::number(m_serverSettings.postMortemWaitTime.value())}); if (m_originalSettings.animatedCursor.value() != m_serverSettings.animatedCursor.value()) result.append({"setCursorAnimation", m_serverSettings.animatedCursor.value() ? QString("on") : QString("off")}); return result; } void SquishServerSettingsWidget::editApplicationOrPath() { const QModelIndex &idx = m_applicationsView.currentIndex(); QTC_ASSERT(idx.isValid(), return); SquishServerItem *item = m_model.itemForIndex(idx); QTC_ASSERT(item && item->level() == 2, return); const int category = idx.parent().row(); QTC_ASSERT(category >= 0 && category <= 2, return); TreeItem *categoryItem = m_model.rootItem()->childAt(category); switch (category) { case MappedAutCategory: addMappedAut(categoryItem, item); break; case AutPathCategory: addAutPath(categoryItem, item); break; case AttachableAutCategory: addAttachableAut(categoryItem, item); break; } } SquishServerSettingsDialog::SquishServerSettingsDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(Tr::tr("Squish Server Settings")); QVBoxLayout *mainLayout = new QVBoxLayout(this); auto settingsWidget = new SquishServerSettingsWidget(this); mainLayout->addWidget(settingsWidget); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, this); mainLayout->addWidget(buttonBox); setLayout(mainLayout); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, [this, settingsWidget, buttonBox] { const QList configChanges = settingsWidget->toConfigChangeArguments(); if (configChanges.isEmpty()) { accept(); return; } connect(SquishTools::instance(), &SquishTools::configChangesFailed, this, &SquishServerSettingsDialog::configWriteFailed); connect(SquishTools::instance(), &SquishTools::configChangesWritten, this, &QDialog::accept); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); SquishTools::instance()->writeServerSettingsChanges(configChanges); }); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &QDialog::reject); } void SquishServerSettingsDialog::configWriteFailed(QProcess::ProcessError error) { QMessageBox::critical(Core::ICore::dialogParent(), Tr::tr("Error"), Tr::tr("Failed to write configuration changes.\n" "Squish server finished with process error %1.").arg(error)); } } // namespace Internal } // namespace Squish