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