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:LGPL$ |
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 Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "quicktest_p.h" |
41 | #include "quicktestresult_p.h" |
42 | #include <QtTest/qtestsystem.h> |
43 | #include "qtestoptions_p.h" |
44 | #include <QtQml/qqml.h> |
45 | #include <QtQml/qqmlengine.h> |
46 | #include <QtQml/qqmlcontext.h> |
47 | #include <QtQuick/private/qquickitem_p.h> |
48 | #include <QtQuick/qquickitem.h> |
49 | #include <QtQuick/qquickview.h> |
50 | #include <QtQml/qjsvalue.h> |
51 | #include <QtQml/qjsengine.h> |
52 | #include <QtQml/qqmlpropertymap.h> |
53 | #include <QtQuick/private/qquickitem_p.h> |
54 | #include <QtQuick/qquickitem.h> |
55 | #include <QtGui/qopengl.h> |
56 | #include <QtCore/qurl.h> |
57 | #include <QtCore/qfileinfo.h> |
58 | #include <QtCore/qdir.h> |
59 | #include <QtCore/qdiriterator.h> |
60 | #include <QtCore/qfile.h> |
61 | #include <QtCore/qdebug.h> |
62 | #include <QtCore/qeventloop.h> |
63 | #include <QtCore/qtextstream.h> |
64 | #include <QtCore/qtimer.h> |
65 | #include <QtGui/qtextdocument.h> |
66 | #include <stdio.h> |
67 | #include <QtGui/QGuiApplication> |
68 | #include <QtCore/QTranslator> |
69 | #include <QtTest/QSignalSpy> |
70 | #include <QtQml/QQmlFileSelector> |
71 | |
72 | #include <private/qqmlcomponent_p.h> |
73 | #include <private/qv4executablecompilationunit_p.h> |
74 | |
75 | #ifdef QT_QMLTEST_WITH_WIDGETS |
76 | #include <QtWidgets/QApplication> |
77 | #endif |
78 | |
79 | QT_BEGIN_NAMESPACE |
80 | |
81 | /*! |
82 | \since 5.13 |
83 | |
84 | Returns \c true if \l {QQuickItem::}{updatePolish()} has not been called |
85 | on \a item since the last call to \l {QQuickItem::}{polish()}, |
86 | otherwise returns \c false. |
87 | |
88 | When assigning values to properties in QML, any layouting the item |
89 | must do as a result of the assignment might not take effect immediately, |
90 | but can instead be postponed until the item is polished. For these cases, |
91 | you can use this function to ensure that the item has been polished |
92 | before the execution of the test continues. For example: |
93 | |
94 | \code |
95 | QVERIFY(QQuickTest::qIsPolishScheduled(item)); |
96 | QVERIFY(QQuickTest::qWaitForItemPolished(item)); |
97 | \endcode |
98 | |
99 | Without the call to \c qIsPolishScheduled() above, the |
100 | call to \c qWaitForItemPolished() might see that no polish |
101 | was scheduled and therefore pass instantly, assuming that |
102 | the item had already been polished. This function |
103 | makes it obvious why an item wasn't polished and allows tests to |
104 | fail early under such circumstances. |
105 | |
106 | The QML equivalent of this function is |
107 | \l {TestCase::}{isPolishScheduled()}. |
108 | |
109 | \sa QQuickItem::polish(), QQuickItem::updatePolish() |
110 | */ |
111 | bool QQuickTest::qIsPolishScheduled(const QQuickItem *item) |
112 | { |
113 | return QQuickItemPrivate::get(item)->polishScheduled; |
114 | } |
115 | |
116 | /*! |
117 | \since 5.13 |
118 | |
119 | Waits for \a timeout milliseconds or until |
120 | \l {QQuickItem::}{updatePolish()} has been called on \a item. |
121 | |
122 | Returns \c true if \c updatePolish() was called on \a item within |
123 | \a timeout milliseconds, otherwise returns \c false. |
124 | |
125 | The QML equivalent of this function is |
126 | \l {TestCase::}{waitForItemPolished()}. |
127 | |
128 | \sa QQuickItem::polish(), QQuickItem::updatePolish(), |
129 | QQuickTest::qIsPolishScheduled() |
130 | */ |
131 | bool QQuickTest::qWaitForItemPolished(const QQuickItem *item, int timeout) |
132 | { |
133 | return QTest::qWaitFor(predicate: [&]() { return !QQuickItemPrivate::get(item)->polishScheduled; }, timeout); |
134 | } |
135 | |
136 | static QObject *testRootObject(QQmlEngine *engine, QJSEngine *jsEngine) |
137 | { |
138 | Q_UNUSED(engine); |
139 | Q_UNUSED(jsEngine); |
140 | return QTestRootObject::instance(); |
141 | } |
142 | |
143 | static inline QString stripQuotes(const QString &s) |
144 | { |
145 | if (s.length() >= 2 && s.startsWith(c: QLatin1Char('"')) && s.endsWith(c: QLatin1Char('"'))) |
146 | return s.mid(position: 1, n: s.length() - 2); |
147 | else |
148 | return s; |
149 | } |
150 | |
151 | void handleCompileErrors(const QFileInfo &fi, QQuickView *view) |
152 | { |
153 | // Error compiling the test - flag failure in the log and continue. |
154 | const QList<QQmlError> errors = view->errors(); |
155 | QuickTestResult results; |
156 | results.setTestCaseName(fi.baseName()); |
157 | results.startLogging(); |
158 | results.setFunctionName(QLatin1String("compile" )); |
159 | // Verbose warning output of all messages and relevant parameters |
160 | QString message; |
161 | QTextStream str(&message); |
162 | str << "\n " << QDir::toNativeSeparators(pathName: fi.absoluteFilePath()) << " produced " |
163 | << errors.size() << " error(s):\n" ; |
164 | for (const QQmlError &e : errors) { |
165 | str << " " ; |
166 | if (e.url().isLocalFile()) { |
167 | str << QDir::toNativeSeparators(pathName: e.url().toLocalFile()); |
168 | } else { |
169 | str << e.url().toString(); |
170 | } |
171 | if (e.line() > 0) |
172 | str << ':' << e.line() << ',' << e.column(); |
173 | str << ": " << e.description() << '\n'; |
174 | } |
175 | str << " Working directory: " << QDir::toNativeSeparators(pathName: QDir::current().absolutePath()) << '\n'; |
176 | if (QQmlEngine *engine = view->engine()) { |
177 | str << " View: " << view->metaObject()->className() << ", import paths:\n" ; |
178 | const auto importPaths = engine->importPathList(); |
179 | for (const QString &i : importPaths) |
180 | str << " '" << QDir::toNativeSeparators(pathName: i) << "'\n" ; |
181 | const QStringList pluginPaths = engine->pluginPathList(); |
182 | str << " Plugin paths:\n" ; |
183 | for (const QString &p : pluginPaths) |
184 | str << " '" << QDir::toNativeSeparators(pathName: p) << "'\n" ; |
185 | } |
186 | qWarning(msg: "%s" , qPrintable(message)); |
187 | // Fail with error 0. |
188 | results.fail(message: errors.at(i: 0).description(), |
189 | location: errors.at(i: 0).url(), line: errors.at(i: 0).line()); |
190 | results.finishTestData(); |
191 | results.finishTestDataCleanup(); |
192 | results.finishTestFunction(); |
193 | results.setFunctionName(QString()); |
194 | results.stopLogging(); |
195 | } |
196 | |
197 | bool qWaitForSignal(QObject *obj, const char* signal, int timeout = 5000) |
198 | { |
199 | QSignalSpy spy(obj, signal); |
200 | QElapsedTimer timer; |
201 | timer.start(); |
202 | |
203 | while (!spy.size()) { |
204 | int remaining = timeout - int(timer.elapsed()); |
205 | if (remaining <= 0) |
206 | break; |
207 | QCoreApplication::processEvents(flags: QEventLoop::AllEvents, maxtime: remaining); |
208 | QCoreApplication::sendPostedEvents(receiver: nullptr, event_type: QEvent::DeferredDelete); |
209 | QTest::qSleep(ms: 10); |
210 | } |
211 | |
212 | return spy.size(); |
213 | } |
214 | |
215 | void maybeInvokeSetupMethod(QObject *setupObject, const char *member, QGenericArgument val0 = QGenericArgument(nullptr)) |
216 | { |
217 | // It's OK if it doesn't exist: since we have more than one callback that |
218 | // can be called, it makes sense if the user only implements one of them. |
219 | // We do this the long way rather than just calling the static |
220 | // QMetaObject::invokeMethod(), because that will issue a warning if the |
221 | // function doesn't exist, which we don't want. |
222 | const QMetaObject *setupMetaObject = setupObject->metaObject(); |
223 | const int methodIndex = setupMetaObject->indexOfMethod(method: member); |
224 | if (methodIndex != -1) { |
225 | const QMetaMethod method = setupMetaObject->method(index: methodIndex); |
226 | method.invoke(object: setupObject, val0); |
227 | } |
228 | } |
229 | |
230 | using namespace QV4::CompiledData; |
231 | |
232 | class TestCaseCollector |
233 | { |
234 | public: |
235 | typedef QList<QString> TestCaseList; |
236 | |
237 | TestCaseCollector(const QFileInfo &fileInfo, QQmlEngine *engine) |
238 | { |
239 | QString path = fileInfo.absoluteFilePath(); |
240 | if (path.startsWith(s: QLatin1String(":/" ))) |
241 | path.prepend(s: QLatin1String("qrc" )); |
242 | |
243 | QQmlComponent component(engine, path); |
244 | m_errors += component.errors(); |
245 | |
246 | if (component.isReady()) { |
247 | QQmlRefPointer<QV4::ExecutableCompilationUnit> rootCompilationUnit |
248 | = QQmlComponentPrivate::get(c: &component)->compilationUnit; |
249 | TestCaseEnumerationResult result = enumerateTestCases(compilationUnit: rootCompilationUnit.data()); |
250 | m_testCases = result.testCases + result.finalizedPartialTestCases(); |
251 | m_errors += result.errors; |
252 | } |
253 | } |
254 | |
255 | TestCaseList testCases() const { return m_testCases; } |
256 | QList<QQmlError> errors() const { return m_errors; } |
257 | |
258 | private: |
259 | TestCaseList m_testCases; |
260 | QList<QQmlError> m_errors; |
261 | |
262 | struct TestCaseEnumerationResult |
263 | { |
264 | TestCaseList testCases; |
265 | QList<QQmlError> errors; |
266 | |
267 | // Partially constructed test cases |
268 | bool isTestCase = false; |
269 | TestCaseList testFunctions; |
270 | QString testCaseName; |
271 | |
272 | TestCaseList finalizedPartialTestCases() const |
273 | { |
274 | TestCaseList result; |
275 | for (const QString &function : testFunctions) |
276 | result << QString(QStringLiteral("%1::%2" )).arg(a: testCaseName).arg(a: function); |
277 | return result; |
278 | } |
279 | |
280 | TestCaseEnumerationResult &operator<<(const TestCaseEnumerationResult &other) |
281 | { |
282 | testCases += other.testCases + other.finalizedPartialTestCases(); |
283 | errors += other.errors; |
284 | return *this; |
285 | } |
286 | }; |
287 | |
288 | TestCaseEnumerationResult enumerateTestCases( |
289 | const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit, |
290 | const Object *object = nullptr) |
291 | { |
292 | QQmlType testCaseType; |
293 | for (quint32 i = 0, count = compilationUnit->importCount(); i < count; ++i) { |
294 | const Import *import = compilationUnit->importAt(index: i); |
295 | if (compilationUnit->stringAt(index: import->uriIndex) != QLatin1String("QtTest" )) |
296 | continue; |
297 | |
298 | QString testCaseTypeName(QStringLiteral("TestCase" )); |
299 | QString typeQualifier = compilationUnit->stringAt(index: import->qualifierIndex); |
300 | if (!typeQualifier.isEmpty()) |
301 | testCaseTypeName = typeQualifier % QLatin1Char('.') % testCaseTypeName; |
302 | |
303 | testCaseType = compilationUnit->typeNameCache->query(testCaseTypeName).type; |
304 | if (testCaseType.isValid()) |
305 | break; |
306 | } |
307 | |
308 | TestCaseEnumerationResult result; |
309 | |
310 | if (!object) // Start at root of compilation unit if not enumerating a specific child |
311 | object = compilationUnit->objectAt(index: 0); |
312 | if (object->flags & Object::IsInlineComponentRoot) |
313 | return result; |
314 | |
315 | if (const auto superTypeUnit = compilationUnit->resolvedTypes.value( |
316 | akey: object->inheritedTypeNameIndex)->compilationUnit()) { |
317 | // We have a non-C++ super type, which could indicate we're a subtype of a TestCase |
318 | if (testCaseType.isValid() && superTypeUnit->url() == testCaseType.sourceUrl()) |
319 | result.isTestCase = true; |
320 | else if (superTypeUnit->url() != compilationUnit->url()) { // urls are the same for inline component, avoid infinite recursion |
321 | result = enumerateTestCases(compilationUnit: superTypeUnit); |
322 | } |
323 | |
324 | if (result.isTestCase) { |
325 | // Look for override of name in this type |
326 | for (auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) { |
327 | if (compilationUnit->stringAt(index: binding->propertyNameIndex) == QLatin1String("name" )) { |
328 | if (binding->type == QV4::CompiledData::Binding::Type_String) { |
329 | result.testCaseName = compilationUnit->stringAt(index: binding->stringIndex); |
330 | } else { |
331 | QQmlError error; |
332 | error.setUrl(compilationUnit->url()); |
333 | error.setLine(binding->location.line); |
334 | error.setColumn(binding->location.column); |
335 | error.setDescription(QStringLiteral("the 'name' property of a TestCase must be a literal string" )); |
336 | result.errors << error; |
337 | } |
338 | break; |
339 | } |
340 | } |
341 | |
342 | // Look for additional functions in this type |
343 | auto functionsEnd = compilationUnit->objectFunctionsEnd(object); |
344 | for (auto function = compilationUnit->objectFunctionsBegin(object); function != functionsEnd; ++function) { |
345 | QString functionName = compilationUnit->stringAt(index: function->nameIndex); |
346 | if (!(functionName.startsWith(s: QLatin1String("test_" )) || functionName.startsWith(s: QLatin1String("benchmark_" )))) |
347 | continue; |
348 | |
349 | if (functionName.endsWith(s: QLatin1String("_data" ))) |
350 | continue; |
351 | |
352 | result.testFunctions << functionName; |
353 | } |
354 | } |
355 | } |
356 | |
357 | for (auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) { |
358 | if (binding->type == QV4::CompiledData::Binding::Type_Object) { |
359 | const Object *child = compilationUnit->objectAt(index: binding->value.objectIndex); |
360 | result << enumerateTestCases(compilationUnit, object: child); |
361 | } |
362 | } |
363 | |
364 | return result; |
365 | } |
366 | }; |
367 | |
368 | int quick_test_main(int argc, char **argv, const char *name, const char *sourceDir) |
369 | { |
370 | return quick_test_main_with_setup(argc, argv, name, sourceDir, setup: nullptr); |
371 | } |
372 | |
373 | int quick_test_main_with_setup(int argc, char **argv, const char *name, const char *sourceDir, QObject *setup) |
374 | { |
375 | // Peek at arguments to check for '-widgets' argument |
376 | #ifdef QT_QMLTEST_WITH_WIDGETS |
377 | bool withWidgets = false; |
378 | for (int index = 1; index < argc; ++index) { |
379 | if (strcmp(s1: argv[index], s2: "-widgets" ) == 0) { |
380 | withWidgets = true; |
381 | break; |
382 | } |
383 | } |
384 | #endif |
385 | |
386 | QCoreApplication *app = nullptr; |
387 | if (!QCoreApplication::instance()) { |
388 | #ifdef QT_QMLTEST_WITH_WIDGETS |
389 | if (withWidgets) |
390 | app = new QApplication(argc, argv); |
391 | else |
392 | #endif |
393 | { |
394 | app = new QGuiApplication(argc, argv); |
395 | } |
396 | } |
397 | |
398 | if (setup) |
399 | maybeInvokeSetupMethod(setupObject: setup, member: "applicationAvailable()" ); |
400 | |
401 | // Look for QML-specific command-line options. |
402 | // -import dir Specify an import directory. |
403 | // -plugins dir Specify a directory where to search for plugins. |
404 | // -input dir Specify the input directory for test cases. |
405 | // -translation file Specify the translation file. |
406 | // -file-selector Specify a file selector |
407 | QStringList imports; |
408 | QStringList pluginPaths; |
409 | QString testPath; |
410 | QString translationFile; |
411 | QStringList fileSelectors; |
412 | int index = 1; |
413 | QScopedArrayPointer<char *> testArgV(new char *[argc + 1]); |
414 | testArgV[0] = argv[0]; |
415 | int testArgC = 1; |
416 | while (index < argc) { |
417 | if (strcmp(s1: argv[index], s2: "-import" ) == 0 && (index + 1) < argc) { |
418 | imports += stripQuotes(s: QString::fromLocal8Bit(str: argv[index + 1])); |
419 | index += 2; |
420 | } else if (strcmp(s1: argv[index], s2: "-plugins" ) == 0 && (index + 1) < argc) { |
421 | pluginPaths += stripQuotes(s: QString::fromLocal8Bit(str: argv[index + 1])); |
422 | index += 2; |
423 | } else if (strcmp(s1: argv[index], s2: "-input" ) == 0 && (index + 1) < argc) { |
424 | testPath = stripQuotes(s: QString::fromLocal8Bit(str: argv[index + 1])); |
425 | index += 2; |
426 | } else if (strcmp(s1: argv[index], s2: "-opengl" ) == 0) { |
427 | ++index; |
428 | #ifdef QT_QMLTEST_WITH_WIDGETS |
429 | } else if (strcmp(s1: argv[index], s2: "-widgets" ) == 0) { |
430 | withWidgets = true; |
431 | ++index; |
432 | #endif |
433 | } else if (strcmp(s1: argv[index], s2: "-translation" ) == 0 && (index + 1) < argc) { |
434 | translationFile = stripQuotes(s: QString::fromLocal8Bit(str: argv[index + 1])); |
435 | index += 2; |
436 | } else if (strcmp(s1: argv[index], s2: "-file-selector" ) == 0 && (index + 1) < argc) { |
437 | fileSelectors += stripQuotes(s: QString::fromLocal8Bit(str: argv[index + 1])); |
438 | index += 2; |
439 | } else { |
440 | testArgV[testArgC++] = argv[index++]; |
441 | } |
442 | } |
443 | testArgV[testArgC] = 0; |
444 | |
445 | // Setting currentAppname and currentTestObjectName (via setProgramName) are needed |
446 | // for the code coverage analysis. Must be done before parseArgs is called. |
447 | QuickTestResult::setCurrentAppname(argv[0]); |
448 | QuickTestResult::setProgramName(name); |
449 | |
450 | QuickTestResult::parseArgs(argc: testArgC, argv: testArgV.data()); |
451 | |
452 | #if QT_CONFIG(translation) |
453 | QTranslator translator; |
454 | if (!translationFile.isEmpty()) { |
455 | if (translator.load(filename: translationFile)) { |
456 | app->installTranslator(messageFile: &translator); |
457 | } else { |
458 | qWarning(msg: "Could not load the translation file '%s'." , qPrintable(translationFile)); |
459 | } |
460 | } |
461 | #endif |
462 | |
463 | #if defined(Q_OS_WINRT) |
464 | if (testPath.isEmpty()) |
465 | testPath = QLatin1String(":/" ); |
466 | #endif |
467 | |
468 | // Determine where to look for the test data. |
469 | if (testPath.isEmpty() && sourceDir) { |
470 | const QString s = QString::fromLocal8Bit(str: sourceDir); |
471 | if (QFile::exists(fileName: s)) |
472 | testPath = s; |
473 | } |
474 | |
475 | #if defined(Q_OS_ANDROID) |
476 | if (testPath.isEmpty()) |
477 | testPath = QLatin1String(":/" ); |
478 | #endif |
479 | |
480 | if (testPath.isEmpty()) { |
481 | QDir current = QDir::current(); |
482 | #ifdef Q_OS_WIN |
483 | // Skip release/debug subfolders |
484 | if (!current.dirName().compare(QLatin1String("Release" ), Qt::CaseInsensitive) |
485 | || !current.dirName().compare(QLatin1String("Debug" ), Qt::CaseInsensitive)) |
486 | current.cdUp(); |
487 | #endif // Q_OS_WIN |
488 | testPath = current.absolutePath(); |
489 | } |
490 | QStringList files; |
491 | |
492 | const QFileInfo testPathInfo(testPath); |
493 | if (testPathInfo.isFile()) { |
494 | if (!testPath.endsWith(s: QLatin1String(".qml" ))) { |
495 | qWarning(msg: "'%s' does not have the suffix '.qml'." , qPrintable(testPath)); |
496 | return 1; |
497 | } |
498 | files << testPath; |
499 | } else if (testPathInfo.isDir()) { |
500 | // Scan the test data directory recursively, looking for "tst_*.qml" files. |
501 | const QStringList filters(QStringLiteral("tst_*.qml" )); |
502 | QDirIterator iter(testPathInfo.absoluteFilePath(), filters, QDir::Files, |
503 | QDirIterator::Subdirectories | |
504 | QDirIterator::FollowSymlinks); |
505 | while (iter.hasNext()) |
506 | files += iter.next(); |
507 | files.sort(); |
508 | if (files.isEmpty()) { |
509 | qWarning(msg: "The directory '%s' does not contain any test files matching '%s'" , |
510 | qPrintable(testPath), qPrintable(filters.front())); |
511 | return 1; |
512 | } |
513 | } else { |
514 | qWarning(msg: "'%s' does not exist under '%s'." , |
515 | qPrintable(testPath), qPrintable(QDir::currentPath())); |
516 | return 1; |
517 | } |
518 | |
519 | qputenv(varName: "QT_QTESTLIB_RUNNING" , value: "1" ); |
520 | |
521 | // Register the custom factory function |
522 | qmlRegisterSingletonType<QTestRootObject>(uri: "Qt.test.qtestroot" , versionMajor: 1, versionMinor: 0, typeName: "QTestRootObject" , callback: testRootObject); |
523 | |
524 | QSet<QString> commandLineTestFunctions(QTest::testFunctions.cbegin(), QTest::testFunctions.cend()); |
525 | const bool filteringTestFunctions = !commandLineTestFunctions.isEmpty(); |
526 | |
527 | // Scan through all of the "tst_*.qml" files and run each of them |
528 | // in turn with a separate QQuickView (for test isolation). |
529 | for (const QString &file : qAsConst(t&: files)) { |
530 | const QFileInfo fi(file); |
531 | if (!fi.exists()) |
532 | continue; |
533 | |
534 | QQmlEngine engine; |
535 | for (const QString &path : qAsConst(t&: imports)) |
536 | engine.addImportPath(dir: path); |
537 | for (const QString &path : qAsConst(t&: pluginPaths)) |
538 | engine.addPluginPath(dir: path); |
539 | |
540 | if (!fileSelectors.isEmpty()) { |
541 | QQmlFileSelector* const qmlFileSelector = new QQmlFileSelector(&engine, &engine); |
542 | qmlFileSelector->setExtraSelectors(fileSelectors); |
543 | } |
544 | |
545 | // Do this down here so that import paths, plugin paths, file selectors, etc. are available |
546 | // in case the user needs access to them. Do it _before_ the TestCaseCollector parses the |
547 | // QML files though, because it attempts to import modules, which might not be available |
548 | // if qmlRegisterType()/QQmlEngine::addImportPath() are called in qmlEngineAvailable(). |
549 | if (setup) |
550 | maybeInvokeSetupMethod(setupObject: setup, member: "qmlEngineAvailable(QQmlEngine*)" , Q_ARG(QQmlEngine*, &engine)); |
551 | |
552 | TestCaseCollector testCaseCollector(fi, &engine); |
553 | if (!testCaseCollector.errors().isEmpty()) { |
554 | for (const QQmlError &error : testCaseCollector.errors()) |
555 | qWarning() << error; |
556 | exit(status: 1); |
557 | } |
558 | |
559 | TestCaseCollector::TestCaseList availableTestFunctions = testCaseCollector.testCases(); |
560 | if (QTest::printAvailableFunctions) { |
561 | for (const QString &function : availableTestFunctions) |
562 | qDebug(msg: "%s()" , qPrintable(function)); |
563 | continue; |
564 | } |
565 | |
566 | const QSet<QString> availableTestSet(availableTestFunctions.cbegin(), availableTestFunctions.cend()); |
567 | if (filteringTestFunctions && !availableTestSet.intersects(other: commandLineTestFunctions)) |
568 | continue; |
569 | commandLineTestFunctions.subtract(other: availableTestSet); |
570 | |
571 | QQuickView view(&engine, nullptr); |
572 | view.setFlags(Qt::Window | Qt::WindowSystemMenuHint |
573 | | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint |
574 | | Qt::WindowCloseButtonHint); |
575 | QEventLoop eventLoop; |
576 | QObject::connect(sender: view.engine(), SIGNAL(quit()), |
577 | receiver: QTestRootObject::instance(), SLOT(quit())); |
578 | QObject::connect(sender: view.engine(), SIGNAL(quit()), |
579 | receiver: &eventLoop, SLOT(quit())); |
580 | view.rootContext()->setContextProperty |
581 | (QLatin1String("qtest" ), QTestRootObject::instance()); // Deprecated. Use QTestRootObject from Qt.test.qtestroot instead |
582 | |
583 | view.setObjectName(fi.baseName()); |
584 | view.setTitle(view.objectName()); |
585 | QTestRootObject::instance()->init(); |
586 | QString path = fi.absoluteFilePath(); |
587 | if (path.startsWith(s: QLatin1String(":/" ))) |
588 | view.setSource(QUrl(QLatin1String("qrc:" ) + path.midRef(position: 1))); |
589 | else |
590 | view.setSource(QUrl::fromLocalFile(localfile: path)); |
591 | |
592 | while (view.status() == QQuickView::Loading) |
593 | QTest::qWait(ms: 10); |
594 | if (view.status() == QQuickView::Error) { |
595 | handleCompileErrors(fi, view: &view); |
596 | continue; |
597 | } |
598 | if (!QTestRootObject::instance()->hasQuit) { |
599 | // If the test already quit, then it was performed |
600 | // synchronously during setSource(). Otherwise it is |
601 | // an asynchronous test and we need to show the window |
602 | // and wait for the first frame to be rendered |
603 | // and then wait for quit indication. |
604 | view.setFramePosition(QPoint(50, 50)); |
605 | if (view.size().isEmpty()) { // Avoid hangs with empty windows. |
606 | view.resize(w: 200, h: 200); |
607 | } |
608 | view.show(); |
609 | if (!QTest::qWaitForWindowExposed(window: &view)) { |
610 | qWarning().nospace() |
611 | << "Test '" << QDir::toNativeSeparators(pathName: path) << "' window not exposed after show()." ; |
612 | } |
613 | view.requestActivate(); |
614 | if (!QTest::qWaitForWindowActive(window: &view)) { |
615 | qWarning().nospace() |
616 | << "Test '" << QDir::toNativeSeparators(pathName: path) << "' window not active after requestActivate()." ; |
617 | } |
618 | if (view.isExposed()) { |
619 | // Defer property update until event loop has started |
620 | QTimer::singleShot(interval: 0, slot: []() { |
621 | QTestRootObject::instance()->setWindowShown(true); |
622 | }); |
623 | } else { |
624 | qWarning().nospace() |
625 | << "Test '" << QDir::toNativeSeparators(pathName: path) << "' window was never exposed! " |
626 | << "If the test case was expecting windowShown, it will hang." ; |
627 | } |
628 | if (!QTestRootObject::instance()->hasQuit && QTestRootObject::instance()->hasTestCase()) |
629 | eventLoop.exec(); |
630 | } |
631 | } |
632 | |
633 | if (setup) |
634 | maybeInvokeSetupMethod(setupObject: setup, member: "cleanupTestCase()" ); |
635 | |
636 | // Flush the current logging stream. |
637 | QuickTestResult::setProgramName(nullptr); |
638 | delete app; |
639 | |
640 | // Check that all test functions passed on the command line were found |
641 | if (!commandLineTestFunctions.isEmpty()) { |
642 | qWarning() << "Could not find the following test functions:" ; |
643 | for (const QString &functionName : qAsConst(t&: commandLineTestFunctions)) |
644 | qWarning(msg: " %s()" , qUtf8Printable(functionName)); |
645 | return commandLineTestFunctions.count(); |
646 | } |
647 | |
648 | // Return the number of failures as the exit code. |
649 | return QuickTestResult::exitCode(); |
650 | } |
651 | |
652 | QT_END_NAMESPACE |
653 | |