1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | #include <qtest.h> |
29 | #include <qdir.h> |
30 | #include <QtQml/qqmlengine.h> |
31 | #include <QtQml/qqmlcomponent.h> |
32 | #include <QtQml/qqmlcontext.h> |
33 | #include <QtQml/qqmlextensionplugin.h> |
34 | #include <QtCore/qjsondocument.h> |
35 | #include <QtCore/qjsonarray.h> |
36 | #include <QDebug> |
37 | |
38 | #if defined(Q_OS_MAC) |
39 | // For _PC_CASE_SENSITIVE |
40 | #include <unistd.h> |
41 | #endif |
42 | |
43 | #include "../../shared/testhttpserver.h" |
44 | #include "../../shared/util.h" |
45 | |
46 | // Note: this test does not use module identifier directives in the qmldir files, because |
47 | // it would result in repeated attempts to insert types into the same namespace. |
48 | // This occurs because type registration is process-global, while the test |
49 | // cases should really be run in proper per-process isolation. |
50 | |
51 | class tst_qqmlmoduleplugin : public QQmlDataTest |
52 | { |
53 | Q_OBJECT |
54 | public: |
55 | |
56 | private slots: |
57 | virtual void initTestCase(); |
58 | void importsPlugin(); |
59 | void importsPlugin_data(); |
60 | void importsMixedQmlCppPlugin(); |
61 | void incorrectPluginCase(); |
62 | void importPluginWithQmlFile(); |
63 | void remoteImportWithQuotedUrl(); |
64 | void remoteImportWithUnquotedUri(); |
65 | void versionNotInstalled(); |
66 | void versionNotInstalled_data(); |
67 | void implicitQmldir(); |
68 | void implicitQmldir_data(); |
69 | void importsNested(); |
70 | void importsNested_data(); |
71 | void importLocalModule(); |
72 | void importLocalModule_data(); |
73 | void importStrictModule(); |
74 | void importStrictModule_data(); |
75 | void importProtectedModule(); |
76 | void importVersionedModule(); |
77 | void importsChildPlugin(); |
78 | void importsChildPlugin2(); |
79 | void importsChildPlugin21(); |
80 | void parallelPluginImport(); |
81 | void multiSingleton(); |
82 | |
83 | private: |
84 | QString m_importsDirectory; |
85 | QString m_dataImportsDirectory; |
86 | }; |
87 | |
88 | class PluginThatWaits : public QQmlExtensionPlugin |
89 | { |
90 | public: |
91 | static QByteArray metaData; |
92 | |
93 | static QMutex initializeEngineEntered; |
94 | static QWaitCondition waitingForInitializeEngineEntry; |
95 | static QMutex leavingInitializeEngine; |
96 | static QWaitCondition waitingForInitializeEngineLeave; |
97 | |
98 | void registerTypes(const char *uri) override |
99 | { |
100 | qmlRegisterModule(uri, versionMajor: 1, versionMinor: 0); |
101 | } |
102 | |
103 | void initializeEngine(QQmlEngine *, const char *) override |
104 | { |
105 | initializeEngineEntered.lock(); |
106 | leavingInitializeEngine.lock(); |
107 | waitingForInitializeEngineEntry.wakeOne(); |
108 | initializeEngineEntered.unlock(); |
109 | waitingForInitializeEngineLeave.wait(lockedMutex: &leavingInitializeEngine); |
110 | leavingInitializeEngine.unlock(); |
111 | } |
112 | }; |
113 | QByteArray PluginThatWaits::metaData; |
114 | QMutex PluginThatWaits::initializeEngineEntered; |
115 | QWaitCondition PluginThatWaits::waitingForInitializeEngineEntry; |
116 | QMutex PluginThatWaits::leavingInitializeEngine; |
117 | QWaitCondition PluginThatWaits::waitingForInitializeEngineLeave; |
118 | |
119 | class SecondStaticPlugin : public QQmlExtensionPlugin |
120 | { |
121 | public: |
122 | static QByteArray metaData; |
123 | |
124 | void registerTypes(const char *uri) override |
125 | { |
126 | qmlRegisterModule(uri, versionMajor: 1, versionMinor: 0); |
127 | } |
128 | }; |
129 | QByteArray SecondStaticPlugin::metaData; |
130 | |
131 | template <typename PluginType> |
132 | void registerStaticPlugin(const char *uri) |
133 | { |
134 | QStaticPlugin plugin; |
135 | plugin.instance = []() { |
136 | static PluginType plugin; |
137 | return static_cast<QObject*>(&plugin); |
138 | }; |
139 | |
140 | QJsonObject md; |
141 | md.insert(QStringLiteral("IID" ), QQmlExtensionInterface_iid); |
142 | QJsonArray uris; |
143 | uris.append(value: uri); |
144 | md.insert(QStringLiteral("uri" ), uris); |
145 | |
146 | PluginType::metaData.append(QLatin1String("QTMETADATA " )); |
147 | PluginType::metaData.append(QJsonDocument(md).toBinaryData()); |
148 | |
149 | plugin.rawMetaData = []() { |
150 | return PluginType::metaData.constData(); |
151 | }; |
152 | qRegisterStaticPluginFunction(staticPlugin: plugin); |
153 | }; |
154 | |
155 | void tst_qqmlmoduleplugin::initTestCase() |
156 | { |
157 | QQmlDataTest::initTestCase(); |
158 | m_importsDirectory = QFINDTESTDATA(QStringLiteral("imports" )); |
159 | QVERIFY2(QFileInfo(m_importsDirectory).isDir(), |
160 | qPrintable(QString::fromLatin1("Imports directory '%1' does not exist." ).arg(m_importsDirectory))); |
161 | m_dataImportsDirectory = directory() + QStringLiteral("/imports" ); |
162 | QVERIFY2(QFileInfo(m_dataImportsDirectory).isDir(), |
163 | qPrintable(QString::fromLatin1("Imports directory '%1' does not exist." ).arg(m_dataImportsDirectory))); |
164 | |
165 | registerStaticPlugin<PluginThatWaits>(uri: "moduleWithWaitingPlugin" ); |
166 | registerStaticPlugin<SecondStaticPlugin>(uri: "moduleWithStaticPlugin" ); |
167 | } |
168 | |
169 | #define VERIFY_ERRORS(errorfile) \ |
170 | if (!errorfile) { \ |
171 | if (qgetenv("DEBUG") != "" && !component.errors().isEmpty()) \ |
172 | qWarning() << "Unexpected Errors:" << component.errors(); \ |
173 | QVERIFY(!component.isError()); \ |
174 | QVERIFY(component.errors().isEmpty()); \ |
175 | } else { \ |
176 | QString verify_errors_file_name = testFile(errorfile); \ |
177 | QFile file(verify_errors_file_name); \ |
178 | QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); \ |
179 | QByteArray data = file.readAll(); \ |
180 | file.close(); \ |
181 | QList<QByteArray> expected = data.split('\n'); \ |
182 | expected.removeAll(QByteArray("")); \ |
183 | QList<QQmlError> errors = component.errors(); \ |
184 | QList<QByteArray> actual; \ |
185 | for (int ii = 0; ii < errors.count(); ++ii) { \ |
186 | const QQmlError &error = errors.at(ii); \ |
187 | QByteArray errorStr = QByteArray::number(error.line()) + ':' + \ |
188 | QByteArray::number(error.column()) + ':' + \ |
189 | error.description().toUtf8(); \ |
190 | actual << errorStr; \ |
191 | } \ |
192 | if (qgetenv("DEBUG") != "" && expected != actual) { \ |
193 | qWarning() << "Expected:" << expected << "Actual:" << actual; \ |
194 | } \ |
195 | if (qgetenv("QDECLARATIVELANGUAGE_UPDATEERRORS") != "" && expected != actual) {\ |
196 | QFile file(testFile(errorfile)); \ |
197 | QVERIFY(file.open(QIODevice::WriteOnly)); \ |
198 | for (int ii = 0; ii < actual.count(); ++ii) { \ |
199 | file.write(actual.at(ii)); file.write("\n"); \ |
200 | } \ |
201 | file.close(); \ |
202 | } else { \ |
203 | QCOMPARE(expected, actual); \ |
204 | } \ |
205 | } |
206 | |
207 | void tst_qqmlmoduleplugin::importsPlugin() |
208 | { |
209 | QFETCH(QString, suffix); |
210 | QFETCH(QString, qmlFile); |
211 | |
212 | QQmlEngine engine; |
213 | engine.addImportPath(dir: m_importsDirectory); |
214 | QTest::ignoreMessage(type: QtWarningMsg, qPrintable(QString("plugin%1 created" ).arg(suffix))); |
215 | QTest::ignoreMessage(type: QtWarningMsg, qPrintable(QString("import%1 worked" ).arg(suffix))); |
216 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlPluginType' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
217 | QQmlComponent component(&engine, testFileUrl(fileName: qmlFile)); |
218 | foreach (QQmlError err, component.errors()) |
219 | qWarning() << err; |
220 | VERIFY_ERRORS(0); |
221 | QObject *object = component.create(); |
222 | QVERIFY(object != nullptr); |
223 | QCOMPARE(object->property("value" ).toInt(),123); |
224 | delete object; |
225 | } |
226 | |
227 | void tst_qqmlmoduleplugin::importsPlugin_data() |
228 | { |
229 | QTest::addColumn<QString>(name: "suffix" ); |
230 | QTest::addColumn<QString>(name: "qmlFile" ); |
231 | |
232 | QTest::newRow(dataTag: "1.0" ) << "" << "works.qml" ; |
233 | QTest::newRow(dataTag: "2.0" ) << "2" << "works2.qml" ; |
234 | QTest::newRow(dataTag: "2.1" ) << "2.1" << "works21.qml" ; |
235 | QTest::newRow(dataTag: "2.2" ) << "2.2" << "works22.qml" ; |
236 | } |
237 | |
238 | void tst_qqmlmoduleplugin::incorrectPluginCase() |
239 | { |
240 | QQmlEngine engine; |
241 | engine.addImportPath(dir: m_importsDirectory); |
242 | |
243 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("incorrectCase.qml" ))); |
244 | |
245 | QList<QQmlError> errors = component.errors(); |
246 | QCOMPARE(errors.count(), 1); |
247 | |
248 | QString expectedError = QLatin1String("module \"org.qtproject.WrongCase\" plugin \"PluGin\" not found" ); |
249 | |
250 | #if defined(Q_OS_MAC) || defined(Q_OS_WIN32) |
251 | bool caseSensitive = true; |
252 | #if defined(Q_OS_MAC) |
253 | caseSensitive = pathconf(QDir::currentPath().toLatin1().constData(), _PC_CASE_SENSITIVE); |
254 | QString libname = "libPluGin.dylib" ; |
255 | #elif defined(Q_OS_WIN32) |
256 | caseSensitive = false; |
257 | QString libname = "PluGin.dll" ; |
258 | #endif |
259 | if (!caseSensitive) { |
260 | expectedError = QLatin1String("plugin cannot be loaded for module \"org.qtproject.WrongCase\": File name case mismatch for \"" ) |
261 | + QDir(m_importsDirectory).filePath("org/qtproject/WrongCase/" + libname) |
262 | + QLatin1Char('"'); |
263 | } |
264 | #endif |
265 | |
266 | QCOMPARE(errors.at(0).description(), expectedError); |
267 | } |
268 | |
269 | void tst_qqmlmoduleplugin::importPluginWithQmlFile() |
270 | { |
271 | QString path = m_importsDirectory; |
272 | |
273 | // QTBUG-16885: adding an import path with a lower-case "c:" causes assert failure |
274 | // (this only happens if the plugin includes pure QML files) |
275 | #ifdef Q_OS_WIN |
276 | QVERIFY(path.at(0).isUpper() && path.at(1) == QLatin1Char(':')); |
277 | path = path.at(0).toLower() + path.mid(1); |
278 | #endif |
279 | |
280 | QQmlEngine engine; |
281 | engine.addImportPath(dir: path); |
282 | |
283 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestPluginWithQmlFile' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
284 | |
285 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("pluginWithQmlFile.qml" ))); |
286 | foreach (QQmlError err, component.errors()) |
287 | qWarning() << err; |
288 | VERIFY_ERRORS(0); |
289 | QObject *object = component.create(); |
290 | QVERIFY(object != nullptr); |
291 | delete object; |
292 | } |
293 | |
294 | void tst_qqmlmoduleplugin::remoteImportWithQuotedUrl() |
295 | { |
296 | ThreadedTestHTTPServer server(m_dataImportsDirectory); |
297 | |
298 | QQmlEngine engine; |
299 | QQmlComponent component(&engine); |
300 | const QString qml = "import \"" + server.urlString(documentPath: "/org/qtproject/PureQmlModule" ) + "\" \nComponentA { width: 300; ComponentB{} }" ; |
301 | component.setData(qml.toUtf8(), baseUrl: QUrl()); |
302 | |
303 | QTRY_COMPARE(component.status(), QQmlComponent::Ready); |
304 | QObject *object = component.create(); |
305 | QCOMPARE(object->property("width" ).toInt(), 300); |
306 | QVERIFY(object != nullptr); |
307 | delete object; |
308 | |
309 | foreach (QQmlError err, component.errors()) |
310 | qWarning() << err; |
311 | VERIFY_ERRORS(0); |
312 | } |
313 | |
314 | void tst_qqmlmoduleplugin::remoteImportWithUnquotedUri() |
315 | { |
316 | TestHTTPServer server; |
317 | QVERIFY2(server.listen(), qPrintable(server.errorString())); |
318 | server.serveDirectory(m_dataImportsDirectory); |
319 | |
320 | QQmlEngine engine; |
321 | engine.addImportPath(dir: m_dataImportsDirectory); |
322 | QQmlComponent component(&engine); |
323 | component.setData("import org.qtproject.PureQmlModule 1.0 \nComponentA { width: 300; ComponentB{} }" , baseUrl: QUrl()); |
324 | |
325 | |
326 | QTRY_COMPARE(component.status(), QQmlComponent::Ready); |
327 | QObject *object = component.create(); |
328 | QVERIFY(object != nullptr); |
329 | QCOMPARE(object->property("width" ).toInt(), 300); |
330 | delete object; |
331 | |
332 | foreach (QQmlError err, component.errors()) |
333 | qWarning() << err; |
334 | VERIFY_ERRORS(0); |
335 | } |
336 | |
337 | static QByteArray msgComponentError(const QQmlComponent &c, const QQmlEngine *engine /* = 0 */) |
338 | { |
339 | QString result; |
340 | const QList<QQmlError> errors = c.errors(); |
341 | QTextStream str(&result); |
342 | str << "Component '" << c.url().toString() << "' has " << errors.size() << " errors: '" ; |
343 | for (int i = 0; i < errors.size(); ++i) { |
344 | if (i) |
345 | str << ", '" ; |
346 | str << errors.at(i).toString() << '\''; |
347 | } |
348 | if (!engine) { |
349 | if (QQmlContext *context = c.creationContext()) |
350 | engine = context->engine(); |
351 | } |
352 | if (engine) { |
353 | str << " Import paths: (" << engine->importPathList().join(QStringLiteral(", " )) |
354 | << ") Plugin paths: (" << engine->pluginPathList().join(QStringLiteral(", " )) |
355 | << ')'; |
356 | } |
357 | return result.toLocal8Bit(); |
358 | } |
359 | |
360 | // QTBUG-17324 |
361 | |
362 | void tst_qqmlmoduleplugin::importsMixedQmlCppPlugin() |
363 | { |
364 | QQmlEngine engine; |
365 | engine.addImportPath(dir: m_importsDirectory); |
366 | |
367 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlMixedPluginType' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
368 | |
369 | { |
370 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("importsMixedQmlCppPlugin.qml" ))); |
371 | |
372 | QObject *o = component.create(); |
373 | QVERIFY2(o != nullptr, msgComponentError(component, &engine)); |
374 | QCOMPARE(o->property("test" ).toBool(), true); |
375 | delete o; |
376 | } |
377 | |
378 | { |
379 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("importsMixedQmlCppPlugin.2.qml" ))); |
380 | |
381 | QObject *o = component.create(); |
382 | QVERIFY2(o != nullptr, msgComponentError(component, &engine)); |
383 | QCOMPARE(o->property("test" ).toBool(), true); |
384 | QCOMPARE(o->property("test2" ).toBool(), true); |
385 | delete o; |
386 | } |
387 | |
388 | |
389 | } |
390 | |
391 | void tst_qqmlmoduleplugin::versionNotInstalled_data() |
392 | { |
393 | QTest::addColumn<QString>(name: "file" ); |
394 | QTest::addColumn<QString>(name: "errorFile" ); |
395 | |
396 | QTest::newRow(dataTag: "versionNotInstalled" ) << "versionNotInstalled.qml" << "versionNotInstalled.errors.txt" ; |
397 | QTest::newRow(dataTag: "versionNotInstalled" ) << "versionNotInstalled.2.qml" << "versionNotInstalled.2.errors.txt" ; |
398 | } |
399 | |
400 | void tst_qqmlmoduleplugin::versionNotInstalled() |
401 | { |
402 | QFETCH(QString, file); |
403 | QFETCH(QString, errorFile); |
404 | |
405 | QQmlEngine engine; |
406 | engine.addImportPath(dir: m_importsDirectory); |
407 | |
408 | static int count = 0; |
409 | if (++count == 1) |
410 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlVersionPluginType' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
411 | |
412 | QQmlComponent component(&engine, testFileUrl(fileName: file)); |
413 | VERIFY_ERRORS(errorFile.toLatin1().constData()); |
414 | } |
415 | |
416 | |
417 | // test that errors are reporting correctly for plugin loading and qmldir parsing |
418 | void tst_qqmlmoduleplugin::implicitQmldir_data() |
419 | { |
420 | QTest::addColumn<QString>(name: "directory" ); |
421 | QTest::addColumn<QString>(name: "file" ); |
422 | QTest::addColumn<QString>(name: "errorFile" ); |
423 | |
424 | // parsing qmldir succeeds, but plugin specified in the qmldir file doesn't exist |
425 | QTest::newRow(dataTag: "implicitQmldir" ) << "implicit1" << "temptest.qml" << "implicitQmldir.errors.txt" ; |
426 | |
427 | // parsing qmldir fails due to syntax errors, etc. |
428 | QTest::newRow(dataTag: "implicitQmldir2" ) << "implicit2" << "temptest2.qml" << "implicitQmldir.2.errors.txt" ; |
429 | } |
430 | void tst_qqmlmoduleplugin::implicitQmldir() |
431 | { |
432 | QFETCH(QString, directory); |
433 | QFETCH(QString, file); |
434 | QFETCH(QString, errorFile); |
435 | |
436 | QString importPath = testFile(fileName: directory); |
437 | QString fileName = directory + QDir::separator() + file; |
438 | QString errorFileName = directory + QDir::separator() + errorFile; |
439 | QUrl testUrl = testFileUrl(fileName); |
440 | |
441 | QQmlEngine engine; |
442 | engine.addImportPath(dir: importPath); |
443 | |
444 | QQmlComponent component(&engine, testUrl); |
445 | QList<QQmlError> errors = component.errors(); |
446 | VERIFY_ERRORS(errorFileName.toLatin1().constData()); |
447 | QTest::ignoreMessage(type: QtWarningMsg, message: "QQmlComponent: Component is not ready" ); |
448 | QObject *obj = component.create(); |
449 | QVERIFY(!obj); |
450 | delete obj; |
451 | } |
452 | |
453 | void tst_qqmlmoduleplugin::importsNested_data() |
454 | { |
455 | QTest::addColumn<QString>(name: "file" ); |
456 | QTest::addColumn<QString>(name: "errorFile" ); |
457 | |
458 | // Note: no other test case should import the plugin used for this test, or the |
459 | // wrong order test will pass spuriously |
460 | QTest::newRow(dataTag: "wrongOrder" ) << "importsNested.1.qml" << "importsNested.1.errors.txt" ; |
461 | QTest::newRow(dataTag: "missingImport" ) << "importsNested.3.qml" << "importsNested.3.errors.txt" ; |
462 | QTest::newRow(dataTag: "invalidVersion" ) << "importsNested.4.qml" << "importsNested.4.errors.txt" ; |
463 | QTest::newRow(dataTag: "correctOrder" ) << "importsNested.2.qml" << QString(); |
464 | } |
465 | void tst_qqmlmoduleplugin::importsNested() |
466 | { |
467 | QFETCH(QString, file); |
468 | QFETCH(QString, errorFile); |
469 | |
470 | // Note: because imports are cached between test case data rows (and the plugins remain loaded), |
471 | // these tests should really be run in new instances of the app... |
472 | |
473 | QQmlEngine engine; |
474 | engine.addImportPath(dir: m_importsDirectory); |
475 | |
476 | if (!errorFile.isEmpty()) { |
477 | QTest::ignoreMessage(type: QtWarningMsg, message: "QQmlComponent: Component is not ready" ); |
478 | } |
479 | |
480 | static int count = 0; |
481 | if (++count == 1) |
482 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlNestedPluginType' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
483 | |
484 | QQmlComponent component(&engine, testFile(fileName: file)); |
485 | QObject *obj = component.create(); |
486 | |
487 | if (errorFile.isEmpty()) { |
488 | if (qgetenv(varName: "DEBUG" ) != "" && !component.errors().isEmpty()) |
489 | qWarning() << "Unexpected Errors:" << component.errors(); |
490 | QVERIFY(obj); |
491 | delete obj; |
492 | } else { |
493 | QList<QQmlError> errors = component.errors(); |
494 | VERIFY_ERRORS(errorFile.toLatin1().constData()); |
495 | QVERIFY(!obj); |
496 | } |
497 | } |
498 | |
499 | void tst_qqmlmoduleplugin::importLocalModule() |
500 | { |
501 | QFETCH(QString, qml); |
502 | QFETCH(int, majorVersion); |
503 | QFETCH(int, minorVersion); |
504 | |
505 | QQmlEngine engine; |
506 | QQmlComponent component(&engine); |
507 | component.setData(qml.toUtf8(), baseUrl: testFileUrl(fileName: "empty.qml" )); |
508 | |
509 | QScopedPointer<QObject> object(component.create()); |
510 | QVERIFY(object != nullptr); |
511 | QCOMPARE(object->property("majorVersion" ).value<int>(), majorVersion); |
512 | QCOMPARE(object->property("minorVersion" ).value<int>(), minorVersion); |
513 | } |
514 | |
515 | void tst_qqmlmoduleplugin::importLocalModule_data() |
516 | { |
517 | QTest::addColumn<QString>(name: "qml" ); |
518 | QTest::addColumn<int>(name: "majorVersion" ); |
519 | QTest::addColumn<int>(name: "minorVersion" ); |
520 | |
521 | QTest::newRow(dataTag: "default version" ) |
522 | << "import \"localModule\"\n" |
523 | "TestComponent {}" |
524 | << 2 << 0; |
525 | |
526 | QTest::newRow(dataTag: "specific version" ) |
527 | << "import \"localModule\" 1.1\n" |
528 | "TestComponent {}" |
529 | << 1 << 1; |
530 | |
531 | QTest::newRow(dataTag: "lesser version" ) |
532 | << "import \"localModule\" 1.0\n" |
533 | "TestComponent {}" |
534 | << 1 << 0; |
535 | |
536 | // Note: this does not match the behaviour of installed modules, which fail for this case: |
537 | QTest::newRow(dataTag: "nonexistent version" ) |
538 | << "import \"localModule\" 1.3\n" |
539 | "TestComponent {}" |
540 | << 1 << 1; |
541 | |
542 | QTest::newRow(dataTag: "high version" ) |
543 | << "import \"localModule\" 2.0\n" |
544 | "TestComponent {}" |
545 | << 2 << 0; |
546 | } |
547 | |
548 | void tst_qqmlmoduleplugin::importStrictModule() |
549 | { |
550 | QFETCH(QString, qml); |
551 | QFETCH(QString, warning); |
552 | QFETCH(QString, error); |
553 | |
554 | if (!warning.isEmpty()) |
555 | QTest::ignoreMessage(type: QtWarningMsg, qPrintable(warning)); |
556 | |
557 | QQmlEngine engine; |
558 | engine.addImportPath(dir: m_importsDirectory); |
559 | |
560 | QUrl url(testFileUrl(fileName: "empty.qml" )); |
561 | |
562 | QQmlComponent component(&engine); |
563 | component.setData(qml.toUtf8(), baseUrl: url); |
564 | |
565 | if (error.isEmpty()) { |
566 | QScopedPointer<QObject> object(component.create()); |
567 | QVERIFY(object != nullptr); |
568 | } else { |
569 | QVERIFY(!component.isReady()); |
570 | QCOMPARE(component.errors().count(), 1); |
571 | QCOMPARE(component.errors().first().toString(), url.toString() + error); |
572 | } |
573 | } |
574 | |
575 | void tst_qqmlmoduleplugin::importStrictModule_data() |
576 | { |
577 | QTest::addColumn<QString>(name: "qml" ); |
578 | QTest::addColumn<QString>(name: "warning" ); |
579 | QTest::addColumn<QString>(name: "error" ); |
580 | |
581 | QTest::newRow(dataTag: "success" ) |
582 | << "import org.qtproject.StrictModule 1.0\n" |
583 | "MyPluginType {}" |
584 | << QString() |
585 | << QString(); |
586 | |
587 | QTest::newRow(dataTag: "two strict modules with different major version" ) |
588 | << "import org.qtproject.StrictModule 1.0\n" |
589 | "import org.qtproject.StrictModule 2.0\n" |
590 | "MyPluginType {}" |
591 | << QString() |
592 | << QString(); |
593 | |
594 | QTest::newRow(dataTag: "old namespaced strict module" ) |
595 | << "import org.qtproject.StrictModule 1.0 as Old\n" |
596 | "import org.qtproject.StrictModule 2.0 as New\n" |
597 | "Old.MyPluginType {}" |
598 | << QString() |
599 | << QString(); |
600 | |
601 | QTest::newRow(dataTag: "new namespaced strict modules" ) |
602 | << "import org.qtproject.StrictModule 1.0 as Old\n" |
603 | "import org.qtproject.StrictModule 2.0 as New\n" |
604 | "New.MyPluginType {}" |
605 | << QString() |
606 | << QString(); |
607 | |
608 | QTest::newRow(dataTag: "non-strict clash" ) |
609 | << "import org.qtproject.NonstrictModule 1.0\n" |
610 | "MyPluginType {}" |
611 | << "Module 'org.qtproject.NonstrictModule' does not contain a module identifier directive - it cannot be protected from external registrations." |
612 | << ":1:1: plugin cannot be loaded for module \"org.qtproject.NonstrictModule\": Cannot install element 'MyPluginType' into protected module 'org.qtproject.StrictModule' version '1'" ; |
613 | |
614 | QTest::newRow(dataTag: "non-strict preemption" ) |
615 | << "import org.qtproject.PreemptiveModule 1.0\n" |
616 | "import org.qtproject.PreemptedStrictModule 1.0\n" |
617 | "MyPluginType {}" |
618 | << "Module 'org.qtproject.PreemptiveModule' does not contain a module identifier directive - it cannot be protected from external registrations." |
619 | << ":2:1: plugin cannot be loaded for module \"org.qtproject.PreemptedStrictModule\": Namespace 'org.qtproject.PreemptedStrictModule' has already been used for type registration" ; |
620 | |
621 | QTest::newRow(dataTag: "invalid namespace" ) |
622 | << "import org.qtproject.InvalidNamespaceModule 1.0\n" |
623 | "MyPluginType {}" |
624 | << QString() |
625 | << ":1:1: plugin cannot be loaded for module \"org.qtproject.InvalidNamespaceModule\": Module namespace 'org.qtproject.AwesomeModule' does not match import URI 'org.qtproject.InvalidNamespaceModule'" ; |
626 | |
627 | QTest::newRow(dataTag: "module directive must be first" ) |
628 | << "import org.qtproject.InvalidFirstCommandModule 1.0\n" |
629 | "MyPluginType {}" |
630 | << QString() |
631 | << ":1:1: module identifier directive must be the first directive in a qmldir file" ; |
632 | } |
633 | |
634 | void tst_qqmlmoduleplugin::importProtectedModule() |
635 | { |
636 | //TODO: More than basic test (test errors,test inverse works...) |
637 | qmlRegisterType<QObject>(uri: "org.qtproject.ProtectedModule" , versionMajor: 1, versionMinor: 0, qmlName: "TestType" ); |
638 | qmlProtectModule(uri: "org.qtproject.ProtectedModule" , majVersion: 1); |
639 | |
640 | QQmlEngine engine; |
641 | engine.addImportPath(dir: m_importsDirectory); |
642 | |
643 | QUrl url(testFileUrl(fileName: "empty.qml" )); |
644 | |
645 | QString qml = "import org.qtproject.ProtectedModule 1.0\n TestType {}\n" ; |
646 | QQmlComponent component(&engine); |
647 | component.setData(qml.toUtf8(), baseUrl: url); |
648 | //If plugin is loaded due to import, should assert |
649 | QScopedPointer<QObject> object(component.create()); |
650 | //qDebug() << component.errorString(); |
651 | QVERIFY(object != nullptr); |
652 | } |
653 | |
654 | void tst_qqmlmoduleplugin::importVersionedModule() |
655 | { |
656 | qmlRegisterType<QObject>(uri: "org.qtproject.VersionedModule" , versionMajor: 1, versionMinor: 0, qmlName: "TestType" ); |
657 | qmlRegisterModule(uri: "org.qtproject.VersionedModule" , versionMajor: 1, versionMinor: 1); |
658 | |
659 | QQmlEngine engine; |
660 | engine.addImportPath(dir: m_importsDirectory); |
661 | |
662 | QUrl url(testFileUrl(fileName: "empty.qml" )); |
663 | |
664 | QQmlComponent component(&engine); |
665 | component.setData("import org.qtproject.VersionedModule 1.0\n TestType {}\n" , baseUrl: url); |
666 | QScopedPointer<QObject> object10(component.create()); |
667 | QVERIFY(!object10.isNull()); |
668 | |
669 | component.setData("import org.qtproject.VersionedModule 1.1\n TestType {}\n" , baseUrl: url); |
670 | QScopedPointer<QObject> object11(component.create()); |
671 | QVERIFY(!object11.isNull()); |
672 | |
673 | component.setData("import org.qtproject.VersionedModule 1.2\n TestType {}\n" , baseUrl: url); |
674 | QTest::ignoreMessage(type: QtWarningMsg, message: "QQmlComponent: Component is not ready" ); |
675 | QScopedPointer<QObject> object12(component.create()); |
676 | QVERIFY(object12.isNull()); |
677 | QCOMPARE(component.errorString(), QString("%1:1 module \"org.qtproject.VersionedModule\" version 1.2 is not installed\n" ).arg(url.toString())); |
678 | } |
679 | |
680 | void tst_qqmlmoduleplugin::importsChildPlugin() |
681 | { |
682 | QQmlEngine engine; |
683 | engine.addImportPath(dir: m_importsDirectory); |
684 | QTest::ignoreMessage(type: QtWarningMsg, message: "child plugin created" ); |
685 | QTest::ignoreMessage(type: QtWarningMsg, message: "child import worked" ); |
686 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlPluginType.ChildPlugin' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
687 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("child.qml" ))); |
688 | foreach (QQmlError err, component.errors()) |
689 | qWarning() << err; |
690 | VERIFY_ERRORS(0); |
691 | QObject *object = component.create(); |
692 | QVERIFY(object != nullptr); |
693 | QCOMPARE(object->property("value" ).toInt(),123); |
694 | delete object; |
695 | } |
696 | |
697 | void tst_qqmlmoduleplugin::importsChildPlugin2() |
698 | { |
699 | QQmlEngine engine; |
700 | engine.addImportPath(dir: m_importsDirectory); |
701 | QTest::ignoreMessage(type: QtWarningMsg, message: "child plugin2 created" ); |
702 | QTest::ignoreMessage(type: QtWarningMsg, message: "child import2 worked" ); |
703 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlPluginType.ChildPlugin' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
704 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("child2.qml" ))); |
705 | foreach (QQmlError err, component.errors()) |
706 | qWarning() << err; |
707 | VERIFY_ERRORS(0); |
708 | QObject *object = component.create(); |
709 | QVERIFY(object != nullptr); |
710 | QCOMPARE(object->property("value" ).toInt(),123); |
711 | delete object; |
712 | } |
713 | |
714 | void tst_qqmlmoduleplugin::importsChildPlugin21() |
715 | { |
716 | QQmlEngine engine; |
717 | engine.addImportPath(dir: m_importsDirectory); |
718 | QTest::ignoreMessage(type: QtWarningMsg, message: "child plugin2.1 created" ); |
719 | QTest::ignoreMessage(type: QtWarningMsg, message: "child import2.1 worked" ); |
720 | QTest::ignoreMessage(type: QtWarningMsg, message: "Module 'org.qtproject.AutoTestQmlPluginType.ChildPlugin' does not contain a module identifier directive - it cannot be protected from external registrations." ); |
721 | QQmlComponent component(&engine, testFileUrl(QStringLiteral("child21.qml" ))); |
722 | foreach (QQmlError err, component.errors()) |
723 | qWarning() << err; |
724 | VERIFY_ERRORS(0); |
725 | QObject *object = component.create(); |
726 | QVERIFY(object != nullptr); |
727 | QCOMPARE(object->property("value" ).toInt(),123); |
728 | delete object; |
729 | } |
730 | |
731 | void tst_qqmlmoduleplugin::parallelPluginImport() |
732 | { |
733 | QMutexLocker locker(&PluginThatWaits::initializeEngineEntered); |
734 | |
735 | QThread worker; |
736 | QObject::connect(sender: &worker, signal: &QThread::started, slot: [&worker](){ |
737 | // Engines in separate threads are tricky, but as long as we do not create a graphical |
738 | // object and move objects created by the engines across thread boundaries, this is safe. |
739 | // At the same time this allows us to place the engine's loader thread into the position |
740 | // where, without the fix for this bug, the global lock is acquired. |
741 | QQmlEngine engineInThread; |
742 | |
743 | QQmlComponent component(&engineInThread); |
744 | component.setData("import moduleWithWaitingPlugin 1.0\nimport QtQml 2.0\nQtObject {}" , |
745 | baseUrl: QUrl()); |
746 | |
747 | QScopedPointer<QObject> obj(component.create()); |
748 | QVERIFY(!obj.isNull()); |
749 | |
750 | worker.quit(); |
751 | }); |
752 | worker.start(); |
753 | |
754 | PluginThatWaits::waitingForInitializeEngineEntry.wait(lockedMutex: &PluginThatWaits::initializeEngineEntered); |
755 | |
756 | { |
757 | // After acquiring this lock, the engine in the other thread as well as its type loader |
758 | // thread are blocked. However they should not hold the global plugin lock |
759 | // qmlEnginePluginsWithRegisteredTypes()->mutex in qqmllimports.cpp, allowing for the load |
760 | // of a component in a different engine with its own plugin to proceed. |
761 | QMutexLocker continuationLock(&PluginThatWaits::leavingInitializeEngine); |
762 | |
763 | QQmlEngine secondEngine; |
764 | QQmlComponent secondComponent(&secondEngine); |
765 | secondComponent.setData("import moduleWithStaticPlugin 1.0\nimport QtQml 2.0\nQtObject {}" , |
766 | baseUrl: QUrl()); |
767 | QScopedPointer<QObject> o(secondComponent.create()); |
768 | QVERIFY(!o.isNull()); |
769 | |
770 | PluginThatWaits::waitingForInitializeEngineLeave.wakeOne(); |
771 | } |
772 | |
773 | worker.wait(); |
774 | } |
775 | |
776 | void tst_qqmlmoduleplugin::multiSingleton() |
777 | { |
778 | QQmlEngine engine; |
779 | QObject obj; |
780 | qmlRegisterSingletonInstance(uri: "Test" , versionMajor: 1, versionMinor: 0, typeName: "Tracker" , cppObject: &obj); |
781 | engine.addImportPath(dir: m_importsDirectory); |
782 | QQmlComponent component(&engine, testFileUrl(fileName: "multiSingleton.qml" )); |
783 | QObject *object = component.create(); |
784 | QVERIFY(object != nullptr); |
785 | QCOMPARE(obj.objectName(), QLatin1String("first" )); |
786 | delete object; |
787 | } |
788 | |
789 | |
790 | QTEST_MAIN(tst_qqmlmoduleplugin) |
791 | |
792 | #include "tst_qqmlmoduleplugin.moc" |
793 | |