// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "scriptstringprops.h" using namespace Qt::StringLiterals; class tst_qmlcachegen: public QQmlDataTest { Q_OBJECT public: tst_qmlcachegen(); private slots: void initTestCase() override; void loadGeneratedFile(); void translationExpressionSupport(); void signalHandlerParameters(); void errorOnArgumentsInSignalHandler(); void aheadOfTimeCompilation(); void functionExpressions(); void versionChecksForAheadOfTimeUnits(); void retainedResources(); void skippedResources(); void workerScripts(); void trickyPaths_data(); void trickyPaths(); void qrcScriptImport(); void fsScriptImport(); void moduleScriptImport(); void esModulesViaQJSEngine(); void enums(); void sourceFileIndices(); void reproducibleCache_data(); void reproducibleCache(); void parameterAdjustment(); void inlineComponent(); void posthocRequired(); void scriptStringCachegenInteraction(); void saveableUnitPointer(); }; // A wrapper around QQmlComponent to ensure the temporary reference counts // on the type data as a result of the main thread <> loader thread communication // are dropped. 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. A plain sendPostedEvents() however is insufficient because // we can't be sure that the event is posted after the constructor finished. class CleanlyLoadingComponent : public QQmlComponent { public: CleanlyLoadingComponent(QQmlEngine *engine, const QUrl &url) : QQmlComponent(engine, url, QQmlComponent::Asynchronous) { waitForLoad(); } CleanlyLoadingComponent(QQmlEngine *engine, const QString &fileName) : QQmlComponent(engine, fileName, QQmlComponent::Asynchronous) { waitForLoad(); } void waitForLoad() { QTRY_VERIFY(status() == QQmlComponent::Ready || status() == QQmlComponent::Error); } }; static bool generateCache(const QString &qmlFileName, QByteArray *capturedStderr = nullptr) { #if defined(QTEST_CROSS_COMPILED) QTest::qFail("You cannot call qmlcachegen on the target.", __FILE__, __LINE__); return false; #endif QProcess proc; if (capturedStderr == nullptr) proc.setProcessChannelMode(QProcess::ForwardedChannels); proc.setProgram(QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath) + QLatin1String("/qmlcachegen")); proc.setArguments(QStringList() << qmlFileName); proc.start(); if (!proc.waitForFinished()) return false; if (capturedStderr) *capturedStderr = proc.readAllStandardError(); if (proc.exitStatus() != QProcess::NormalExit) return false; return proc.exitCode() == 0; } tst_qmlcachegen::tst_qmlcachegen() : QQmlDataTest(QT_QMLTEST_DATADIR) { } void tst_qmlcachegen::initTestCase() { if (qEnvironmentVariableIsEmpty("QML_DISK_CACHE")) qputenv("QML_FORCE_DISK_CACHE", "1"); QStandardPaths::setTestModeEnabled(true); // make sure there's no pre-existing cache dir QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); if (!cacheDir.isEmpty()) //QDir(cacheDir).removeRecursively(); qDebug() << cacheDir; QQmlDataTest::initTestCase(); } #if QT_CONFIG(process) static void testWithEnvironment( const QString &function, const QString &testFilePath, const QByteArray &value, bool success) { QProcess child; child.setProgram(QCoreApplication::applicationFilePath()); child.setArguments(QStringList(function)); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove(QLatin1String("QML_FORCE_DISK_CACHE")); env.insert(QLatin1String("QMLCACHEGEN_TEST_FILE_PATH"), testFilePath); env.insert(QLatin1String("QML_DISK_CACHE"), QLatin1String(value)); child.setProcessEnvironment(env); child.start(); QVERIFY(child.waitForFinished()); if (success) QCOMPARE(child.exitCode(), 0); else QVERIFY(child.exitCode() != 0); } #endif void tst_qmlcachegen::loadGeneratedFile() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const QByteArray path = qgetenv("QMLCACHEGEN_TEST_FILE_PATH"); QString testFilePath; if (path.isEmpty()) { const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; testFilePath = writeTempFile("test.qml", "import QtQml 2.0\n" "QtObject {\n" " property int value: Math.min(100, 42);\n" "}"); QVERIFY(generateCache(testFilePath)); } else { testFilePath = QString::fromUtf8(path); } const QString cacheFilePath = testFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(cacheFilePath)); { QFile cache(cacheFilePath); QVERIFY(cache.open(QIODevice::ReadOnly)); const QV4::CompiledData::Unit *cacheUnit = reinterpret_cast(cache.map(/*offset*/0, sizeof(QV4::CompiledData::Unit))); QVERIFY(cacheUnit); QVERIFY(cacheUnit->flags & QV4::CompiledData::Unit::StaticData); QVERIFY(cacheUnit->flags & QV4::CompiledData::Unit::PendingTypeCompilation); QCOMPARE(uint(cacheUnit->sourceFileIndex), uint(0)); } if (path.isEmpty()) { QVERIFY(QFile::remove(testFilePath)); } else { #if QT_CONFIG(process) testWithEnvironment("loadGeneratedFile", testFilePath, "qmlc-read", true); testWithEnvironment("loadGeneratedFile", testFilePath, "qmlc-write", false); testWithEnvironment("loadGeneratedFile", testFilePath, "qmlc-read,qmlc-read", true); testWithEnvironment("loadGeneratedFile", testFilePath, "qmlc", true); testWithEnvironment("loadGeneratedFile", testFilePath, "wrong", false); #endif } QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("value").toInt(), 42); auto componentPrivate = QQmlComponentPrivate::get(&component); QVERIFY(componentPrivate); auto compilationUnit = componentPrivate->compilationUnit; QVERIFY(compilationUnit); auto unitData = compilationUnit->unitData(); QVERIFY(unitData); QVERIFY(unitData->flags & QV4::CompiledData::Unit::StaticData); } class QTestTranslator : public QTranslator { public: QString translate(const char *context, const char *sourceText, const char */*disambiguation*/, int /*n*/) const override { m_lastContext = QString::fromUtf8(context); return QString::fromUtf8(sourceText).toUpper(); } bool isEmpty() const override { return true; } mutable QString m_lastContext; }; void tst_qmlcachegen::translationExpressionSupport() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); QTestTranslator translator; qApp->installTranslator(&translator); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile("test.qml", "import QtQml.Models 2.2\n" "import QtQml 2.2\n" "QtObject {\n" " property ListModel model: ListModel {\n" " ListElement {\n" " text: qsTr(\"All\")\n" " }\n" " ListElement {\n" " text: QT_TR_NOOP(\"Ok\")\n" " }\n" " }\n" " property string text: model.get(0).text + \" \" + model.get(1).text\n" "}"); QVERIFY(generateCache(testFilePath)); const QString cacheFilePath = testFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(cacheFilePath)); QVERIFY(QFile::remove(testFilePath)); QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("text").toString(), QString("ALL Ok")); QCOMPARE(translator.m_lastContext, QStringLiteral("test")); } void tst_qmlcachegen::signalHandlerParameters() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile("test.qml", "import QtQml 2.0\n" "QtObject {\n" " property real result: 0\n" " signal testMe(real value);\n" " onTestMe: result = value;\n" " function runTest() { testMe(42); }\n" "}"); QVERIFY(generateCache(testFilePath)); const QString cacheFilePath = testFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(cacheFilePath)); QVERIFY(QFile::remove(testFilePath)); { QFile cache(cacheFilePath); QVERIFY(cache.open(QIODevice::ReadOnly)); const QV4::CompiledData::Unit *cacheUnit = reinterpret_cast(cache.map(/*offset*/0, sizeof(QV4::CompiledData::Unit))); QVERIFY(cacheUnit); } QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QMetaObject::invokeMethod(obj.data(), "runTest"); QCOMPARE(obj->property("result").toInt(), 42); { auto componentPrivate = QQmlComponentPrivate::get(&component); QVERIFY(componentPrivate); auto compilationUnit = componentPrivate->compilationUnit; QVERIFY(compilationUnit); QVERIFY(compilationUnit->unitData()); // Verify that the QML objects don't come from the original data. QVERIFY(compilationUnit->objectAt(0) != compilationUnit->unitData()->qmlUnit()->objectAt(0)); // Typically the final file name is one of those strings that is not in the original // pre-compiled qml file's string table, while for example the signal parameter // name ("value") is. const auto isStringIndexInStringTable = [compilationUnit](uint index) { return index < compilationUnit->unitData()->stringTableSize; }; QVERIFY(isStringIndexInStringTable(compilationUnit->objectAt(0)->signalAt(0)->parameterAt(0)->nameIndex)); QVERIFY(!compilationUnit->dynamicStrings.isEmpty()); } } void tst_qmlcachegen::errorOnArgumentsInSignalHandler() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile("test.qml", "import QtQml 2.2\n" "QtObject {\n" " signal mySignal(var arguments);\n" " onMySignal: console.log(arguments);\n" "}"); QByteArray errorOutput; QVERIFY(!generateCache(testFilePath, &errorOutput)); QVERIFY2(errorOutput.contains("error: The use of eval() or the use of the arguments object in signal handlers is"), errorOutput); } void tst_qmlcachegen::aheadOfTimeCompilation() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile("test.qml", "import QtQml 2.0\n" "QtObject {\n" " function runTest() { var x = 0; while (x < 42) { ++x }; return x; }\n" "}"); QVERIFY(generateCache(testFilePath)); const QString cacheFilePath = testFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(cacheFilePath)); QVERIFY(QFile::remove(testFilePath)); QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QVariant result; QMetaObject::invokeMethod(obj.data(), "runTest", Q_RETURN_ARG(QVariant, result)); QCOMPARE(result.toInt(), 42); } static QQmlPrivate::CachedQmlUnit *temporaryModifiedCachedUnit = nullptr; static const char *versionCheckErrorString(QQmlMetaType::CachedUnitLookupError error) { switch (error) { case QQmlMetaType::CachedUnitLookupError::NoError: return "no error"; case QQmlMetaType::CachedUnitLookupError::NoUnitFound: return "no unit found"; case QQmlMetaType::CachedUnitLookupError::VersionMismatch: return "version mismatch"; case QQmlMetaType::CachedUnitLookupError::NotFullyTyped: return "unit not fully typed"; } return "wat?"; } void tst_qmlcachegen::versionChecksForAheadOfTimeUnits() { QVERIFY(QFile::exists(":/data/versionchecks.qml")); QVERIFY(QFileInfo(":/data/versionchecks.qml").size() > 0); Q_ASSERT(!temporaryModifiedCachedUnit); QQmlMetaType::CachedUnitLookupError error = QQmlMetaType::CachedUnitLookupError::NoError; QLoggingCategory::setFilterRules("qt.qml.diskcache.debug=true"); const QQmlPrivate::CachedQmlUnit *originalUnit = QQmlMetaType::findCachedCompilationUnit( QUrl("qrc:/data/versionchecks.qml"), QQmlMetaType::AcceptUntyped, &error); QLoggingCategory::setFilterRules(QString()); QVERIFY2(originalUnit, versionCheckErrorString(error)); QV4::CompiledData::Unit *tweakedUnit = (QV4::CompiledData::Unit *)malloc(originalUnit->qmlData->unitSize); memcpy(reinterpret_cast(tweakedUnit), reinterpret_cast(originalUnit->qmlData), originalUnit->qmlData->unitSize); tweakedUnit->version = QV4_DATA_STRUCTURE_VERSION - 1; const auto testHandler = [](const QUrl &url) -> const QQmlPrivate::CachedQmlUnit * { if (url == QUrl("qrc:/data/versionchecks.qml")) return temporaryModifiedCachedUnit; return nullptr; }; const auto dropModifiedUnit = qScopeGuard([&testHandler]() { Q_ASSERT(temporaryModifiedCachedUnit); free(const_cast(temporaryModifiedCachedUnit->qmlData)); delete temporaryModifiedCachedUnit; temporaryModifiedCachedUnit = nullptr; QQmlMetaType::removeCachedUnitLookupFunction(testHandler); }); temporaryModifiedCachedUnit = new QQmlPrivate::CachedQmlUnit{tweakedUnit, nullptr, nullptr}; QQmlMetaType::prependCachedUnitLookupFunction(testHandler); { QQmlMetaType::CachedUnitLookupError error = QQmlMetaType::CachedUnitLookupError::NoError; QVERIFY(!QQmlMetaType::findCachedCompilationUnit( QUrl("qrc:/data/versionchecks.qml"), QQmlMetaType::AcceptUntyped, &error)); QCOMPARE(error, QQmlMetaType::CachedUnitLookupError::VersionMismatch); } { QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:/data/versionchecks.qml")); QCOMPARE(component.status(), QQmlComponent::Ready); } } void tst_qmlcachegen::retainedResources() { QFile file(":/Retain.qml"); QVERIFY(file.open(QIODevice::ReadOnly)); QVERIFY(file.readAll().startsWith("import QtQml 2.0")); } void tst_qmlcachegen::skippedResources() { QFile file(":/not/Skip.qml"); QVERIFY(file.open(QIODevice::ReadOnly)); QVERIFY(file.readAll().startsWith("import QtQml 2.0")); QQmlMetaType::CachedUnitLookupError error = QQmlMetaType::CachedUnitLookupError::NoError; const QQmlPrivate::CachedQmlUnit *unit = QQmlMetaType::findCachedCompilationUnit( QUrl("qrc:/not/Skip.qml"), QQmlMetaType::AcceptUntyped, &error); QCOMPARE(unit, nullptr); QCOMPARE(error, QQmlMetaType::CachedUnitLookupError::NoUnitFound); } void tst_qmlcachegen::workerScripts() { QVERIFY(QFile::exists(":/workerscripts/data/worker.js")); QVERIFY(QFile::exists(":/workerscripts/data/worker.qml")); QVERIFY(QFileInfo(":/workerscripts/data/worker.js").size() > 0); QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:///workerscripts/data/worker.qml")); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QTRY_VERIFY(obj->property("success").toBool()); } void tst_qmlcachegen::functionExpressions() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile( "test.qml", "import QtQuick 2.0\n" "Item {\n" " id: di\n" " \n" " property var f\n" " property bool f_called: false\n" " f : function() { f_called = true }\n" " \n" " signal g\n" " property bool g_handler_called: false\n" " onG: function() { g_handler_called = true }\n" " \n" " signal h(int i)\n" " property bool h_connections_handler_called: false\n" " Connections {\n" " target: di\n" " onH: function(magic) { h_connections_handler_called = (magic == 42)\n }\n" " }\n" " \n" " function runTest() { \n" " f()\n" " g()\n" " h(42)\n" " }\n" "}"); QVERIFY(generateCache(testFilePath)); const QString cacheFilePath = testFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(cacheFilePath)); QVERIFY(QFile::remove(testFilePath)); QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("f_called").toBool(), false); QCOMPARE(obj->property("g_handler_called").toBool(), false); QCOMPARE(obj->property("h_connections_handler_called").toBool(), false); QMetaObject::invokeMethod(obj.data(), "runTest"); QCOMPARE(obj->property("f_called").toBool(), true); QCOMPARE(obj->property("g_handler_called").toBool(), true); QCOMPARE(obj->property("h_connections_handler_called").toBool(), true); } void tst_qmlcachegen::trickyPaths_data() { QTest::addColumn("filePath"); QTest::newRow("path with spaces") << QStringLiteral(":/directory with spaces/file name with spaces.qml"); QTest::newRow("version style suffix 1") << QStringLiteral(":/directory with spaces/versionStyleSuffix-1.2-core-yc.qml"); QTest::newRow("version style suffix 2") << QStringLiteral(":/directory with spaces/versionStyleSuffix-1.2-more.qml"); // QTBUG-46375 #if !defined(Q_OS_WIN) QTest::newRow("path with umlaut") << QStringLiteral(":/Bäh.qml"); #endif } void tst_qmlcachegen::trickyPaths() { QFETCH(QString, filePath); QVERIFY2(QFile::exists(filePath), qPrintable(filePath)); QVERIFY(QFileInfo(filePath).size() > 0); QQmlEngine engine; QQmlComponent component(&engine, QUrl("qrc" + filePath)); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("success").toInt(), 42); } void tst_qmlcachegen::qrcScriptImport() { QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:///data/jsimport.qml")); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QTRY_COMPARE(obj->property("value").toInt(), 42); #if QT_CONFIG(process) if (qEnvironmentVariableIsEmpty("QMLCACHEGEN_TEST_FILE_PATH")) { testWithEnvironment("qrcScriptImport", ":/data/jsimport.qml", "aot-native", false); testWithEnvironment("qrcScriptImport", ":/data/jsimport.qml", "aot-bytecode", true); testWithEnvironment("qrcScriptImport", ":/data/jsimport.qml", "aot-bytecode,aot-native", true); testWithEnvironment("qrcScriptImport", ":/data/jsimport.qml", "aot", true); testWithEnvironment("qrcScriptImport", ":/data/jsimport.qml", "wrong", false); } #endif auto componentPrivate = QQmlComponentPrivate::get(&component); QVERIFY(componentPrivate); auto compilationUnit = componentPrivate->compilationUnit; QVERIFY(compilationUnit); auto unitData = compilationUnit->unitData(); QVERIFY(unitData); QVERIFY(unitData->flags & QV4::CompiledData::Unit::StaticData); } void tst_qmlcachegen::fsScriptImport() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const auto writeTempFile = [&tempDir](const QString &fileName, const char *contents) { QFile f(tempDir.path() + '/' + fileName); const bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); Q_ASSERT(ok); f.write(contents); return f.fileName(); }; const QString testFilePath = writeTempFile( "test.qml", "import QtQml 2.0\n" "import \"test.js\" as ScriptTest\n" "QtObject {\n" " property int value: ScriptTest.value\n" "}\n"); const QString scriptFilePath = writeTempFile( "test.js", "var value = 42" ); QVERIFY(generateCache(scriptFilePath)); QVERIFY(generateCache(testFilePath)); const QString scriptCacheFilePath = scriptFilePath + QLatin1Char('c'); QVERIFY(QFile::exists(scriptFilePath)); { QFile cache(scriptCacheFilePath); QVERIFY(cache.open(QIODevice::ReadOnly)); const QV4::CompiledData::Unit *cacheUnit = reinterpret_cast(cache.map(/*offset*/0, sizeof(QV4::CompiledData::Unit))); QVERIFY(cacheUnit); QVERIFY(cacheUnit->flags & QV4::CompiledData::Unit::StaticData); QVERIFY(!(cacheUnit->flags & QV4::CompiledData::Unit::PendingTypeCompilation)); QCOMPARE(uint(cacheUnit->sourceFileIndex), uint(0)); } // Remove source code to make sure that when loading succeeds, it is because we loaded // the existing cache files. QVERIFY(QFile::remove(testFilePath)); QVERIFY(QFile::remove(scriptFilePath)); QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl::fromLocalFile(testFilePath)); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("value").toInt(), 42); } void tst_qmlcachegen::moduleScriptImport() { QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:///data/jsmoduleimport.qml")); QVERIFY2(!component.isError(), qPrintable(component.errorString())); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QTRY_VERIFY(obj->property("ok").toBool()); QVERIFY(QFile::exists(":/data/script.mjs")); QVERIFY(QFileInfo(":/data/script.mjs").size() > 0); { auto componentPrivate = QQmlComponentPrivate::get(&component); QVERIFY(componentPrivate); auto compilationUnit = componentPrivate->compilationUnit->dependentScripts.first()->compilationUnit(); QVERIFY(compilationUnit); auto unitData = compilationUnit->unitData(); QVERIFY(unitData); QVERIFY(unitData->flags & QV4::CompiledData::Unit::StaticData); QVERIFY(unitData->flags & QV4::CompiledData::Unit::IsESModule); QQmlMetaType::CachedUnitLookupError error = QQmlMetaType::CachedUnitLookupError::NoError; const QQmlPrivate::CachedQmlUnit *unitFromResources = QQmlMetaType::findCachedCompilationUnit( QUrl("qrc:/data/script.mjs"), QQmlMetaType::AcceptUntyped, &error); QVERIFY(unitFromResources); QCOMPARE(unitFromResources->qmlData, compilationUnit->unitData()); } } void tst_qmlcachegen::esModulesViaQJSEngine() { QJSEngine engine; QJSValue module = engine.importModule(":/data/module.mjs"); QJSValue result = module.property("entry").call(); QCOMPARE(result.toString(), "ok"); } void tst_qmlcachegen::enums() { QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:///data/Enums.qml")); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QTRY_COMPARE(obj->property("value").toInt(), 200); } void tst_qmlcachegen::sourceFileIndices() { QVERIFY(QFile::exists(":/data/versionchecks.qml")); QVERIFY(QFileInfo(":/data/versionchecks.qml").size() > 0); QQmlMetaType::CachedUnitLookupError error = QQmlMetaType::CachedUnitLookupError::NoError; const QQmlPrivate::CachedQmlUnit *unitFromResources = QQmlMetaType::findCachedCompilationUnit( QUrl("qrc:/data/versionchecks.qml"), QQmlMetaType::AcceptUntyped, &error); QVERIFY(unitFromResources); QVERIFY(unitFromResources->qmlData->flags & QV4::CompiledData::Unit::PendingTypeCompilation); QCOMPARE(uint(unitFromResources->qmlData->sourceFileIndex), uint(0)); } void tst_qmlcachegen::reproducibleCache_data() { QTest::addColumn("filePath"); QDir dir(dataDirectory()); for (const QString &entry : dir.entryList((QStringList() << "*.qml" << "*.js" << "*.mjs"), QDir::Files)) { QTest::newRow(entry.toUtf8().constData()) << dir.filePath(entry); } } void tst_qmlcachegen::reproducibleCache() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QFETCH(QString, filePath); QFile file(filePath); QVERIFY(file.exists()); auto generate = [](const QString &path) { if (!generateCache(path)) return QByteArray(); QFile generated(path + 'c'); [&](){ QVERIFY(generated.open(QIODevice::ReadOnly)); }(); const QByteArray result = generated.readAll(); generated.remove(); return result; }; const QByteArray contents1 = generate(file.fileName()); const QByteArray contents2 = generate(file.fileName()); QCOMPARE(contents1, contents2); } void tst_qmlcachegen::parameterAdjustment() { QQmlEngine engine; CleanlyLoadingComponent component(&engine, QUrl("qrc:///data/parameterAdjustment.qml")); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); // Doesn't crash } void tst_qmlcachegen::inlineComponent() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif QByteArray errors; bool ok = generateCache(testFile("inlineComponentWithId.qml"), &errors); QVERIFY2(ok, errors); QQmlEngine engine; CleanlyLoadingComponent component(&engine, testFileUrl("inlineComponentWithId.qml")); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QTest::ignoreMessage(QtMsgType::QtInfoMsg, "42"); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); } void tst_qmlcachegen::posthocRequired() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif bool ok = generateCache(testFile("posthocrequired.qml")); QVERIFY(ok); QQmlEngine engine; CleanlyLoadingComponent component(&engine, testFileUrl("posthocrequired.qml")); QScopedPointer obj(component.create()); QVERIFY(obj.isNull() && component.isError()); QVERIFY2(component.errorString().contains( QStringLiteral("Required property x was not initialized")), qPrintable(component.errorString())); } void tst_qmlcachegen::scriptStringCachegenInteraction() { #if defined(QTEST_CROSS_COMPILED) QSKIP("Cannot call qmlcachegen on cross-compiled target."); #endif bool ok = generateCache(testFile("scriptstring.qml")); QVERIFY(ok); QQmlEngine engine; CleanlyLoadingComponent component(&engine, testFileUrl("scriptstring.qml")); QScopedPointer root(component.create()); QVERIFY2(!root.isNull(), qPrintable(component.errorString())); auto scripty = qobject_cast(root.get()); QVERIFY(scripty); QVERIFY(scripty->m_undef.isUndefinedLiteral()); QVERIFY(scripty->m_nul.isNullLiteral()); QCOMPARE(scripty->m_str.stringLiteral(), u"hello"_s); QCOMPARE(scripty->m_num.numberLiteral(&ok), 42); ok = false; scripty->m_bol.booleanLiteral(&ok); QVERIFY(ok); } void tst_qmlcachegen::saveableUnitPointer() { QV4::CompiledData::Unit unit; unit.flags = QV4::CompiledData::Unit::StaticData | QV4::CompiledData::Unit::IsJavascript; const auto flags = unit.flags; QV4::CompiledData::SaveableUnitPointer pointer(&unit); QVERIFY(pointer.saveToDisk([](const char *, quint32) { return true; })); QCOMPARE(unit.flags, flags); } const QQmlScriptString &ScriptStringProps::undef() const { return m_undef; } void ScriptStringProps::setUndef(const QQmlScriptString &newUndef) { if (m_undef == newUndef) return; m_undef = newUndef; emit undefChanged(); } const QQmlScriptString &ScriptStringProps::nul() const { return m_nul; } void ScriptStringProps::setNul(const QQmlScriptString &newNul) { if (m_nul == newNul) return; m_nul = newNul; emit nulChanged(); } const QQmlScriptString &ScriptStringProps::str() const { return m_str; } void ScriptStringProps::setStr(const QQmlScriptString &newStr) { if (m_str == newStr) return; m_str = newStr; emit strChanged(); } const QQmlScriptString &ScriptStringProps::num() const { return m_num; } void ScriptStringProps::setNum(const QQmlScriptString &newNum) { if (m_num == newNum) return; m_num = newNum; emit numChanged(); } const QQmlScriptString &ScriptStringProps::bol() const { return m_bol; } void ScriptStringProps::setBol(const QQmlScriptString &newBol) { if (m_bol == newBol) return; m_bol = newBol; emit bolChanged(); } QTEST_GUILESS_MAIN(tst_qmlcachegen) #include "tst_qmlcachegen.moc"