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
48QT_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*/
80bool 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*/
108bool 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*/
131bool 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*/
150bool 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*/
170bool QQuickTest::qWaitForPolish(const QQuickWindow *window, int timeout)
171{
172 return QTest::qWaitFor(predicate: [&]() { return QQuickWindowPrivate::get(c: window)->itemsToPolish.isEmpty(); }, timeout);
173}
174
175static 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
183static 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
233class SimpleReceiver : public QObject {
234 Q_OBJECT
235public:
236 bool signalReceived = false;
237public slots:
238 void slotFun() { signalReceived = true; }
239};
240
241bool 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
274template <typename... Args>
275void 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
290using namespace QV4::CompiledData;
291
292class TestCaseCollector
293{
294public:
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
319private:
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
431int 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
437static QFile androidExitCodeFile()
438{
439 const QString testHome = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
440 return QFile(testHome + "/qtest_last_exit_code");
441}
442#endif
443
444int 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
731QT_END_NAMESPACE
732
733#include "moc_quicktest_p.cpp"
734#include "quicktest.moc"
735

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtdeclarative/src/qmltest/quicktest.cpp