/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "../../shared/util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class tst_qqmlengine : public QQmlDataTest { Q_OBJECT public: tst_qqmlengine() {} private slots: void initTestCase() override; void rootContext(); void networkAccessManager(); void synchronousNetworkAccessManager(); void baseUrl(); void contextForObject(); void offlineStoragePath(); void offlineDatabaseStoragePath(); void clearComponentCache(); void trimComponentCache(); void trimComponentCache_data(); void repeatedCompilation(); void failedCompilation(); void failedCompilation_data(); void outputWarningsToStandardError(); void objectOwnership(); void multipleEngines(); void qtqmlModule_data(); void qtqmlModule(); void urlInterceptor_data(); void urlInterceptor(); void qmlContextProperties(); void testGCCorruption(); void testGroupedPropertyRevisions(); void componentFromEval(); void qrcUrls(); void cppSignalAndEval(); void singletonInstance(); void aggressiveGc(); void cachedGetterLookup_qtbug_75335(); void createComponentOnSingletonDestruction(); void uiLanguage(); void captureQProperty(); public slots: QObject *createAQObjectForOwnershipTest () { static QObject *ptr = new QObject(); return ptr; } private: QTemporaryDir m_tempDir; }; void tst_qqmlengine::initTestCase() { QVERIFY2(m_tempDir.isValid(), qPrintable(m_tempDir.errorString())); QQmlDataTest::initTestCase(); } void tst_qqmlengine::rootContext() { QQmlEngine engine; QVERIFY(engine.rootContext()); QCOMPARE(engine.rootContext()->engine(), &engine); QVERIFY(!engine.rootContext()->parentContext()); } class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { public: NetworkAccessManagerFactory() : manager(nullptr) {} QNetworkAccessManager *create(QObject *parent) { manager = new QNetworkAccessManager(parent); return manager; } QNetworkAccessManager *manager; }; void tst_qqmlengine::networkAccessManager() { QQmlEngine *engine = new QQmlEngine; // Test QQmlEngine created manager QPointer manager = engine->networkAccessManager(); QVERIFY(manager != nullptr); delete engine; // Test factory created manager engine = new QQmlEngine; NetworkAccessManagerFactory factory; engine->setNetworkAccessManagerFactory(&factory); QCOMPARE(engine->networkAccessManagerFactory(), &factory); QNetworkAccessManager *engineNam = engine->networkAccessManager(); // calls NetworkAccessManagerFactory::create() QCOMPARE(engineNam, factory.manager); delete engine; } class ImmediateReply : public QNetworkReply { Q_OBJECT public: ImmediateReply() { setFinished(true); } virtual qint64 readData(char* , qint64 ) { return 0; } virtual void abort() { } }; class ImmediateManager : public QNetworkAccessManager { Q_OBJECT public: ImmediateManager(QObject *parent = nullptr) : QNetworkAccessManager(parent) { } QNetworkReply *createRequest(Operation, const QNetworkRequest & , QIODevice * outgoingData = nullptr) { Q_UNUSED(outgoingData); return new ImmediateReply; } }; class ImmediateFactory : public QQmlNetworkAccessManagerFactory { public: QNetworkAccessManager *create(QObject *) { return new ImmediateManager; } }; void tst_qqmlengine::synchronousNetworkAccessManager() { ImmediateFactory factory; QQmlEngine engine; engine.setNetworkAccessManagerFactory(&factory); QQmlComponent c(&engine, QUrl("myScheme://test.qml")); // reply is finished, so should not be in loading state. QVERIFY(!c.isLoading()); } void tst_qqmlengine::baseUrl() { QQmlEngine engine; QUrl cwd = QUrl::fromLocalFile(QDir::currentPath() + QDir::separator()); QCOMPARE(engine.baseUrl(), cwd); QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd.resolved(QUrl("main.qml"))); QDir dir = QDir::current(); dir.cdUp(); QVERIFY(dir != QDir::current()); QDir::setCurrent(dir.path()); QCOMPARE(QDir::current(), dir); QUrl cwd2 = QUrl::fromLocalFile(QDir::currentPath() + QDir::separator()); QCOMPARE(engine.baseUrl(), cwd2); QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd2.resolved(QUrl("main.qml"))); engine.setBaseUrl(cwd); QCOMPARE(engine.baseUrl(), cwd); QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd.resolved(QUrl("main.qml"))); const QString testPath = QDir::currentPath() + QLatin1String("/"); const QString rootPath = QDir::rootPath(); engine.setBaseUrl(QUrl()); // Check that baseUrl returns a url to a localFile QCOMPARE(engine.baseUrl().toLocalFile(), testPath); QDir::setCurrent(QDir::rootPath()); // Make sure this also works when in the rootPath QCOMPARE(engine.baseUrl().toLocalFile(), rootPath); } void tst_qqmlengine::contextForObject() { QQmlEngine *engine = new QQmlEngine; // Test null-object QVERIFY(!QQmlEngine::contextForObject(nullptr)); // Test an object with no context QObject object; QVERIFY(!QQmlEngine::contextForObject(&object)); // Test setting null-object QQmlEngine::setContextForObject(nullptr, engine->rootContext()); // Test setting null-context QQmlEngine::setContextForObject(&object, nullptr); // Test setting context QQmlEngine::setContextForObject(&object, engine->rootContext()); QCOMPARE(QQmlEngine::contextForObject(&object), engine->rootContext()); QQmlContext context(engine->rootContext()); // Try changing context QTest::ignoreMessage(QtWarningMsg, "QQmlEngine::setContextForObject(): Object already has a QQmlContext"); QQmlEngine::setContextForObject(&object, &context); QCOMPARE(QQmlEngine::contextForObject(&object), engine->rootContext()); // Delete context delete engine; engine = nullptr; QVERIFY(!QQmlEngine::contextForObject(&object)); } void tst_qqmlengine::offlineStoragePath() { // Without these set, QDesktopServices::storageLocation returns // strings with extra "//" at the end. We set them to ignore this problem. qApp->setApplicationName("tst_qqmlengine"); qApp->setOrganizationName("QtProject"); qApp->setOrganizationDomain("www.qt-project.org"); QQmlEngine engine; QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QCOMPARE(dataLocation.isEmpty(), engine.offlineStoragePath().isEmpty()); QDir dir(dataLocation); dir.mkpath("QML"); dir.cd("QML"); dir.mkpath("OfflineStorage"); dir.cd("OfflineStorage"); QCOMPARE(QDir::fromNativeSeparators(engine.offlineStoragePath()), dir.path()); engine.setOfflineStoragePath(QDir::homePath()); QCOMPARE(engine.offlineStoragePath(), QDir::homePath()); } void tst_qqmlengine::offlineDatabaseStoragePath() { // Without these set, QDesktopServices::storageLocation returns // strings with extra "//" at the end. We set them to ignore this problem. qApp->setApplicationName("tst_qqmlengine"); qApp->setOrganizationName("QtProject"); qApp->setOrganizationDomain("www.qt-project.org"); QQmlEngine engine; QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); const QString databaseName = QLatin1String("foo"); QString databaseLocation = engine.offlineStorageDatabaseFilePath(databaseName); QCOMPARE(dataLocation.isEmpty(), databaseLocation.isEmpty()); QDir dir(dataLocation); dir.mkpath("QML"); dir.cd("QML"); dir.mkpath("OfflineStorage"); dir.cd("OfflineStorage"); dir.mkpath("Databases"); dir.cd("Databases"); QCOMPARE(QFileInfo(databaseLocation).dir().path(), dir.path()); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(databaseName.toUtf8()); QCOMPARE(databaseLocation, QDir::toNativeSeparators(dir.filePath(QLatin1String(md5.result().toHex())))); } void tst_qqmlengine::clearComponentCache() { QQmlEngine engine; const QString fileName = m_tempDir.filePath(QStringLiteral("temp.qml")); const QUrl fileUrl = QUrl::fromLocalFile(fileName); // Create original qml file { QFile file(fileName); QVERIFY(file.open(QIODevice::WriteOnly)); file.write("import QtQuick 2.0\nQtObject {\nproperty int test: 10\n}\n"); file.close(); } // Test "test" property { QQmlComponent component(&engine, fileUrl); QObject *obj = component.create(); QVERIFY(obj != nullptr); QCOMPARE(obj->property("test").toInt(), 10); delete obj; } // Modify qml file { // On macOS with HFS+ the precision of file times is measured in seconds, so to ensure that // the newly written file has a modification date newer than an existing cache file, we must // wait. // Similar effects of lacking precision have been observed on some Linux systems. QThread::sleep(1); QFile file(fileName); QVERIFY(file.open(QIODevice::WriteOnly)); file.write("import QtQuick 2.0\nQtObject {\nproperty int test: 11\n}\n"); file.close(); } // Test cache hit { QQmlComponent component(&engine, fileUrl); QObject *obj = component.create(); QVERIFY(obj != nullptr); QCOMPARE(obj->property("test").toInt(), 10); delete obj; } // Clear cache engine.clearComponentCache(); // Test cache refresh { QQmlComponent component(&engine, fileUrl); QObject *obj = component.create(); QVERIFY(obj != nullptr); QCOMPARE(obj->property("test").toInt(), 11); delete obj; } // Regular Synchronous loading will leave us with an event posted // to the gui thread and an extra refcount that will only be dropped after the // event delivery. Call sendPostedEvents() to get rid of it so that // the temporary directory can be removed. QCoreApplication::sendPostedEvents(); } struct ComponentCacheFunctions : public QObject, public QQmlIncubationController { Q_OBJECT public: QQmlEngine *engine; ComponentCacheFunctions(QQmlEngine &e) : engine(&e) {} Q_INVOKABLE void trim() { // Wait for any pending deletions to occur QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); QCoreApplication::processEvents(); // There might be JS function objects around that hold a last ref to the compilation unit that's // keeping the type compilation data (CompilationUnit) around. Let's collect them as well so that // trim works well. engine->collectGarbage(); engine->trimComponentCache(); } Q_INVOKABLE bool isTypeLoaded(QString file) { return QQmlEnginePrivate::get(engine)->isTypeLoaded(tst_qqmlengine::instance()->testFileUrl(file)); } Q_INVOKABLE bool isScriptLoaded(QString file) { return QQmlEnginePrivate::get(engine)->isScriptLoaded(tst_qqmlengine::instance()->testFileUrl(file)); } Q_INVOKABLE void beginIncubation() { startTimer(0); } Q_INVOKABLE void waitForIncubation() { while (incubatingObjectCount() > 0) { QCoreApplication::processEvents(); } } private: virtual void timerEvent(QTimerEvent *) { incubateFor(1000); } }; void tst_qqmlengine::trimComponentCache() { QFETCH(QString, file); QQmlEngine engine; ComponentCacheFunctions componentCache(engine); engine.setIncubationController(&componentCache); QQmlComponent component(&engine, testFileUrl(file)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer object(component.createWithInitialProperties({ {"componentCache", QVariant::fromValue(&componentCache)} })); QVERIFY(object != nullptr); QCOMPARE(object->property("success").toBool(), true); } void tst_qqmlengine::trimComponentCache_data() { QTest::addColumn("file"); // The various tests here are for two types of components: those that are // empty apart from their inherited elements, and those that define new properties. // For each there are five types of composition: extension, aggregation, // aggregation via component, property and object-created-via-transient-component. foreach (const QString &test, (QStringList() << "EmptyComponent" << "VMEComponent" << "EmptyExtendEmptyComponent" << "VMEExtendEmptyComponent" << "EmptyExtendVMEComponent" << "VMEExtendVMEComponent" << "EmptyAggregateEmptyComponent" << "VMEAggregateEmptyComponent" << "EmptyAggregateVMEComponent" << "VMEAggregateVMEComponent" << "EmptyPropertyEmptyComponent" << "VMEPropertyEmptyComponent" << "EmptyPropertyVMEComponent" << "VMEPropertyVMEComponent" << "VMETransientEmptyComponent" << "VMETransientVMEComponent")) { // For these cases, we first test that the component instance keeps the components // referenced, and then that the instantiated object keeps the components referenced for (int i = 1; i <= 2; ++i) { QString name(QString("%1-%2").arg(test).arg(i)); QString file(QString("test%1.%2.qml").arg(test).arg(i)); QTest::newRow(name.toLatin1().constData()) << file; } } // Test that a transient component is correctly referenced QTest::newRow("TransientComponent-1") << "testTransientComponent.1.qml"; QTest::newRow("TransientComponent-2") << "testTransientComponent.2.qml"; // Test that components can be reloaded after unloading QTest::newRow("ReloadComponent") << "testReloadComponent.qml"; // Test that components are correctly referenced when dynamically loaded QTest::newRow("LoaderComponent") << "testLoaderComponent.qml"; // Test that components are correctly referenced when incubated QTest::newRow("IncubatedComponent") << "testIncubatedComponent.qml"; // Test that a top-level omponents is correctly referenced QTest::newRow("TopLevelComponent") << "testTopLevelComponent.qml"; // TODO: // Test that scripts are unloaded when no longer referenced QTest::newRow("ScriptComponent") << "testScriptComponent.qml"; } void tst_qqmlengine::repeatedCompilation() { QQmlEngine engine; for (int i = 0; i < 100; ++i) { engine.collectGarbage(); engine.trimComponentCache(); QQmlComponent component(&engine, testFileUrl("repeatedCompilation.qml")); QVERIFY(component.isReady()); QScopedPointer object(component.create()); QVERIFY(object != nullptr); QCOMPARE(object->property("success").toBool(), true); } } void tst_qqmlengine::failedCompilation() { QFETCH(QString, file); QQmlEngine engine; QQmlComponent component(&engine, testFileUrl(file)); QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QQmlComponent: Component is not ready"); QVERIFY(!component.isReady()); QScopedPointer object(component.create()); QVERIFY(object.isNull()); engine.collectGarbage(); engine.trimComponentCache(); engine.clearComponentCache(); } void tst_qqmlengine::failedCompilation_data() { QTest::addColumn("file"); QTest::newRow("Invalid URL") << "failedCompilation.does.not.exist.qml"; QTest::newRow("Invalid content") << "failedCompilation.1.qml"; } void tst_qqmlengine::outputWarningsToStandardError() { QQmlEngine engine; QCOMPARE(engine.outputWarningsToStandardError(), true); QQmlComponent c(&engine); c.setData("import QtQuick 2.0; QtObject { property int a: undefined }", QUrl()); QVERIFY(c.isReady()); QQmlTestMessageHandler messageHandler; QScopedPointer o(c.create()); QVERIFY(o != nullptr); o.reset(); QCOMPARE(messageHandler.messages().count(), 1); QCOMPARE(messageHandler.messages().at(0), QLatin1String(":1:32: Unable to assign [undefined] to int")); messageHandler.clear(); engine.setOutputWarningsToStandardError(false); QCOMPARE(engine.outputWarningsToStandardError(), false); o.reset(c.create()); QVERIFY(o != nullptr); o.reset(); QVERIFY2(messageHandler.messages().isEmpty(), qPrintable(messageHandler.messageString())); } void tst_qqmlengine::objectOwnership() { { QCOMPARE(QQmlEngine::objectOwnership(nullptr), QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(nullptr, QQmlEngine::JavaScriptOwnership); QCOMPARE(QQmlEngine::objectOwnership(nullptr), QQmlEngine::CppOwnership); } { QObject o; QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(&o, QQmlEngine::CppOwnership); QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(&o, QQmlEngine::JavaScriptOwnership); QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::JavaScriptOwnership); QQmlEngine::setObjectOwnership(&o, QQmlEngine::CppOwnership); QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership); } { QQmlEngine engine; QQmlComponent c(&engine); c.setData("import QtQuick 2.0; QtObject { property QtObject object: QtObject {} }", QUrl()); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QCOMPARE(QQmlEngine::objectOwnership(o.data()), QQmlEngine::CppOwnership); QObject *o2 = qvariant_cast(o->property("object")); QCOMPARE(QQmlEngine::objectOwnership(o2), QQmlEngine::JavaScriptOwnership); o.reset(); } { QObject *ptr = createAQObjectForOwnershipTest(); QSignalSpy spy(ptr, SIGNAL(destroyed())); { QQmlEngine engine; QQmlComponent c(&engine); QQmlEngine::setObjectOwnership(ptr, QQmlEngine::JavaScriptOwnership); c.setData("import QtQuick 2.0; Item { required property QtObject test; property int data: test.createAQObjectForOwnershipTest() ? 0 : 1 }", QUrl()); QVERIFY(c.isReady()); QScopedPointer o( c.createWithInitialProperties({{"test", QVariant::fromValue(this)}})); QVERIFY(o != nullptr); } QTRY_VERIFY(spy.count()); } { QObject *ptr = new QObject(); QSignalSpy spy(ptr, SIGNAL(destroyed())); { QQmlEngine engine; QQmlComponent c(&engine); QQmlEngine::setObjectOwnership(ptr, QQmlEngine::JavaScriptOwnership); c.setData("import QtQuick 2.0; QtObject { required property QtObject test; property var object: { var i = test; test ? 0 : 1 } }", QUrl()); QVERIFY(c.isReady()); QScopedPointer o( c.createWithInitialProperties({{"test", QVariant::fromValue(ptr)}})); QVERIFY(o != nullptr); QQmlProperty testProp(o.data(), "test"); testProp.write(QVariant::fromValue(nullptr)); } QTRY_VERIFY(spy.count()); } } // Test an object can be accessed by multiple engines void tst_qqmlengine::multipleEngines() { QObject o; o.setObjectName("TestName"); // Simultaneous engines { QQmlEngine engine1; QQmlEngine engine2; engine1.rootContext()->setContextProperty("object", &o); engine2.rootContext()->setContextProperty("object", &o); QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName")); QQmlExpression expr2(engine2.rootContext(), nullptr, QString("object.objectName")); QCOMPARE(expr1.evaluate().toString(), QString("TestName")); QCOMPARE(expr2.evaluate().toString(), QString("TestName")); } // Serial engines { QQmlEngine engine1; engine1.rootContext()->setContextProperty("object", &o); QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName")); QCOMPARE(expr1.evaluate().toString(), QString("TestName")); } { QQmlEngine engine1; engine1.rootContext()->setContextProperty("object", &o); QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName")); QCOMPARE(expr1.evaluate().toString(), QString("TestName")); } } void tst_qqmlengine::qtqmlModule_data() { QTest::addColumn("testFile"); QTest::addColumn("expectedError"); QTest::addColumn("expectedWarnings"); QTest::newRow("import QtQml of correct version (2.0)") << testFileUrl("qtqmlModule.1.qml") << QString() << QStringList(); QTest::newRow("import QtQml of incorrect version (3.0)") << testFileUrl("qtqmlModule.2.qml") << QString(testFileUrl("qtqmlModule.2.qml").toString() + QLatin1String(":1 module \"QtQml\" version 3.0 is not installed\n")) << QStringList(); QTest::newRow("import QtQml of incorrect version (1.0)") << testFileUrl("qtqmlModule.3.qml") << QString(testFileUrl("qtqmlModule.3.qml").toString() + QLatin1String(":1 module \"QtQml\" version 1.0 is not installed\n")) << QStringList(); QTest::newRow("import QtQml of old version (2.50)") << testFileUrl("qtqmlModule.4.qml") << QString() << QStringList(); QTest::newRow("QtQml 2.0 module provides Component, QtObject, Connections, Binding and Timer") << testFileUrl("qtqmlModule.5.qml") << QString() << QStringList(); QTest::newRow("can import QtQml then QtQuick") << testFileUrl("qtqmlModule.6.qml") << QString() << QStringList(); QTest::newRow("can import QtQuick then QtQml") << testFileUrl("qtqmlModule.7.qml") << QString() << QStringList(); QTest::newRow("no import results in no QtObject availability") << testFileUrl("qtqmlModule.8.qml") << QString(testFileUrl("qtqmlModule.8.qml").toString() + QLatin1String(":4 QtObject is not a type\n")) << QStringList(); QTest::newRow("importing QtQml only results in no Item availability") << testFileUrl("qtqmlModule.9.qml") << QString(testFileUrl("qtqmlModule.9.qml").toString() + QLatin1String(":4 Item is not a type\n")) << QStringList(); QTest::newRow("import QtQml of incorrect version (6.50)") << testFileUrl("qtqmlModule.10.qml") << QString(testFileUrl("qtqmlModule.10.qml").toString() + QLatin1String(":1 module \"QtQml\" version 6.50 is not installed\n")) << QStringList(); } // Test that the engine registers the QtQml module void tst_qqmlengine::qtqmlModule() { QFETCH(QUrl, testFile); QFETCH(QString, expectedError); QFETCH(QStringList, expectedWarnings); foreach (const QString &w, expectedWarnings) QTest::ignoreMessage(QtWarningMsg, qPrintable(w)); QQmlEngine e; QQmlComponent c(&e, testFile); if (expectedError.isEmpty()) { QScopedPointer o(c.create()); QVERIFY(o); } else { QCOMPARE(c.errorString(), expectedError); } } class CustomSelector : public QQmlAbstractUrlInterceptor { public: CustomSelector(const QUrl &base):m_base(base){} virtual QUrl intercept(const QUrl &url, QQmlAbstractUrlInterceptor::DataType d) { if ((url.scheme() != QStringLiteral("file") && url.scheme() != QStringLiteral("qrc")) || url.path().contains("QtQml")) return url; if (!m_interceptionPoints.contains(d)) return url; if (url.path().endsWith("Test.2/qmldir")) {//Special case QUrl url = m_base; url.setPath(m_base.path() + "interception/module/intercepted/qmldir"); return url; } // Special case: with 5.10 we always add the implicit import, so we need to explicitly handle this case now if (url.path().endsWith("intercepted/qmldir")) return url; QString alteredPath = url.path(); int a = alteredPath.lastIndexOf('/'); if (a < 0) a = 0; alteredPath.insert(a, QStringLiteral("/intercepted")); QUrl ret = url; ret.setPath(alteredPath); return ret; } QList m_interceptionPoints; QUrl m_base; }; Q_DECLARE_METATYPE(QList); void tst_qqmlengine::urlInterceptor_data() { QTest::addColumn("testFile"); QTest::addColumn >("interceptionPoint"); QTest::addColumn("expectedChildString"); QTest::addColumn("expectedScriptString"); QTest::addColumn("expectedResolvedUrl"); QTest::addColumn("expectedAbsoluteUrl"); QTest::newRow("InterceptTypes") << testFileUrl("interception/types/urlInterceptor.qml") << (QList() << QQmlAbstractUrlInterceptor::QmlFile << QQmlAbstractUrlInterceptor::JavaScriptFile << QQmlAbstractUrlInterceptor::UrlString) << QStringLiteral("intercepted") << QStringLiteral("intercepted") << testFileUrl("interception/types/intercepted/doesNotExist.file").toString() << QStringLiteral("file:///intercepted/doesNotExist.file"); QTest::newRow("InterceptQmlDir") << testFileUrl("interception/qmldir/urlInterceptor.qml") << (QList() << QQmlAbstractUrlInterceptor::QmldirFile << QQmlAbstractUrlInterceptor::UrlString) << QStringLiteral("intercepted") << QStringLiteral("base file") << testFileUrl("interception/qmldir/intercepted/doesNotExist.file").toString() << QStringLiteral("file:///intercepted/doesNotExist.file"); QTest::newRow("InterceptModule")//just a Test{}, needs to intercept the module import for it to work << testFileUrl("interception/module/urlInterceptor.qml") << (QList() << QQmlAbstractUrlInterceptor::QmldirFile ) << QStringLiteral("intercepted") << QStringLiteral("intercepted") << testFileUrl("interception/module/intercepted/doesNotExist.file").toString() << QStringLiteral("file:///doesNotExist.file"); QTest::newRow("InterceptStrings") << testFileUrl("interception/strings/urlInterceptor.qml") << (QList() << QQmlAbstractUrlInterceptor::UrlString) << QStringLiteral("base file") << QStringLiteral("base file") << testFileUrl("interception/strings/intercepted/doesNotExist.file").toString() << QStringLiteral("file:///intercepted/doesNotExist.file"); QTest::newRow("InterceptIncludes") << testFileUrl("interception/includes/urlInterceptor.qml") << (QList() << QQmlAbstractUrlInterceptor::JavaScriptFile) << QStringLiteral("base file") << QStringLiteral("intercepted include file") << testFileUrl("interception/includes/doesNotExist.file").toString() << QStringLiteral("file:///doesNotExist.file"); } void tst_qqmlengine::urlInterceptor() { QFETCH(QUrl, testFile); QFETCH(QList, interceptionPoint); QFETCH(QString, expectedChildString); QFETCH(QString, expectedScriptString); QFETCH(QString, expectedResolvedUrl); QFETCH(QString, expectedAbsoluteUrl); QQmlEngine e; e.addImportPath(testFileUrl("interception/imports").url()); CustomSelector cs(testFileUrl("")); cs.m_interceptionPoints = interceptionPoint; e.addUrlInterceptor(&cs); QQmlComponent c(&e, testFile); //Note that this can get intercepted too QScopedPointer o(c.create()); if (!o) qDebug() << c.errorString(); QVERIFY(o); //Test a URL as a property initialization QCOMPARE(o->property("filePath").toString(), QUrl("doesNotExist.file").toString()); //Test a URL as a Type location QCOMPARE(o->property("childString").toString(), expectedChildString); //Test a URL as a Script location QCOMPARE(o->property("scriptString").toString(), expectedScriptString); //Test a URL as a resolveUrl() call QCOMPARE(o->property("resolvedUrl").toString(), expectedResolvedUrl); QCOMPARE(o->property("absoluteUrl").toString(), expectedAbsoluteUrl); } void tst_qqmlengine::qmlContextProperties() { QQmlEngine e; QQmlComponent c(&e, testFileUrl("TypeofQmlProperty.qml")); QScopedPointer o(c.create()); if (!o) { qDebug() << c.errorString(); } QVERIFY(o); } void tst_qqmlengine::testGCCorruption() { #ifdef SKIP_GCCORRUPTION_TEST QSKIP("Test too heavy for qemu"); #endif QQmlEngine e; QQmlComponent c(&e, testFileUrl("testGCCorruption.qml")); QScopedPointer o(c.create()); QVERIFY2(o, qPrintable(c.errorString())); } void tst_qqmlengine::testGroupedPropertyRevisions() { QQmlEngine e; QQmlComponent c(&e, testFileUrl("testGroupedPropertiesRevision.1.qml")); QScopedPointer object(c.create()); QVERIFY2(object.data(), qPrintable(c.errorString())); QQmlComponent c2(&e, testFileUrl("testGroupedPropertiesRevision.2.qml")); QVERIFY(!c2.errorString().isEmpty()); } void tst_qqmlengine::componentFromEval() { QQmlEngine engine; const QUrl testUrl = testFileUrl("EmptyComponent.qml"); QJSValue result = engine.evaluate("Qt.createComponent(\"" + testUrl.toString() + "\");"); QPointer component(qobject_cast(result.toQObject())); QVERIFY(!component.isNull()); QScopedPointer item(component->create()); QVERIFY(!item.isNull()); } void tst_qqmlengine::qrcUrls() { QQmlEngine engine; QQmlEnginePrivate *pEngine = QQmlEnginePrivate::get(&engine); { QQmlRefPointer oneQml(pEngine->typeLoader.getType(QUrl("qrc:/qrcurls.qml"))); QVERIFY(oneQml.data() != nullptr); QQmlRefPointer twoQml(pEngine->typeLoader.getType(QUrl("qrc:///qrcurls.qml"))); QVERIFY(twoQml.data() != nullptr); QCOMPARE(oneQml.data(), twoQml.data()); } { QQmlRefPointer oneJS(pEngine->typeLoader.getType(QUrl("qrc:/qrcurls.js"))); QVERIFY(oneJS.data() != nullptr); QQmlRefPointer twoJS(pEngine->typeLoader.getType(QUrl("qrc:///qrcurls.js"))); QVERIFY(twoJS.data() != nullptr); QCOMPARE(oneJS.data(), twoJS.data()); } } class ObjectCaller : public QObject { Q_OBJECT signals: void doubleReply(const double a); }; void tst_qqmlengine::cppSignalAndEval() { ObjectCaller objectCaller; QQmlEngine engine; qmlRegisterSingletonInstance("Test", 1, 0, "CallerCpp", &objectCaller); QQmlComponent c(&engine); c.setData("import QtQuick 2.9\n" "import Test 1.0\n" "Item {\n" " property var r: 0\n" " Connections {\n" " target: CallerCpp;\n" " function onDoubleReply() {\n" " eval('var z = 1');\n" " r = a;\n" " }\n" " }\n" "}", QUrl(QStringLiteral("qrc:/main.qml"))); QScopedPointer object(c.create()); QVERIFY(!object.isNull()); emit objectCaller.doubleReply(1.1234); QCOMPARE(object->property("r"), 1.1234); } class CppSingleton : public QObject { Q_OBJECT public: CppSingleton() {} static QObject *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) { Q_UNUSED(qmlEngine); Q_UNUSED(jsEngine); return new CppSingleton(); } }; class JsSingleton : public QObject { Q_OBJECT public: JsSingleton() {} static QJSValue create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) { Q_UNUSED(qmlEngine); QJSValue value = jsEngine->newQObject(new JsSingleton()); return value; } }; class SomeQObjectClass : public QObject { Q_OBJECT public: SomeQObjectClass() : QObject(nullptr){} }; class Dayfly : public QObject { Q_OBJECT }; void tst_qqmlengine::singletonInstance() { QQmlEngine engine; int cppSingletonTypeId = qmlRegisterSingletonType("Test", 1, 0, "CppSingleton", &CppSingleton::create); int jsValueSingletonTypeId = qmlRegisterSingletonType("Test", 1, 0, "JsSingleton", &JsSingleton::create); { // Cpp QObject singleton type QJSValue value = engine.singletonInstance(cppSingletonTypeId); QVERIFY(!value.isUndefined()); QVERIFY(value.isQObject()); QObject *instance = value.toQObject(); QVERIFY(instance); QCOMPARE(instance->metaObject()->className(), "CppSingleton"); } { // QJSValue QObject singleton type QJSValue value = engine.singletonInstance(jsValueSingletonTypeId); QVERIFY(!value.isUndefined()); QVERIFY(value.isQObject()); QObject *instance = value.toQObject(); QVERIFY(instance); QCOMPARE(instance->metaObject()->className(), "JsSingleton"); } { int data = 30; auto id = qmlRegisterSingletonType("Qt.test",1,0,"CapturingLambda",[data](QQmlEngine*, QJSEngine*){ // register qobject singleton with capturing lambda auto o = new CppSingleton; o->setProperty("data", data); return o; }); QJSValue value = engine.singletonInstance(id); QVERIFY(!value.isUndefined()); QVERIFY(value.isQObject()); QObject *instance = value.toQObject(); QVERIFY(instance); QCOMPARE(instance->metaObject()->className(), "CppSingleton"); QCOMPARE(instance->property("data"), data); } { qmlRegisterSingletonType("Qt.test",1,0,"NotAmbiguous", [](QQmlEngine* qeng, QJSEngine* jeng) -> QObject* {return CppSingleton::create(qeng, jeng);}); // test that overloads for qmlRegisterSingleton are not ambiguous } { // Register QObject* directly CppSingleton single; int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned", &single); QQmlEngine engine2; CppSingleton *singlePtr = engine2.singletonInstance(id); QVERIFY(singlePtr); QCOMPARE(&single, singlePtr); QVERIFY(engine2.objectOwnership(singlePtr) == QQmlEngine::CppOwnership); } { CppSingleton single; QQmlEngine engineA; QQmlEngine engineB; int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned", &single); auto singlePtr = engineA.singletonInstance(id); QVERIFY(singlePtr); singlePtr = engineA.singletonInstance(id); // accessing the singleton multiple times from the same engine is fine QVERIFY(singlePtr); QTest::ignoreMessage(QtMsgType::QtCriticalMsg, ": qmlRegisterSingletonType(): \"CppOwned\" is not available because the callback function returns a null pointer."); QTest::ignoreMessage(QtMsgType::QtWarningMsg, ": Singleton registered by registerSingletonInstance must only be accessed from one engine"); QCOMPARE(&single, singlePtr); auto noSinglePtr = engineB.singletonInstance(id); QVERIFY(!noSinglePtr); } { CppSingleton single; QThread newThread {}; single.moveToThread(&newThread); QCOMPARE(single.thread(), &newThread); QQmlEngine engineB; int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned", &single); QTest::ignoreMessage(QtMsgType::QtCriticalMsg, ": qmlRegisterSingletonType(): \"CppOwned\" is not available because the callback function returns a null pointer."); QTest::ignoreMessage(QtMsgType::QtWarningMsg, ": Registered object must live in the same thread as the engine it was registered with"); auto noSinglePtr = engineB.singletonInstance(id); QVERIFY(!noSinglePtr); } { // Invalid types QJSValue value; value = engine.singletonInstance(-4711); QVERIFY(value.isUndefined()); value = engine.singletonInstance(1701); QVERIFY(value.isUndefined()); } { // Valid, but non-singleton type int typeId = qmlRegisterType("Test", 1, 0, "NotASingleton"); QJSValue value = engine.singletonInstance(typeId); QVERIFY(value.isUndefined()); } { // Cpp QObject singleton type CppSingleton *instance = engine.singletonInstance(cppSingletonTypeId); QVERIFY(instance); QCOMPARE(instance->metaObject()->className(), "CppSingleton"); QCOMPARE(instance, engine.singletonInstance(cppSingletonTypeId).toQObject()); } { // Wrong destination type SomeQObjectClass * instance = engine.singletonInstance(cppSingletonTypeId); QVERIFY(!instance); } { // deleted object auto dayfly = new Dayfly{}; auto id = qmlRegisterSingletonInstance("Vanity", 1, 0, "Dayfly", dayfly); delete dayfly; QTest::ignoreMessage(QtMsgType::QtWarningMsg, ": The registered singleton has already been deleted. Ensure that it outlives the engine."); QObject *instance = engine.singletonInstance(id); QVERIFY(!instance); } } void tst_qqmlengine::aggressiveGc() { const QByteArray origAggressiveGc = qgetenv("QV4_MM_AGGRESSIVE_GC"); qputenv("QV4_MM_AGGRESSIVE_GC", "true"); { QQmlEngine engine; // freezing should not run into infinite recursion QJSValue obj = engine.newObject(); QVERIFY(obj.isObject()); } qputenv("QV4_MM_AGGRESSIVE_GC", origAggressiveGc); } void tst_qqmlengine::cachedGetterLookup_qtbug_75335() { QQmlEngine engine; const QUrl testUrl = testFileUrl("CachedGetterLookup.qml"); QQmlComponent component(&engine, testUrl); QVERIFY(component.isReady()); QScopedPointer object(component.create()); QVERIFY(object != nullptr); } class EvilSingleton : public QObject { Q_OBJECT public: QPointer m_engine; EvilSingleton(QQmlEngine *engine) : m_engine(engine) { connect(this, &QObject::destroyed, this, [this]() { QQmlComponent component(m_engine); component.setData("import QtQml 2.0\nQtObject {}", QUrl("file://Stuff.qml")); QVERIFY(component.isReady()); QScopedPointer obj(component.create()); QVERIFY(obj); }); } }; void tst_qqmlengine::createComponentOnSingletonDestruction() { qmlRegisterSingletonType("foo.foo", 1, 0, "Singleton", [](QQmlEngine *engine, QJSEngine *) { return new EvilSingleton(engine); }); QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("evilSingletonInstantiation.qml")); QVERIFY(component.isReady()); QScopedPointer obj(component.create()); QVERIFY(obj); } void tst_qqmlengine::uiLanguage() { { QQmlEngine engine; QObject::connect(&engine, &QJSEngine::uiLanguageChanged, [&engine]() { engine.retranslate(); }); QSignalSpy uiLanguageChangeSpy(&engine, SIGNAL(uiLanguageChanged())); QQmlComponent component(&engine, testFileUrl("uiLanguage.qml")); QTest::ignoreMessage(QtMsgType::QtWarningMsg, (component.url().toString() + ":2:1: QML QtObject: Binding loop detected for property \"textToTranslate\"").toLatin1()); QScopedPointer object(component.create()); QVERIFY(!object.isNull()); QVERIFY(engine.uiLanguage().isEmpty()); QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 1); QTest::ignoreMessage(QtMsgType::QtWarningMsg, (component.url().toString() + ":2:1: QML QtObject: Binding loop detected for property \"textToTranslate\"").toLatin1()); engine.setUiLanguage("TestLanguage"); QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 2); QCOMPARE(object->property("chosenLanguage").toString(), "TestLanguage"); QTest::ignoreMessage(QtMsgType::QtWarningMsg, (component.url().toString() + ":2:1: QML QtObject: Binding loop detected for property \"textToTranslate\"").toLatin1()); engine.evaluate("Qt.uiLanguage = \"anotherLanguage\""); QCOMPARE(engine.uiLanguage(), QString("anotherLanguage")); QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 3); QCOMPARE(object->property("chosenLanguage").toString(), "anotherLanguage"); } { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("uiLanguage.qml")); QTest::ignoreMessage(QtMsgType::QtWarningMsg, (component.url().toString() + ":2:1: QML QtObject: Binding loop detected for property \"textToTranslate\"").toLatin1()); QScopedPointer object(component.create()); QVERIFY(!object.isNull()); engine.setUiLanguage("TestLanguage"); QCOMPARE(object->property("chosenLanguage").toString(), "TestLanguage"); } } class WithQProperty : public QObject { Q_OBJECT Q_PROPERTY(int foo READ foo WRITE setFoo BINDABLE fooBindable) public: WithQProperty(QObject *parent = nullptr) : QObject(parent) { m_foo.setValue(12); } int foo() const { return m_foo.value(); } void setFoo(int foo) { m_foo.setValue(foo); } QBindable fooBindable() { return QBindable(&m_foo); } int getFooWithCapture() { const QMetaObject *m = metaObject(); currentEngine->captureProperty(this, m->property(m->indexOfProperty("foo"))); return m_foo.value(); } static QQmlEngine *currentEngine; private: QProperty m_foo; }; class WithoutQProperty : public QObject { Q_OBJECT Q_PROPERTY(int foo READ foo WRITE setFoo NOTIFY fooChanged) public: WithoutQProperty(QObject *parent = nullptr) : QObject(parent), m_foo(new WithQProperty(this)) {} int foo() const { return m_foo->getFooWithCapture(); } void setFoo(int foo) { if (foo != m_foo->foo()) { m_foo->setFoo(foo); emit fooChanged(); } } void triggerBinding(int val) { m_foo->setFoo(val); } signals: void fooChanged(); private: WithQProperty *m_foo; }; QQmlEngine *WithQProperty::currentEngine = nullptr; void tst_qqmlengine::captureQProperty() { qmlRegisterType("Foo", 1, 0, "WithoutQProperty"); QQmlEngine engine; WithQProperty::currentEngine = &engine; QQmlComponent c(&engine); c.setData("import Foo\n" "WithoutQProperty {\n" " property int x: foo\n" "}", QUrl()); QVERIFY2(c.isReady(), c.errorString().toUtf8()); QScopedPointer o(c.create()); QCOMPARE(o->property("x").toInt(), 12); static_cast(o.data())->triggerBinding(13); QCOMPARE(o->property("x").toInt(), 13); } QTEST_MAIN(tst_qqmlengine) #include "tst_qqmlengine.moc"