1/****************************************************************************
2**
3** Copyright (C) 2016 Canonical Limited and/or its subsidiary(-ies).
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
29#include <QtTest/QtTest>
30#include <QtQml/qqmlengine.h>
31#include <QtQml/qqmlfile.h>
32#include <QtQml/qqmlnetworkaccessmanagerfactory.h>
33#include <QtQuick/qquickview.h>
34#include <QtQuick/qquickitem.h>
35#if QT_CONFIG(process)
36#include <QtCore/qprocess.h>
37#endif
38#include <QtQml/private/qqmlengine_p.h>
39#include <QtQml/private/qqmltypedata_p.h>
40#include <QtQml/private/qqmltypeloader_p.h>
41#include "../../shared/testhttpserver.h"
42#include "../../shared/util.h"
43
44class tst_QQMLTypeLoader : public QQmlDataTest
45{
46 Q_OBJECT
47
48private slots:
49 void testLoadComplete();
50 void loadComponentSynchronously();
51 void trimCache();
52 void trimCache2();
53 void trimCache3();
54 void keepSingleton();
55 void keepRegistrations();
56 void intercept();
57 void redirect();
58 void qmlSingletonWithinModule();
59 void multiSingletonModule();
60 void implicitComponentModule();
61 void customDiskCachePath();
62 void qrcRootPathUrl();
63 void implicitImport();
64 void compositeSingletonCycle();
65 void declarativeCppType();
66 void circularDependency();
67};
68
69void tst_QQMLTypeLoader::testLoadComplete()
70{
71 QQuickView *window = new QQuickView();
72 window->engine()->addImportPath(QT_TESTCASE_BUILDDIR);
73 qDebug() << window->engine()->importPathList();
74 window->setGeometry(posx: 0,posy: 0,w: 240,h: 320);
75 window->setSource(testFileUrl(fileName: "test_load_complete.qml"));
76 window->show();
77 QVERIFY(QTest::qWaitForWindowExposed(window));
78
79 QObject *rootObject = window->rootObject();
80 QTRY_VERIFY(rootObject != nullptr);
81 QTRY_COMPARE(rootObject->property("created").toInt(), 2);
82 QTRY_COMPARE(rootObject->property("loaded").toInt(), 2);
83 delete window;
84}
85
86void tst_QQMLTypeLoader::loadComponentSynchronously()
87{
88 QQmlEngine engine;
89 QTest::ignoreMessage(type: QtWarningMsg, messagePattern: QRegularExpression(
90 QLatin1String(".*nonprotocol::1:1: QtObject is not a type.*")));
91 QQmlComponent component(&engine, testFileUrl(fileName: "load_synchronous.qml"));
92 QScopedPointer<QObject> o(component.create());
93 QVERIFY(o);
94}
95
96void tst_QQMLTypeLoader::trimCache()
97{
98 QQmlEngine engine;
99 QQmlTypeLoader &loader = QQmlEnginePrivate::get(e: &engine)->typeLoader;
100 QVector<QQmlTypeData *> releaseLater;
101 QVector<QV4::ExecutableCompilationUnit *> releaseCompilationUnitLater;
102 for (int i = 0; i < 256; ++i) {
103 QUrl url = testFileUrl(fileName: "trim_cache.qml");
104 url.setQuery(query: QString::number(i));
105
106 QQmlTypeData *data = loader.getType(unNormalizedUrl: url).take();
107 // Run an event loop to receive the callback that release()es.
108 QTRY_COMPARE(data->count(), 2);
109
110 // keep references to some of them so that they aren't trimmed. References to either the
111 // QQmlTypeData or its compiledData() should prevent the trimming.
112 if (i % 10 == 0) {
113 // keep ref on data, don't add ref on data->compiledData()
114 releaseLater.append(t: data);
115 } else if (i % 5 == 0) {
116 data->compilationUnit()->addref();
117 releaseCompilationUnitLater.append(t: data->compilationUnit());
118 data->release();
119 } else {
120 data->release();
121 }
122 }
123
124 for (int i = 0; i < 256; ++i) {
125 QUrl url = testFileUrl(fileName: "trim_cache.qml");
126 url.setQuery(query: QString::number(i));
127 if (i % 5 == 0)
128 QVERIFY(loader.isTypeLoaded(url));
129 else if (i < 128)
130 QVERIFY(!loader.isTypeLoaded(url));
131 // The cache is free to keep the others.
132 }
133
134 for (auto *data : qAsConst(t&: releaseCompilationUnitLater))
135 data->release();
136
137 for (auto *data : qAsConst(t&: releaseLater))
138 data->release();
139}
140
141void tst_QQMLTypeLoader::trimCache2()
142{
143 QScopedPointer<QQuickView> window(new QQuickView());
144 window->setSource(testFileUrl(fileName: "trim_cache2.qml"));
145 QQmlTypeLoader &loader = QQmlEnginePrivate::get(e: window->engine())->typeLoader;
146 // in theory if gc has already run this could be false
147 // QCOMPARE(loader.isTypeLoaded(testFileUrl("MyComponent2.qml")), true);
148 window->engine()->collectGarbage();
149 QTest::qWait(ms: 1); // force event loop
150 window->engine()->trimComponentCache();
151 QCOMPARE(loader.isTypeLoaded(testFileUrl("MyComponent2.qml")), false);
152}
153
154// test trimming the cache of an item that contains sub-items created via incubation
155void tst_QQMLTypeLoader::trimCache3()
156{
157 QScopedPointer<QQuickView> window(new QQuickView());
158 window->setSource(testFileUrl(fileName: "trim_cache3.qml"));
159 QQmlTypeLoader &loader = QQmlEnginePrivate::get(e: window->engine())->typeLoader;
160 QCOMPARE(loader.isTypeLoaded(testFileUrl("ComponentWithIncubator.qml")), true);
161
162 QQmlProperty::write(window->rootObject(), "source", QString());
163
164 // handle our deleteLater and cleanup
165 QCoreApplication::sendPostedEvents(receiver: nullptr, event_type: QEvent::DeferredDelete);
166 QCoreApplication::processEvents();
167 window->engine()->collectGarbage();
168
169 window->engine()->trimComponentCache();
170
171 QCOMPARE(loader.isTypeLoaded(testFileUrl("ComponentWithIncubator.qml")), false);
172}
173
174static void checkSingleton(const QString &dataDirectory)
175{
176 QQmlEngine engine;
177 engine.addImportPath(dir: dataDirectory);
178 QQmlComponent component(&engine);
179 component.setData("import ClusterDemo 1.0\n"
180 "import QtQuick 2.6\n"
181 "import \"..\"\n"
182 "Item { property int t: ValueSource.something }",
183 baseUrl: QUrl::fromLocalFile(localfile: dataDirectory + "/abc/Xyz.qml"));
184 QCOMPARE(component.status(), QQmlComponent::Ready);
185 QScopedPointer<QObject> o(component.create());
186 QVERIFY(o.data());
187 QCOMPARE(o->property("t").toInt(), 10);
188}
189
190void tst_QQMLTypeLoader::keepSingleton()
191{
192 qmlRegisterSingletonType(url: testFileUrl(fileName: "ValueSource.qml"), uri: "ClusterDemo", versionMajor: 1, versionMinor: 0, qmlName: "ValueSource");
193 checkSingleton(dataDirectory: dataDirectory());
194 QQmlMetaType::freeUnusedTypesAndCaches();
195 checkSingleton(dataDirectory: dataDirectory());
196}
197
198class TestObject : public QObject
199{
200 Q_OBJECT
201public:
202 TestObject(QObject *parent = nullptr) : QObject(parent) {}
203};
204
205QML_DECLARE_TYPE(TestObject)
206
207static void verifyTypes(bool shouldHaveTestObject, bool shouldHaveFast)
208{
209 bool hasTestObject = false;
210 bool hasFast = false;
211 for (const QQmlType &type : QQmlMetaType::qmlAllTypes()) {
212 if (type.elementName() == QLatin1String("Fast"))
213 hasFast = true;
214 else if (type.elementName() == QLatin1String("TestObject"))
215 hasTestObject = true;
216 }
217 QCOMPARE(hasTestObject, shouldHaveTestObject);
218 QCOMPARE(hasFast, shouldHaveFast);
219}
220
221void tst_QQMLTypeLoader::keepRegistrations()
222{
223 verifyTypes(shouldHaveTestObject: false, shouldHaveFast: false);
224 qmlRegisterType<TestObject>(uri: "Test", versionMajor: 1, versionMinor: 0, qmlName: "TestObject");
225 verifyTypes(shouldHaveTestObject: true, shouldHaveFast: false);
226
227 {
228 QQmlEngine engine;
229 engine.addImportPath(dir: dataDirectory());
230 QQmlComponent component(&engine);
231 component.setData("import Fast 1.0\nFast {}", baseUrl: QUrl());
232 QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8().constData());
233 QCOMPARE(component.status(), QQmlComponent::Ready);
234 QScopedPointer<QObject> o(component.create());
235 QVERIFY(o.data());
236 verifyTypes(shouldHaveTestObject: true, shouldHaveFast: true);
237 }
238
239 verifyTypes(shouldHaveTestObject: true, shouldHaveFast: false); // Fast is gone again, even though an event was still scheduled.
240 QQmlMetaType::freeUnusedTypesAndCaches();
241 verifyTypes(shouldHaveTestObject: true, shouldHaveFast: false); // qmlRegisterType creates an undeletable type.
242}
243
244class NetworkReply : public QNetworkReply
245{
246public:
247 NetworkReply()
248 {
249 open(mode: QIODevice::ReadOnly);
250 }
251
252 void setData(const QByteArray &data)
253 {
254 if (isFinished())
255 return;
256 m_buffer = data;
257 emit readyRead();
258 setFinished(true);
259 emit finished();
260 }
261
262 void fail()
263 {
264 if (isFinished())
265 return;
266 m_buffer.clear();
267 setError(errorCode: ContentNotFoundError, errorString: "content not found");
268 emit error(ContentNotFoundError);
269 setFinished(true);
270 emit finished();
271 }
272
273 qint64 bytesAvailable() const override
274 {
275 return m_buffer.size();
276 }
277
278 qint64 readData(char *data, qint64 maxlen) override
279 {
280 if (m_buffer.length() < maxlen)
281 maxlen = m_buffer.length();
282 std::memcpy(dest: data, src: m_buffer.data(), n: maxlen);
283 m_buffer.remove(index: 0, len: maxlen);
284 return maxlen;
285 }
286
287 void abort() override
288 {
289 if (isFinished())
290 return;
291 m_buffer.clear();
292 setFinished(true);
293 emit finished();
294 }
295
296private:
297 QByteArray m_buffer;
298};
299
300class NetworkAccessManager : public QNetworkAccessManager
301{
302 Q_OBJECT
303public:
304
305 NetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent)
306 {
307 }
308
309 QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
310 QIODevice *outgoingData) override
311 {
312 QUrl url = request.url();
313 QString scheme = url.scheme();
314 if (op != GetOperation || !scheme.endsWith(s: "+debug"))
315 return QNetworkAccessManager::createRequest(op, request, outgoingData);
316
317 scheme.chop(n: sizeof("+debug") - 1);
318 url.setScheme(scheme);
319
320 NetworkReply *reply = new NetworkReply;
321 QString filename = QQmlFile::urlToLocalFileOrQrc(url);
322 QTimer::singleShot(interval: 10, context: reply, slot: [this, reply, filename]() {
323 if (filename.isEmpty()) {
324 reply->fail();
325 } else {
326 QFile file(filename);
327 if (file.open(flags: QIODevice::ReadOnly)) {
328 emit loaded(filename);
329 reply->setData(transformQmldir(filename, content: file.readAll()));
330 } else
331 reply->fail();
332 }
333 });
334 return reply;
335 }
336
337 QByteArray transformQmldir(const QString &filename, const QByteArray &content)
338 {
339 if (!filename.endsWith(s: "/qmldir"))
340 return content;
341
342 // Make qmldir plugin paths absolute, so that we don't try to load them over the network
343 QByteArray result;
344 QByteArray path = filename.toUtf8();
345 path.chop(n: sizeof("qmldir") - 1);
346 for (QByteArray line : content.split(sep: '\n')) {
347 if (line.isEmpty())
348 continue;
349 QList<QByteArray> segments = line.split(sep: ' ');
350 if (segments.startsWith(t: "plugin")) {
351 if (segments.length() == 2) {
352 segments.append(t: path);
353 } else if (segments.length() == 3) {
354 if (!segments[2].startsWith(c: '/'))
355 segments[2] = path + segments[2];
356 } else {
357 // Invalid plugin declaration. Ignore
358 }
359 result.append(a: segments.join(sep: ' '));
360 } else {
361 result.append(a: line);
362 }
363 result.append(c: '\n');
364 }
365 return result;
366 }
367
368signals:
369 void loaded(const QString &filename);
370};
371
372class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
373{
374public:
375 QStringList loadedFiles;
376
377 QNetworkAccessManager *create(QObject *parent) override
378 {
379 NetworkAccessManager *manager = new NetworkAccessManager(parent);
380 QObject::connect(sender: manager, signal: &NetworkAccessManager::loaded, slot: [this](const QString &filename) {
381 loadedFiles.append(t: filename);
382 });
383 return manager;
384 }
385};
386
387class UrlInterceptor : public QQmlAbstractUrlInterceptor
388{
389public:
390 QUrl intercept(const QUrl &path, DataType type) override
391 {
392 Q_UNUSED(type);
393 if (!QQmlFile::isLocalFile(url: path))
394 return path;
395
396 QUrl result = path;
397 QString scheme = result.scheme();
398 if (!scheme.endsWith(s: "+debug"))
399 result.setScheme(scheme + "+debug");
400 return result;
401 }
402};
403
404void tst_QQMLTypeLoader::intercept()
405{
406 qmlClearTypeRegistrations();
407
408 QQmlEngine engine;
409 engine.addImportPath(dir: dataDirectory());
410 engine.addImportPath(QT_TESTCASE_BUILDDIR);
411
412 UrlInterceptor interceptor;
413 NetworkAccessManagerFactory factory;
414
415 engine.setUrlInterceptor(&interceptor);
416 engine.setNetworkAccessManagerFactory(&factory);
417
418 QQmlComponent component(&engine, testFileUrl(fileName: "test_intercept.qml"));
419
420 QVERIFY(component.status() != QQmlComponent::Ready);
421 QTRY_VERIFY2(component.status() == QQmlComponent::Ready,
422 component.errorString().toUtf8().constData());
423
424 QScopedPointer<QObject> o(component.create());
425 QVERIFY(o.data());
426
427 QTRY_COMPARE(o->property("created").toInt(), 2);
428 QTRY_COMPARE(o->property("loaded").toInt(), 2);
429
430 QVERIFY(factory.loadedFiles.length() >= 6);
431 QVERIFY(factory.loadedFiles.contains(dataDirectory() + "/test_intercept.qml"));
432 QVERIFY(factory.loadedFiles.contains(dataDirectory() + "/Intercept.qml"));
433 QVERIFY(factory.loadedFiles.contains(dataDirectory() + "/Fast/qmldir"));
434 QVERIFY(factory.loadedFiles.contains(dataDirectory() + "/Fast/Fast.qml"));
435 QVERIFY(factory.loadedFiles.contains(dataDirectory() + "/GenericView.qml"));
436 QVERIFY(factory.loadedFiles.contains(QLatin1String(QT_TESTCASE_BUILDDIR) + "/Slow/qmldir"));
437}
438
439void tst_QQMLTypeLoader::redirect()
440{
441 TestHTTPServer server;
442 QVERIFY2(server.listen(), qPrintable(server.errorString()));
443 QVERIFY(server.serveDirectory(dataDirectory()));
444 server.addRedirect(filename: "Base.qml", redirectName: server.urlString(documentPath: "/redirected/Redirected.qml"));
445
446 QQmlEngine engine;
447 QQmlComponent component(&engine);
448 component.loadUrl(url: server.urlString(documentPath: "/Load.qml"), mode: QQmlComponent::Asynchronous);
449 QTRY_VERIFY2(component.isReady(), qPrintable(component.errorString()));
450
451 QScopedPointer<QObject> object {component.create()};
452 QTRY_COMPARE(object->property("xy").toInt(), 323232);
453}
454
455void tst_QQMLTypeLoader::qmlSingletonWithinModule()
456{
457 qmlClearTypeRegistrations();
458 QQmlEngine engine;
459 qmlRegisterSingletonType(url: testFileUrl(fileName: "Singleton.qml"), uri: "modulewithsingleton", versionMajor: 1, versionMinor: 0, qmlName: "Singleton");
460
461 QQmlComponent component(&engine, testFileUrl(fileName: "singletonuser.qml"));
462 QCOMPARE(component.status(), QQmlComponent::Ready);
463 QScopedPointer<QObject> obj(component.create());
464 QVERIFY(!obj.isNull());
465 QVERIFY(obj->property("ok").toBool());
466}
467
468static void checkCleanCacheLoad(const QString &testCase)
469{
470#if QT_CONFIG(process)
471 const char *skipKey = "QT_TST_QQMLTYPELOADER_SKIP_MISMATCH";
472 if (qEnvironmentVariableIsSet(varName: skipKey))
473 return;
474 for (int i = 0; i < 5; ++i) {
475 QProcess child;
476 child.setProgram(QCoreApplication::applicationFilePath());
477 child.setArguments(QStringList(testCase));
478 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
479 env.insert(name: QLatin1String("QT_LOGGING_RULES"), value: QLatin1String("qt.qml.diskcache.debug=true"));
480 env.insert(name: QLatin1String(skipKey), value: QLatin1String("1"));
481 child.setProcessEnvironment(env);
482 child.start();
483 QVERIFY(child.waitForFinished());
484 QCOMPARE(child.exitCode(), 0);
485 QVERIFY(!child.readAllStandardOutput().contains("Checksum mismatch for cached version"));
486 QVERIFY(!child.readAllStandardError().contains("Checksum mismatch for cached version"));
487 }
488#else
489 Q_UNUSED(testCase);
490#endif
491}
492
493void tst_QQMLTypeLoader::multiSingletonModule()
494{
495 qmlClearTypeRegistrations();
496 QQmlEngine engine;
497 engine.addImportPath(dir: testFile(fileName: "imports"));
498
499 qmlRegisterSingletonType(url: testFileUrl(fileName: "CppRegisteredSingleton1.qml"), uri: "cppsingletonmodule",
500 versionMajor: 1, versionMinor: 0, qmlName: "CppRegisteredSingleton1");
501 qmlRegisterSingletonType(url: testFileUrl(fileName: "CppRegisteredSingleton2.qml"), uri: "cppsingletonmodule",
502 versionMajor: 1, versionMinor: 0, qmlName: "CppRegisteredSingleton2");
503
504 QQmlComponent component(&engine, testFileUrl(fileName: "multisingletonuser.qml"));
505 QCOMPARE(component.status(), QQmlComponent::Ready);
506 QScopedPointer<QObject> obj(component.create());
507 QVERIFY(!obj.isNull());
508 QVERIFY(obj->property("ok").toBool());
509
510 checkCleanCacheLoad(testCase: QLatin1String("multiSingletonModule"));
511}
512
513void tst_QQMLTypeLoader::implicitComponentModule()
514{
515 QQmlEngine engine;
516 QQmlComponent component(&engine, testFileUrl(fileName: "implicitcomponent.qml"));
517 QCOMPARE(component.status(), QQmlComponent::Ready);
518 QScopedPointer<QObject> obj(component.create());
519 QVERIFY(!obj.isNull());
520
521 checkCleanCacheLoad(testCase: QLatin1String("implicitComponentModule"));
522}
523
524void tst_QQMLTypeLoader::customDiskCachePath()
525{
526#if QT_CONFIG(process)
527 const char *skipKey = "QT_TST_QQMLTYPELOADER_SKIP_MISMATCH";
528 if (qEnvironmentVariableIsSet(varName: skipKey)) {
529 QQmlEngine engine;
530 QQmlComponent component(&engine, testFileUrl(fileName: "Base.qml"));
531 QCOMPARE(component.status(), QQmlComponent::Ready);
532 QScopedPointer<QObject> obj(component.create());
533 QVERIFY(!obj.isNull());
534 return;
535 }
536
537 QTemporaryDir dir;
538 QProcess child;
539 child.setProgram(QCoreApplication::applicationFilePath());
540 child.setArguments(QStringList(QLatin1String("customDiskCachePath")));
541 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
542 env.insert(name: QLatin1String(skipKey), value: QLatin1String("1"));
543 env.insert(name: QLatin1String("QML_DISK_CACHE_PATH"), value: dir.path());
544 child.setProcessEnvironment(env);
545 child.start();
546 QVERIFY(child.waitForFinished());
547 QCOMPARE(child.exitCode(), 0);
548 QDir cacheDir(dir.path());
549 QVERIFY(!cacheDir.isEmpty());
550#endif
551}
552
553void tst_QQMLTypeLoader::qrcRootPathUrl()
554{
555 QQmlEngine engine;
556 QQmlComponent component(&engine, testFileUrl(fileName: "qrcRootPath.qml"));
557 QCOMPARE(component.status(), QQmlComponent::Ready);
558}
559
560void tst_QQMLTypeLoader::implicitImport()
561{
562 QQmlEngine engine;
563 engine.addImportPath(dir: testFile(fileName: "imports"));
564 QQmlComponent component(&engine, testFileUrl(fileName: "implicitimporttest.qml"));
565 QVERIFY2(component.isReady(), qPrintable(component.errorString()));
566 QScopedPointer<QObject> obj(component.create());
567 QVERIFY(!obj.isNull());
568}
569
570void tst_QQMLTypeLoader::compositeSingletonCycle()
571{
572 TestHTTPServer server;
573 QVERIFY2(server.listen(), qPrintable(server.errorString()));
574 QVERIFY(server.serveDirectory(dataDirectory()));
575
576 QQmlEngine engine;
577 QQmlComponent component(&engine);
578 engine.addImportPath(dir: server.baseUrl().toString());
579 component.loadUrl(url: server.urlString(documentPath: "Com/Orga/Handlers/Handler.qml"), mode: QQmlComponent::Asynchronous);
580 QTRY_VERIFY2(component.isReady(), qPrintable(component.errorString()));
581
582 QScopedPointer<QObject> object {component.create()};
583 QVERIFY(object);
584 QCOMPARE(qvariant_cast<QColor>(object->property("color")), QColorConstants::Black);
585}
586
587void tst_QQMLTypeLoader::declarativeCppType()
588{
589 QQmlEngine engine;
590 QQmlComponent component(&engine, testFileUrl(fileName: "declarativeCppType.qml"));
591 QCOMPARE(component.status(), QQmlComponent::Ready);
592 QScopedPointer<QObject> obj(component.create());
593 QVERIFY(!obj.isNull());
594}
595
596void tst_QQMLTypeLoader::circularDependency()
597{
598 QQmlEngine engine;
599 QTest::ignoreMessage(type: QtWarningMsg, messagePattern: QRegularExpression("Cyclic dependency detected between (.*) and (.*)"));
600 QQmlComponent component(&engine, testFileUrl(fileName: "CircularDependency.qml"));
601 QCOMPARE(component.status(), QQmlComponent::Null);
602}
603
604QTEST_MAIN(tst_QQMLTypeLoader)
605
606#include "tst_qqmltypeloader.moc"
607

source code of qtdeclarative/tests/auto/qml/qqmltypeloader/tst_qqmltypeloader.cpp