Commit d5ee0abf authored by Manuel Breugelmans's avatar Manuel Breugelmans

Make QTestCase execution asychronous. Transform QTestOutputParser into a...

Make QTestCase execution asychronous. Transform QTestOutputParser into a recoverable XML stream parser.
parent 394fa863
......@@ -66,6 +66,7 @@ int TestRoot::run()
kWarning(!f.exists()) << "Failure: testresult dump does not exist [" << f.fileName();
OutputParser parser(&f);
parser.go(this);
emit executionFinished();
return 0;
}
......
......@@ -22,10 +22,11 @@
#include <kasserts.h>
#include <qtest_kde.h>
#include <ui_runnerwindow.h>
#include <runnerwindow.h>
#include <runnermodel.h>
#include "plugins/xtest/cppunit/register.h"
#include "plugins/xtest/qtest/tests/ui_runnerwindow.h"
#include <KDebug>
#include <QBuffer>
......@@ -219,9 +220,6 @@ void CheckRunnerTest::initNrun(const char* exe)
m_window->setModel(model);
m_window->show();
m_window->ui()->actionStart->trigger();
if (!QTest::kWaitForSignal(m_window->runnerModel(), SIGNAL(allItemsCompleted()), 2000))
// if (!QTest::kWaitForSignal(win->runnerModel(), SIGNAL(allItemsCompleted())))
QFAIL("Timeout while waiting for runner items to complete execution");
}
// helper
......
......@@ -63,6 +63,7 @@ int TestRoot::run()
proc.waitForFinished(-1);
OutputParser parser(&proc);
parser.go(this);
emit executionFinished();
return 0;
}
......
......@@ -22,11 +22,11 @@
#include <kasserts.h>
#include <qtest_kde.h>
#include <ui_runnerwindow.h>
#include <runnerwindow.h>
#include <runnermodel.h>
#include <testresult.h>
#include "register.h"
#include "plugins/xtest/qtest/tests/ui_runnerwindow.h"
#include <KDebug>
#include <QBuffer>
......@@ -207,9 +207,6 @@ void CppUnitRunnerTest::initNrun(const char* exe)
m_window->setModel(model);
m_window->show();
m_window->ui()->actionStart->trigger();
if (!QTest::kWaitForSignal(m_window->runnerModel(), SIGNAL(allItemsCompleted()), 2000))
// if (!QTest::kWaitForSignal(win->runnerModel(), SIGNAL(allItemsCompleted())))
QFAIL("Timeout while waiting for runner items to complete execution");
}
// helper
......
......@@ -5,10 +5,10 @@ add_definitions( ${KDE4_ENABLE_EXCEPTIONS} )
add_definitions( -DKDE_DEFAULT_DEBUG_AREA=9003 )
set(TestBuild on)
if(TestBuild)
if(TestBuild AND NOT WIN32)
# expose all symbols
add_definitions( -fvisibility=default )
endif(TestBuild)
endif(TestBuild AND NOT WIN32)
include_directories(
config
......
......@@ -25,9 +25,10 @@
#include "config/qtestsettings.h"
#include <QDir>
#include <QThread>
#include <KDebug>
#include <KProcess>
#include <KTemporaryFile>
using QTest::ISettings;
using QTest::QTestCase;
......@@ -38,11 +39,18 @@ using QTest::QTestOutputParser;
using Veritas::Test;
QTestCase::QTestCase(const QString& name, const QFileInfo& exe, QTestSuite* parent)
: Test(name, parent), m_exe(exe)
{}
: Test(name, parent), m_exe(exe), m_output(0), m_proc(0), m_parser(0)
{
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
}
QTestCase::~QTestCase()
{}
{
if (m_output) delete m_output;
if (m_proc) delete m_proc;
if (m_parser) delete m_parser;
}
bool QTestCase::shouldRun() const
{
......@@ -53,8 +61,7 @@ QFileInfo QTestCase::executable()
{
QFileInfo exe(m_exe);
Test* suite = parent();
if(suite != 0 && qobject_cast<QTestSuite*>(suite) != 0)
{
if (suite != 0 && qobject_cast<QTestSuite*>(suite) != 0) {
QDir path = QDir(qobject_cast<QTestSuite*>(suite)->path().filePath());
exe.setFile(path, m_exe.filePath());
}
......@@ -71,28 +78,123 @@ QTestCommand* QTestCase::child(int i) const
return static_cast<QTestCommand*>(Test::child(i));
}
// dependency injection
void QTestCase::setProcess(KProcess* proc)
{
if (m_proc) delete m_proc;
m_proc = proc;
}
// dependency injection
void QTestCase::setOutputParser(QTestOutputParser* p)
{
m_parser = p;
}
// execute the test and parse result back in.
int QTestCase::run()
{
QString dir = QDir::currentPath();
KProcess* proc = new KProcess;
// preconditions
assertProcessSet();
assertOutputParserSet();
if (not createTempOutputFile()) return -1;
initProcArguments();
setUpProcSignals();
initOutputParser();
executeProc();
return 1;
}
// helper for run()
void QTestCase::setUpProcSignals()
{
m_proc->disconnect();
connect(m_proc, SIGNAL(readyRead()), m_parser, SLOT(go()));
connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)),
this, SIGNAL(executionFinished()));
}
// void QTestCase::maybeParse()
// {
// if (!m_timer->isActive()) {
// m_timer->start(100);
// }
// }
// helper for run()
void QTestCase::initOutputParser()
{
m_parser->setDevice(m_proc);
m_parser->reset();
m_parser->setCase(this);
m_timer->disconnect();
connect(m_timer, SIGNAL(timeout()), m_parser, SLOT(go()));
}
// helper for run()
bool QTestCase::createTempOutputFile()
{
bool isOk = true;
if (m_output) delete m_output;
m_output = new KTemporaryFile;
m_output->open();
if (!m_output->isOpen()) {
kWarning() << "Failed to create temporary file for QTest output.";
isOk = false;
}
m_output->close();
return isOk;
}
// precondition for run()
void QTestCase::assertProcessSet()
{
Q_ASSERT_X(m_proc, "QTestCase::run()",
"Wrong usage. Client class should instantiate a "
"KProcess and pass it with setProcess().");
}
// precondition for run()
void QTestCase::assertOutputParserSet()
{
Q_ASSERT_X(m_parser, "QTestCase::run()",
"Wrong usage. Client class should instantiate a "
"QTestOutputParser and pass it with setParser().");
}
// helper for run()
void QTestCase::initProcArguments()
{
m_proc->clearProgram();
QStringList argv;
argv << "-xml";
if (m_settings && m_settings->printAsserts())
//<< QString("-o ") + m_output->fileName();
if (m_settings && m_settings->printAsserts()) {
argv << "-v2";
if (m_settings && m_settings->printSignals())
}
if (m_settings && m_settings->printSignals()) {
argv << "-vs";
}
m_proc->setProgram("./" + executable().fileName(), argv);
m_proc->setOutputChannelMode(KProcess::SeparateChannels);
}
// helper for run()
void QTestCase::executeProc()
{
QString dir = QDir::currentPath();
QDir::setCurrent(executable().dir().absolutePath());
proc->setProgram("./" + executable().fileName(), argv);
kDebug() << "executing " << proc->program() << " [ " << executable().filePath() << " ]";
proc->setOutputChannelMode(KProcess::SeparateChannels);
proc->start();
proc->waitForFinished(-1);
QTestOutputParser parser(proc);
parser.go(this);
kDebug() << "Executing " << m_proc->program() << " [ " << executable().filePath() << " ]";
m_proc->start();
QDir::setCurrent(dir);
delete proc;
return 1;
}
void QTestCase::setSettings(ISettings* s)
{
m_settings = s;
}
#include "qtestcase.moc"
......@@ -22,15 +22,20 @@
#define QXQTEST_QTESTCASE_H
#include <QString>
#include <QTimer>
#include <QFileInfo>
#include <kdevplatform/veritas/test.h>
#include "qtestcommand.h"
class KTemporaryFile;
class KProcess;
namespace QTest
{
class QTestSuite;
class QTestOutputParser;
class ISettings;
class QTestCase : public Veritas::Test
......@@ -42,16 +47,41 @@ public:
QTestCommand* child(int i) const;
QFileInfo executable();
void setExecutable(const QFileInfo&);
int run();
bool shouldRun() const;
void setSettings(ISettings* s) { m_settings = s; }
/*! Client classes should instantiate a KProcess.
QTestCase takes ownership.
Sole purpose is to increase testability through DI */
void setProcess(KProcess*);
void setOutputParser(QTestOutputParser*);
void setSettings(ISettings*);
void initProcArguments();
void setUpProcSignals();
void setExecutable(const QFileInfo&);
private:
// preconditions for run()
inline void assertProcessSet();
inline void assertOutputParserSet();
// helpers for run()
bool createTempOutputFile();
void executeProc();
void initOutputParser();
// private slots:
// void maybeParse();
// void surelyParse();
private:
QFileInfo m_exe;
ISettings* m_settings;
KTemporaryFile* m_output;
KProcess* m_proc;
QTestOutputParser* m_parser;
QTimer* m_timer;
};
} // end namespace QTest
......
......@@ -62,16 +62,21 @@ const QString QTestOutputParser::c_fail("fail");
const QString QTestOutputParser::c_initTestCase("initTestCase");
const QString QTestOutputParser::c_cleanupTestCase("cleanupTestCase");
QTestOutputParser::QTestOutputParser(QIODevice* device)
: QXmlStreamReader(device), m_result(0),
m_processingTestFunction(false),
m_fillingResult(false),
m_settingFailure(false)
{
}
QTestOutputParser::QTestOutputParser()
: m_state(Main),
m_result(0),
m_buzzy(false)
{}
QTestOutputParser::~QTestOutputParser()
{}
void QTestOutputParser::reset()
{
m_case = 0;
m_result = 0;
m_buzzy = false;
m_state = Main;
}
bool QTestOutputParser::isStartElement_(const QString& elementName)
......@@ -84,27 +89,55 @@ bool QTestOutputParser::isEndElement_(const QString& elementName)
return isEndElement() && (name() == elementName);
}
void QTestOutputParser::go(QTestCase* caze)
void QTestOutputParser::assertDeviceSet()
{
Q_ASSERT_X(device(), "QTestOutputParser::go()",
"Illegal usage. Client classes should set a QIODevice*, with setDevice().");
}
void QTestOutputParser::assertCaseSet()
{
Q_ASSERT_X(m_case, "QTestOutputParser::go()",
"Illegal usage. TestCase should have been set, with setCase().");
}
void QTestOutputParser::setCase(QTestCase* caze)
{
Q_ASSERT(caze);
m_case = caze;
}
void QTestOutputParser::go()
{
if (!device()->isOpen())
if (m_buzzy) return; // do not disturb.
m_buzzy = true;
assertCaseSet();
assertDeviceSet();
if (!device()->isOpen()) {
device()->open(QIODevice::ReadOnly);
}
if (!device()->isReadable()) {
// do something
kWarning() << "Device not readable. Failed to parse test output.";
m_buzzy = false;
return;
}
m_case = caze;
if (m_settingFailure)
setFailure();
if (m_processingTestFunction)
processTestFunction();
while (!atEnd() && doingOK()) {
readNext();
if (isStartElement_(c_testfunction)) {
// recover from previous state by falling through.
switch (m_state) {
case Failure: {
setFailure();
} case TestFunction: {
processTestFunction();
} case Main: {
iterateTestFunctions();
break;
} default: {
kError() << "Serious corruption, impossible switch value.";
}
}
kError(hasError()) << errorString() << " @ " << lineNumber() << ":" << columnNumber();
m_buzzy = false;
}
bool QTestOutputParser::doingOK() const
......@@ -115,60 +148,66 @@ bool QTestOutputParser::doingOK() const
bool QTestOutputParser::fixtureFailed(const QString& cmd)
{
if (cmd != c_initTestCase && cmd != c_cleanupTestCase)
if (cmd != c_initTestCase && cmd != c_cleanupTestCase) {
return false;
return !doingOK();
} else {
return !doingOK();
}
}
void QTestOutputParser::processTestFunction()
void QTestOutputParser::iterateTestFunctions()
{
QString cmdName;
if (!m_processingTestFunction) {
cmdName = attributes().value("name").toString();
Test* cmd = m_case->childNamed(cmdName);
m_result = new TestResult; // this probably leaks. TODO
if (cmd)
cmd->signalStarted();
// main loop
while (!atEnd() && doingOK()) {
readNext();
if (isStartElement_(c_testfunction)) {
m_cmdName = attributes().value("name").toString();
kDebug() << m_cmdName;
m_cmd = m_case->childNamed(m_cmdName);
m_result = new TestResult; // hmm leaks I suppose..
if (m_cmd) m_cmd->signalStarted();
m_state = TestFunction;
processTestFunction();
if (m_state != Main) return;
}
}
m_processingTestFunction = true;
kError(hasError()) << errorString() << " @ " << lineNumber() << ":" << columnNumber();
}
void QTestOutputParser::processTestFunction()
{
while (!atEnd() && !isEndElement_(c_testfunction)) {
readNext();
if (isStartElement_(c_incident))
if (isStartElement_(c_incident)) {
fillResult();
if (isStartElement_(c_message))
appendMsg();
if (m_state != TestFunction) return;
}
}
if (isEndElement_(c_testfunction)) {
Test* cmd = m_case->childNamed(cmdName);
if (cmd) {
cmd->setResult(m_result);
cmd->signalFinished();
} else if (fixtureFailed(cmdName)) {
if (m_cmd) {
m_cmd->setResult(m_result);
m_cmd->signalFinished();
} else if (fixtureFailed(m_cmdName)) {
kDebug() << "init/cleanup TestCase failed";
m_case->signalStarted();
m_case->setResult(m_result);
m_case->signalFinished();
}
m_processingTestFunction = false;
m_state = Main;
}
}
void QTestOutputParser::fillResult()
{
QString type = attributes().value(c_type).toString();
if (type == c_pass)
if (type == c_pass) {
setSuccess();
else if (type == c_fail)
} else if (type == c_fail) {
m_result->setState(Veritas::RunError);
m_result->setFile(QFileInfo(attributes().value(c_file).toString()));
m_result->setLine(attributes().value(c_line).toString().toInt());
m_state = Failure;
setFailure();
}
void QTestOutputParser::appendMsg()
{
while (!atEnd() && !isEndElement_(c_message)) {
readNext();
/* if (isStartElement_(c_description)) {
m_result->addOutputLine(readElementText().toAscii());
}*/
}
}
......@@ -179,19 +218,15 @@ void QTestOutputParser::setSuccess()
void QTestOutputParser::setFailure()
{
if (!m_settingFailure) {
m_result->setState(Veritas::RunError);
m_result->setFile(QFileInfo(attributes().value(c_file).toString()));
m_result->setLine(attributes().value(c_line).toString().toInt());
}
m_settingFailure = true;
while (!atEnd() && !isEndElement_(c_description)) {
readNext();
if (isCDATA())
m_result->setMessage(text().toString());
}
if (isEndElement_(c_description))
m_settingFailure = false;
if (isEndElement_(c_description)) {
m_state = TestFunction;
}
}
#include "qtestoutputparser.moc"
......@@ -22,48 +22,64 @@
#define QXQTEST_QTESTOUTPUTPARSER
#include <QXmlStreamReader>
#include <kdevplatform/veritas/testresult.h>
class QIODevice;
namespace Veritas { class Test; }
namespace QTest
{
class QTestCase;
class QTestOutputParser : public QXmlStreamReader
/*! Recoverable QTest XML-output parser */
class QTestOutputParser : public QObject, public QXmlStreamReader
{
Q_OBJECT
public:
QTestOutputParser(QIODevice* device);
QTestOutputParser();
virtual ~QTestOutputParser();
/**
* Start parsing the output of a whole
* testcase and emit results
**/
void go(QTestCase* caze);
/*! Must be called before parsing */
void setCase(QTestCase*caze);
/*! Reset internal parser state. Call this
* Before reusing this object. */
void reset();
private: // helpers
public slots:
/*! Start parsing the output of a testcase.
* Emit signals for each command being started/finished.
* This slot is triggered repeatedly as soon as new data is available
* and recovers from previus errors in the XML due to abortion. */
void go();
private:
// precondition
void assertDeviceSet();
void assertCaseSet();
// helpers
void iterateTestFunctions();
void processTestFunction();
void fillResult();
void setFailure();
void setSuccess();
void appendMsg();
inline bool isStartElement_(const QString& elem);
inline bool isEndElement_(const QString& elem);
inline bool fixtureFailed(const QString&);
inline bool doingOK() const;
private: // state
private:
// remember state to continue parsing
enum State { Main = 0, TestFunction = 1, Failure = 2 };
State m_state;
bool m_buzzy;
Veritas::TestResult* m_result;
QTestCase* m_case;
// remember state to continue when parsing
// incrementally
bool m_processingTestFunction;
bool m_fillingResult;
bool m_settingFailure;
Veritas::Test* m_cmd;
QString m_cmdName;
private: // some xml constants
static const QString c_testfunction;
......
......@@ -19,18 +19,24 @@
*/
#include "qtestregister.h"
#include "qtestsuite.h"
#include "qtestcase.h"
#include "qtestcommand.h"
#include "qtestoutputparser.h"
#include "qtestsuite.h"
#include <QIODevice>
#include <KLocalizedString>
#include <KDebug>
#include <KLocalizedString>
#include <KProcess>
#include <QIODevice>
using QTest::QTestRegister;
using QTest::QTestSuite;
using QTest::QTestCase;
using QTest::QTestCommand;
using QTest::QTestOutputParser;
using QTest::QTestRegister;
using QTest::QTestSuite;
using Veritas::Test;
const QString QTestRegister::c_suite("suite");
......@@ -44,10 +50,10 @@ const QString QTestRegister::c_exe("exe");
QTestRegister::QTestRegister()
: m_root(""), m_settings(0)
{
// Data for column headers is stored in the root item.
// Data for column headers is stored in the root item.
QList<QVariant> rootData;
rootData << i18n("Test Name") << i18n("Result") << i18n("Message")
<< i18n("File Name") << i18n("Line Number");
<< i18n("File Name") << i18n("Line Number");
m_rootItem = new Test(rootData);
}
......@@ -74,18 +80,18 @@ void QTestRegister::addFromXml(QIODevice* dev)
{
Q_ASSERT(dev != 0);
setDevice(dev);
if (!device()->isOpen())
if (!device()->isOpen()) {
device()->open(QIODevice::ReadOnly);
while (!atEnd())
{
}
while (!atEnd()) {
readNext();
if (isStartElement_(c_root) && m_root.isEmpty())
if (isStartElement_(c_root) && m_root.isEmpty()) {
m_root = attributes().value(c_dir).