1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 Research In Motion |
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 <QDebug> |
30 | #include <QQmlEngine> |
31 | #include <QQmlComponent> |
32 | #include <QQmlContext> |
33 | #include <qqml.h> |
34 | #include <QMetaMethod> |
35 | #if QT_CONFIG(process) |
36 | #include <QProcess> |
37 | #endif |
38 | |
39 | #include "../../shared/util.h" |
40 | |
41 | class ExportedClass : public QObject |
42 | { |
43 | Q_OBJECT |
44 | Q_PROPERTY(int qmlObjectProp READ qmlObjectProp NOTIFY qmlObjectPropChanged) |
45 | Q_PROPERTY(int cppObjectProp READ cppObjectProp NOTIFY cppObjectPropChanged) |
46 | Q_PROPERTY(int unboundProp READ unboundProp NOTIFY unboundPropChanged) |
47 | Q_PROPERTY(int v8BindingProp READ v8BindingProp NOTIFY v8BindingPropChanged) |
48 | Q_PROPERTY(int v4BindingProp READ v4BindingProp NOTIFY v4BindingPropChanged) |
49 | Q_PROPERTY(int v4BindingProp2 READ v4BindingProp2 NOTIFY v4BindingProp2Changed) |
50 | Q_PROPERTY(int scriptBindingProp READ scriptBindingProp NOTIFY scriptBindingPropChanged) |
51 | public: |
52 | int qmlObjectPropConnections = 0; |
53 | int cppObjectPropConnections = 0; |
54 | int unboundPropConnections = 0; |
55 | int v8BindingPropConnections = 0; |
56 | int v4BindingPropConnections = 0; |
57 | int v4BindingProp2Connections = 0; |
58 | int scriptBindingPropConnections = 0; |
59 | int boundSignalConnections = 0; |
60 | int unusedSignalConnections = 0; |
61 | |
62 | ExportedClass() {} |
63 | |
64 | ~ExportedClass() |
65 | { |
66 | QCOMPARE(qmlObjectPropConnections, 0); |
67 | QCOMPARE(cppObjectPropConnections, 0); |
68 | QCOMPARE(unboundPropConnections, 0); |
69 | QCOMPARE(v8BindingPropConnections, 0); |
70 | QCOMPARE(v4BindingPropConnections, 0); |
71 | QCOMPARE(v4BindingProp2Connections, 0); |
72 | QCOMPARE(scriptBindingPropConnections, 0); |
73 | QCOMPARE(boundSignalConnections, 0); |
74 | QCOMPARE(unusedSignalConnections, 0); |
75 | } |
76 | |
77 | int qmlObjectProp() const { return 42; } |
78 | int unboundProp() const { return 42; } |
79 | int v8BindingProp() const { return 42; } |
80 | int v4BindingProp() const { return 42; } |
81 | int v4BindingProp2() const { return 42; } |
82 | int cppObjectProp() const { return 42; } |
83 | int scriptBindingProp() const { return 42; } |
84 | |
85 | void verifyReceiverCount() |
86 | { |
87 | //Note: QTBUG-34829 means we can't call this from within disconnectNotify or it can lock |
88 | QCOMPARE(receivers(SIGNAL(qmlObjectPropChanged())), qmlObjectPropConnections); |
89 | QCOMPARE(receivers(SIGNAL(cppObjectPropChanged())), cppObjectPropConnections); |
90 | QCOMPARE(receivers(SIGNAL(unboundPropChanged())), unboundPropConnections); |
91 | QCOMPARE(receivers(SIGNAL(v8BindingPropChanged())), v8BindingPropConnections); |
92 | QCOMPARE(receivers(SIGNAL(v4BindingPropChanged())), v4BindingPropConnections); |
93 | QCOMPARE(receivers(SIGNAL(v4BindingProp2Changed())), v4BindingProp2Connections); |
94 | QCOMPARE(receivers(SIGNAL(scriptBindingPropChanged())), scriptBindingPropConnections); |
95 | QCOMPARE(receivers(SIGNAL(boundSignal())), boundSignalConnections); |
96 | QCOMPARE(receivers(SIGNAL(unusedSignal())), unusedSignalConnections); |
97 | } |
98 | |
99 | protected: |
100 | void connectNotify(const QMetaMethod &signal) override { |
101 | if (signal.name() == "qmlObjectPropChanged" ) qmlObjectPropConnections++; |
102 | if (signal.name() == "cppObjectPropChanged" ) cppObjectPropConnections++; |
103 | if (signal.name() == "unboundPropChanged" ) unboundPropConnections++; |
104 | if (signal.name() == "v8BindingPropChanged" ) v8BindingPropConnections++; |
105 | if (signal.name() == "v4BindingPropChanged" ) v4BindingPropConnections++; |
106 | if (signal.name() == "v4BindingProp2Changed" ) v4BindingProp2Connections++; |
107 | if (signal.name() == "scriptBindingPropChanged" ) scriptBindingPropConnections++; |
108 | if (signal.name() == "boundSignal" ) boundSignalConnections++; |
109 | if (signal.name() == "unusedSignal" ) unusedSignalConnections++; |
110 | verifyReceiverCount(); |
111 | //qDebug() << Q_FUNC_INFO << this << signal.name(); |
112 | } |
113 | |
114 | void disconnectNotify(const QMetaMethod &signal) override { |
115 | if (signal.name() == "qmlObjectPropChanged" ) qmlObjectPropConnections--; |
116 | if (signal.name() == "cppObjectPropChanged" ) cppObjectPropConnections--; |
117 | if (signal.name() == "unboundPropChanged" ) unboundPropConnections--; |
118 | if (signal.name() == "v8BindingPropChanged" ) v8BindingPropConnections--; |
119 | if (signal.name() == "v4BindingPropChanged" ) v4BindingPropConnections--; |
120 | if (signal.name() == "v4BindingProp2Changed" ) v4BindingProp2Connections--; |
121 | if (signal.name() == "scriptBindingPropChanged" ) scriptBindingPropConnections--; |
122 | if (signal.name() == "boundSignal" ) boundSignalConnections--; |
123 | if (signal.name() == "unusedSignal" ) unusedSignalConnections--; |
124 | //qDebug() << Q_FUNC_INFO << this << signal.methodSignature(); |
125 | } |
126 | |
127 | signals: |
128 | void qmlObjectPropChanged(); |
129 | void cppObjectPropChanged(); |
130 | void unboundPropChanged(); |
131 | void v8BindingPropChanged(); |
132 | void v4BindingPropChanged(); |
133 | void v4BindingProp2Changed(); |
134 | void scriptBindingPropChanged(); |
135 | void boundSignal(); |
136 | void unusedSignal(); |
137 | }; |
138 | |
139 | class tst_qqmlnotifier : public QQmlDataTest |
140 | { |
141 | Q_OBJECT |
142 | public: |
143 | tst_qqmlnotifier() {} |
144 | |
145 | private slots: |
146 | void initTestCase() override; |
147 | void cleanupTestCase(); |
148 | void testConnectNotify(); |
149 | |
150 | void removeV4Binding(); |
151 | void removeV4Binding2(); |
152 | void removeV8Binding(); |
153 | void removeScriptBinding(); |
154 | // No need to test value type proxy bindings - the user can't override disconnectNotify() anyway, |
155 | // as the classes are private to the QML engine |
156 | |
157 | void readProperty(); |
158 | void propertyChange(); |
159 | void disconnectOnDestroy(); |
160 | void lotsOfBindings(); |
161 | |
162 | void deleteFromHandler(); |
163 | |
164 | private: |
165 | void createObjects(); |
166 | |
167 | QQmlEngine engine; |
168 | QObject *root = nullptr; |
169 | ExportedClass *exportedClass = nullptr; |
170 | ExportedClass *exportedObject = nullptr; |
171 | }; |
172 | |
173 | void tst_qqmlnotifier::initTestCase() |
174 | { |
175 | QQmlDataTest::initTestCase(); |
176 | qmlRegisterType<ExportedClass>(uri: "Test" , versionMajor: 1, versionMinor: 0, qmlName: "ExportedClass" ); |
177 | } |
178 | |
179 | void tst_qqmlnotifier::createObjects() |
180 | { |
181 | delete root; |
182 | root = nullptr; |
183 | exportedClass = exportedObject = nullptr; |
184 | |
185 | QQmlComponent component(&engine, testFileUrl(fileName: "connectnotify.qml" )); |
186 | exportedObject = new ExportedClass(); |
187 | exportedObject->setObjectName("exportedObject" ); |
188 | root = component.createWithInitialProperties(initialProperties: {{"exportedObject" , QVariant::fromValue(value: exportedObject)}}); |
189 | QVERIFY(root != nullptr); |
190 | |
191 | exportedClass = qobject_cast<ExportedClass *>( |
192 | object: root->findChild<ExportedClass*>(aName: "exportedClass" )); |
193 | QVERIFY(exportedClass != nullptr); |
194 | exportedClass->verifyReceiverCount(); |
195 | } |
196 | |
197 | void tst_qqmlnotifier::cleanupTestCase() |
198 | { |
199 | delete root; |
200 | root = nullptr; |
201 | delete exportedObject; |
202 | exportedObject = nullptr; |
203 | } |
204 | |
205 | void tst_qqmlnotifier::testConnectNotify() |
206 | { |
207 | createObjects(); |
208 | |
209 | QCOMPARE(exportedClass->qmlObjectPropConnections, 1); |
210 | QCOMPARE(exportedClass->cppObjectPropConnections, 0); |
211 | QCOMPARE(exportedClass->unboundPropConnections, 0); |
212 | QCOMPARE(exportedClass->v8BindingPropConnections, 1); |
213 | QCOMPARE(exportedClass->v4BindingPropConnections, 1); |
214 | QCOMPARE(exportedClass->v4BindingProp2Connections, 2); |
215 | QCOMPARE(exportedClass->scriptBindingPropConnections, 1); |
216 | QCOMPARE(exportedClass->boundSignalConnections, 1); |
217 | QCOMPARE(exportedClass->unusedSignalConnections, 0); |
218 | exportedClass->verifyReceiverCount(); |
219 | |
220 | QCOMPARE(exportedObject->qmlObjectPropConnections, 0); |
221 | QCOMPARE(exportedObject->cppObjectPropConnections, 1); |
222 | QCOMPARE(exportedObject->unboundPropConnections, 0); |
223 | QCOMPARE(exportedObject->v8BindingPropConnections, 0); |
224 | QCOMPARE(exportedObject->v4BindingPropConnections, 0); |
225 | QCOMPARE(exportedObject->v4BindingProp2Connections, 0); |
226 | QCOMPARE(exportedObject->scriptBindingPropConnections, 0); |
227 | QCOMPARE(exportedObject->boundSignalConnections, 0); |
228 | QCOMPARE(exportedObject->unusedSignalConnections, 0); |
229 | exportedObject->verifyReceiverCount(); |
230 | } |
231 | |
232 | void tst_qqmlnotifier::removeV4Binding() |
233 | { |
234 | createObjects(); |
235 | |
236 | // Removing a binding should disconnect all of its guarded properties |
237 | QVERIFY(QMetaObject::invokeMethod(root, "removeV4Binding" )); |
238 | QCOMPARE(exportedClass->v4BindingPropConnections, 0); |
239 | exportedClass->verifyReceiverCount(); |
240 | } |
241 | |
242 | void tst_qqmlnotifier::removeV4Binding2() |
243 | { |
244 | createObjects(); |
245 | |
246 | // In this case, the v4BindingProp2 property is used by two v4 bindings. |
247 | // Make sure that removing one binding doesn't by accident disconnect all. |
248 | QVERIFY(QMetaObject::invokeMethod(root, "removeV4Binding2" )); |
249 | QCOMPARE(exportedClass->v4BindingProp2Connections, 1); |
250 | exportedClass->verifyReceiverCount(); |
251 | } |
252 | |
253 | void tst_qqmlnotifier::removeV8Binding() |
254 | { |
255 | createObjects(); |
256 | |
257 | // Removing a binding should disconnect all of its guarded properties |
258 | QVERIFY(QMetaObject::invokeMethod(root, "removeV8Binding" )); |
259 | QCOMPARE(exportedClass->v8BindingPropConnections, 0); |
260 | exportedClass->verifyReceiverCount(); |
261 | } |
262 | |
263 | void tst_qqmlnotifier::removeScriptBinding() |
264 | { |
265 | createObjects(); |
266 | |
267 | // Removing a binding should disconnect all of its guarded properties |
268 | QVERIFY(QMetaObject::invokeMethod(root, "removeScriptBinding" )); |
269 | QCOMPARE(exportedClass->scriptBindingPropConnections, 0); |
270 | exportedClass->verifyReceiverCount(); |
271 | } |
272 | |
273 | void tst_qqmlnotifier::readProperty() |
274 | { |
275 | createObjects(); |
276 | |
277 | // Reading a property should not connect to it |
278 | QVERIFY(QMetaObject::invokeMethod(root, "readProperty" )); |
279 | QCOMPARE(exportedClass->unboundPropConnections, 0); |
280 | exportedClass->verifyReceiverCount(); |
281 | } |
282 | |
283 | void tst_qqmlnotifier::propertyChange() |
284 | { |
285 | createObjects(); |
286 | |
287 | // Changing the state will trigger the PropertyChange to overwrite a value with a binding. |
288 | // For this, the new binding needs to be connected, and afterwards disconnected. |
289 | QVERIFY(QMetaObject::invokeMethod(root, "changeState" )); |
290 | QCOMPARE(exportedClass->unboundPropConnections, 1); |
291 | exportedClass->verifyReceiverCount(); |
292 | QVERIFY(QMetaObject::invokeMethod(root, "changeState" )); |
293 | QCOMPARE(exportedClass->unboundPropConnections, 0); |
294 | exportedClass->verifyReceiverCount(); |
295 | } |
296 | |
297 | void tst_qqmlnotifier::disconnectOnDestroy() |
298 | { |
299 | createObjects(); |
300 | |
301 | // Deleting a QML object should remove all connections. For exportedClass, this is tested in |
302 | // the destructor, and for exportedObject, it is tested below. |
303 | delete root; |
304 | root = nullptr; |
305 | QCOMPARE(exportedObject->cppObjectPropConnections, 0); |
306 | exportedObject->verifyReceiverCount(); |
307 | } |
308 | |
309 | class TestObject : public QObject |
310 | { |
311 | Q_OBJECT |
312 | Q_PROPERTY(int a READ a NOTIFY aChanged) |
313 | |
314 | public: |
315 | int a() const { return 0; } |
316 | |
317 | signals: |
318 | void aChanged(); |
319 | }; |
320 | |
321 | void tst_qqmlnotifier::lotsOfBindings() |
322 | { |
323 | TestObject o; |
324 | QQmlEngine *e = new QQmlEngine; |
325 | |
326 | qmlRegisterSingletonInstance(uri: "Test" , versionMajor: 1, versionMinor: 0, typeName: "Test" , cppObject: &o); |
327 | |
328 | QList<QQmlComponent *> components; |
329 | for (int i = 0; i < 20000; ++i) { |
330 | QQmlComponent *component = new QQmlComponent(e); |
331 | component->setData("import QtQuick 2.0; import Test 1.0; Item {width: Test.a; }" , baseUrl: QUrl()); |
332 | component->create(context: e->rootContext()); |
333 | components.append(t: component); |
334 | } |
335 | |
336 | o.aChanged(); |
337 | |
338 | qDeleteAll(c: components); |
339 | delete e; |
340 | } |
341 | |
342 | void tst_qqmlnotifier::deleteFromHandler() |
343 | { |
344 | #ifdef Q_OS_ANDROID |
345 | QSKIP("Android seems to have problems with QProcess" ); |
346 | #endif |
347 | #if !QT_CONFIG(process) |
348 | QSKIP("Need QProcess support to test qFatal." ); |
349 | #else |
350 | if (qEnvironmentVariableIsSet(varName: "TST_QQMLNOTIFIER_DO_CRASH" )) { |
351 | QQmlEngine engine; |
352 | QQmlComponent component(&engine, testFileUrl(fileName: "objectRenamer.qml" )); |
353 | QPointer<QObject> mess = component.create(); |
354 | QObject::connect(sender: mess.data(), signal: &QObject::objectNameChanged, slot: [&]() { delete mess; }); |
355 | QTRY_VERIFY(mess.isNull()); // BANG! |
356 | } else { |
357 | QProcess process; |
358 | QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); |
359 | env.insert(name: "TST_QQMLNOTIFIER_DO_CRASH" , value: "bang" ); |
360 | process.setProcessEnvironment(env); |
361 | process.setProgram(QCoreApplication::applicationFilePath()); |
362 | process.setArguments({"deleteFromHandler" }); |
363 | process.start(); |
364 | QTRY_COMPARE(process.exitStatus(), QProcess::CrashExit); |
365 | const QByteArray output = process.readAllStandardOutput(); |
366 | QVERIFY(output.contains("QFATAL" )); |
367 | QVERIFY(output.contains("destroyed while one of its QML signal handlers is in progress" )); |
368 | } |
369 | #endif |
370 | } |
371 | |
372 | QTEST_MAIN(tst_qqmlnotifier) |
373 | |
374 | #include "tst_qqmlnotifier.moc" |
375 | |