2014-10-07 15:51:02 +02:00
|
|
|
/****************************************************************************
|
|
|
|
**
|
|
|
|
** Copyright (C) 2014 Digia Plc
|
|
|
|
** All rights reserved.
|
|
|
|
** For any questions to Digia, please use contact form at http://qt.digia.com
|
|
|
|
**
|
|
|
|
** 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 Digia.
|
|
|
|
**
|
|
|
|
** If you have questions regarding the use of this file, please use
|
|
|
|
** contact form at http://qt.digia.com
|
|
|
|
**
|
|
|
|
****************************************************************************/
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
#include "autotestconstants.h"
|
2014-10-07 15:51:02 +02:00
|
|
|
#include "testresultspane.h"
|
|
|
|
#include "testrunner.h"
|
|
|
|
|
|
|
|
#include <QDebug> // REMOVE
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
#include <coreplugin/progressmanager/futureprogress.h>
|
|
|
|
#include <coreplugin/progressmanager/progressmanager.h>
|
|
|
|
|
2014-10-07 15:51:02 +02:00
|
|
|
#include <projectexplorer/buildmanager.h>
|
|
|
|
#include <projectexplorer/project.h>
|
|
|
|
#include <projectexplorer/projectexplorer.h>
|
|
|
|
#include <projectexplorer/projectexplorersettings.h>
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
#include <utils/multitask.h>
|
|
|
|
|
|
|
|
#include <QFuture>
|
|
|
|
#include <QFutureInterface>
|
2014-10-07 15:51:02 +02:00
|
|
|
#include <QTime>
|
|
|
|
|
|
|
|
namespace Autotest {
|
|
|
|
namespace Internal {
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
static TestRunner *m_instance = 0;
|
|
|
|
static QProcess *m_runner = 0;
|
|
|
|
static QFutureInterface<void> *m_currentFuture = 0;
|
2014-10-07 15:51:02 +02:00
|
|
|
|
|
|
|
TestRunner *TestRunner::instance()
|
|
|
|
{
|
|
|
|
if (!m_instance)
|
|
|
|
m_instance = new TestRunner;
|
|
|
|
return m_instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
TestRunner::TestRunner(QObject *parent) :
|
|
|
|
QObject(parent),
|
|
|
|
m_building(false)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
TestRunner::~TestRunner()
|
|
|
|
{
|
|
|
|
qDeleteAll(m_selectedTests);
|
|
|
|
m_selectedTests.clear();
|
|
|
|
m_instance = 0;
|
2014-11-04 12:35:00 +01:00
|
|
|
if (m_runner)
|
|
|
|
delete m_runner;
|
2014-10-07 15:51:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void TestRunner::setSelectedTests(const QList<TestConfiguration *> &selected)
|
|
|
|
{
|
|
|
|
qDeleteAll(m_selectedTests);
|
|
|
|
m_selectedTests.clear();
|
|
|
|
m_selectedTests = selected;
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************** XML line parser helper ********************/
|
|
|
|
|
|
|
|
static bool xmlStartsWith(const QString &code, const QString &start, QString &result)
|
|
|
|
{
|
|
|
|
if (code.startsWith(start)) {
|
|
|
|
result = code.mid(start.length());
|
|
|
|
result = result.left(result.indexOf(QLatin1Char('"')));
|
|
|
|
result = result.left(result.indexOf(QLatin1String("</")));
|
|
|
|
return !result.isEmpty();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool xmlCData(const QString &code, const QString &start, QString &result)
|
|
|
|
{
|
|
|
|
if (code.startsWith(start)) {
|
|
|
|
int index = code.indexOf(QLatin1String("<![CDATA[")) + 9;
|
|
|
|
result = code.mid(index, code.indexOf(QLatin1String("]]>"), index) - index);
|
|
|
|
return !result.isEmpty();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool xmlExtractTypeFileLine(const QString &code, const QString &tagStart,
|
|
|
|
ResultType &result, QString &file, int &line)
|
|
|
|
{
|
|
|
|
if (code.startsWith(tagStart)) {
|
|
|
|
int start = code.indexOf(QLatin1String(" type=\"")) + 7;
|
|
|
|
result = TestResult::resultFromString(
|
|
|
|
code.mid(start, code.indexOf(QLatin1Char('"'), start) - start));
|
|
|
|
start = code.indexOf(QLatin1String(" file=\"")) + 7;
|
|
|
|
file = code.mid(start, code.indexOf(QLatin1Char('"'), start) - start);
|
|
|
|
start = code.indexOf(QLatin1String(" line=\"")) + 7;
|
|
|
|
line = code.mid(start, code.indexOf(QLatin1Char('"'), start) - start).toInt();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/****************** XML line parser helper end ******************/
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
void processOutput()
|
2014-10-07 15:51:02 +02:00
|
|
|
{
|
2014-11-04 12:35:00 +01:00
|
|
|
if (!m_runner)
|
|
|
|
return;
|
2014-10-07 15:51:02 +02:00
|
|
|
static QString className;
|
|
|
|
static QString testCase;
|
|
|
|
static QString dataTag;
|
|
|
|
static ResultType result = ResultType::UNKNOWN;
|
|
|
|
static QString description;
|
|
|
|
static QString file;
|
|
|
|
static int lineNumber = 0;
|
|
|
|
static QString duration;
|
|
|
|
static bool readingDescription = false;
|
|
|
|
static QString qtVersion;
|
|
|
|
static QString qtestVersion;
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
while (m_runner->canReadLine()) {
|
2014-10-07 15:51:02 +02:00
|
|
|
// TODO Qt5 uses UTF-8 - while Qt4 uses ISO-8859-1 - could this be a problem?
|
2014-11-04 12:35:00 +01:00
|
|
|
const QString line = QString::fromUtf8(m_runner->readLine()).trimmed();
|
2014-10-07 15:51:02 +02:00
|
|
|
if (line.isEmpty() || line.startsWith(QLatin1String("<?xml version"))) {
|
|
|
|
className = QString();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (xmlStartsWith(line, QLatin1String("<TestCase name=\""), className))
|
|
|
|
continue;
|
|
|
|
if (xmlStartsWith(line, QLatin1String("<TestFunction name=\""), testCase)) {
|
|
|
|
dataTag = QString();
|
|
|
|
description = QString();
|
|
|
|
duration = QString();
|
|
|
|
file = QString();
|
|
|
|
result = ResultType::UNKNOWN;
|
|
|
|
lineNumber = 0;
|
|
|
|
readingDescription = false;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (xmlStartsWith(line, QLatin1String("<Duration msecs=\""), duration)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (xmlExtractTypeFileLine(line, QLatin1String("<Message"), result, file, lineNumber))
|
|
|
|
continue;
|
|
|
|
if (xmlCData(line, QLatin1String("<DataTag>"), dataTag))
|
|
|
|
continue;
|
|
|
|
if (xmlCData(line, QLatin1String("<Description>"), description)) {
|
|
|
|
if (!line.endsWith(QLatin1String("</Description>")))
|
|
|
|
readingDescription = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (xmlExtractTypeFileLine(line, QLatin1String("<Incident"), result, file, lineNumber)) {
|
|
|
|
if (line.endsWith(QLatin1String("/>"))) {
|
|
|
|
TestResult testResult(className, testCase, dataTag, result, description);
|
|
|
|
if (!file.isEmpty())
|
2014-11-04 12:35:00 +01:00
|
|
|
file = QFileInfo(m_runner->workingDirectory(), file).canonicalFilePath();
|
2014-10-07 15:51:02 +02:00
|
|
|
testResult.setFileName(file);
|
|
|
|
testResult.setLine(lineNumber);
|
|
|
|
TestResultsPane::instance()->addTestResult(testResult);
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (line == QLatin1String("</Message>") || line == QLatin1String("</Incident>")) {
|
|
|
|
TestResult testResult(className, testCase, dataTag, result, description);
|
|
|
|
if (!file.isEmpty())
|
2014-11-04 12:35:00 +01:00
|
|
|
file = QFileInfo(m_runner->workingDirectory(), file).canonicalFilePath();
|
2014-10-07 15:51:02 +02:00
|
|
|
testResult.setFileName(file);
|
|
|
|
testResult.setLine(lineNumber);
|
|
|
|
TestResultsPane::instance()->addTestResult(testResult);
|
|
|
|
description = QString();
|
|
|
|
} else if (line == QLatin1String("</TestFunction>") && !duration.isEmpty()) {
|
|
|
|
TestResult testResult(className, testCase, QString(), ResultType::MESSAGE_INTERNAL,
|
2014-11-04 12:35:00 +01:00
|
|
|
QObject::tr("execution took %1ms").arg(duration));
|
2014-10-07 15:51:02 +02:00
|
|
|
TestResultsPane::instance()->addTestResult(testResult);
|
2014-11-04 12:35:00 +01:00
|
|
|
m_currentFuture->setProgressValue(m_currentFuture->progressValue() + 1);
|
2014-10-07 15:51:02 +02:00
|
|
|
} else if (line == QLatin1String("</TestCase>") && !duration.isEmpty()) {
|
|
|
|
TestResult testResult(className, QString(), QString(), ResultType::MESSAGE_INTERNAL,
|
2014-11-04 12:35:00 +01:00
|
|
|
QObject::tr("Test execution took %1ms").arg(duration));
|
2014-10-07 15:51:02 +02:00
|
|
|
TestResultsPane::instance()->addTestResult(testResult);
|
|
|
|
} else if (readingDescription) {
|
|
|
|
if (line.endsWith(QLatin1String("]]></Description>"))) {
|
|
|
|
description.append(QLatin1Char('\n'));
|
|
|
|
description.append(line.left(line.indexOf(QLatin1String("]]></Description>"))));
|
|
|
|
readingDescription = false;
|
|
|
|
} else {
|
|
|
|
description.append(QLatin1Char('\n'));
|
|
|
|
description.append(line);
|
|
|
|
}
|
|
|
|
} else if (xmlStartsWith(line, QLatin1String("<QtVersion>"), qtVersion)) {
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_INTERNAL,
|
2014-11-04 12:35:00 +01:00
|
|
|
QObject::tr("Qt Version: %1").arg(qtVersion)));
|
2014-10-07 15:51:02 +02:00
|
|
|
} else if (xmlStartsWith(line, QLatin1String("<QTestVersion>"), qtestVersion)) {
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_INTERNAL,
|
2014-11-04 12:35:00 +01:00
|
|
|
QObject::tr("QTest Version: %1").arg(qtestVersion)));
|
2014-10-07 15:51:02 +02:00
|
|
|
} else {
|
|
|
|
// qDebug() << "Unhandled line:" << line; // TODO remove
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static QString which(const QString &path, const QString &cmd)
|
|
|
|
{
|
|
|
|
if (path.isEmpty() || cmd.isEmpty())
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
QStringList paths;
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
paths = path.split(QLatin1Char(';'));
|
|
|
|
#else
|
|
|
|
paths = path.split(QLatin1Char(':'));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
foreach (const QString p, paths) {
|
2014-11-04 12:35:00 +01:00
|
|
|
const QString fName = p + QDir::separator() + cmd;
|
2014-10-07 15:51:02 +02:00
|
|
|
QFileInfo fi(fName);
|
|
|
|
if (fi.exists() && fi.isExecutable())
|
|
|
|
return fName;
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
fi = QFileInfo(fName + QLatin1String(".exe"));
|
|
|
|
if (fi.exists())
|
|
|
|
return fi.absoluteFilePath();
|
|
|
|
fi = QFileInfo(fName + QLatin1String(".bat"));
|
|
|
|
if (fi.exists())
|
|
|
|
return fi.absoluteFilePath();
|
|
|
|
fi = QFileInfo(fName + QLatin1String(".cmd"));
|
|
|
|
if (fi.exists())
|
|
|
|
return fi.absoluteFilePath();
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
bool performExec(const QString &cmd, const QStringList &args, const QString &workingDir,
|
|
|
|
const Utils::Environment &env, int timeout = 60000)
|
2014-10-07 15:51:02 +02:00
|
|
|
{
|
|
|
|
QString runCmd;
|
|
|
|
if (!QDir::toNativeSeparators(cmd).contains(QDir::separator())) {
|
|
|
|
if (env.hasKey(QLatin1String("PATH")))
|
|
|
|
runCmd = which(env.value(QLatin1String("PATH")), cmd);
|
|
|
|
} else if (QFileInfo(cmd).exists()) {
|
|
|
|
runCmd = cmd;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (runCmd.isEmpty()) {
|
2014-11-04 12:35:00 +01:00
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
QObject::tr("*** Could not find command '%1' ***").arg(cmd)));
|
2014-10-07 15:51:02 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
m_runner->setWorkingDirectory(workingDir);
|
|
|
|
m_runner->setProcessEnvironment(env.toProcessEnvironment());
|
2014-10-07 15:51:02 +02:00
|
|
|
QTime executionTimer;
|
|
|
|
|
|
|
|
if (args.count()) {
|
2014-11-04 12:35:00 +01:00
|
|
|
m_runner->start(runCmd, args);
|
2014-10-07 15:51:02 +02:00
|
|
|
} else {
|
2014-11-04 12:35:00 +01:00
|
|
|
m_runner->start(runCmd);
|
2014-10-07 15:51:02 +02:00
|
|
|
}
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
bool ok = m_runner->waitForStarted();
|
2014-10-07 15:51:02 +02:00
|
|
|
executionTimer.start();
|
|
|
|
if (ok) {
|
2014-11-04 12:35:00 +01:00
|
|
|
while (m_runner->state() == QProcess::Running && executionTimer.elapsed() < timeout) {
|
|
|
|
if (m_currentFuture->isCanceled()) {
|
|
|
|
m_runner->kill();
|
|
|
|
m_runner->waitForFinished();
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
QObject::tr("*** Test Run canceled by user ***")));
|
|
|
|
}
|
2014-10-07 15:51:02 +02:00
|
|
|
qApp->processEvents();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ok && executionTimer.elapsed() < timeout) {
|
2014-11-04 12:35:00 +01:00
|
|
|
return m_runner->exitCode() == 0;
|
2014-10-07 15:51:02 +02:00
|
|
|
} else {
|
2014-11-05 15:52:34 +01:00
|
|
|
if (m_runner->state() != QProcess::NotRunning) {
|
|
|
|
m_runner->kill();
|
|
|
|
m_runner->waitForFinished();
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
QObject::tr("*** Test Case canceled due to timeout ***\n"
|
|
|
|
"Maybe raise the timeout?")));
|
|
|
|
}
|
2014-10-07 15:51:02 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
void performTestRun(QFutureInterface<void> &future, const QList<TestConfiguration *> selectedTests)
|
|
|
|
{
|
|
|
|
int testCaseCount = 0;
|
|
|
|
foreach (const TestConfiguration *config, selectedTests)
|
|
|
|
testCaseCount += config->testCaseCount();
|
|
|
|
|
|
|
|
m_currentFuture = &future;
|
|
|
|
m_runner = new QProcess;
|
|
|
|
m_runner->setReadChannelMode(QProcess::MergedChannels);
|
|
|
|
m_runner->setReadChannel(QProcess::StandardOutput);
|
|
|
|
|
|
|
|
QObject::connect(m_runner, &QProcess::readyReadStandardOutput, &processOutput);
|
|
|
|
|
|
|
|
future.setProgressRange(0, testCaseCount);
|
|
|
|
future.setProgressValue(0);
|
|
|
|
|
|
|
|
foreach (const TestConfiguration *tc, selectedTests) {
|
|
|
|
if (future.isCanceled())
|
|
|
|
break;
|
|
|
|
QString cmd = tc->targetFile();
|
|
|
|
QString workDir = tc->workingDirectory();
|
|
|
|
QStringList args;
|
|
|
|
Utils::Environment env = tc->environment();
|
|
|
|
|
|
|
|
args << QLatin1String("-xml");
|
|
|
|
if (tc->testCases().count())
|
|
|
|
args << tc->testCases();
|
|
|
|
|
|
|
|
performExec(cmd, args, workDir, env);
|
|
|
|
}
|
|
|
|
future.setProgressValue(testCaseCount);
|
|
|
|
|
|
|
|
delete m_runner;
|
|
|
|
m_runner = 0;
|
|
|
|
m_currentFuture = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TestRunner::runTests()
|
|
|
|
{
|
2014-11-04 13:42:38 +01:00
|
|
|
// clear old log and output pane
|
|
|
|
TestResultsPane::instance()->clearContents();
|
|
|
|
|
2014-11-04 12:35:00 +01:00
|
|
|
if (m_selectedTests.empty()) {
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
tr("*** No tests selected - canceling Test Run ***")));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ProjectExplorer::Project *project = m_selectedTests.at(0)->project();
|
|
|
|
if (!project) // add a warning or info to output? possible at all?
|
|
|
|
return;
|
|
|
|
|
|
|
|
ProjectExplorer::ProjectExplorerPlugin *pep = ProjectExplorer::ProjectExplorerPlugin::instance();
|
|
|
|
ProjectExplorer::Internal::ProjectExplorerSettings pes = pep->projectExplorerSettings();
|
|
|
|
if (pes.buildBeforeDeploy) {
|
|
|
|
if (!project->hasActiveBuildSettings()) {
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
tr("*** Project is not configured - canceling Test Run ***")));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
buildProject(project);
|
|
|
|
while (m_building) {
|
|
|
|
qApp->processEvents();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!m_buildSucceeded) {
|
|
|
|
TestResultsPane::instance()->addTestResult(
|
|
|
|
TestResult(QString(), QString(), QString(), ResultType::MESSAGE_FATAL,
|
|
|
|
tr("*** Build failed - canceling Test Run ***")));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
emit testRunStarted();
|
|
|
|
QFuture<void> future = QtConcurrent::run(&performTestRun , m_selectedTests);
|
|
|
|
Core::FutureProgress *progress = Core::ProgressManager::addTask(future, tr("Running Tests"),
|
|
|
|
Autotest::Constants::TASK_INDEX);
|
|
|
|
connect(progress, &Core::FutureProgress::finished,
|
|
|
|
TestRunner::instance(), &TestRunner::testRunFinished);
|
|
|
|
}
|
|
|
|
|
|
|
|
void TestRunner::buildProject(ProjectExplorer::Project *project)
|
|
|
|
{
|
|
|
|
m_building = true;
|
|
|
|
m_buildSucceeded = false;
|
|
|
|
ProjectExplorer::BuildManager *mgr = static_cast<ProjectExplorer::BuildManager *>(
|
|
|
|
ProjectExplorer::BuildManager::instance());
|
|
|
|
ProjectExplorer::ProjectExplorerPlugin *pep = ProjectExplorer::ProjectExplorerPlugin::instance();
|
|
|
|
pep->buildProject(project);
|
|
|
|
connect(mgr, &ProjectExplorer::BuildManager::buildQueueFinished,
|
|
|
|
this, &TestRunner::buildFinished);
|
|
|
|
}
|
|
|
|
|
|
|
|
void TestRunner::buildFinished(bool success)
|
|
|
|
{
|
|
|
|
ProjectExplorer::BuildManager *mgr = static_cast<ProjectExplorer::BuildManager *>(
|
|
|
|
ProjectExplorer::BuildManager::instance());
|
|
|
|
disconnect(mgr, &ProjectExplorer::BuildManager::buildQueueFinished,
|
|
|
|
this, &TestRunner::buildFinished);
|
|
|
|
m_building = false;
|
|
|
|
m_buildSucceeded = success;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TestRunner::stopTestRun()
|
|
|
|
{
|
|
|
|
if (m_runner && m_runner->state() != QProcess::NotRunning && m_currentFuture)
|
|
|
|
m_currentFuture->cancel();
|
|
|
|
}
|
|
|
|
|
2014-10-07 15:51:02 +02:00
|
|
|
} // namespace Internal
|
|
|
|
} // namespace Autotest
|