From 9d4509540b7a67fcb4f4087708eb207164bdee5f Mon Sep 17 00:00:00 2001 From: Christian Stenger Date: Fri, 8 May 2015 13:36:17 +0200 Subject: Provide some interaction with squish related tools This patch provides a rudimentary way to interact with squishserver and squishrunner. It allows to a run single test case or several test cases of a single suite. The results will be actually stored already in their respective results.xml files. Change-Id: I7983584e40dc6b0ef4dacff2c72559b7408c6433 Reviewed-by: Riitta-Leena Miettinen Reviewed-by: Robert Loehning --- plugins/autotest/autotest.pro | 6 +- plugins/autotest/autotest.qbs | 4 +- plugins/autotest/testnavigationwidget.cpp | 22 +- plugins/autotest/testresultspane.cpp | 39 +- plugins/autotest/testresultspane.h | 5 + plugins/autotest/testsquishfilehandler.cpp | 55 ++- plugins/autotest/testsquishfilehandler.h | 8 + plugins/autotest/testsquishtools.cpp | 678 +++++++++++++++++++++++++++++ plugins/autotest/testsquishtools.h | 121 +++++ plugins/autotest/testtreemodel.cpp | 21 + plugins/autotest/testtreemodel.h | 2 + 11 files changed, 945 insertions(+), 16 deletions(-) create mode 100644 plugins/autotest/testsquishtools.cpp create mode 100644 plugins/autotest/testsquishtools.h diff --git a/plugins/autotest/autotest.pro b/plugins/autotest/autotest.pro index b60a83f656..85ab704576 100644 --- a/plugins/autotest/autotest.pro +++ b/plugins/autotest/autotest.pro @@ -31,7 +31,8 @@ SOURCES += \ testxmloutputreader.cpp \ testsquishfilehandler.cpp \ opensquishsuitesdialog.cpp \ - testsquishutils.cpp + testsquishutils.cpp \ + testsquishtools.cpp HEADERS += \ squishsettings.h \ @@ -58,7 +59,8 @@ HEADERS += \ testxmloutputreader.h \ testsquishfilehandler.h \ opensquishsuitesdialog.h \ - testsquishutils.h + testsquishutils.h \ + testsquishtools.h RESOURCES += \ autotest.qrc diff --git a/plugins/autotest/autotest.qbs b/plugins/autotest/autotest.qbs index 09581cfdee..faf80f0484 100644 --- a/plugins/autotest/autotest.qbs +++ b/plugins/autotest/autotest.qbs @@ -77,7 +77,9 @@ QtcPlugin { "opensquishsuitesdialog.h", "opensquishsuitesdialog.ui", "testsquishutils.cpp", - "testsquishutils.h" + "testsquishutils.h", + "testsquishtools.cpp", + "testsquishtools.h" ] Group { diff --git a/plugins/autotest/testnavigationwidget.cpp b/plugins/autotest/testnavigationwidget.cpp index 33c5f4342b..26c1bfd3d8 100644 --- a/plugins/autotest/testnavigationwidget.cpp +++ b/plugins/autotest/testnavigationwidget.cpp @@ -95,6 +95,11 @@ TestNavigationWidget::TestNavigationWidget(QWidget *parent) : connect(m_view, &QTreeView::collapsed, this, &TestNavigationWidget::onCollapsed); connect(m_model, &QAbstractItemModel::rowsInserted, this, &TestNavigationWidget::onRowsInserted); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &TestNavigationWidget::onRowsRemoved); + + connect(m_view, &TestTreeView::runTestCase, + TestSquishFileHandler::instance(), &TestSquishFileHandler::runTestCase); + connect(m_view, &TestTreeView::runTestSuite, + TestSquishFileHandler::instance(), &TestSquishFileHandler::runTestSuite); } TestNavigationWidget::~TestNavigationWidget() @@ -111,12 +116,15 @@ bool TestNavigationWidget::handleSquishContextMenuEvent(QContextMenuEvent *event const QModelIndexList list = m_view->selectionModel()->selectedIndexes(); // 3 columns - but we only need the data of the first column as the others contain only an icon if (list.size() == 3) { - QRect rect(m_view->visualRect(list.first())); + const QModelIndex &index = list.first(); + QRect rect(m_view->visualRect(index)); rect.adjust(0, 0, defaultSectionSize * 2, 0); if (rect.contains(event->pos())) { - TestTreeItem::Type type = TestTreeItem::toTestType(list.first().data(TypeRole).toInt()); + TestTreeItem::Type type = TestTreeItem::toTestType(index.data(TypeRole).toInt()); if (type == TestTreeItem::SQUISH_TESTCASE) { + const QString caseName = index.data().toString(); + const QString suiteName = index.parent().data().toString(); isSquishMenu = true; QAction *runThisTestCase = new QAction(tr("Run This Test Case"), &menu); menu.addAction(runThisTestCase); @@ -125,8 +133,12 @@ bool TestNavigationWidget::handleSquishContextMenuEvent(QContextMenuEvent *event menu.addAction(deleteTestCase); deleteTestCase->setEnabled(enabled); menu.addSeparator(); + + connect(runThisTestCase, &QAction::triggered, [suiteName, caseName] () { + TestSquishFileHandler::instance()->runTestCase(suiteName, caseName); + }); } else if (type == TestTreeItem::SQUISH_SUITE) { - const QString suiteName = list.first().data().toString(); + const QString suiteName = index.data().toString(); isSquishMenu = true; QAction *runThisTestSuite = new QAction(tr("Run This Test Suite"), &menu); menu.addAction(runThisTestSuite); @@ -143,6 +155,10 @@ bool TestNavigationWidget::handleSquishContextMenuEvent(QContextMenuEvent *event deleteTestSuite->setEnabled(enabled); menu.addSeparator(); + connect(runThisTestSuite, &QAction::triggered, + [suiteName] () { + TestSquishFileHandler::instance()->runTestSuite(suiteName); + }); connect(closeTestSuite, &QAction::triggered, [suiteName] () { TestSquishFileHandler::instance()->closeTestSuite(suiteName); diff --git a/plugins/autotest/testresultspane.cpp b/plugins/autotest/testresultspane.cpp index e5d9bd6084..1378b6cfcd 100644 --- a/plugins/autotest/testresultspane.cpp +++ b/plugins/autotest/testresultspane.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include @@ -48,6 +50,9 @@ TestResultsPane::TestResultsPane(QObject *parent) : m_context(new Core::IContext(this)), m_wasVisibleBefore(false) { + m_outputPane = new QTabWidget; + m_outputPane->setDocumentMode(true); + m_outputWidget = new QWidget; QVBoxLayout *outputLayout = new QVBoxLayout; outputLayout->setMargin(0); @@ -86,6 +91,15 @@ TestResultsPane::TestResultsPane(QObject *parent) : createToolButtons(); + m_runnerServerLog = new QPlainTextEdit; + m_runnerServerLog->setMaximumBlockCount(10000); + + m_outputPane->addTab(m_outputWidget, tr("Test Results")); + m_outputPane->addTab(m_runnerServerLog, tr("Runner/Server Log")); + + connect(m_outputPane, &QTabWidget::currentChanged, [this] () { + navigateStateChanged(); + }); connect(m_treeView, &Utils::TreeView::activated, this, &TestResultsPane::onItemActivated); connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged, trd, &TestResultDelegate::currentChanged); @@ -151,14 +165,19 @@ void TestResultsPane::addTestResult(const TestResult &result) navigateStateChanged(); } +void TestResultsPane::addLogoutput(const QString &output) +{ + m_runnerServerLog->appendPlainText(output); +} + QWidget *TestResultsPane::outputWidget(QWidget *parent) { - if (m_outputWidget) { - m_outputWidget->setParent(parent); + if (m_outputPane) { + m_outputPane->setParent(parent); } else { qDebug() << "This should not happen..."; } - return m_outputWidget; + return m_outputPane; } QList TestResultsPane::toolBarWidgets() const @@ -178,9 +197,13 @@ int TestResultsPane::priorityInStatusBar() const void TestResultsPane::clearContents() { - m_filterModel->clearTestResults(); - navigateStateChanged(); - m_summaryWidget->setVisible(false); + if (m_outputPane->currentIndex() == 0) { + m_filterModel->clearTestResults(); + navigateStateChanged(); + m_summaryWidget->setVisible(false); + } else if (m_outputPane->currentIndex() == 1) { + m_runnerServerLog->clear(); + } } void TestResultsPane::visibilityChanged(bool visible) @@ -208,7 +231,7 @@ void TestResultsPane::setFocus() bool TestResultsPane::hasFocus() const { - return m_treeView->hasFocus(); + return m_treeView->hasFocus() || m_runnerServerLog->hasFocus(); } bool TestResultsPane::canFocus() const @@ -218,7 +241,7 @@ bool TestResultsPane::canFocus() const bool TestResultsPane::canNavigate() const { - return true; + return m_outputPane->currentIndex() == 0; // only support navigation for test results } bool TestResultsPane::canNext() const diff --git a/plugins/autotest/testresultspane.h b/plugins/autotest/testresultspane.h index 0d76c56ff4..765b6ff9ff 100644 --- a/plugins/autotest/testresultspane.h +++ b/plugins/autotest/testresultspane.h @@ -28,6 +28,8 @@ class QFrame; class QLabel; class QModelIndex; class QMenu; +class QPlainTextEdit; +class QTabWidget; class QToolButton; QT_END_NAMESPACE @@ -73,6 +75,7 @@ signals: public slots: void addTestResult(const TestResult &result); + void addLogoutput(const QString &output); private slots: void onItemActivated(const QModelIndex &index); @@ -90,7 +93,9 @@ private: void onTestRunFinished(); void onTestTreeModelChanged(); + QTabWidget *m_outputPane; QWidget *m_outputWidget; + QPlainTextEdit *m_runnerServerLog; QFrame *m_summaryWidget; QLabel *m_summaryLabel; Utils::TreeView *m_treeView; diff --git a/plugins/autotest/testsquishfilehandler.cpp b/plugins/autotest/testsquishfilehandler.cpp index 26a2f718b0..b69e5e2544 100644 --- a/plugins/autotest/testsquishfilehandler.cpp +++ b/plugins/autotest/testsquishfilehandler.cpp @@ -20,6 +20,11 @@ #include "opensquishsuitesdialog.h" #include "testsquishfilehandler.h" #include "testsquishutils.h" +#include "testsquishtools.h" + +#include + +#include #include #include @@ -30,11 +35,18 @@ namespace Internal { static TestSquishFileHandler *m_instance = 0; -TestSquishFileHandler::TestSquishFileHandler(QObject *parent) : QObject(parent) +TestSquishFileHandler::TestSquishFileHandler(QObject *parent) + : QObject(parent), + m_squishTools(new TestSquishTools) { m_instance = this; } +TestSquishFileHandler::~TestSquishFileHandler() +{ + delete m_squishTools; +} + TestSquishFileHandler *TestSquishFileHandler::instance() { if (!m_instance) @@ -64,7 +76,7 @@ void TestSquishFileHandler::openTestSuites() const QStringList cases = TestSquishUtils::validTestCases(suite); const QFileInfo suiteConf(suiteDir, QLatin1String("suite.conf")); if (m_suites.contains(suiteDir.dirName())) { - if (QMessageBox::question(0, tr("Suite Already Open"), + if (QMessageBox::question(Core::ICore::dialogParent(), tr("Suite Already Open"), tr("A test suite with the name \"%1\" is already open. " "The opened test suite will be closed and replaced " "with the new one.").arg(suiteDir.dirName()), @@ -110,5 +122,44 @@ void TestSquishFileHandler::closeAllTestSuites() m_suites.clear(); } +void TestSquishFileHandler::runTestCase(const QString &suiteName, const QString &testCaseName) +{ + QTC_ASSERT(!suiteName.isEmpty() && !testCaseName.isEmpty(), return); + + if (m_squishTools->state() != TestSquishTools::Idle) + return; + const QDir suitePath = QFileInfo(m_suites.value(suiteName)).absoluteDir(); + if (!suitePath.exists() || !suitePath.isReadable()) { + QMessageBox::critical(Core::ICore::dialogParent(), tr("Test Suite Path Not Accessible"), + tr("The path \"%1\" does not exist or is not accessible.\n" + "Refusing to run test case \"%2\".") + .arg(QDir::toNativeSeparators(suitePath.absolutePath())) + .arg(testCaseName)); + return; + } + + m_squishTools->runTestCases(suitePath.absolutePath(), QStringList() << testCaseName); +} + +void TestSquishFileHandler::runTestSuite(const QString &suiteName) +{ + QTC_ASSERT(!suiteName.isEmpty(), return); + + if (m_squishTools->state() != TestSquishTools::Idle) + return; + + const QString suiteConf = m_suites.value(suiteName); + const QDir suitePath = QFileInfo(suiteConf).absoluteDir(); + if (!suitePath.exists() || !suitePath.isReadable()) { + QMessageBox::critical(Core::ICore::dialogParent(), tr("Test Suite Path Not Accessible"), + tr("The path \"%1\" does not exist or is not accessible.\n" + "Refusing to run test cases.") + .arg(QDir::toNativeSeparators(suitePath.absolutePath()))); + return; + } + QStringList testCases = TestTreeModel::instance()->getSelectedSquishTestCases(suiteConf); + m_squishTools->runTestCases(suitePath.absolutePath(), testCases); +} + } // namespace Internal } // namespace Autotest diff --git a/plugins/autotest/testsquishfilehandler.h b/plugins/autotest/testsquishfilehandler.h index a63d435929..1a09952604 100644 --- a/plugins/autotest/testsquishfilehandler.h +++ b/plugins/autotest/testsquishfilehandler.h @@ -30,11 +30,14 @@ namespace Autotest { namespace Internal { +class TestSquishTools; + class TestSquishFileHandler : public QObject { Q_OBJECT public: explicit TestSquishFileHandler(QObject *parent = 0); + ~TestSquishFileHandler(); static TestSquishFileHandler *instance(); void openTestSuites(); @@ -46,8 +49,13 @@ signals: void testTreeItemModified(const TestTreeItem &item, TestTreeModel::Type type, const QString &file); void testTreeItemsRemoved(const QString &filePath, TestTreeModel::Type type); +public slots: + void runTestCase(const QString &suiteName, const QString &testCaseName); + void runTestSuite(const QString &suiteName); + private: QMap m_suites; + TestSquishTools *m_squishTools; }; diff --git a/plugins/autotest/testsquishtools.cpp b/plugins/autotest/testsquishtools.cpp new file mode 100644 index 0000000000..49739271ee --- /dev/null +++ b/plugins/autotest/testsquishtools.cpp @@ -0,0 +1,678 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at +** http://www.qt.io/contact-us +** +** This file is part of the Qt Creator Enterprise Auto Test Add-on. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://www.qt.io/contact-us +** +****************************************************************************/ + +#include "autotestplugin.h" +#include "squishsettings.h" +#include "testsquishtools.h" +#include "testresultspane.h" + +#include // TODO remove + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Autotest { +namespace Internal { + +// make this configurable? +static const QString resultsDirectory = QFileInfo(QDir::home(), + QLatin1String(".squishQC/Test Results") + ).absoluteFilePath(); + +TestSquishTools::TestSquishTools(QObject *parent) + : QObject(parent), + m_serverProcess(0), + m_runnerProcess(0), + m_serverPort(-1), + m_request(None), + m_state(Idle), + m_currentResultsXML(0), + m_resultsFileWatcher(0), + m_testRunning(false) +{ + connect(this, &TestSquishTools::logOutputReceived, + TestResultsPane::instance(), &TestResultsPane::addLogoutput, Qt::QueuedConnection); +} + +TestSquishTools::~TestSquishTools() +{ + // TODO add confirmation dialog somewhere + if (m_runnerProcess) { + m_runnerProcess->terminate(); + if (!m_runnerProcess->waitForFinished(5000)) + m_runnerProcess->kill(); + delete m_runnerProcess; + m_runnerProcess = 0; + } + + if (m_serverProcess) { + m_serverProcess->terminate(); + if (!m_serverProcess->waitForFinished(5000)) + m_serverProcess->kill(); + delete m_serverProcess; + m_serverProcess = 0; + } +} + +struct SquishToolsSettings +{ + SquishToolsSettings() + : serverPath(QLatin1String("squishserver")) + , runnerPath(QLatin1String("squishrunner")) + , isLocalServer(true) + , verboseLog(false) + , serverHost(QLatin1String("localhost")) + , serverPort(9999) + {} + + QString squishPath; + QString serverPath; + QString runnerPath; + bool isLocalServer; + bool verboseLog; + QString serverHost; + int serverPort; + QString licenseKeyPath; +}; + +void TestSquishTools::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(QLatin1String("yyyy-MM-ddTHH-mm-ss")); + m_currentResultsDirectory = QFileInfo(QDir(resultsDirectory), + dateTimeString).absoluteFilePath(); + + m_additionalRunnerArguments = additionalRunnerArgs; + m_additionalRunnerArguments << QLatin1String("--interactive") + << QLatin1String("--resultdir") + << QDir::toNativeSeparators(m_currentResultsDirectory); + + m_testRunning = true; + emit squishTestRunStarted(); + startSquishServer(RunTestRequested); +} + +void TestSquishTools::setState(TestSquishTools::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 = 0; + } + } + m_state = Idle; + break; + case RunnerStartFailed: + case RunnerStopped: + if (m_testCases.isEmpty()) { + m_request = ServerStopRequested; + stopSquishServer(); + // TODO merge result files + logrotateTestResults(); + } else { + startSquishRunner(); + } + break; + default: + break; + } +} + +// will be populated by calling setupSquishTools() with current settings +static SquishToolsSettings toolsSettings; + +void setupSquishTools() +{ + QSharedPointer squishSettings = AutotestPlugin::instance()->squishSettings(); + toolsSettings.squishPath = squishSettings->squishPath.toString(); + + toolsSettings.serverPath + = Utils::HostOsInfo::withExecutableSuffix(QLatin1String("squishserver")); + toolsSettings.runnerPath + = Utils::HostOsInfo::withExecutableSuffix(QLatin1String("squishrunner")); + + if (!toolsSettings.squishPath.isEmpty()) { + const QDir squishBin(toolsSettings.squishPath + QDir::separator() + QLatin1String("bin")); + toolsSettings.serverPath = QFileInfo(squishBin, + toolsSettings.serverPath).absoluteFilePath(); + toolsSettings.runnerPath = QFileInfo(squishBin, + toolsSettings.runnerPath).absoluteFilePath(); + } + + toolsSettings.isLocalServer = squishSettings->local; + toolsSettings.serverHost = squishSettings->serverHost; + toolsSettings.serverPort = squishSettings->serverPort; + toolsSettings.verboseLog = squishSettings->verbose; + toolsSettings.licenseKeyPath = squishSettings->licensePath.toString(); +} + +void TestSquishTools::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; + } + + setupSquishTools(); + m_serverPort = -1; + + const Utils::FileName 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 << QLatin1String("--local"); // for now - although Squish Docs say "don't use it" + else + arguments << QLatin1String("--port") << QString::number(toolsSettings.serverPort); + + if (toolsSettings.verboseLog) + arguments << QLatin1String("--verbose"); + + m_serverProcess->setArguments(arguments); + m_serverProcess->setProcessEnvironment(squishEnvironment()); + + connect(m_serverProcess, &QProcess::readyReadStandardOutput, + this, &TestSquishTools::onServerOutput); + connect(m_serverProcess, &QProcess::readyReadStandardError, + this, &TestSquishTools::onServerErrorOutput); + connect(m_serverProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(onServerFinished(int,QProcess::ExitStatus))); + + setState(ServerStarting); + m_serverProcess->start(); + if (!m_serverProcess->waitForStarted()) { + setState(ServerStartFailed); + qWarning() << "squishserver did not start within 30s"; + } +} + +void TestSquishTools::stopSquishServer() +{ + if (m_serverProcess && m_serverPort > 0) { + QProcess serverKiller; + serverKiller.setProgram(m_serverProcess->program()); + QStringList args; + args << QLatin1String("--stop") << QLatin1String("--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 TestSquishTools::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::FileName 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 << QLatin1String("--host") << toolsSettings.serverHost; + args << QLatin1String("--port") << QString::number(m_serverPort); + args << QLatin1String("--debugLog") << QLatin1String("alpw"); // TODO make this configurable? + + const QFileInfo testCasePath(QDir(m_suitePath), m_testCases.takeFirst()); + args << QLatin1String("--testcase") << testCasePath.absoluteFilePath(); + args << QLatin1String("--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 << QLatin1String("--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, &TestSquishTools::onRunnerErrorOutput); + connect(m_runnerProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(onRunnerFinished(int,QProcess::ExitStatus))); + + 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, &TestSquishTools::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 = 0; + setState(RunnerStartFailed); + return; + } + setState(RunnerStarted); + m_currentResultsXML = new QFile(caseReportFilePath); +} + +QProcessEnvironment TestSquishTools::squishEnvironment() const +{ + Utils::Environment environment = Utils::Environment::systemEnvironment(); + if (!toolsSettings.licenseKeyPath.isEmpty()) + environment.prependOrSet(QLatin1String("SQUISH_LICENSEKEY_DIR"), + toolsSettings.licenseKeyPath); + environment.prependOrSet(QLatin1String("SQUISH_PREFIX"), toolsSettings.squishPath); + return environment.toProcessEnvironment(); +} + +void TestSquishTools::onServerFinished(int, QProcess::ExitStatus) +{ + delete m_serverProcess; + m_serverProcess = 0; + m_serverPort = -1; + setState(ServerStopped); +} + +void TestSquishTools::onRunnerFinished(int, QProcess::ExitStatus) +{ + delete m_runnerProcess; + m_runnerProcess = 0; + + if (m_resultsFileWatcher) { + delete m_resultsFileWatcher; + m_resultsFileWatcher = 0; + } + if (m_currentResultsXML) { + if (m_currentResultsXML->isOpen()) + m_currentResultsXML->close(); + delete m_currentResultsXML; + m_currentResultsXML = 0; + } + setState(RunnerStopped); +} + +void TestSquishTools::onServerOutput() +{ + // output used for getting the port information of the current squishserver + const QByteArray output = m_serverProcess->readAllStandardOutput(); + foreach (const QByteArray &line, output.split('\n')) { + 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(QLatin1String("Server: ") + QLatin1String(trimmed)); + } +} + +void TestSquishTools::onServerErrorOutput() +{ + // output that must be send to the Runner/Server Log + const QByteArray output = m_serverProcess->readAllStandardError(); + foreach (const QByteArray &line, output.split('\n')) { + const QByteArray trimmed = line.trimmed(); + if (!trimmed.isEmpty()) + emit logOutputReceived(QLatin1String("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("', positionStart); + if (positionEnd == -1) + return -1; + + QByteArray endTag = text.mid(positionStart, positionEnd + 1 - positionStart); + if (possibleEndTags.contains(endTag)) + return positionEnd + 1; + else + return positionAfterLastClosingTag(text.mid(0, positionStart)); +} + +void TestSquishTools::onRunnerOutput(const QString) +{ + // 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 + qDebug() << "RunnerOutput:" << output; + } else { + foreach (const QByteArray &line, output.split('\n')) { + const QByteArray trimmed = line.trimmed(); + if (!trimmed.isEmpty()) + emit logOutputReceived(QLatin1String("Runner: ") + QLatin1String(trimmed)); + } + } +} + +void TestSquishTools::onRunnerErrorOutput() +{ + // output that must be send to the Runner/Server Log + const QByteArray output = m_runnerProcess->readAllStandardError(); + foreach (const QByteArray &line, output.split('\n')) { + const QByteArray trimmed = line.trimmed(); + if (!trimmed.isEmpty()) + emit logOutputReceived(QLatin1String("Runner: ") + QLatin1String(trimmed)); + } +} + +void TestSquishTools::onResultsDirChanged(const QString &filePath) +{ + if (m_currentResultsXML->exists()) { + delete m_resultsFileWatcher; + m_resultsFileWatcher = 0; + 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, &TestSquishTools::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 TestSquishTools::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 TestSquishTools::minimizeQtCreatorWindows() +{ + m_lastTopLevelWindows = QApplication::topLevelWindows(); + QWindowList toBeRemoved; + foreach (QWindow *window, m_lastTopLevelWindows) { + if (window->isVisible()) + window->showMinimized(); + else + toBeRemoved.append(window); + } + + foreach (QWindow *window, toBeRemoved) + m_lastTopLevelWindows.removeOne(window); +} + +void TestSquishTools::restoreQtCreatorWindows() +{ + foreach (QWindow *window, m_lastTopLevelWindows) { + window->requestActivate(); + window->showNormal(); + } +} + +} // namespace Internal +} // namespace Autotest diff --git a/plugins/autotest/testsquishtools.h b/plugins/autotest/testsquishtools.h new file mode 100644 index 0000000000..65e23312ac --- /dev/null +++ b/plugins/autotest/testsquishtools.h @@ -0,0 +1,121 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd +** All rights reserved. +** For any questions to The Qt Company, please use contact form at +** http://www.qt.io/contact-us +** +** This file is part of the Qt Creator Enterprise Auto Test Add-on. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. +** +** If you have questions regarding the use of this file, please use +** contact form at http://www.qt.io/contact-us +** +****************************************************************************/ + +#ifndef TESTSQUISHTOOLS_H +#define TESTSQUISHTOOLS_H + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QFile; +class QFileSystemWatcher; +QT_END_NAMESPACE + +namespace Autotest { +namespace Internal { + +class TestSquishTools : public QObject +{ + Q_OBJECT +public: + explicit TestSquishTools(QObject *parent = 0); + ~TestSquishTools(); + + 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(); + +private: + enum Request + { + None, + ServerStopRequested, + ServerQueryRequested, + RunnerQueryRequested, + RunTestRequested, + RecordTestRequested, + KillOldBeforeRunRunner, + KillOldBeforeRecordRunner, + KillOldBeforeQueryRunner + }; + + void setState(State state); + + void startSquishServer(Request request); + void stopSquishServer(); + void startSquishRunner(); + QProcessEnvironment squishEnvironment() const; + 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(const QString); + void onRunnerErrorOutput(); + void onResultsDirChanged(const QString &filePath); + 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; +}; + +} // namespace Internal +} // namespace Autotest + +#endif // TESTSQUISHTOOLS_H diff --git a/plugins/autotest/testtreemodel.cpp b/plugins/autotest/testtreemodel.cpp index 1e40493c02..b0451a40d7 100644 --- a/plugins/autotest/testtreemodel.cpp +++ b/plugins/autotest/testtreemodel.cpp @@ -715,6 +715,27 @@ bool TestTreeModel::hasUnnamedQuickTests() const return false; } +QStringList TestTreeModel::getSelectedSquishTestCases(const QString &suiteConfPath) +{ + QStringList result; + const int count = m_squishTestRootItem->childCount(); + if (count) { + for (int row = 0; row < count; ++row) { + TestTreeItem *child = m_squishTestRootItem->child(row); + if (child->filePath() == suiteConfPath) { + const int testCaseCount = child->childCount(); + for (int testCaseRow = 0; testCaseRow < testCaseCount; ++testCaseRow) { + TestTreeItem *grandChild = child->child(testCaseRow); + if (grandChild->checked()) + result.append(grandChild->name()); + } + break; + } + } + } + return result; +} + TestTreeItem *TestTreeModel::unnamedQuickTests() const { for (int row = 0, count = m_quickTestRootItem->childCount(); row < count; ++row) { diff --git a/plugins/autotest/testtreemodel.h b/plugins/autotest/testtreemodel.h index 8fa854f443..a4a7a5a1e3 100644 --- a/plugins/autotest/testtreemodel.h +++ b/plugins/autotest/testtreemodel.h @@ -80,6 +80,8 @@ public: QSet qmlFilesForProFile(const QString &proFile) const; bool hasUnnamedQuickTests() const; + QStringList getSelectedSquishTestCases(const QString &suiteConfPath); + #ifdef WITH_TESTS int autoTestsCount() const; int namedQuickTestsCount() const; -- cgit v1.2.1