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