// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include "topmachine.h" enum { SpyWaitTime = 8000 }; class tst_StateMachine: public QObject { Q_OBJECT private Q_SLOTS: void stateNames_data(); void stateNames(); void activeStateNames_data(); void activeStateNames(); void connections(); void historyState(); void onExit(); void eventOccurred(); void doneDotStateEvent(); void running(); void restart(); void invokeStateMachine(); void multipleInvokableServices(); // QTBUG-61484 void logWithoutExpr(); void bindings(); void setTableDataUpdatesObjectNames(); }; void tst_StateMachine::stateNames_data() { QTest::addColumn("scxmlFileName"); QTest::addColumn("compressed"); QTest::addColumn("expectedStates"); QTest::newRow("stateNames-compressed") << QString(":/tst_statemachine/statenames.scxml") << true << (QStringList() << QString("a1") << QString("a2") << QString("final")); QTest::newRow("stateNames-notCompressed") << QString(":/tst_statemachine/statenames.scxml") << false << (QStringList() << QString("top") << QString("a") << QString("a1") << QString("a2") << QString("b") << QString("final")); QTest::newRow("stateNamesNested-compressed") << QString(":/tst_statemachine/statenamesnested.scxml") << true << (QStringList() << QString("a") << QString("b")); QTest::newRow("stateNamesNested-notCompressed") << QString(":/tst_statemachine/statenamesnested.scxml") << false << (QStringList() << QString("super_top") << QString("a") << QString("b")); QTest::newRow("ids1") << QString(":/tst_statemachine/ids1.scxml") << false << (QStringList() << QString("foo.bar") << QString("foo-bar") << QString("foo_bar") << QString("_")); } void tst_StateMachine::stateNames() { QFETCH(QString, scxmlFileName); QFETCH(bool, compressed); QFETCH(QStringList, expectedStates); QScopedPointer stateMachine(QScxmlStateMachine::fromFile(scxmlFileName)); QVERIFY(!stateMachine.isNull()); QCOMPARE(stateMachine->parseErrors().size(), 0); QCOMPARE(stateMachine->stateNames(compressed), expectedStates); } void tst_StateMachine::activeStateNames_data() { QTest::addColumn("scxmlFileName"); QTest::addColumn("compressed"); QTest::addColumn("expectedStates"); QTest::newRow("stateNames-compressed") << QString(":/tst_statemachine/statenames.scxml") << true << (QStringList() << QString("a1") << QString("final")); QTest::newRow("stateNames-notCompressed") << QString(":/tst_statemachine/statenames.scxml") << false << (QStringList() << QString("top") << QString("a") << QString("a1") << QString("b") << QString("final")); QTest::newRow("stateNamesNested-compressed") << QString(":/tst_statemachine/statenamesnested.scxml") << true << (QStringList() << QString("a") << QString("b")); QTest::newRow("stateNamesNested-notCompressed") << QString(":/tst_statemachine/statenamesnested.scxml") << false << (QStringList() << QString("super_top") << QString("a") << QString("b")); } void tst_StateMachine::activeStateNames() { QFETCH(QString, scxmlFileName); QFETCH(bool, compressed); QFETCH(QStringList, expectedStates); QScopedPointer stateMachine(QScxmlStateMachine::fromFile(scxmlFileName)); QVERIFY(!stateMachine.isNull()); QSignalSpy stableStateSpy(stateMachine.data(), SIGNAL(reachedStableState())); stateMachine->start(); stableStateSpy.wait(5000); QCOMPARE(stateMachine->activeStateNames(compressed), expectedStates); } class Receiver : public QObject { Q_OBJECT public slots: void a(bool enabled) { aReached = aReached || enabled; } void b(bool enabled) { bReached = bReached || enabled; } void aEnter() { aEntered = true; } void aExit() { aExited = true; } public: bool aReached = false; bool bReached = false; bool aEntered = false; bool aExited = false; }; void tst_StateMachine::connections() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/statenames.scxml"))); QVERIFY(!stateMachine.isNull()); Receiver receiver; bool a1Reached = false; bool finalReached = false; QMetaObject::Connection a = stateMachine->connectToState("a", &receiver, &Receiver::a); QVERIFY(a); QMetaObject::Connection b = stateMachine->connectToState("b", &receiver, SLOT(b(bool))); QVERIFY(b); QMetaObject::Connection a1 = stateMachine->connectToState("a1", &receiver, [&a1Reached](bool enabled) { a1Reached = a1Reached || enabled; }); QVERIFY(a1); QMetaObject::Connection final = stateMachine->connectToState("final", [&finalReached](bool enabled) { finalReached = finalReached || enabled; }); QVERIFY(final); bool a1Entered = false; bool a1Exited = false; bool finalEntered = false; bool finalExited = false; typedef QScxmlStateMachine QXSM; QMetaObject::Connection aEntry = stateMachine->connectToState( "a", QXSM::onEntry(&receiver, &Receiver::aEnter)); QVERIFY(aEntry); QMetaObject::Connection aExit = stateMachine->connectToState( "a", QXSM::onExit(&receiver, &Receiver::aExit)); QVERIFY(aExit); QMetaObject::Connection a1Entry = stateMachine->connectToState("a1", &receiver, QXSM::onEntry([&a1Entered]() { a1Entered = true; })); QVERIFY(a1Entry); QMetaObject::Connection a1Exit = stateMachine->connectToState("a1", &receiver, QXSM::onExit([&a1Exited]() { a1Exited = true; })); QVERIFY(a1Exit); QMetaObject::Connection finalEntry = stateMachine->connectToState( "final", QXSM::onEntry([&finalEntered]() { finalEntered = true; })); QVERIFY(finalEntry); QMetaObject::Connection finalExit = stateMachine->connectToState( "final", QXSM::onExit([&finalExited]() { finalExited = true; })); QVERIFY(finalExit); stateMachine->start(); QTRY_VERIFY(a1Reached); QTRY_VERIFY(finalReached); QTRY_VERIFY(receiver.aReached); QTRY_VERIFY(receiver.bReached); QVERIFY(disconnect(a)); QVERIFY(disconnect(b)); QVERIFY(disconnect(a1)); QVERIFY(disconnect(final)); #if defined(__cpp_return_type_deduction) && __cpp_return_type_deduction == 201304 QVERIFY(receiver.aEntered); QVERIFY(!receiver.aExited); QVERIFY(a1Entered); QVERIFY(!a1Exited); QVERIFY(finalEntered); QVERIFY(!finalExited); QVERIFY(disconnect(aEntry)); QVERIFY(disconnect(aExit)); QVERIFY(disconnect(a1Entry)); QVERIFY(disconnect(a1Exit)); QVERIFY(disconnect(finalEntry)); QVERIFY(disconnect(finalExit)); #endif } void tst_StateMachine::historyState() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/historystate.scxml"))); QVERIFY(!stateMachine.isNull()); bool state2Reached = false; QMetaObject::Connection state2Connection = stateMachine->connectToState("State2", [&state2Reached](bool enabled) { state2Reached = state2Reached || enabled; }); QVERIFY(state2Connection); stateMachine->start(); QTRY_VERIFY(state2Reached); } void tst_StateMachine::onExit() { #if defined(__cpp_return_type_deduction) && __cpp_return_type_deduction == 201304 // Test onExit being actually called typedef QScxmlStateMachine QXSM; QScopedPointer stateMachine(QXSM::fromFile(QString(":/tst_statemachine/eventoccurred.scxml"))); Receiver receiver; bool aExited1 = false; stateMachine->connectToState("a", QXSM::onExit([&aExited1]() { aExited1 = true; })); stateMachine->connectToState("a", QXSM::onExit(&receiver, &Receiver::aExit)); stateMachine->connectToState("a", QXSM::onExit(&receiver, "aEnter")); { // Should not crash Receiver receiver2; stateMachine->connectToState("a", QXSM::onEntry(&receiver2, &Receiver::aEnter)); stateMachine->connectToState("a", QXSM::onEntry(&receiver2, "aExit")); stateMachine->connectToState("a", QXSM::onExit(&receiver2, &Receiver::aExit)); stateMachine->connectToState("a", QXSM::onExit(&receiver2, "aEnter")); } stateMachine->start(); QTRY_VERIFY(receiver.aEntered); QTRY_VERIFY(receiver.aExited); QTRY_VERIFY(aExited1); #endif } bool hasChildEventRouters(QScxmlStateMachine *stateMachine) { // Cast to QObject, to avoid ambigous "children" member. const QObject &parentRouter = QScxmlStateMachinePrivate::get(stateMachine)->m_router; return !parentRouter.children().isEmpty(); } void tst_StateMachine::eventOccurred() { QScopedPointer stateMachine(QScxmlStateMachine::fromFile(QString(":/tst_statemachine/eventoccurred.scxml"))); QVERIFY(!stateMachine.isNull()); qRegisterMetaType(); QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); int events = 0; auto con1 = stateMachine->connectToEvent("internalEvent2", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 1); QCOMPARE(event.name(), QString("internalEvent2")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con1); auto con2 = stateMachine->connectToEvent("externalEvent", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 2); QCOMPARE(event.name(), QString("externalEvent")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con2); auto con3 = stateMachine->connectToEvent("timeout", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 3); QCOMPARE(event.name(), QString("timeout")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con3); auto con4 = stateMachine->connectToEvent("done.*", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 4); QCOMPARE(event.name(), QString("done.state.top")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con4); auto con5 = stateMachine->connectToEvent("done.state", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 5); QCOMPARE(event.name(), QString("done.state.top")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con5); auto con6 = stateMachine->connectToEvent("done.state.top", [&events](const QScxmlEvent &event) { QCOMPARE(++events, 6); QCOMPARE(event.name(), QString("done.state.top")); QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); }); QVERIFY(con6); stateMachine->start(); finishedSpy.wait(5000); QCOMPARE(events, 6); QVERIFY(disconnect(con1)); QVERIFY(disconnect(con2)); QVERIFY(disconnect(con3)); QVERIFY(disconnect(con4)); QVERIFY(disconnect(con5)); QVERIFY(disconnect(con6)); QTRY_VERIFY(!hasChildEventRouters(stateMachine.data())); } void tst_StateMachine::doneDotStateEvent() { QScopedPointer stateMachine(QScxmlStateMachine::fromFile(QString(":/tst_statemachine/stateDotDoneEvent.scxml"))); QVERIFY(!stateMachine.isNull()); QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); stateMachine->start(); finishedSpy.wait(5000); QCOMPARE(finishedSpy.size(), 1); QCOMPARE(stateMachine->activeStateNames(true).size(), 1); QVERIFY(stateMachine->activeStateNames(true).contains(QLatin1String("success"))); } void tst_StateMachine::running() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/statenames.scxml"))); QVERIFY(!stateMachine.isNull()); QSignalSpy runningChangedSpy(stateMachine.data(), SIGNAL(runningChanged(bool))); QCOMPARE(stateMachine->isRunning(), false); stateMachine->start(); QCOMPARE(runningChangedSpy.size(), 1); QCOMPARE(stateMachine->isRunning(), true); stateMachine->stop(); QCOMPARE(runningChangedSpy.size(), 2); QCOMPARE(stateMachine->isRunning(), false); } void tst_StateMachine::restart() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/stateDotDoneEvent.scxml"))); QVERIFY(!stateMachine.isNull()); QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); stateMachine->start(); finishedSpy.wait(5000); QCOMPARE(finishedSpy.size(), 1); QCOMPARE(stateMachine->activeStateNames(true).size(), 1); QVERIFY(stateMachine->activeStateNames(true).contains(QLatin1String("success"))); QTest::ignoreMessage(QtWarningMsg, QRegularExpression("(.*)Can't start finished machine")); stateMachine->start(); } void tst_StateMachine::invokeStateMachine() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/invoke.scxml"))); QVERIFY(!stateMachine.isNull()); stateMachine->start(); QCOMPARE(stateMachine->isRunning(), true); QTRY_VERIFY(stateMachine->activeStateNames().contains(QString("anyplace"))); QList services = stateMachine->invokedServices(); QCOMPARE(services.size(), 1); QVariant subMachineVariant = services[0]->property("stateMachine"); QVERIFY(subMachineVariant.isValid()); QScxmlStateMachine *subMachine = qvariant_cast(subMachineVariant); QVERIFY(subMachine); QTRY_VERIFY(subMachine->activeStateNames().contains("here")); } void tst_StateMachine::multipleInvokableServices() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/multipleinvokableservices.scxml"))); QVERIFY(!stateMachine.isNull()); QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); stateMachine->start(); QCOMPARE(stateMachine->isRunning(), true); finishedSpy.wait(5000); QCOMPARE(finishedSpy.size(), 1); QCOMPARE(stateMachine->activeStateNames(true).size(), 1); QVERIFY(stateMachine->activeStateNames(true).contains(QLatin1String("success"))); } void tst_StateMachine::logWithoutExpr() { QScopedPointer stateMachine( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/emptylog.scxml"))); QVERIFY(!stateMachine.isNull()); QTest::ignoreMessage(QtDebugMsg, "\"Hi2\" : \"\""); stateMachine->start(); QSignalSpy logSpy(stateMachine.data(), SIGNAL(log(QString,QString))); QTRY_COMPARE(logSpy.size(), 1); } void tst_StateMachine::bindings() { // -- QScxmlStateMachine::initialized std::unique_ptr stateMachine1( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/invoke.scxml"))); QVERIFY(stateMachine1.get()); QTestPrivate::testReadOnlyPropertyBasics( *stateMachine1, false, true, "initialized", [&](){ stateMachine1.get()->start(); }); if (QTest::currentTestFailed()) { qWarning() << "QScxmlStateMachine::initialized bindable test failed."; return; } using StateMachinePtr = std::unique_ptr; // -- QScxmlStateMachine::initialValues QVariantMap map1{{"map", 1}}; QVariantMap map2{{"map", 2}}; QTestPrivate::testReadWritePropertyBasics( *stateMachine1, map1, map2, "initialValues", []() { return StateMachinePtr{QScxmlStateMachine::fromFile(QString("not_a_real_file"))}; }); if (QTest::currentTestFailed()) { qWarning() << "QScxmlStateMachine::initialValues bindable test failed."; return; } // -- QScxmlStateMachine::loader class MockLoader: public QScxmlCompiler::Loader { public: QByteArray load(const QString&, const QString&, QStringList*) override { return QByteArray(); } }; MockLoader loader1; MockLoader loader2; QTestPrivate::testReadWritePropertyBasics( *stateMachine1, &loader1, &loader2, "loader", []() { return StateMachinePtr{QScxmlStateMachine::fromFile(QString("not_a_real_file"))}; }); if (QTest::currentTestFailed()) { qWarning() << "QScxmlStateMachine::loader bindable test failed."; return; } // -- QScxmlStateMachine::dataModel // Use non-existent file below, as valid file would initialize the model std::unique_ptr stateMachine2( QScxmlStateMachine::fromFile(QString("not_a_real_file"))); std::unique_ptr stateMachine3( QScxmlStateMachine::fromFile(QString("not_a_real_file"))); QScxmlNullDataModel model1; // data can only change once QTestPrivate::testWriteOncePropertyBasics( *stateMachine2, nullptr, &model1, "dataModel", true, []() { return StateMachinePtr{QScxmlStateMachine::fromFile(QString("not_a_real_file"))}; }); if (QTest::currentTestFailed()) { qWarning() << "QScxmlStateMachine::dataModel bindable test failed."; return; } // -- QScxmlStateMachine::tableData // Use the statemachine to generate the tableData for testing std::unique_ptr stateMachine4( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/invoke.scxml"))); QTestPrivate::testReadWritePropertyBasics( *stateMachine2, stateMachine1.get()->tableData(), stateMachine4.get()->tableData(), "tableData", []() { return StateMachinePtr{QScxmlStateMachine::fromFile(QString("not_a_real_file"))}; }); if (QTest::currentTestFailed()) { qWarning() << "QScxmlStateMachine::tableData bindable test failed."; return; } // -- QScxmlStateMachine::invokedServices // Test executes statemachine and observes as the invoked services change TopMachine topSm; QSignalSpy invokedSpy(&topSm, SIGNAL(invokedServicesChanged(const QList))); QCOMPARE(topSm.invokedServices().size(), 0); // at some point during the topSm execution there are 3 invoked services topSm.start(); QTRY_COMPARE(topSm.invokedServices().size(), 3); QCOMPARE(invokedSpy.size(), 1); // after completion invoked services drop back to 0 QTRY_COMPARE(topSm.isRunning(), false); QCOMPARE(topSm.invokedServices().size(), 0); QCOMPARE(invokedSpy.size(), 2); // bind *to* the invokedservices property and check that we observe same changes // during the topSm execution QProperty invokedServicesObserver; invokedServicesObserver.setBinding([&](){ return topSm.invokedServices().size(); }); QCOMPARE(invokedServicesObserver, 0); topSm.start(); QTRY_COMPARE(invokedServicesObserver, 3); QCOMPARE(topSm.invokedServices().size(), 3); QCOMPARE(invokedSpy.size(), 3); // -- QScxmlDataModel::stateMachine QScxmlNullDataModel dataModel1; std::unique_ptr stateMachine5( QScxmlStateMachine::fromFile(QString("not_a_real_file"))); // data can only change once QTestPrivate::testWriteOncePropertyBasics( dataModel1, nullptr, stateMachine5.get(), "stateMachine"); if (QTest::currentTestFailed()) { qWarning() << "QScxmlDataModel::stateMachine bindable test failed."; return; } } void tst_StateMachine::setTableDataUpdatesObjectNames() { std::unique_ptr stateMachine1( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/emptylog.scxml"))); const QString sm1ObjectName = stateMachine1->objectName(); std::unique_ptr stateMachine2( QScxmlStateMachine::fromFile(QString(":/tst_statemachine/eventoccurred.scxml"))); const QString sm2ObjectName = stateMachine2->objectName(); QCOMPARE_NE(sm1ObjectName, sm2ObjectName); std::unique_ptr sm( QScxmlStateMachine::fromFile(QString("not_a_real_file"))); QVERIFY(sm->objectName().isEmpty()); // no name set, so update object name sm->setTableData(stateMachine1->tableData()); QCOMPARE_EQ(sm->objectName(), sm1ObjectName); // object name already set, so do not update sm->setTableData(stateMachine2->tableData()); QCOMPARE_EQ(sm->objectName(), sm1ObjectName); // did not change } QTEST_MAIN(tst_StateMachine) #include "tst_statemachine.moc"