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 test suite 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 | #include <qtest.h> |
29 | #include <QtCore/qdebug.h> |
30 | #include <QtCore/qtimer.h> |
31 | #include <QtCore/qdir.h> |
32 | #include <QtCore/qfileinfo.h> |
33 | #include <QtCore/qregularexpression.h> |
34 | #include <QtQml/qjsengine.h> |
35 | |
36 | #include <QtQml/qqmlcomponent.h> |
37 | #include <QtQml/qqmlengine.h> |
38 | |
39 | #include <private/qquickworkerscript_p.h> |
40 | #include <private/qqmlengine_p.h> |
41 | #include "../../shared/util.h" |
42 | |
43 | class tst_QQuickWorkerScript : public QQmlDataTest |
44 | { |
45 | Q_OBJECT |
46 | public: |
47 | tst_QQuickWorkerScript() {} |
48 | private slots: |
49 | void source(); |
50 | void ready(); |
51 | void messaging(); |
52 | void messaging_data(); |
53 | void messaging_sendQObjectList(); |
54 | void messaging_sendJsObject(); |
55 | void messaging_sendExternalObject(); |
56 | void script_with_pragma(); |
57 | void script_included(); |
58 | void scriptError_onLoad(); |
59 | void scriptError_onCall(); |
60 | void script_function(); |
61 | void script_var(); |
62 | void stressDispose(); |
63 | void xmlHttpRequest(); |
64 | |
65 | private: |
66 | void waitForEchoMessage(QQuickWorkerScript *worker) { |
67 | QEventLoop loop; |
68 | QVERIFY(connect(worker, SIGNAL(done()), &loop, SLOT(quit()))); |
69 | QTimer timer; |
70 | timer.setSingleShot(true); |
71 | connect(sender: &timer, SIGNAL(timeout()), receiver: &loop, SLOT(quit())); |
72 | timer.start(msec: 10000); |
73 | loop.exec(); |
74 | QVERIFY(timer.isActive()); |
75 | } |
76 | |
77 | QQmlEngine m_engine; |
78 | }; |
79 | |
80 | void tst_QQuickWorkerScript::source() |
81 | { |
82 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker.qml" )); |
83 | QScopedPointer<QQuickWorkerScript>worker(qobject_cast<QQuickWorkerScript*>(object: component.create())); |
84 | QVERIFY(worker != nullptr); |
85 | const QMetaObject *mo = worker->metaObject(); |
86 | |
87 | QVariant value(100); |
88 | QVERIFY(QMetaObject::invokeMethod(worker.data(), "testSend" , Q_ARG(QVariant, value))); |
89 | waitForEchoMessage(worker: worker.data()); |
90 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker.data()).value<QVariant>(), value); |
91 | |
92 | QUrl source = testFileUrl(fileName: "script_fixed_return.js" ); |
93 | worker->setSource(source); |
94 | QCOMPARE(worker->source(), source); |
95 | QVERIFY(QMetaObject::invokeMethod(worker.data(), "testSend" , Q_ARG(QVariant, value))); |
96 | waitForEchoMessage(worker: worker.data()); |
97 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker.data()).value<QVariant>(), QVariant::fromValue(QString("Hello_World" ))); |
98 | |
99 | source = testFileUrl(fileName: "script_module.mjs" ); |
100 | worker->setSource(source); |
101 | QCOMPARE(worker->source(), source); |
102 | QVERIFY(QMetaObject::invokeMethod(worker.data(), "testSend" , Q_ARG(QVariant, value))); |
103 | waitForEchoMessage(worker: worker.data()); |
104 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker.data()).value<QVariant>(), QVariant::fromValue(QString("Hello from the module" ))); |
105 | |
106 | qApp->processEvents(); |
107 | } |
108 | |
109 | void tst_QQuickWorkerScript::ready() |
110 | { |
111 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker.qml" )); |
112 | QScopedPointer<QQuickWorkerScript>worker(qobject_cast<QQuickWorkerScript*>(object: component.create())); |
113 | QVERIFY(worker != nullptr); |
114 | |
115 | const QMetaObject *mo = worker->metaObject(); |
116 | |
117 | QTRY_VERIFY(worker->ready()); |
118 | |
119 | QVariant readyChangedCalled = mo->property(index: mo->indexOfProperty(name: "readyChangedCalled" )).read(obj: worker.data()).value<QVariant>(); |
120 | |
121 | QVERIFY(!readyChangedCalled.isNull()); |
122 | QVERIFY(readyChangedCalled.toBool()); |
123 | } |
124 | |
125 | void tst_QQuickWorkerScript::messaging() |
126 | { |
127 | QFETCH(QVariant, value); |
128 | |
129 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker.qml" )); |
130 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
131 | QVERIFY(worker != nullptr); |
132 | |
133 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
134 | waitForEchoMessage(worker); |
135 | |
136 | const QMetaObject *mo = worker->metaObject(); |
137 | QVariant response = mo->property(index: mo->indexOfProperty(name: "response" )).read(obj: worker).value<QVariant>(); |
138 | if (response.userType() == qMetaTypeId<QJSValue>()) |
139 | response = response.value<QJSValue>().toVariant(); |
140 | |
141 | if (value.type() == QMetaType::QRegExp && response.type() == QMetaType::QRegularExpression) { |
142 | // toVariant() doesn't know if we want QRegExp or QRegularExpression. It always creates |
143 | // a QRegularExpression from a JavaScript regular expression. |
144 | const QRegularExpression responseRegExp = response.toRegularExpression(); |
145 | const QRegExp valueRegExp = value.toRegExp(); |
146 | QCOMPARE(responseRegExp.pattern(), valueRegExp.pattern()); |
147 | QCOMPARE(bool(responseRegExp.patternOptions() & QRegularExpression::CaseInsensitiveOption), |
148 | bool(valueRegExp.caseSensitivity() == Qt::CaseInsensitive)); |
149 | } else { |
150 | QCOMPARE(response, value); |
151 | } |
152 | |
153 | qApp->processEvents(); |
154 | delete worker; |
155 | } |
156 | |
157 | void tst_QQuickWorkerScript::messaging_data() |
158 | { |
159 | QTest::addColumn<QVariant>(name: "value" ); |
160 | |
161 | QTest::newRow(dataTag: "invalid" ) << QVariant(); |
162 | QTest::newRow(dataTag: "bool" ) << QVariant::fromValue(value: true); |
163 | QTest::newRow(dataTag: "int" ) << QVariant::fromValue(value: 1001); |
164 | QTest::newRow(dataTag: "real" ) << QVariant::fromValue(value: 10334.375); |
165 | QTest::newRow(dataTag: "string" ) << QVariant::fromValue(value: QString("More cheeeese, Gromit!" )); |
166 | QTest::newRow(dataTag: "variant list" ) << QVariant::fromValue(value: (QVariantList() << "a" << "b" << "c" )); |
167 | QTest::newRow(dataTag: "date time" ) << QVariant::fromValue(value: QDateTime::currentDateTime()); |
168 | QTest::newRow(dataTag: "regexp" ) << QVariant::fromValue(value: QRegExp("^\\d\\d?$" , Qt::CaseInsensitive, |
169 | QRegExp::RegExp2)); |
170 | QTest::newRow(dataTag: "regularexpression" ) << QVariant::fromValue(value: QRegularExpression( |
171 | "^\\d\\d?$" , QRegularExpression::CaseInsensitiveOption)); |
172 | QTest::newRow(dataTag: "url" ) << QVariant::fromValue(value: QUrl("http://example.com/foo/bar" )); |
173 | } |
174 | |
175 | void tst_QQuickWorkerScript::messaging_sendQObjectList() |
176 | { |
177 | // Not allowed to send QObjects other than QQmlListModelWorkerAgent |
178 | // instances. If objects are sent in a list, they will be sent as 'undefined' |
179 | // js values. |
180 | |
181 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker.qml" )); |
182 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
183 | QVERIFY(worker != nullptr); |
184 | |
185 | QVariantList objects; |
186 | for (int i=0; i<3; i++) |
187 | objects << QVariant::fromValue(value: new QObject(this)); |
188 | |
189 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, QVariant::fromValue(objects)))); |
190 | waitForEchoMessage(worker); |
191 | |
192 | const QMetaObject *mo = worker->metaObject(); |
193 | QVariantList result = mo->property(index: mo->indexOfProperty(name: "response" )).read(obj: worker).value<QVariantList>(); |
194 | QCOMPARE(result, (QVariantList() << QVariant() << QVariant() << QVariant())); |
195 | |
196 | qApp->processEvents(); |
197 | delete worker; |
198 | } |
199 | |
200 | void tst_QQuickWorkerScript::messaging_sendJsObject() |
201 | { |
202 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker.qml" )); |
203 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
204 | QVERIFY(worker != nullptr); |
205 | |
206 | // Properties are in alphabetical order to enable string-based comparison after |
207 | // QVariant roundtrip, since the properties will be stored in a QVariantMap. |
208 | QString jsObject = "{'haste': 1125, 'name': 'zyz', 'spell power': 3101}" ; |
209 | |
210 | QVariantMap map; |
211 | map.insert(key: "haste" , value: 1125); |
212 | map.insert(key: "name" , value: "zyz" ); |
213 | map.insert(key: "spell power" , value: 3101); |
214 | |
215 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, QVariant::fromValue(map)))); |
216 | waitForEchoMessage(worker); |
217 | |
218 | QVariant result = QVariant::fromValue(value: false); |
219 | QVERIFY(QMetaObject::invokeMethod(worker, "compareLiteralResponse" , Qt::DirectConnection, |
220 | Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, jsObject))); |
221 | QVERIFY(result.toBool()); |
222 | |
223 | qApp->processEvents(); |
224 | delete worker; |
225 | } |
226 | |
227 | void tst_QQuickWorkerScript::messaging_sendExternalObject() |
228 | { |
229 | QQmlComponent component(&m_engine, testFileUrl(fileName: "externalObjectWorker.qml" )); |
230 | QObject *obj = component.create(); |
231 | QVERIFY(obj); |
232 | QMetaObject::invokeMethod(obj, member: "testExternalObject" ); |
233 | QTest::qWait(ms: 100); // shouldn't crash. |
234 | delete obj; |
235 | } |
236 | |
237 | void tst_QQuickWorkerScript::script_with_pragma() |
238 | { |
239 | QVariant value(100); |
240 | |
241 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_pragma.qml" )); |
242 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
243 | QVERIFY(worker != nullptr); |
244 | |
245 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
246 | waitForEchoMessage(worker); |
247 | |
248 | const QMetaObject *mo = worker->metaObject(); |
249 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker).value<QVariant>(), value); |
250 | |
251 | qApp->processEvents(); |
252 | delete worker; |
253 | } |
254 | |
255 | void tst_QQuickWorkerScript::script_included() |
256 | { |
257 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_include.qml" )); |
258 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
259 | QVERIFY(worker != nullptr); |
260 | |
261 | QString value("Hello" ); |
262 | |
263 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
264 | waitForEchoMessage(worker); |
265 | |
266 | const QMetaObject *mo = worker->metaObject(); |
267 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker).toString(), value + " World" ); |
268 | |
269 | qApp->processEvents(); |
270 | delete worker; |
271 | } |
272 | |
273 | static QString qquickworkerscript_lastWarning; |
274 | static void qquickworkerscript_warningsHandler(QtMsgType type, const QMessageLogContext &, const QString &msg) |
275 | { |
276 | if (type == QtWarningMsg) |
277 | qquickworkerscript_lastWarning = msg; |
278 | } |
279 | |
280 | void tst_QQuickWorkerScript::scriptError_onLoad() |
281 | { |
282 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_error_onLoad.qml" )); |
283 | |
284 | QtMessageHandler previousMsgHandler = qInstallMessageHandler(qquickworkerscript_warningsHandler); |
285 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
286 | QVERIFY(worker != nullptr); |
287 | |
288 | QTRY_COMPARE(qquickworkerscript_lastWarning, |
289 | testFileUrl("script_error_onLoad.js" ).toString() + QLatin1String(":3:10: SyntaxError: Expected token `,'" )); |
290 | |
291 | qInstallMessageHandler(previousMsgHandler); |
292 | qApp->processEvents(); |
293 | delete worker; |
294 | } |
295 | |
296 | void tst_QQuickWorkerScript::scriptError_onCall() |
297 | { |
298 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_error_onCall.qml" )); |
299 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
300 | QVERIFY(worker != nullptr); |
301 | |
302 | QtMessageHandler previousMsgHandler = qInstallMessageHandler(qquickworkerscript_warningsHandler); |
303 | QVariant value; |
304 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
305 | |
306 | QTRY_COMPARE(qquickworkerscript_lastWarning, |
307 | testFileUrl("script_error_onCall.js" ).toString() + QLatin1String(":4: ReferenceError: getData is not defined" )); |
308 | |
309 | qInstallMessageHandler(previousMsgHandler); |
310 | qApp->processEvents(); |
311 | delete worker; |
312 | } |
313 | |
314 | void tst_QQuickWorkerScript::script_function() |
315 | { |
316 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_function.qml" )); |
317 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
318 | QVERIFY(worker != nullptr); |
319 | |
320 | QString value("Hello" ); |
321 | |
322 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
323 | waitForEchoMessage(worker); |
324 | |
325 | const QMetaObject *mo = worker->metaObject(); |
326 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker).toString(), value + " World" ); |
327 | |
328 | qApp->processEvents(); |
329 | delete worker; |
330 | } |
331 | |
332 | void tst_QQuickWorkerScript::script_var() |
333 | { |
334 | QQmlComponent component(&m_engine, testFileUrl(fileName: "worker_var.qml" )); |
335 | QQuickWorkerScript *worker = qobject_cast<QQuickWorkerScript*>(object: component.create()); |
336 | QVERIFY(worker != nullptr); |
337 | |
338 | QString value("Hello" ); |
339 | |
340 | QVERIFY(QMetaObject::invokeMethod(worker, "testSend" , Q_ARG(QVariant, value))); |
341 | waitForEchoMessage(worker); |
342 | |
343 | const QMetaObject *mo = worker->metaObject(); |
344 | QCOMPARE(mo->property(mo->indexOfProperty("response" )).read(worker).toString(), value + " World" ); |
345 | |
346 | qApp->processEvents(); |
347 | delete worker; |
348 | } |
349 | |
350 | // Rapidly create and destroy worker scripts to test resources are being disposed |
351 | // in the correct isolate |
352 | void tst_QQuickWorkerScript::stressDispose() |
353 | { |
354 | for (int ii = 0; ii < 100; ++ii) { |
355 | QQmlEngine engine; |
356 | QQmlComponent component(&engine, testFileUrl(fileName: "stressDispose.qml" )); |
357 | QObject *o = component.create(); |
358 | QVERIFY(o); |
359 | delete o; |
360 | } |
361 | } |
362 | |
363 | void tst_QQuickWorkerScript::xmlHttpRequest() |
364 | { |
365 | QQmlComponent component(&m_engine, testFileUrl(fileName: "xmlHttpRequest.qml" )); |
366 | QScopedPointer<QObject> root{component.create()}; // should not crash |
367 | QVERIFY(root); |
368 | } |
369 | |
370 | QTEST_MAIN(tst_QQuickWorkerScript) |
371 | |
372 | #include "tst_qquickworkerscript.moc" |
373 | |