983 lines
34 KiB
C++
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"
|