Use QXmlStreamReader to parse test run output

Using the QXmlStreamReader will be easier to extend current
functionality and should be more robust than parsing on our own.

Change-Id: I9e1df7083a1af7681987f3971550e19a35b29df9
Reviewed-by: David Schulz <david.schulz@theqtcompany.com>
This commit is contained in:
Christian Stenger
2015-09-23 07:47:12 +02:00
committed by David Schulz
parent 17fbe92100
commit 23bdcf77b6
4 changed files with 176 additions and 175 deletions

View File

@@ -23,18 +23,20 @@ namespace Autotest {
namespace Internal { namespace Internal {
FaultyTestResult::FaultyTestResult(Result::Type result, const QString &description) FaultyTestResult::FaultyTestResult(Result::Type result, const QString &description)
: TestResult(QString(), QString(), QString(), result, description) {
setResult(result);
setDescription(description);
}
TestResult::TestResult()
: TestResult(QString())
{ {
} }
TestResult::TestResult(const QString &className, const QString &testCase, const QString &dataTag, TestResult::TestResult(const QString &className)
Result::Type result, const QString &description) : m_class(className)
: m_class(className), , m_result(Result::INVALID)
m_case(testCase), , m_line(0)
m_dataTag(dataTag),
m_result(result),
m_description(description),
m_line(0)
{ {
} }

View File

@@ -57,10 +57,8 @@ enum Type {
class TestResult class TestResult
{ {
public: public:
TestResult();
TestResult(const QString &className = QString(), const QString &testCase = QString(), TestResult(const QString &className);
const QString &dataTag = QString(),
Result::Type result = Result::INVALID, const QString &description = QString());
QString className() const { return m_class; } QString className() const { return m_class; }
QString testCase() const { return m_case; } QString testCase() const { return m_case; }
@@ -74,6 +72,8 @@ public:
void setFileName(const QString &fileName) { m_file = fileName; } void setFileName(const QString &fileName) { m_file = fileName; }
void setLine(int line) { m_line = line; } void setLine(int line) { m_line = line; }
void setResult(Result::Type type) { m_result = type; } void setResult(Result::Type type) { m_result = type; }
void setTestCase(const QString &testCase) { m_case = testCase; }
void setDataTag(const QString &dataTag) { m_dataTag = dataTag; }
static Result::Type resultFromString(const QString &resultString); static Result::Type resultFromString(const QString &resultString);
static Result::Type toResultType(int rt); static Result::Type toResultType(int rt);

View File

@@ -21,6 +21,7 @@
#include "testresult.h" #include "testresult.h"
#include <utils/hostosinfo.h> #include <utils/hostosinfo.h>
#include <utils/qtcassert.h>
#include <QRegExp> #include <QRegExp>
#include <QProcess> #include <QProcess>
@@ -60,44 +61,6 @@ static QString constructSourceFilePath(const QString &path, const QString &fileP
return QFileInfo(path, filePath).canonicalFilePath(); return QFileInfo(path, filePath).canonicalFilePath();
} }
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,
Result::Type &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 = decode(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;
}
// adapted from qplaintestlogger.cpp // adapted from qplaintestlogger.cpp
static QString formatResult(double value) static QString formatResult(double value)
{ {
@@ -146,35 +109,24 @@ static QString formatResult(double value)
return result; return result;
} }
static bool xmlExtractBenchmarkInformation(const QString &code, const QString &tagStart, static QString constructBenchmarkInformation(const QString &metric, double value, int iterations)
QString &description)
{ {
if (code.startsWith(tagStart)) { QString metricsText;
int start = code.indexOf(QLatin1String(" metric=\"")) + 9; if (metric == QLatin1String("WalltimeMilliseconds")) // default
const QString metric = code.mid(start, code.indexOf(QLatin1Char('"'), start) - start); metricsText = QLatin1String("msecs");
start = code.indexOf(QLatin1String(" value=\"")) + 8; else if (metric == QLatin1String("CPUTicks")) // -tickcounter
const double value = code.mid(start, code.indexOf(QLatin1Char('"'), start) - start).toDouble(); metricsText = QLatin1String("CPU ticks");
start = code.indexOf(QLatin1String(" iterations=\"")) + 13; else if (metric == QLatin1String("Events")) // -eventcounter
const int iterations = code.mid(start, code.indexOf(QLatin1Char('"'), start) - start).toInt(); metricsText = QLatin1String("events");
QString metricsText; else if (metric == QLatin1String("InstructionReads")) // -callgrind
if (metric == QLatin1String("WalltimeMilliseconds")) // default metricsText = QLatin1String("instruction reads");
metricsText = QLatin1String("msecs"); else if (metric == QLatin1String("CPUCycles")) // -perf
else if (metric == QLatin1String("CPUTicks")) // -tickcounter metricsText = QLatin1String("CPU cycles");
metricsText = QLatin1String("CPU ticks"); return QObject::tr("%1 %2 per iteration (total: %3, iterations: %4)")
else if (metric == QLatin1String("Events")) // -eventcounter .arg(formatResult(value))
metricsText = QLatin1String("events"); .arg(metricsText)
else if (metric == QLatin1String("InstructionReads")) // -callgrind .arg(formatResult(value * (double)iterations))
metricsText = QLatin1String("instruction reads"); .arg(iterations);
else if (metric == QLatin1String("CPUCycles")) // -perf
metricsText = QLatin1String("CPU cycles");
description = QObject::tr("%1 %2 per iteration (total: %3, iterations: %4)")
.arg(formatResult(value))
.arg(metricsText)
.arg(formatResult(value * (double)iterations))
.arg(iterations);
return true;
}
return false;
} }
TestXmlOutputReader::TestXmlOutputReader(QProcess *testApplication) TestXmlOutputReader::TestXmlOutputReader(QProcess *testApplication)
@@ -184,14 +136,24 @@ TestXmlOutputReader::TestXmlOutputReader(QProcess *testApplication)
this, &TestXmlOutputReader::processOutput); this, &TestXmlOutputReader::processOutput);
} }
TestXmlOutputReader::~TestXmlOutputReader() enum CDATAMode {
{ None,
} DataTag,
Description,
QtVersion,
QTestVersion
};
void TestXmlOutputReader::processOutput() void TestXmlOutputReader::processOutput()
{ {
if (!m_testApplication || m_testApplication->state() != QProcess::Running) if (!m_testApplication || m_testApplication->state() != QProcess::Running)
return; return;
static QStringList validEndTags = { QStringLiteral("Incident"),
QStringLiteral("Message"),
QStringLiteral("BenchmarkResult"),
QStringLiteral("QtVersion"),
QStringLiteral("QTestVersion") };
static CDATAMode cdataMode = None;
static QString className; static QString className;
static QString testCase; static QString testCase;
static QString dataTag; static QString dataTag;
@@ -200,98 +162,136 @@ void TestXmlOutputReader::processOutput()
static QString file; static QString file;
static int lineNumber = 0; static int lineNumber = 0;
static QString duration; static QString duration;
static bool readingDescription = false; static QXmlStreamReader xmlReader;
static QString qtVersion;
static QString qtestVersion;
static QString benchmarkDescription;
while (m_testApplication->canReadLine()) { while (m_testApplication->canReadLine()) {
// TODO Qt5 uses UTF-8 - while Qt4 uses ISO-8859-1 - could this be a problem? xmlReader.addData(m_testApplication->readLine());
const QString line = QString::fromUtf8(m_testApplication->readLine()).trimmed(); while (!xmlReader.atEnd()) {
if (line.isEmpty() || xmlStartsWith(line, QLatin1String("<TestCase name=\""), className)) { QXmlStreamReader::TokenType token = xmlReader.readNext();
testResultCreated(new TestResult(className, QString(), QString(), switch (token) {
Result::MESSAGE_TEST_CASE_START, case QXmlStreamReader::StartDocument:
QObject::tr("Executing test case %1").arg(className))); className.clear();
continue; break;
} case QXmlStreamReader::EndDocument:
if (line.startsWith(QLatin1String("<?xml version"))) { xmlReader.clear();
className = QString(); return;
continue; case QXmlStreamReader::StartElement: {
} const QString currentTag = xmlReader.name().toString();
if (xmlStartsWith(line, QLatin1String("<TestFunction name=\""), testCase)) { if (currentTag == QStringLiteral("TestCase")) {
dataTag = QString(); className = xmlReader.attributes().value(QStringLiteral("name")).toString();
description = QString(); QTC_ASSERT(!className.isEmpty(), continue);
duration = QString(); auto testResult = new TestResult(className);
file = QString(); testResult->setResult(Result::MESSAGE_TEST_CASE_START);
result = Result::INVALID; testResult->setDescription(tr("Executing test case %1").arg(className));
lineNumber = 0; testResultCreated(testResult);
readingDescription = false; } else if (currentTag == QStringLiteral("TestFunction")) {
testResultCreated(new TestResult(QString(), QString(), QString(), Result::MESSAGE_CURRENT_TEST, testCase = xmlReader.attributes().value(QStringLiteral("name")).toString();
QObject::tr("Entering test function %1::%2").arg(className).arg(testCase))); QTC_ASSERT(!testCase.isEmpty(), continue);
continue; auto testResult = new TestResult();
} testResult->setResult(Result::MESSAGE_CURRENT_TEST);
if (xmlStartsWith(line, QLatin1String("<Duration msecs=\""), duration)) { testResult->setDescription(tr("Entering test function %1::%2").arg(className,
continue; testCase));
} testResultCreated(testResult);
if (xmlExtractTypeFileLine(line, QLatin1String("<Message"), result, file, lineNumber)) } else if (currentTag == QStringLiteral("Duration")) {
continue; duration = xmlReader.attributes().value(QStringLiteral("msecs")).toString();
if (xmlCData(line, QLatin1String("<DataTag>"), dataTag)) QTC_ASSERT(!duration.isEmpty(), continue);
continue; } else if (currentTag == QStringLiteral("Message")
if (xmlCData(line, QLatin1String("<Description>"), description)) { || currentTag == QStringLiteral("Incident")) {
if (!line.endsWith(QLatin1String("</Description>"))) dataTag.clear();
readingDescription = true; description.clear();
continue; duration.clear();
} file.clear();
if (xmlExtractTypeFileLine(line, QLatin1String("<Incident"), result, file, lineNumber)) { result = Result::INVALID;
if (line.endsWith(QLatin1String("/>"))) { lineNumber = 0;
TestResult *testResult = new TestResult(className, testCase, dataTag, result, description); const QXmlStreamAttributes &attributes = xmlReader.attributes();
if (!file.isEmpty()) result = TestResult::resultFromString(
file = constructSourceFilePath(m_testApplication->workingDirectory(), file, attributes.value(QStringLiteral("type")).toString());
m_testApplication->program()); file = decode(attributes.value(QStringLiteral("file")).toString());
testResult->setFileName(file); if (!file.isEmpty())
testResult->setLine(lineNumber); file = constructSourceFilePath(m_testApplication->workingDirectory(), file,
testResultCreated(testResult); m_testApplication->program());
lineNumber = attributes.value(QStringLiteral("line")).toInt();
} else if (currentTag == QStringLiteral("BenchmarkResult")) {
const QXmlStreamAttributes &attributes = xmlReader.attributes();
const QString metric = attributes.value(QStringLiteral("metrics")).toString();
const double value = attributes.value(QStringLiteral("value")).toDouble();
const int iterations = attributes.value(QStringLiteral("iterations")).toInt();
description = constructBenchmarkInformation(metric, value, iterations);
result = Result::BENCHMARK;
} else if (currentTag == QStringLiteral("DataTag")) {
cdataMode = DataTag;
} else if (currentTag == QStringLiteral("Description")) {
cdataMode = Description;
} else if (currentTag == QStringLiteral("QtVersion")) {
result = Result::MESSAGE_INTERNAL;
cdataMode = QtVersion;
} else if (currentTag == QStringLiteral("QTestVersion")) {
result = Result::MESSAGE_INTERNAL;
cdataMode = QTestVersion;
}
break;
} }
continue; case QXmlStreamReader::Characters: {
} QStringRef text = xmlReader.text().trimmed();
if (xmlExtractBenchmarkInformation(line, QLatin1String("<BenchmarkResult"), benchmarkDescription)) { if (text.isEmpty())
testResultCreated(new TestResult(className, testCase, dataTag, Result::BENCHMARK, break;
benchmarkDescription));
continue; switch (cdataMode) {
} case DataTag:
if (line == QLatin1String("</Message>") || line == QLatin1String("</Incident>")) { dataTag = text.toString();
TestResult *testResult = new TestResult(className, testCase, dataTag, result, description); break;
if (!file.isEmpty()) case Description:
file = constructSourceFilePath(m_testApplication->workingDirectory(), file, if (!description.isEmpty())
m_testApplication->program()); description.append(QLatin1Char('\n'));
testResult->setFileName(file); description.append(text);
testResult->setLine(lineNumber); break;
testResultCreated(testResult); case QtVersion:
description = QString(); description = tr("Qt version: %1").arg(text.toString());
} else if (line == QLatin1String("</TestFunction>") && !duration.isEmpty()) { break;
testResultCreated(new TestResult(className, testCase, QString(), Result::MESSAGE_INTERNAL, case QTestVersion:
QObject::tr("Execution took %1 ms.").arg(duration))); description = tr("QTest version: %1").arg(text.toString());
emit increaseProgress(); break;
} else if (line == QLatin1String("</TestCase>") && !duration.isEmpty()) { default:
testResultCreated(new TestResult(className, QString(), QString(), Result::MESSAGE_TEST_CASE_END, QString message = QString::fromLatin1("unexpected cdatamode %1 for text \"%2\"")
QObject::tr("Test execution took %1 ms.").arg(duration))); .arg(cdataMode)
} else if (readingDescription) { .arg(text.toString());
if (line.endsWith(QLatin1String("]]></Description>"))) { QTC_ASSERT(false, qWarning() << message);
description.append(QLatin1Char('\n')); break;
description.append(line.left(line.indexOf(QLatin1String("]]></Description>")))); }
readingDescription = false; break;
} else { }
description.append(QLatin1Char('\n')); case QXmlStreamReader::EndElement: {
description.append(line); cdataMode = None;
const QStringRef currentTag = xmlReader.name();
if (currentTag == QStringLiteral("TestFunction")) {
if (!duration.isEmpty()) {
auto testResult = new TestResult(className);
testResult->setTestCase(testCase);
testResult->setResult(Result::MESSAGE_INTERNAL);
testResult->setDescription(tr("Execution took %1 ms.").arg(duration));
testResultCreated(testResult);
}
emit increaseProgress();
} else if (currentTag == QStringLiteral("TestCase") && !duration.isEmpty()) {
auto testResult = new TestResult(className);
testResult->setResult(Result::MESSAGE_TEST_CASE_END);
testResult->setDescription(tr("Test execution took %1 ms.").arg(duration));
testResultCreated(testResult);
} else if (validEndTags.contains(currentTag.toString())) {
auto testResult = new TestResult(className);
testResult->setTestCase(testCase);
testResult->setDataTag(dataTag);
testResult->setResult(result);
testResult->setFileName(file);
testResult->setLine(lineNumber);
testResult->setDescription(description);
testResultCreated(testResult);
}
break;
}
default:
break;
} }
} else if (xmlStartsWith(line, QLatin1String("<QtVersion>"), qtVersion)) {
testResultCreated(new TestResult(className, QString(), QString(), Result::MESSAGE_INTERNAL,
QObject::tr("Qt version: %1").arg(qtVersion)));
} else if (xmlStartsWith(line, QLatin1String("<QTestVersion>"), qtestVersion)) {
testResultCreated(new TestResult(className, QString(), QString(), Result::MESSAGE_INTERNAL,
QObject::tr("QTest version: %1").arg(qtestVersion)));
} else {
// qDebug() << "Unhandled line:" << line; // TODO remove
} }
} }
} }

View File

@@ -24,10 +24,9 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QXmlStreamReader>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QXmlStreamReader;
class QIODevice;
class QProcess; class QProcess;
QT_END_NAMESPACE QT_END_NAMESPACE
@@ -37,18 +36,18 @@ namespace Internal {
class TestXmlOutputReader : public QObject class TestXmlOutputReader : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
TestXmlOutputReader(QProcess *testApplication); TestXmlOutputReader(QProcess *testApplication);
~TestXmlOutputReader();
public slots: public slots:
void processOutput(); void processOutput();
signals: signals:
void testResultCreated(TestResult *testResult); void testResultCreated(TestResult *testResult);
void increaseProgress(); void increaseProgress();
private: private:
QProcess *m_testApplication; QProcess *m_testApplication; // not owned
}; };
} // namespace Internal } // namespace Internal