184 const QFileInfo &fi,
const QList<QQmlError> &errors, QQmlEngine *engine,
185 QQuickView *view =
nullptr)
189 results.setTestCaseName(fi.baseName());
190 results.startLogging();
191 results.setFunctionName(QLatin1String(
"compile"));
194 QTextStream str(&message);
195 str <<
"\n " << QDir::toNativeSeparators(fi.absoluteFilePath()) <<
" produced "
196 << errors.size() <<
" error(s):\n";
197 for (
const QQmlError &e : errors) {
199 if (e.url().isLocalFile()) {
200 str << QDir::toNativeSeparators(e.url().toLocalFile());
202 str << e.url().toString();
205 str <<
':' << e.line() <<
',' << e.column();
206 str <<
": " << e.description() <<
'\n';
208 str <<
" Working directory: " << QDir::toNativeSeparators(QDir::current().absolutePath()) <<
'\n';
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(i) <<
"'\n";
217 const QStringList pluginPaths = engine->pluginPathList();
218 str <<
" Plugin paths:\n";
219 for (
const QString &p : pluginPaths)
220 str <<
" '" << QDir::toNativeSeparators(p) <<
"'\n";
222 qWarning(
"%s", qPrintable(message));
224 results.fail(errors.at(0).description(),
225 errors.at(0).url(), errors.at(0).line());
226 results.finishTestData();
227 results.finishTestDataCleanup();
228 results.finishTestFunction();
229 results.setFunctionName(QString());
230 results.stopLogging();
299 QString path = fileInfo.absoluteFilePath();
300 if (path.startsWith(QLatin1String(
":/")))
301 path.prepend(QLatin1String(
"qrc"));
303 QQmlComponent component(engine, path);
304 m_errors += component.errors();
306 if (component.isReady()) {
307 QQmlRefPointer<QV4::ExecutableCompilationUnit> rootCompilationUnit
308 = QQmlComponentPrivate::get(&component)->compilationUnit();
309 TestCaseEnumerationResult result = enumerateTestCases(
310 rootCompilationUnit->baseCompilationUnit().data());
311 m_testCases = result.testCases + result.finalizedPartialTestCases();
312 m_errors += result.errors;
321 QList<QQmlError> m_errors;
322 QQmlEngine *m_engine =
nullptr;
324 struct TestCaseEnumerationResult
327 QList<QQmlError> errors;
330 bool isTestCase =
false;
332 QString testCaseName;
337 for (
const QString &function : testFunctions)
338 result << QString(QStringLiteral(
"%1::%2")).arg(testCaseName).arg(function);
342 TestCaseEnumerationResult &operator<<(
const TestCaseEnumerationResult &other)
344 testCases += other.testCases + other.finalizedPartialTestCases();
345 errors += other.errors;
350 TestCaseEnumerationResult enumerateTestCases(
351 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit,
352 const QV4::CompiledData::Object *object =
nullptr)
354 QQmlType testCaseType;
355 for (quint32 i = 0, count = compilationUnit->importCount(); i < count; ++i) {
356 const Import *import = compilationUnit->importAt(i);
357 if (compilationUnit->stringAt(import->uriIndex) != QLatin1String(
"QtTest"))
360 QString testCaseTypeName(QStringLiteral(
"TestCase"));
361 QString typeQualifier = compilationUnit->stringAt(import->qualifierIndex);
362 if (!typeQualifier.isEmpty())
363 testCaseTypeName = typeQualifier % QLatin1Char(
'.') % testCaseTypeName;
365 testCaseType = compilationUnit->typeNameCache->query(
366 testCaseTypeName, QQmlTypeLoader::get(m_engine)).type;
367 if (testCaseType.isValid())
371 TestCaseEnumerationResult result;
374 object = compilationUnit->objectAt(0);
375 if (object->hasFlag(QV4::CompiledData::Object::IsInlineComponentRoot))
378 if (
const auto superType
379 = compilationUnit->resolvedType(object->inheritedTypeNameIndex)->type();
380 superType.isComposite() || superType.isInlineComponentType()) {
383 QUrl baseUrl = superType.sourceUrl();
384 baseUrl.setFragment(QString());
387 if (testCaseType.isValid() && baseUrl == testCaseType.sourceUrl()) {
388 result.isTestCase =
true;
389 }
else if (baseUrl != compilationUnit->url()) {
391 result = enumerateTestCases(QQmlMetaType::obtainCompilationUnit(baseUrl));
394 if (result.isTestCase) {
396 for (
auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) {
397 if (compilationUnit->stringAt(binding->propertyNameIndex) == QLatin1String(
"name")) {
398 if (binding->type() == QV4::CompiledData::Binding::Type_String) {
399 result.testCaseName = compilationUnit->stringAt(binding->stringIndex);
402 error.setUrl(compilationUnit->url());
403 error.setLine(binding->location.line());
404 error.setColumn(binding->location.column());
405 error.setDescription(QStringLiteral(
"the 'name' property of a TestCase must be a literal string"));
406 result.errors << error;
413 auto functionsEnd = compilationUnit->objectFunctionsEnd(object);
414 for (
auto function = compilationUnit->objectFunctionsBegin(object); function != functionsEnd; ++function) {
415 QString functionName = compilationUnit->stringAt(function->nameIndex);
416 if (!(functionName.startsWith(QLatin1String(
"test_")) || functionName.startsWith(QLatin1String(
"benchmark_"))))
419 if (functionName.endsWith(QLatin1String(
"_data")))
422 result.testFunctions << functionName;
427 for (
auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) {
428 if (binding->type() == QV4::CompiledData::Binding::Type_Object) {
429 const QV4::CompiledData::Object *child = compilationUnit->objectAt(binding->value.objectIndex);
430 result << enumerateTestCases(compilationUnit, child);
445 QScopedPointer<QCoreApplication> app;
446 if (!QCoreApplication::instance())
447 app.reset(
new QGuiApplication(argc, argv));
450 clearAndroidExitCode();
454 maybeInvokeSetupMethod(setup,
"applicationAvailable()");
463 QStringList pluginPaths;
465 QString translationFile;
466 QStringList fileSelectors;
468 QScopedArrayPointer<
char *> testArgV(
new char *[argc + 1]);
469 testArgV[0] = argv[0];
471 while (index < argc) {
472 if (strcmp(argv[index],
"-import") == 0 && (index + 1) < argc) {
473 imports += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
475 }
else if (strcmp(argv[index],
"-plugins") == 0 && (index + 1) < argc) {
476 pluginPaths += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
478 }
else if (strcmp(argv[index],
"-input") == 0 && (index + 1) < argc) {
479 testPath = stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
481 }
else if (strcmp(argv[index],
"-opengl") == 0) {
483 }
else if (strcmp(argv[index],
"-translation") == 0 && (index + 1) < argc) {
484 translationFile = stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
486 }
else if (strcmp(argv[index],
"-file-selector") == 0 && (index + 1) < argc) {
487 fileSelectors += stripQuotes(QString::fromLocal8Bit(argv[index + 1]));
490 testArgV[testArgC++] = argv[index++];
493 testArgV[testArgC] = 0;
497 QuickTestResult::setCurrentAppname(argv[0]);
498 QuickTestResult::setProgramName(name);
500 QuickTestResult::parseArgs(testArgC, testArgV.data());
502#if QT_CONFIG(translation)
503 QTranslator translator;
504 if (!translationFile.isEmpty()) {
505 if (translator.load(translationFile)) {
506 app->installTranslator(&translator);
508 qWarning(
"Could not load the translation file '%s'.", qPrintable(translationFile));
514 if (testPath.isEmpty() && sourceDir) {
515 const QString s = QString::fromLocal8Bit(sourceDir);
516 if (QFile::exists(s))
520#if defined(Q_OS_ANDROID) || defined(Q_OS_INTEGRITY)
521 if (testPath.isEmpty())
522 testPath = QLatin1String(
":/");
525 if (testPath.isEmpty()) {
526 QDir current = QDir::current();
529 if (!current.dirName().compare(QLatin1String(
"Release"), Qt::CaseInsensitive)
530 || !current.dirName().compare(QLatin1String(
"Debug"), Qt::CaseInsensitive))
533 testPath = current.absolutePath();
537 const QFileInfo testPathInfo(testPath);
538 if (testPathInfo.isFile()) {
539 if (testPath.endsWith(QLatin1String(
".qml"))) {
541 }
else if (testPath.endsWith(QLatin1String(
".qmltests"))) {
542 QFile file(testPath);
543 if (file.open(QIODevice::ReadOnly)) {
544 while (!file.atEnd()) {
545 const QString filePath = testPathInfo.dir()
546 .filePath(QString::fromUtf8(file.readLine()))
548 const QFileInfo f(filePath);
550 files.append(filePath);
552 qWarning(
"The test file '%s' does not exists", qPrintable(filePath));
556 if (files.isEmpty()) {
557 qWarning(
"The file '%s' does not contain any tests files",
558 qPrintable(testPath));
562 qWarning(
"Could not read '%s'", qPrintable(testPath));
565 qWarning(
"'%s' does not have the suffix '.qml' or '.qmltests'.", qPrintable(testPath));
568 }
else if (testPathInfo.isDir()) {
570 const QStringList filters(QStringLiteral(
"tst_*.qml"));
571 QDirIterator iter(testPathInfo.absoluteFilePath(), filters, QDir::Files,
572 QDirIterator::Subdirectories |
573 QDirIterator::FollowSymlinks);
574 while (iter.hasNext())
575 files += iter.next();
577 if (files.isEmpty()) {
578 qWarning(
"The directory '%s' does not contain any test files matching '%s'",
579 qPrintable(testPath), qPrintable(filters.front()));
583 qWarning(
"'%s' does not exist under '%s'.",
584 qPrintable(testPath), qPrintable(QDir::currentPath()));
588 std::optional<QTest::CrashHandler::FatalSignalHandler> handler;
589 QTest::CrashHandler::prepareStackTrace();
590 if (!QTest::Internal::noCrashHandler)
593 qputenv(
"QT_QTESTLIB_RUNNING",
"1");
595 QSet<QString> commandLineTestFunctions(QTest::testFunctions.cbegin(), QTest::testFunctions.cend());
596 const bool filteringTestFunctions = !commandLineTestFunctions.isEmpty();
600 for (
const QString &file : std::as_const(files)) {
601 const QFileInfo fi(file);
606 for (
const QString &path : std::as_const(imports))
607 engine.addImportPath(path);
608 for (
const QString &path : std::as_const(pluginPaths))
609 engine.addPluginPath(path);
611 if (!fileSelectors.isEmpty()) {
612 QQmlFileSelector*
const qmlFileSelector =
new QQmlFileSelector(&engine, &engine);
613 qmlFileSelector->setExtraSelectors(fileSelectors);
621 maybeInvokeSetupMethod(setup,
"qmlEngineAvailable(QQmlEngine*)", Q_ARG(QQmlEngine*, &engine));
623 TestCaseCollector testCaseCollector(fi, &engine);
624 if (!testCaseCollector.errors().isEmpty()) {
625 handleCompileErrors(fi, testCaseCollector.errors(), &engine);
629 TestCaseCollector::TestCaseList availableTestFunctions = testCaseCollector.testCases();
630 if (QTest::printAvailableFunctions) {
631 for (
const QString &function : availableTestFunctions)
632 qDebug(
"%s()", qPrintable(function));
636 const QSet<QString> availableTestSet(availableTestFunctions.cbegin(), availableTestFunctions.cend());
637 if (filteringTestFunctions && !availableTestSet.intersects(commandLineTestFunctions))
639 commandLineTestFunctions.subtract(availableTestSet);
641 QQuickView view(&engine,
nullptr);
642 view.setFlags(Qt::Window | Qt::WindowSystemMenuHint
643 | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint
644 | Qt::WindowCloseButtonHint);
645 QEventLoop eventLoop;
646 QObject::connect(view.engine(), SIGNAL(quit()),
647 QTestRootObject::instance(), SLOT(quit()));
648 QObject::connect(view.engine(), SIGNAL(quit()),
649 &eventLoop, SLOT(quit()));
650 view.rootContext()->setContextProperty
651 (QLatin1String(
"qtest"), QTestRootObject::instance());
653 view.setObjectName(fi.baseName());
654 view.setTitle(view.objectName());
655 QTestRootObject::instance()->init();
656 QString path = fi.absoluteFilePath();
657 if (path.startsWith(QLatin1String(
":/")))
658 view.setSource(QUrl(QLatin1String(
"qrc:") + QStringView{path}.mid(1)));
660 view.setSource(QUrl::fromLocalFile(path));
662 while (view.status() == QQuickView::Loading)
664 if (view.status() == QQuickView::Error) {
665 handleCompileErrors(fi, view.errors(), view.engine(), &view);
669 view.setFramePosition(QPoint(50, 50));
670 if (view.size().isEmpty()) {
671 view.resize(200, 200);
674 if (!QTest::qWaitForWindowExposed(&view)) {
676 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window not exposed after show().";
678 if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) {
679 view.requestActivate();
680 if (!QTest::qWaitForWindowActive(&view)) {
682 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window not active after requestActivate().";
685 if (view.isExposed()) {
687 QTimer::singleShot(0, []() {
688 QTestRootObject::instance()->setWindowShown(
true);
692 <<
"Test '" << QDir::toNativeSeparators(path) <<
"' window was never exposed! "
693 <<
"If the test case was expecting windowShown, it will hang.";
695 if (!QTestRootObject::instance()->hasQuit && QTestRootObject::instance()->hasTestCase())
700 maybeInvokeSetupMethod(setup,
"cleanupTestCase()");
703 QuickTestResult::setProgramName(
nullptr);
707 if (!commandLineTestFunctions.isEmpty()) {
708 qWarning() <<
"Could not find the following test functions:";
709 for (
const QString &functionName : std::as_const(commandLineTestFunctions))
710 qWarning(
" %s()", qUtf8Printable(functionName));
711 return commandLineTestFunctions.size();
714 const int exitCode = QuickTestResult::exitCode();
717 writeAndroidExitCode(exitCode);