178 const QFileInfo &fi,
const QList<QQmlError> &errors, QQmlEngine *engine,
179 QQuickView *view =
nullptr)
183 results.setTestCaseName(fi.baseName());
184 results.startLogging();
185 results.setFunctionName(QLatin1String(
"compile"));
188 QTextStream str(&message);
189 str <<
"\n " << QDir::toNativeSeparators(fi.absoluteFilePath()) <<
" produced "
190 << errors.size() <<
" error(s):\n";
191 for (
const QQmlError &e : errors) {
193 if (e.url().isLocalFile()) {
194 str << QDir::toNativeSeparators(e.url().toLocalFile());
196 str << e.url().toString();
199 str <<
':' << e.line() <<
',' << e.column();
200 str <<
": " << e.description() <<
'\n';
202 str <<
" Working directory: " << QDir::toNativeSeparators(QDir::current().absolutePath()) <<
'\n';
206 str <<
"View: " << view->metaObject()->className() <<
", ";
207 str <<
"Import paths:\n";
208 const auto importPaths = engine->importPathList();
209 for (
const QString &i : importPaths)
210 str <<
" '" << QDir::toNativeSeparators(i) <<
"'\n";
211 const QStringList pluginPaths = engine->pluginPathList();
212 str <<
" Plugin paths:\n";
213 for (
const QString &p : pluginPaths)
214 str <<
" '" << QDir::toNativeSeparators(p) <<
"'\n";
216 qWarning(
"%s", qPrintable(message));
218 results.fail(errors.at(0).description(),
219 errors.at(0).url(), errors.at(0).line());
220 results.finishTestData();
221 results.finishTestDataCleanup();
222 results.finishTestFunction();
223 results.setFunctionName(QString());
224 results.stopLogging();
293 QString path = fileInfo.absoluteFilePath();
294 if (path.startsWith(QLatin1String(
":/")))
295 path.prepend(QLatin1String(
"qrc"));
297 QQmlComponent component(engine, path);
298 m_errors += component.errors();
300 if (component.isReady()) {
301 QQmlRefPointer<QV4::ExecutableCompilationUnit> rootCompilationUnit
302 = QQmlComponentPrivate::get(&component)->compilationUnit();
303 TestCaseEnumerationResult result = enumerateTestCases(
304 rootCompilationUnit->baseCompilationUnit().data());
305 m_testCases = result.testCases + result.finalizedPartialTestCases();
306 m_errors += result.errors;
315 QList<QQmlError> m_errors;
316 QQmlEngine *m_engine =
nullptr;
318 struct TestCaseEnumerationResult
321 QList<QQmlError> errors;
324 bool isTestCase =
false;
326 QString testCaseName;
331 for (
const QString &function : testFunctions)
332 result << QString(QStringLiteral(
"%1::%2")).arg(testCaseName).arg(function);
336 TestCaseEnumerationResult &operator<<(
const TestCaseEnumerationResult &other)
338 testCases += other.testCases + other.finalizedPartialTestCases();
339 errors += other.errors;
344 TestCaseEnumerationResult enumerateTestCases(
345 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit,
346 const QV4::CompiledData::Object *object =
nullptr)
348 QQmlType testCaseType;
349 for (quint32 i = 0, count = compilationUnit->importCount(); i < count; ++i) {
350 const Import *import = compilationUnit->importAt(i);
351 if (compilationUnit->stringAt(import->uriIndex) != QLatin1String(
"QtTest"))
354 QString testCaseTypeName(QStringLiteral(
"TestCase"));
355 QString typeQualifier = compilationUnit->stringAt(import->qualifierIndex);
356 if (!typeQualifier.isEmpty())
357 testCaseTypeName = typeQualifier % QLatin1Char(
'.') % testCaseTypeName;
359 testCaseType = compilationUnit->typeNameCache->query(
360 testCaseTypeName, QQmlTypeLoader::get(m_engine)).type;
361 if (testCaseType.isValid())
365 TestCaseEnumerationResult result;
368 object = compilationUnit->objectAt(0);
369 if (object->hasFlag(QV4::CompiledData::Object::IsInlineComponentRoot))
372 if (
const auto superType
373 = compilationUnit->resolvedType(object->inheritedTypeNameIndex)->type();
374 superType.isComposite() || superType.isInlineComponentType()) {
377 QUrl baseUrl = superType.sourceUrl();
378 baseUrl.setFragment(QString());
381 if (testCaseType.isValid() && baseUrl == testCaseType.sourceUrl()) {
382 result.isTestCase =
true;
383 }
else if (baseUrl != compilationUnit->url()) {
385 result = enumerateTestCases(QQmlMetaType::obtainCompilationUnit(baseUrl));
388 if (result.isTestCase) {
390 for (
auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) {
391 if (compilationUnit->stringAt(binding->propertyNameIndex) == QLatin1String(
"name")) {
392 if (binding->type() == QV4::CompiledData::Binding::Type_String) {
393 result.testCaseName = compilationUnit->stringAt(binding->stringIndex);
396 error.setUrl(compilationUnit->url());
397 error.setLine(binding->location.line());
398 error.setColumn(binding->location.column());
399 error.setDescription(QStringLiteral(
"the 'name' property of a TestCase must be a literal string"));
400 result.errors << error;
407 auto functionsEnd = compilationUnit->objectFunctionsEnd(object);
408 for (
auto function = compilationUnit->objectFunctionsBegin(object); function != functionsEnd; ++function) {
409 QString functionName = compilationUnit->stringAt(function->nameIndex);
410 if (!(functionName.startsWith(QLatin1String(
"test_")) || functionName.startsWith(QLatin1String(
"benchmark_"))))
413 if (functionName.endsWith(QLatin1String(
"_data")))
416 result.testFunctions << functionName;
421 for (
auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) {
422 if (binding->type() == QV4::CompiledData::Binding::Type_Object) {
423 const QV4::CompiledData::Object *child = compilationUnit->objectAt(binding->value.objectIndex);
424 result << enumerateTestCases(compilationUnit, child);
439 QScopedPointer<QCoreApplication> app;
440 if (!QCoreApplication::instance())
441 app.reset(
new QGuiApplication(argc, argv));
444 clearAndroidExitCode();
448 maybeInvokeSetupMethod(setup,
"applicationAvailable()");
457 QStringList pluginPaths;
459 QString translationFile;
460 QStringList fileSelectors;
462 QScopedArrayPointer<
char *> testArgV(
new char *[argc + 1]);
463 testArgV[0] = argv[0];
465 while (index < argc) {
466 if (strcmp(argv[index],
"-import") == 0 && (index + 1) < argc) {
467 imports += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
469 }
else if (strcmp(argv[index],
"-plugins") == 0 && (index + 1) < argc) {
470 pluginPaths += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
472 }
else if (strcmp(argv[index],
"-input") == 0 && (index + 1) < argc) {
473 testPath = stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
475 }
else if (strcmp(argv[index],
"-opengl") == 0) {
477 }
else if (strcmp(argv[index],
"-translation") == 0 && (index + 1) < argc) {
478 translationFile = stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
480 }
else if (strcmp(argv[index],
"-file-selector") == 0 && (index + 1) < argc) {
481 fileSelectors += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
484 testArgV[testArgC++] = argv[index++];
487 testArgV[testArgC] = 0;
491 QuickTestResult::setCurrentAppname(argv[0]);
492 QuickTestResult::setProgramName(name);
494 QuickTestResult::parseArgs(testArgC, testArgV.data());
496#if QT_CONFIG(translation)
497 QTranslator translator;
498 if (!translationFile.isEmpty()) {
499 if (translator.load(translationFile)) {
500 app->installTranslator(&translator);
502 qWarning(
"Could not load the translation file '%s'.", qPrintable(translationFile));
508 if (testPath.isEmpty() && sourceDir) {
509 const QString s = QString::fromLocal8Bit(sourceDir);
510 if (QFile::exists(s))
514#if defined(Q_OS_ANDROID) || defined(Q_OS_INTEGRITY)
515 if (testPath.isEmpty())
516 testPath = QLatin1String(
":/");
519 if (testPath.isEmpty()) {
520 QDir current = QDir::current();
523 if (!current.dirName().compare(QLatin1String(
"Release"), Qt::CaseInsensitive)
524 || !current.dirName().compare(QLatin1String(
"Debug"), Qt::CaseInsensitive))
527 testPath = current.absolutePath();
531 const QFileInfo testPathInfo(testPath);
532 if (testPathInfo.isFile()) {
533 if (testPath.endsWith(QLatin1String(
".qml"))) {
535 }
else if (testPath.endsWith(QLatin1String(
".qmltests"))) {
536 QFile file(testPath);
537 if (file.open(QIODevice::ReadOnly)) {
538 while (!file.atEnd()) {
539 const QString filePath = testPathInfo.dir()
540 .filePath(QString::fromUtf8(file.readLine()))
542 const QFileInfo f(filePath);
544 files.append(filePath);
546 qWarning(
"The test file '%s' does not exists", qPrintable(filePath));
550 if (files.isEmpty()) {
551 qWarning(
"The file '%s' does not contain any tests files",
552 qPrintable(testPath));
556 qWarning(
"Could not read '%s'", qPrintable(testPath));
559 qWarning(
"'%s' does not have the suffix '.qml' or '.qmltests'.", qPrintable(testPath));
562 }
else if (testPathInfo.isDir()) {
564 const QStringList filters(QStringLiteral(
"tst_*.qml"));
565 QDirIterator iter(testPathInfo.absoluteFilePath(), filters, QDir::Files,
566 QDirIterator::Subdirectories |
567 QDirIterator::FollowSymlinks);
568 while (iter.hasNext())
569 files += iter.next();
571 if (files.isEmpty()) {
572 qWarning(
"The directory '%s' does not contain any test files matching '%s'",
573 qPrintable(testPath), qPrintable(filters.front()));
577 qWarning(
"'%s' does not exist under '%s'.",
578 qPrintable(testPath), qPrintable(QDir::currentPath()));
582 std::optional<QTest::CrashHandler::FatalSignalHandler> handler;
583 QTest::CrashHandler::prepareStackTrace();
584 if (!QTest::Internal::noCrashHandler)
587 qputenv(
"QT_QTESTLIB_RUNNING",
"1");
589 QSet<QString> commandLineTestFunctions(QTest::testFunctions.cbegin(), QTest::testFunctions.cend());
590 const bool filteringTestFunctions = !commandLineTestFunctions.isEmpty();
594 for (
const QString &file : std::as_const(files)) {
595 const QFileInfo fi(file);
600 for (
const QString &path : std::as_const(imports))
601 engine.addImportPath(path);
602 for (
const QString &path : std::as_const(pluginPaths))
603 engine.addPluginPath(path);
605 if (!fileSelectors.isEmpty()) {
606 QQmlFileSelector*
const qmlFileSelector =
new QQmlFileSelector(&engine, &engine);
607 qmlFileSelector->setExtraSelectors(fileSelectors);
615 maybeInvokeSetupMethod(setup,
"qmlEngineAvailable(QQmlEngine*)", Q_ARG(QQmlEngine*, &engine));
617 TestCaseCollector testCaseCollector(fi, &engine);
618 if (!testCaseCollector.errors().isEmpty()) {
619 handleCompileErrors(fi, testCaseCollector.errors(), &engine);
623 TestCaseCollector::TestCaseList availableTestFunctions = testCaseCollector.testCases();
624 if (QTest::printAvailableFunctions) {
625 for (
const QString &function : availableTestFunctions)
626 qDebug(
"%s()", qPrintable(function));
630 const QSet<QString> availableTestSet(availableTestFunctions.cbegin(), availableTestFunctions.cend());
631 if (filteringTestFunctions && !availableTestSet.intersects(commandLineTestFunctions))
633 commandLineTestFunctions.subtract(availableTestSet);
635 QQuickView view(&engine,
nullptr);
636 view.setFlags(Qt::Window | Qt::WindowSystemMenuHint
637 | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint
638 | Qt::WindowCloseButtonHint);
639 QEventLoop eventLoop;
640 QObject::connect(view.engine(), SIGNAL(quit()),
641 QTestRootObject::instance(), SLOT(quit()));
642 QObject::connect(view.engine(), SIGNAL(quit()),
643 &eventLoop, SLOT(quit()));
644 view.rootContext()->setContextProperty
645 (QLatin1String(
"qtest"), QTestRootObject::instance());
647 view.setObjectName(fi.baseName());
648 view.setTitle(view.objectName());
649 QTestRootObject::instance()->init();
650 QString path = fi.absoluteFilePath();
651 if (path.startsWith(QLatin1String(
":/")))
652 view.setSource(QUrl(QLatin1String(
"qrc:") + QStringView{path}.mid(1)));
654 view.setSource(QUrl::fromLocalFile(path));
656 while (view.status() == QQuickView::Loading)
658 if (view.status() == QQuickView::Error) {
659 handleCompileErrors(fi, view.errors(), view.engine(), &view);
663 view.setFramePosition(QPoint(50, 50));
664 if (view.size().isEmpty()) {
665 view.resize(200, 200);
668 if (!QTest::qWaitForWindowExposed(&view)) {
670 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window not exposed after show().";
672 if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) {
673 view.requestActivate();
674 if (!QTest::qWaitForWindowActive(&view)) {
676 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window not active after requestActivate().";
679 if (view.isExposed()) {
681 QTimer::singleShot(0, []() {
682 QTestRootObject::instance()->setWindowShown(
true);
686 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window was never exposed! "
687 <<
"If the test case was expecting windowShown, it will hang.";
689 if (!QTestRootObject::instance()->hasQuit && QTestRootObject::instance()->hasTestCase())
694 maybeInvokeSetupMethod(setup,
"cleanupTestCase()");
697 QuickTestResult::setProgramName(
nullptr);
701 if (!commandLineTestFunctions.isEmpty()) {
702 qWarning() <<
"Could not find the following test functions:";
703 for (
const QString &functionName : std::as_const(commandLineTestFunctions))
704 qWarning(
" %s()", qUtf8Printable(functionName));
705 return commandLineTestFunctions.size();
708 const int exitCode = QuickTestResult::exitCode();
711 writeAndroidExitCode(exitCode);