qtdeclarative/tests/auto/qml/qmlcachegen/tst_qmlcachegen.cpp

983 lines
34 KiB
C++

// 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 <qtest.h>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QProcess>
#include <QLibraryInfo>
#include <QStandardPaths>
#include <QSysInfo>
#include <QLoggingCategory>
#include <private/qqmlcomponent_p.h>
#include <private/qqmlscriptdata_p.h>
#include <private/qv4compileddata_p.h>
#include <qtranslator.h>
#include <qqmlscriptstring.h>
#include <QString>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#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 gracefullyHandleTruncatedCacheFile();
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<const QV4::CompiledData::Unit *>(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<QObject> 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<QObject> 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<const QV4::CompiledData::Unit *>(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<QObject> 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->baseCompilationUnit()->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<QObject> 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<void *>(tweakedUnit),
reinterpret_cast<const void *>(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<QV4::CompiledData::Unit *>(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<QObject> 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<QObject> 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<QString>("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<QObject> 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<QObject> 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<const QV4::CompiledData::Unit *>(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<QObject> 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<QObject> 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<QObject> 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<QString>("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<QObject> 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<QObject> 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<QObject> 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::gracefullyHandleTruncatedCacheFile()
{
#if defined(QTEST_CROSS_COMPILED)
QSKIP("Cannot call qmlcachegen on cross-compiled target.");
#endif
bool ok = generateCache(testFile("truncateTest.qml"));
QVERIFY(ok);
const QString qmlcFile = testFile("truncateTest.qmlc");
QVERIFY(QFile::exists(qmlcFile));
QFile::resize(qmlcFile, QFileInfo(qmlcFile).size() / 2);
QQmlEngine engine;
CleanlyLoadingComponent component(&engine, testFileUrl("truncateTest.qml"));
QScopedPointer<QObject> obj(component.create());
QVERIFY(!obj.isNull());
}
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<QObject> root(component.create());
QVERIFY2(!root.isNull(), qPrintable(component.errorString()));
auto scripty = qobject_cast<ScriptStringProps *>(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<char>([](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"