1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtScxml module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtTest> |
30 | #include <QObject> |
31 | #include <QXmlStreamReader> |
32 | #include <QtScxml/qscxmlcompiler.h> |
33 | #include <QtScxml/qscxmlstatemachine.h> |
34 | #include <QtScxml/qscxmlinvokableservice.h> |
35 | #include <QtScxml/private/qscxmlstatemachine_p.h> |
36 | |
37 | enum { SpyWaitTime = 8000 }; |
38 | |
39 | class tst_StateMachine: public QObject |
40 | { |
41 | Q_OBJECT |
42 | |
43 | private Q_SLOTS: |
44 | void stateNames_data(); |
45 | void stateNames(); |
46 | void activeStateNames_data(); |
47 | void activeStateNames(); |
48 | void connections(); |
49 | void historyState(); |
50 | void onExit(); |
51 | void eventOccurred(); |
52 | |
53 | void doneDotStateEvent(); |
54 | void running(); |
55 | |
56 | void invokeStateMachine(); |
57 | |
58 | void multipleInvokableServices(); // QTBUG-61484 |
59 | void logWithoutExpr(); |
60 | }; |
61 | |
62 | void tst_StateMachine::stateNames_data() |
63 | { |
64 | QTest::addColumn<QString>(name: "scxmlFileName" ); |
65 | QTest::addColumn<bool>(name: "compressed" ); |
66 | QTest::addColumn<QStringList>(name: "expectedStates" ); |
67 | |
68 | QTest::newRow(dataTag: "stateNames-compressed" ) << QString(":/tst_statemachine/statenames.scxml" ) |
69 | << true |
70 | << (QStringList() << QString("a1" ) << QString("a2" ) << QString("final" )); |
71 | QTest::newRow(dataTag: "stateNames-notCompressed" ) << QString(":/tst_statemachine/statenames.scxml" ) |
72 | << false |
73 | << (QStringList() << QString("top" ) << QString("a" ) << QString("a1" ) << QString("a2" ) << QString("b" ) << QString("final" )); |
74 | QTest::newRow(dataTag: "stateNamesNested-compressed" ) << QString(":/tst_statemachine/statenamesnested.scxml" ) |
75 | << true |
76 | << (QStringList() << QString("a" ) << QString("b" )); |
77 | QTest::newRow(dataTag: "stateNamesNested-notCompressed" ) << QString(":/tst_statemachine/statenamesnested.scxml" ) |
78 | << false |
79 | << (QStringList() << QString("super_top" ) << QString("a" ) << QString("b" )); |
80 | |
81 | QTest::newRow(dataTag: "ids1" ) << QString(":/tst_statemachine/ids1.scxml" ) |
82 | << false |
83 | << (QStringList() << QString("foo.bar" ) << QString("foo-bar" ) |
84 | << QString("foo_bar" ) << QString("_" )); |
85 | } |
86 | |
87 | void tst_StateMachine::stateNames() |
88 | { |
89 | QFETCH(QString, scxmlFileName); |
90 | QFETCH(bool, compressed); |
91 | QFETCH(QStringList, expectedStates); |
92 | |
93 | QScopedPointer<QScxmlStateMachine> stateMachine(QScxmlStateMachine::fromFile(fileName: scxmlFileName)); |
94 | QVERIFY(!stateMachine.isNull()); |
95 | QCOMPARE(stateMachine->parseErrors().count(), 0); |
96 | |
97 | QCOMPARE(stateMachine->stateNames(compressed), expectedStates); |
98 | } |
99 | |
100 | void tst_StateMachine::activeStateNames_data() |
101 | { |
102 | QTest::addColumn<QString>(name: "scxmlFileName" ); |
103 | QTest::addColumn<bool>(name: "compressed" ); |
104 | QTest::addColumn<QStringList>(name: "expectedStates" ); |
105 | |
106 | QTest::newRow(dataTag: "stateNames-compressed" ) << QString(":/tst_statemachine/statenames.scxml" ) |
107 | << true |
108 | << (QStringList() << QString("a1" ) << QString("final" )); |
109 | QTest::newRow(dataTag: "stateNames-notCompressed" ) << QString(":/tst_statemachine/statenames.scxml" ) |
110 | << false |
111 | << (QStringList() << QString("top" ) << QString("a" ) << QString("a1" ) << QString("b" ) << QString("final" )); |
112 | QTest::newRow(dataTag: "stateNamesNested-compressed" ) << QString(":/tst_statemachine/statenamesnested.scxml" ) |
113 | << true |
114 | << (QStringList() << QString("a" ) << QString("b" )); |
115 | QTest::newRow(dataTag: "stateNamesNested-notCompressed" ) << QString(":/tst_statemachine/statenamesnested.scxml" ) |
116 | << false |
117 | << (QStringList() << QString("super_top" ) << QString("a" ) << QString("b" )); |
118 | } |
119 | |
120 | void tst_StateMachine::activeStateNames() |
121 | { |
122 | QFETCH(QString, scxmlFileName); |
123 | QFETCH(bool, compressed); |
124 | QFETCH(QStringList, expectedStates); |
125 | |
126 | QScopedPointer<QScxmlStateMachine> stateMachine(QScxmlStateMachine::fromFile(fileName: scxmlFileName)); |
127 | QVERIFY(!stateMachine.isNull()); |
128 | |
129 | QSignalSpy stableStateSpy(stateMachine.data(), SIGNAL(reachedStableState())); |
130 | |
131 | stateMachine->start(); |
132 | |
133 | stableStateSpy.wait(timeout: 5000); |
134 | |
135 | QCOMPARE(stateMachine->activeStateNames(compressed), expectedStates); |
136 | } |
137 | |
138 | class Receiver : public QObject { |
139 | Q_OBJECT |
140 | public slots: |
141 | void a(bool enabled) |
142 | { |
143 | aReached = aReached || enabled; |
144 | } |
145 | |
146 | void b(bool enabled) |
147 | { |
148 | bReached = bReached || enabled; |
149 | } |
150 | |
151 | void aEnter() |
152 | { |
153 | aEntered = true; |
154 | } |
155 | |
156 | void aExit() |
157 | { |
158 | aExited = true; |
159 | } |
160 | |
161 | public: |
162 | bool aReached = false; |
163 | bool bReached = false; |
164 | bool aEntered = false; |
165 | bool aExited = false; |
166 | }; |
167 | |
168 | void tst_StateMachine::connections() |
169 | { |
170 | QScopedPointer<QScxmlStateMachine> stateMachine( |
171 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/statenames.scxml" ))); |
172 | QVERIFY(!stateMachine.isNull()); |
173 | Receiver receiver; |
174 | |
175 | bool a1Reached = false; |
176 | bool finalReached = false; |
177 | QMetaObject::Connection a = stateMachine->connectToState(scxmlStateName: "a" , receiver: &receiver, method: &Receiver::a); |
178 | QVERIFY(a); |
179 | QMetaObject::Connection b = stateMachine->connectToState(scxmlStateName: "b" , receiver: &receiver, SLOT(b(bool))); |
180 | QVERIFY(b); |
181 | QMetaObject::Connection a1 = stateMachine->connectToState(scxmlStateName: "a1" , context: &receiver, |
182 | functor: [&a1Reached](bool enabled) { |
183 | a1Reached = a1Reached || enabled; |
184 | }); |
185 | QVERIFY(a1); |
186 | QMetaObject::Connection final = stateMachine->connectToState(scxmlStateName: "final" , |
187 | functor: [&finalReached](bool enabled) { |
188 | finalReached = finalReached || enabled; |
189 | }); |
190 | QVERIFY(final); |
191 | |
192 | bool a1Entered = false; |
193 | bool a1Exited = false; |
194 | bool finalEntered = false; |
195 | bool finalExited = false; |
196 | typedef QScxmlStateMachine QXSM; |
197 | |
198 | QMetaObject::Connection aEntry = stateMachine->connectToState( |
199 | scxmlStateName: "a" , functor: QXSM::onEntry(receiver: &receiver, method: &Receiver::aEnter)); |
200 | QVERIFY(aEntry); |
201 | QMetaObject::Connection aExit = stateMachine->connectToState( |
202 | scxmlStateName: "a" , functor: QXSM::onExit(receiver: &receiver, method: &Receiver::aExit)); |
203 | QVERIFY(aExit); |
204 | QMetaObject::Connection a1Entry = stateMachine->connectToState(scxmlStateName: "a1" , context: &receiver, |
205 | functor: QXSM::onEntry(functor: [&a1Entered]() { |
206 | a1Entered = true; |
207 | })); |
208 | QVERIFY(a1Entry); |
209 | QMetaObject::Connection a1Exit = stateMachine->connectToState(scxmlStateName: "a1" , context: &receiver, |
210 | functor: QXSM::onExit(functor: [&a1Exited]() { |
211 | a1Exited = true; |
212 | })); |
213 | QVERIFY(a1Exit); |
214 | |
215 | QMetaObject::Connection finalEntry = stateMachine->connectToState( |
216 | scxmlStateName: "final" , functor: QXSM::onEntry(functor: [&finalEntered]() { |
217 | finalEntered = true; |
218 | })); |
219 | QVERIFY(finalEntry); |
220 | |
221 | QMetaObject::Connection finalExit = stateMachine->connectToState( |
222 | scxmlStateName: "final" , functor: QXSM::onExit(functor: [&finalExited]() { |
223 | finalExited = true; |
224 | })); |
225 | QVERIFY(finalExit); |
226 | |
227 | stateMachine->start(); |
228 | |
229 | QTRY_VERIFY(a1Reached); |
230 | QTRY_VERIFY(finalReached); |
231 | QTRY_VERIFY(receiver.aReached); |
232 | QTRY_VERIFY(receiver.bReached); |
233 | |
234 | QVERIFY(disconnect(a)); |
235 | QVERIFY(disconnect(b)); |
236 | QVERIFY(disconnect(a1)); |
237 | QVERIFY(disconnect(final)); |
238 | |
239 | #if defined(__cpp_return_type_deduction) && __cpp_return_type_deduction == 201304 |
240 | QVERIFY(receiver.aEntered); |
241 | QVERIFY(!receiver.aExited); |
242 | QVERIFY(a1Entered); |
243 | QVERIFY(!a1Exited); |
244 | QVERIFY(finalEntered); |
245 | QVERIFY(!finalExited); |
246 | |
247 | QVERIFY(disconnect(aEntry)); |
248 | QVERIFY(disconnect(aExit)); |
249 | QVERIFY(disconnect(a1Entry)); |
250 | QVERIFY(disconnect(a1Exit)); |
251 | QVERIFY(disconnect(finalEntry)); |
252 | QVERIFY(disconnect(finalExit)); |
253 | #endif |
254 | } |
255 | |
256 | void tst_StateMachine::historyState() |
257 | { |
258 | QScopedPointer<QScxmlStateMachine> stateMachine( |
259 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/historystate.scxml" ))); |
260 | QVERIFY(!stateMachine.isNull()); |
261 | |
262 | bool state2Reached = false; |
263 | QMetaObject::Connection state2Connection = stateMachine->connectToState(scxmlStateName: "State2" , |
264 | functor: [&state2Reached](bool enabled) { |
265 | state2Reached = state2Reached || enabled; |
266 | }); |
267 | QVERIFY(state2Connection); |
268 | |
269 | stateMachine->start(); |
270 | |
271 | QTRY_VERIFY(state2Reached); |
272 | } |
273 | |
274 | void tst_StateMachine::onExit() |
275 | { |
276 | #if defined(__cpp_return_type_deduction) && __cpp_return_type_deduction == 201304 |
277 | // Test onExit being actually called |
278 | |
279 | typedef QScxmlStateMachine QXSM; |
280 | QScopedPointer<QXSM> stateMachine(QXSM::fromFile(fileName: QString(":/tst_statemachine/eventoccurred.scxml" ))); |
281 | |
282 | Receiver receiver; |
283 | bool aExited1 = false; |
284 | |
285 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onExit(functor: [&aExited1]() { aExited1 = true; })); |
286 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onExit(receiver: &receiver, method: &Receiver::aExit)); |
287 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onExit(receiver: &receiver, method: "aEnter" )); |
288 | { |
289 | // Should not crash |
290 | Receiver receiver2; |
291 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onEntry(receiver: &receiver2, method: &Receiver::aEnter)); |
292 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onEntry(receiver: &receiver2, method: "aExit" )); |
293 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onExit(receiver: &receiver2, method: &Receiver::aExit)); |
294 | stateMachine->connectToState(scxmlStateName: "a" , functor: QXSM::onExit(receiver: &receiver2, method: "aEnter" )); |
295 | } |
296 | |
297 | stateMachine->start(); |
298 | QTRY_VERIFY(receiver.aEntered); |
299 | QTRY_VERIFY(receiver.aExited); |
300 | QTRY_VERIFY(aExited1); |
301 | #endif |
302 | } |
303 | |
304 | bool hasChildEventRouters(QScxmlStateMachine *stateMachine) |
305 | { |
306 | // Cast to QObject, to avoid ambigous "children" member. |
307 | const QObject &parentRouter = QScxmlStateMachinePrivate::get(t: stateMachine)->m_router; |
308 | return !parentRouter.children().isEmpty(); |
309 | } |
310 | |
311 | void tst_StateMachine::eventOccurred() |
312 | { |
313 | QScopedPointer<QScxmlStateMachine> stateMachine(QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/eventoccurred.scxml" ))); |
314 | QVERIFY(!stateMachine.isNull()); |
315 | |
316 | qRegisterMetaType<QScxmlEvent>(); |
317 | QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); |
318 | |
319 | int events = 0; |
320 | auto con1 = stateMachine->connectToEvent(scxmlEventSpec: "internalEvent2" , functor: [&events](const QScxmlEvent &event) { |
321 | QCOMPARE(++events, 1); |
322 | QCOMPARE(event.name(), QString("internalEvent2" )); |
323 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
324 | }); |
325 | QVERIFY(con1); |
326 | |
327 | auto con2 = stateMachine->connectToEvent(scxmlEventSpec: "externalEvent" , functor: [&events](const QScxmlEvent &event) { |
328 | QCOMPARE(++events, 2); |
329 | QCOMPARE(event.name(), QString("externalEvent" )); |
330 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
331 | }); |
332 | QVERIFY(con2); |
333 | |
334 | auto con3 = stateMachine->connectToEvent(scxmlEventSpec: "timeout" , functor: [&events](const QScxmlEvent &event) { |
335 | QCOMPARE(++events, 3); |
336 | QCOMPARE(event.name(), QString("timeout" )); |
337 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
338 | }); |
339 | QVERIFY(con3); |
340 | |
341 | auto con4 = stateMachine->connectToEvent(scxmlEventSpec: "done.*" , functor: [&events](const QScxmlEvent &event) { |
342 | QCOMPARE(++events, 4); |
343 | QCOMPARE(event.name(), QString("done.state.top" )); |
344 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
345 | }); |
346 | QVERIFY(con4); |
347 | |
348 | auto con5 = stateMachine->connectToEvent(scxmlEventSpec: "done.state" , functor: [&events](const QScxmlEvent &event) { |
349 | QCOMPARE(++events, 5); |
350 | QCOMPARE(event.name(), QString("done.state.top" )); |
351 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
352 | }); |
353 | QVERIFY(con5); |
354 | |
355 | auto con6 = stateMachine->connectToEvent(scxmlEventSpec: "done.state.top" , functor: [&events](const QScxmlEvent &event) { |
356 | QCOMPARE(++events, 6); |
357 | QCOMPARE(event.name(), QString("done.state.top" )); |
358 | QCOMPARE(event.eventType(), QScxmlEvent::ExternalEvent); |
359 | }); |
360 | QVERIFY(con6); |
361 | |
362 | stateMachine->start(); |
363 | |
364 | finishedSpy.wait(timeout: 5000); |
365 | QCOMPARE(events, 6); |
366 | |
367 | QVERIFY(disconnect(con1)); |
368 | QVERIFY(disconnect(con2)); |
369 | QVERIFY(disconnect(con3)); |
370 | QVERIFY(disconnect(con4)); |
371 | QVERIFY(disconnect(con5)); |
372 | QVERIFY(disconnect(con6)); |
373 | |
374 | QTRY_VERIFY(!hasChildEventRouters(stateMachine.data())); |
375 | } |
376 | |
377 | void tst_StateMachine::doneDotStateEvent() |
378 | { |
379 | QScopedPointer<QScxmlStateMachine> stateMachine(QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/stateDotDoneEvent.scxml" ))); |
380 | QVERIFY(!stateMachine.isNull()); |
381 | |
382 | QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); |
383 | |
384 | stateMachine->start(); |
385 | finishedSpy.wait(timeout: 5000); |
386 | QCOMPARE(finishedSpy.count(), 1); |
387 | QCOMPARE(stateMachine->activeStateNames(true).size(), 1); |
388 | QVERIFY(stateMachine->activeStateNames(true).contains(QLatin1String("success" ))); |
389 | } |
390 | |
391 | void tst_StateMachine::running() |
392 | { |
393 | QScopedPointer<QScxmlStateMachine> stateMachine( |
394 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/statenames.scxml" ))); |
395 | QVERIFY(!stateMachine.isNull()); |
396 | |
397 | QSignalSpy runningChangedSpy(stateMachine.data(), SIGNAL(runningChanged(bool))); |
398 | |
399 | QCOMPARE(stateMachine->isRunning(), false); |
400 | |
401 | stateMachine->start(); |
402 | |
403 | QCOMPARE(runningChangedSpy.count(), 1); |
404 | QCOMPARE(stateMachine->isRunning(), true); |
405 | |
406 | stateMachine->stop(); |
407 | |
408 | QCOMPARE(runningChangedSpy.count(), 2); |
409 | QCOMPARE(stateMachine->isRunning(), false); |
410 | } |
411 | |
412 | void tst_StateMachine::invokeStateMachine() |
413 | { |
414 | QScopedPointer<QScxmlStateMachine> stateMachine( |
415 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/invoke.scxml" ))); |
416 | QVERIFY(!stateMachine.isNull()); |
417 | |
418 | stateMachine->start(); |
419 | QCOMPARE(stateMachine->isRunning(), true); |
420 | QTRY_VERIFY(stateMachine->activeStateNames().contains(QString("anyplace" ))); |
421 | |
422 | QVector<QScxmlInvokableService *> services = stateMachine->invokedServices(); |
423 | QCOMPARE(services.length(), 1); |
424 | QVariant subMachineVariant = services[0]->property(name: "stateMachine" ); |
425 | QVERIFY(subMachineVariant.isValid()); |
426 | QScxmlStateMachine *subMachine = qvariant_cast<QScxmlStateMachine *>(v: subMachineVariant); |
427 | QVERIFY(subMachine); |
428 | QTRY_VERIFY(subMachine->activeStateNames().contains("here" )); |
429 | } |
430 | |
431 | void tst_StateMachine::multipleInvokableServices() |
432 | { |
433 | QScopedPointer<QScxmlStateMachine> stateMachine( |
434 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/multipleinvokableservices.scxml" ))); |
435 | QVERIFY(!stateMachine.isNull()); |
436 | |
437 | QSignalSpy finishedSpy(stateMachine.data(), SIGNAL(finished())); |
438 | stateMachine->start(); |
439 | QCOMPARE(stateMachine->isRunning(), true); |
440 | |
441 | finishedSpy.wait(timeout: 5000); |
442 | QCOMPARE(finishedSpy.count(), 1); |
443 | QCOMPARE(stateMachine->activeStateNames(true).size(), 1); |
444 | QVERIFY(stateMachine->activeStateNames(true).contains(QLatin1String("success" ))); |
445 | } |
446 | |
447 | void tst_StateMachine::logWithoutExpr() |
448 | { |
449 | QScopedPointer<QScxmlStateMachine> stateMachine( |
450 | QScxmlStateMachine::fromFile(fileName: QString(":/tst_statemachine/emptylog.scxml" ))); |
451 | QVERIFY(!stateMachine.isNull()); |
452 | QTest::ignoreMessage(type: QtDebugMsg, message: "\"Hi2\" : \"\"" ); |
453 | stateMachine->start(); |
454 | QSignalSpy logSpy(stateMachine.data(), SIGNAL(log(QString,QString))); |
455 | QTRY_COMPARE(logSpy.count(), 1); |
456 | } |
457 | |
458 | QTEST_MAIN(tst_StateMachine) |
459 | |
460 | #include "tst_statemachine.moc" |
461 | |
462 | |
463 | |