/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtScxml module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include "scxml/scion.h" #include "scxml/compiled_tests.h" Q_DECLARE_METATYPE(std::function); enum { SpyWaitTime = 12000 }; static QSet testFailOnRun = QSet() // Currently we do not support loading data as XML content inside the tag. << QLatin1String("w3c-ecma/test557.txml") // The following test uses the undocumented "exmode" attribute. << QLatin1String("w3c-ecma/test441a.txml") // The following test needs manual inspection of the result. However, note that we do not support the undocumented "exmode" attribute. << QLatin1String("w3c-ecma/test441b.txml") // The following test needs manual inspection of the result. // The following test needs manual inspection of the result. However, note that we do not support multiple identical keys for event data. << QLatin1String("w3c-ecma/test178.txml") // We do not support the optional basic http event i/o processor. << QLatin1String("w3c-ecma/test201.txml") << QLatin1String("w3c-ecma/test230.txml") << QLatin1String("w3c-ecma/test250.txml") << QLatin1String("w3c-ecma/test307.txml") // Qt does not support forcing initial states that are not marked as such. << QLatin1String("w3c-ecma/test413.txml") // FIXME: verify initial state setting... << QLatin1String("w3c-ecma/test576.txml") // FIXME: verify initial state setting... // Scion apparently sets values without a src/expr attribute to 0. We set it to undefined, as specified in B.2.1. << QLatin1String("w3c-ecma/test456.txml") // replaced by modified_test456 // FIXME: qscxmlc fails on improper scxml file, currently no way of testing it properly for compiled case << QLatin1String("w3c-ecma/test301.txml") // FIXME: Currently we do not support nested scxml as a child of assign. << QLatin1String("w3c-ecma/test530.txml") ; class MySignalSpy: public QSignalSpy { public: explicit MySignalSpy(const QObject *obj, const char *aSignal) : QSignalSpy(obj, aSignal) {} bool fastWait() { const int increment = SpyWaitTime / 20; for (int total = 0; total < SpyWaitTime; total += increment) { if (this->wait(increment)) return true; } return false; } }; class DynamicLoader: public QScxmlCompiler::Loader { public: DynamicLoader(); QByteArray load(const QString &name, const QString &baseDir, QStringList *errors) override final; }; DynamicLoader::DynamicLoader() : Loader() {} QByteArray DynamicLoader::load(const QString &name, const QString &baseDir, QStringList *errors) { QStringList errs; QByteArray contents; QUrl url(name); if (!url.isLocalFile() && !url.isRelative()) errs << QStringLiteral("src attribute is not a local file (%1)").arg(name); QFileInfo fInfo = url.isLocalFile() ? url.toLocalFile() : name; if (fInfo.isRelative()) fInfo = QFileInfo(QDir(baseDir).filePath(fInfo.filePath())); fInfo = QFileInfo(QLatin1String(":/") + fInfo.filePath()); // take it from resources if (!fInfo.exists()) { errs << QStringLiteral("src attribute resolves to non existing file (%1)") .arg(fInfo.absoluteFilePath()); } else { QFile f(fInfo.absoluteFilePath()); if (f.open(QFile::ReadOnly)) contents = f.readAll(); else errs << QStringLiteral("Failure opening file %1: %2") .arg(fInfo.absoluteFilePath(), f.errorString()); } if (errors) *errors = errs; return contents; } class TestScion: public QObject { Q_OBJECT private slots: void initTestCase(); void dynamic_data(); void dynamic(); void compiled_data(); void compiled(); private: void generateData(); bool runTest(QScxmlStateMachine *stateMachine, const QJsonObject &testDescription); }; void TestScion::initTestCase() { } enum TestStatus { TestIsOk, TestFailsOnRun }; Q_DECLARE_METATYPE(TestStatus) void TestScion::generateData() { QTest::addColumn("scxml"); QTest::addColumn("json"); QTest::addColumn("testStatus"); QTest::addColumn>("creator"); const int nrOfTests = sizeof(testBases) / sizeof(const char *); for (int i = 0; i < nrOfTests; ++i) { TestStatus testStatus; QString base = QString::fromUtf8(testBases[i]); if (testFailOnRun.contains(base)) testStatus = TestFailsOnRun; else testStatus = TestIsOk; QTest::newRow(testBases[i]) << base + QLatin1String(".scxml") << base + QLatin1String(".json") << testStatus << creators[i]; } } void TestScion::dynamic_data() { generateData(); } void TestScion::dynamic() { QFETCH(QString, scxml); QFETCH(QString, json); QFETCH(TestStatus, testStatus); QFETCH(std::function, creator); QFile jsonFile(QLatin1String(":/") + json); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); auto testDescription = QJsonDocument::fromJson(jsonFile.readAll()); jsonFile.close(); QVERIFY(testDescription.isObject()); QFile scxmlFile(QLatin1String(":/") + scxml); QVERIFY(scxmlFile.open(QIODevice::ReadOnly)); QXmlStreamReader xmlReader(&scxmlFile); QScxmlCompiler compiler(&xmlReader); compiler.setFileName(scxml); DynamicLoader loader; compiler.setLoader(&loader); QScopedPointer stateMachine(compiler.compile()); QVERIFY(compiler.errors().isEmpty()); scxmlFile.close(); QVERIFY(stateMachine != nullptr); stateMachine->setLoader(&loader); const bool runResult = runTest(stateMachine.data(), testDescription.object()); if (runResult == false && testStatus == TestFailsOnRun) QEXPECT_FAIL("", "This is expected to fail on run", Abort); QVERIFY(runResult); QCoreApplication::processEvents(); // flush any pending events } static QStringList getStates(const QJsonObject &obj, const QString &key) { QStringList states; auto jsonStates = obj.value(key).toArray(); for (int i = 0, ei = jsonStates.size(); i != ei; ++i) { QString state = jsonStates.at(i).toString(); Q_ASSERT(!state.isEmpty()); states.append(state); } std::sort(states.begin(), states.end()); return states; } void TestScion::compiled_data() { generateData(); } void TestScion::compiled() { QFETCH(QString, scxml); QFETCH(QString, json); QFETCH(TestStatus, testStatus); QFETCH(std::function, creator); QFile jsonFile(QLatin1String(":/") + json); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); auto testDescription = QJsonDocument::fromJson(jsonFile.readAll()); jsonFile.close(); QScopedPointer stateMachine(creator()); if (stateMachine == nullptr && testStatus == TestFailsOnRun) { QEXPECT_FAIL("", "This is expected to fail", Abort); } QVERIFY(stateMachine != nullptr); DynamicLoader loader; stateMachine->setLoader(&loader); const bool runResult = runTest(stateMachine.data(), testDescription.object()); if (runResult == false && testStatus == TestFailsOnRun) QEXPECT_FAIL("", "This is expected to fail on run", Abort); QVERIFY(runResult); QCoreApplication::processEvents(); // flush any pending events } static bool verifyStates(QScxmlStateMachine *stateMachine, const QJsonObject &stateDescription, const QString &key, int counter) { auto current = stateMachine->activeStateNames(); std::sort(current.begin(), current.end()); auto expected = getStates(stateDescription, key); if (current == expected) return true; qWarning("Incorrect %s (%d)!", qPrintable(key), counter); qWarning() << "Current configuration:" << current; qWarning() << "Expected configuration:" << expected; return false; } static bool playEvent(QScxmlStateMachine *stateMachine, const QJsonObject &eventDescription, int counter) { if (!stateMachine->isRunning()) { qWarning() << "State machine stopped running!"; return false; } Q_ASSERT(eventDescription.contains(QLatin1String("event"))); auto event = eventDescription.value(QLatin1String("event")).toObject(); auto eventName = event.value(QLatin1String("name")).toString(); Q_ASSERT(!eventName.isEmpty()); QScxmlEvent::EventType type = QScxmlEvent::ExternalEvent; if (event.contains(QLatin1String("type"))) { QString typeStr = event.value(QLatin1String("type")).toString(); if (typeStr.compare(QLatin1String("external"), Qt::CaseInsensitive) == 0) type = QScxmlEvent::InternalEvent; else if (typeStr.compare(QLatin1String("platform"), Qt::CaseInsensitive) == 0) type = QScxmlEvent::PlatformEvent; else { qWarning() << "unexpected event type in " << eventDescription; return false; } } QVariant data; // remove ifs and rely on defaults? if (event.contains(QLatin1String("data"))) { data = event.value(QLatin1String("data")).toVariant(); } QString sendid; if (event.contains(QLatin1String("sendid"))) sendid = event.value(QLatin1String("sendid")).toString(); QString origin; if (event.contains(QLatin1String("origin"))) origin = event.value(QLatin1String("origin")).toString(); QString origintype; if (event.contains(QLatin1String("origintype"))) origintype = event.value(QLatin1String("origintype")).toString(); QString invokeid; if (event.contains(QLatin1String("invokeid"))) invokeid = event.value(QLatin1String("invokeid")).toString(); QScxmlEvent *e = new QScxmlEvent; e->setName(eventName); e->setEventType(type); e->setData(data); e->setSendId(sendid); e->setOrigin(origin); e->setOriginType(origintype); e->setInvokeId(invokeid); if (eventDescription.contains(QLatin1String("after"))) QTest::qWait(eventDescription.value(QLatin1String("after")).toInt()); stateMachine->submitEvent(e); if (!MySignalSpy(stateMachine, SIGNAL(reachedStableState())).fastWait()) { qWarning() << "State machine did not reach a stable state!"; } else if (verifyStates(stateMachine, eventDescription, QLatin1String("nextConfiguration"), counter)) { return true; } qWarning() << "... after sending event" << event; return false; } static bool playEvents(QScxmlStateMachine *stateMachine, const QJsonObject &testDescription) { auto jsonEvents = testDescription.value(QLatin1String("events")); Q_ASSERT(!jsonEvents.isNull()); auto eventsArray = jsonEvents.toArray(); for (int i = 0, ei = eventsArray.size(); i != ei; ++i) { if (!playEvent(stateMachine, eventsArray.at(i).toObject(), i + 1)) return false; } return true; } QT_BEGIN_NAMESPACE QDebug operator<<(QDebug debug, const QScxmlEvent &event) { QJsonObject obj; obj.insert(QLatin1String("name"), event.name()); obj.insert(QLatin1String("type"), event.eventType()); obj.insert(QLatin1String("data"), QJsonValue::fromVariant(event.data())); obj.insert(QLatin1String("sendid"), event.sendId()); obj.insert(QLatin1String("origin"), event.origin()); obj.insert(QLatin1String("originType"), event.originType()); obj.insert(QLatin1String("invokeid"), event.invokeId()); return debug << obj; } QT_END_NAMESPACE static int verifyEvent(const QList &receivedEvents, const QJsonObject &event, int position) { QScxmlEvent::EventType eventType = QScxmlEvent::ExternalEvent; const bool verifyEventType = event.contains(QLatin1String("type")); if (verifyEventType) { QString typeStr = event.value(QLatin1String("type")).toString(); if (typeStr.compare(QLatin1String("external"), Qt::CaseInsensitive) == 0) eventType = QScxmlEvent::InternalEvent; else if (typeStr.compare(QLatin1String("platform"), Qt::CaseInsensitive) == 0) eventType = QScxmlEvent::PlatformEvent; else { qWarning() << "unexpected event type in " << event; return -1; } } const bool verifyName = event.contains(QLatin1String("name")); const QString name = verifyName ? event.value(QLatin1String("name")).toString() : QString(); const bool verifyData = event.contains(QLatin1String("data")); const QVariant data = verifyData ? event.value(QLatin1String("data")).toVariant() : QVariant(); const bool verifySendId = event.contains(QLatin1String("sendid")); const QString sendId = verifySendId ? event.value(QLatin1String("sendid")).toString() : QString(); const bool verifyOrigin = event.contains(QLatin1String("origin")); const QString origin = verifyOrigin ? event.value(QLatin1String("origin")).toString() : QString(); const bool verifyOriginType = event.contains(QLatin1String("originType")); const QString originType = verifyOriginType ? event.value(QLatin1String("origintype")).toString() : QString(); const bool verifyInvokeId = event.contains(QLatin1String("invokeid")); const QString invokeId = verifyInvokeId ? event.value(QLatin1String("invokeid")).toString() : QString(); while (position < receivedEvents.length()) { const QScxmlEvent &receivedEvent = receivedEvents[position]; if ((verifyName && receivedEvent.name() != name) || (verifyEventType && receivedEvent.eventType() != eventType) || (verifyData && receivedEvent.data() != data) || (verifySendId && receivedEvent.sendId() != sendId) || (verifyOrigin && receivedEvent.origin() != origin) || (verifyOriginType && receivedEvent.originType() != originType) || (verifyInvokeId && receivedEvent.invokeId() != invokeId)) { ++position; } else { return position; } } qWarning("Did not receive expected event:"); qWarning() << event; return -1; // nothing found } static bool verifyEvents(const QList &receivedEvents, const QJsonObject &testDescription) { auto jsonEvents = testDescription.value(QLatin1String("expectedEvents")); if (jsonEvents.isNull()) return true; auto eventsArray = jsonEvents.toArray(); int position = 0; for (int i = 0, ei = eventsArray.size(); i != ei; ++i) { position = verifyEvent(receivedEvents, eventsArray.at(i).toObject(), position); if (position < 0) { qWarning("received events:"); qWarning() << receivedEvents; qWarning("expected events"); qWarning() << eventsArray; return false; } else { ++position; // Don't use the same event twice. } } return true; } bool TestScion::runTest(QScxmlStateMachine *stateMachine, const QJsonObject &testDescription) { MySignalSpy stableStateSpy(stateMachine, SIGNAL(reachedStableState())); MySignalSpy finishedSpy(stateMachine, SIGNAL(finished())); QList receivedEvents; stateMachine->connectToEvent(QLatin1String("*"), this, [&](const QScxmlEvent &event) { receivedEvents.append(event); }); if (!stateMachine->init() && stateMachine->name() != QStringLiteral("test487")) { // test487 relies on a failing init to see if an error event gets posted. qWarning() << "init failed"; return false; } stateMachine->start(); if (testDescription.contains(QLatin1String("events")) && !testDescription.value(QLatin1String("events")).toArray().isEmpty()) { if (!stableStateSpy.fastWait()) { qWarning() << "Failed to reach stable initial state!"; return false; } if (!verifyStates(stateMachine, testDescription, QLatin1String("initialConfiguration"), 0)) return false; return playEvents(stateMachine, testDescription); } else { // Wait for all events (delayed or otherwise) to propagate. if (stateMachine->isRunning()) { finishedSpy.fastWait(); // Some tests don't have a final state, so don't check for the // result } if (!verifyEvents(receivedEvents, testDescription)) return false; return verifyStates(stateMachine, testDescription, QLatin1String("initialConfiguration"), 0); } } QTEST_GUILESS_MAIN(TestScion) #include "tst_scion.moc"