diff options
Diffstat (limited to 'src/qmltest/quicktest.cpp')
-rw-r--r-- | src/qmltest/quicktest.cpp | 303 |
1 files changed, 186 insertions, 117 deletions
diff --git a/src/qmltest/quicktest.cpp b/src/qmltest/quicktest.cpp index 276463c7c5..ce29dadb47 100644 --- a/src/qmltest/quicktest.cpp +++ b/src/qmltest/quicktest.cpp @@ -1,52 +1,19 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "quicktest_p.h" #include "quicktestresult_p.h" #include <QtTest/qtestsystem.h> +#include <QtTest/private/qtestcrashhandler_p.h> #include "qtestoptions_p.h" #include <QtQml/qqml.h> #include <QtQml/qqmlengine.h> #include <QtQml/qqmlcontext.h> #include <QtQuick/private/qquickitem_p.h> +#include <QtQuick/private/qquickwindow_p.h> #include <QtQuick/qquickitem.h> #include <QtQuick/qquickview.h> +#include <QtQuick/qquickwindow.h> #include <QtQml/qjsvalue.h> #include <QtQml/qjsengine.h> #include <QtQml/qqmlpropertymap.h> @@ -110,7 +77,38 @@ bool QQuickTest::qIsPolishScheduled(const QQuickItem *item) } /*! + \since 6.4 + \overload qIsPolishScheduled() + + Returns \c true if there are any items managed by this window for + which \c qIsPolishScheduled(item) returns \c true, otherwise + returns \c false. + + For example, if an item somewhere within the scene may or may not + be polished, but you need to wait for it if it is, you can use + the following code: + + \code + if (QQuickTest::qIsPolishScheduled(window)) + QVERIFY(QQuickTest::qWaitForPolish(window)); + \endcode + + The QML equivalent of this function is + \l [QML]{TestCase::}{isPolishScheduled()}. + + \sa QQuickItem::polish(), QQuickItem::updatePolish(), + QQuickTest::qWaitForPolish() +*/ +bool QQuickTest::qIsPolishScheduled(const QQuickWindow *window) +{ + return !QQuickWindowPrivate::get(window)->itemsToPolish.isEmpty(); +} + +#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) +#if QT_DEPRECATED_SINCE(6, 4) +/*! \since 5.13 + \deprecated [6.4] Use \l qWaitForPolish() instead. Waits for \a timeout milliseconds or until \l {QQuickItem::}{updatePolish()} has been called on \a item. @@ -126,21 +124,61 @@ bool QQuickTest::qIsPolishScheduled(const QQuickItem *item) */ bool QQuickTest::qWaitForItemPolished(const QQuickItem *item, int timeout) { + return qWaitForPolish(item, timeout); +} +#endif +#endif + +/*! + \since 6.4 + + Waits for \a timeout milliseconds or until + \l {QQuickItem::}{updatePolish()} has been called on \a item. + + Returns \c true if \c updatePolish() was called on \a item within + \a timeout milliseconds, otherwise returns \c false. + + \sa QQuickItem::polish(), QQuickItem::updatePolish(), + QQuickTest::qIsPolishScheduled() +*/ +bool QQuickTest::qWaitForPolish(const QQuickItem *item, int timeout) +{ return QTest::qWaitFor([&]() { return !QQuickItemPrivate::get(item)->polishScheduled; }, timeout); } +/*! + \since 6.4 + + Waits for \a timeout milliseconds or until \c qIsPolishScheduled(item) + returns \c false for all items managed by \a window. + + Returns \c true if \c qIsPolishScheduled(item) returns false for all items + within \a timeout milliseconds, otherwise returns \c false. + + The QML equivalent of this function is + \l [QML]{TestCase::}{waitForPolish()}. + + \sa QQuickItem::polish(), QQuickItem::updatePolish(), + QQuickTest::qIsPolishScheduled() +*/ +bool QQuickTest::qWaitForPolish(const QQuickWindow *window, int timeout) +{ + return QTest::qWaitFor([&]() { return QQuickWindowPrivate::get(window)->itemsToPolish.isEmpty(); }, timeout); +} + static inline QString stripQuotes(const QString &s) { - if (s.length() >= 2 && s.startsWith(QLatin1Char('"')) && s.endsWith(QLatin1Char('"'))) - return s.mid(1, s.length() - 2); + if (s.size() >= 2 && s.startsWith(QLatin1Char('"')) && s.endsWith(QLatin1Char('"'))) + return s.mid(1, s.size() - 2); else return s; } -void handleCompileErrors(const QFileInfo &fi, QQuickView *view) +static void handleCompileErrors( + const QFileInfo &fi, const QList<QQmlError> &errors, QQmlEngine *engine, + QQuickView *view = nullptr) { // Error compiling the test - flag failure in the log and continue. - const QList<QQmlError> errors = view->errors(); QuickTestResult results; results.setTestCaseName(fi.baseName()); results.startLogging(); @@ -162,8 +200,11 @@ void handleCompileErrors(const QFileInfo &fi, QQuickView *view) str << ": " << e.description() << '\n'; } str << " Working directory: " << QDir::toNativeSeparators(QDir::current().absolutePath()) << '\n'; - if (QQmlEngine *engine = view->engine()) { - str << " View: " << view->metaObject()->className() << ", import paths:\n"; + if (engine) { + str << " "; + if (view) + str << "View: " << view->metaObject()->className() << ", "; + str << "Import paths:\n"; const auto importPaths = engine->importPathList(); for (const QString &i : importPaths) str << " '" << QDir::toNativeSeparators(i) << "'\n"; @@ -183,25 +224,49 @@ void handleCompileErrors(const QFileInfo &fi, QQuickView *view) results.stopLogging(); } -bool qWaitForSignal(QObject *obj, const char* signal, int timeout = 5000) +class SimpleReceiver : public QObject { + Q_OBJECT +public: + bool signalReceived = false; +public slots: + void slotFun() { signalReceived = true; } +}; + +bool qWaitForSignal(QObject *obj, const char* signal, int timeout) { - QSignalSpy spy(obj, signal); - QElapsedTimer timer; - timer.start(); - - while (!spy.size()) { - int remaining = timeout - int(timer.elapsed()); - if (remaining <= 0) - break; - QCoreApplication::processEvents(QEventLoop::AllEvents, remaining); - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - QTest::qSleep(10); + if (!obj || !signal) { + qWarning("qWaitForSignal: invalid arguments"); + return false; + } + if (((signal[0] - '0') & 0x03) != QSIGNAL_CODE) { + qWarning("qWaitForSignal: not a valid signal, use the SIGNAL macro"); + return false; + } + + int sig = obj->metaObject()->indexOfSignal(signal + 1); + if (sig == -1) { + const QByteArray ba = QMetaObject::normalizedSignature(signal + 1); + sig = obj->metaObject()->indexOfSignal(ba.constData()); + if (sig == -1) { + qWarning("qWaitForSignal: no such signal %s::%s", obj->metaObject()->className(), + signal); + return false; + } + } + + SimpleReceiver receiver; + static int slot = receiver.metaObject()->indexOfSlot("slotFun()"); + if (!QMetaObject::connect(obj, sig, &receiver, slot)) { + qWarning("qWaitForSignal: failed to connect to signal %s::%s", + obj->metaObject()->className(), signal); + return false; } - return spy.size(); + return QTest::qWaitFor([&]() { return receiver.signalReceived; }, timeout); } -void maybeInvokeSetupMethod(QObject *setupObject, const char *member, QGenericArgument val0 = QGenericArgument(nullptr)) +template <typename... Args> +void maybeInvokeSetupMethod(QObject *setupObject, const char *member, Args &&... args) { // It's OK if it doesn't exist: since we have more than one callback that // can be called, it makes sense if the user only implements one of them. @@ -212,7 +277,7 @@ void maybeInvokeSetupMethod(QObject *setupObject, const char *member, QGenericAr const int methodIndex = setupMetaObject->indexOfMethod(member); if (methodIndex != -1) { const QMetaMethod method = setupMetaObject->method(methodIndex); - method.invoke(setupObject, val0); + method.invoke(setupObject, std::forward<Args>(args)...); } } @@ -223,7 +288,7 @@ class TestCaseCollector public: typedef QList<QString> TestCaseList; - TestCaseCollector(const QFileInfo &fileInfo, QQmlEngine *engine) + TestCaseCollector(const QFileInfo &fileInfo, QQmlEngine *engine) : m_engine(engine) { QString path = fileInfo.absoluteFilePath(); if (path.startsWith(QLatin1String(":/"))) @@ -235,7 +300,8 @@ public: if (component.isReady()) { QQmlRefPointer<QV4::ExecutableCompilationUnit> rootCompilationUnit = QQmlComponentPrivate::get(&component)->compilationUnit; - TestCaseEnumerationResult result = enumerateTestCases(rootCompilationUnit.data()); + TestCaseEnumerationResult result = enumerateTestCases( + rootCompilationUnit->baseCompilationUnit().data()); m_testCases = result.testCases + result.finalizedPartialTestCases(); m_errors += result.errors; } @@ -247,6 +313,7 @@ public: private: TestCaseList m_testCases; QList<QQmlError> m_errors; + QQmlEngine *m_engine = nullptr; struct TestCaseEnumerationResult { @@ -275,8 +342,8 @@ private: }; TestCaseEnumerationResult enumerateTestCases( - const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit, - const Object *object = nullptr) + const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, + const QV4::CompiledData::Object *object = nullptr) { QQmlType testCaseType; for (quint32 i = 0, count = compilationUnit->importCount(); i < count; ++i) { @@ -289,7 +356,8 @@ private: if (!typeQualifier.isEmpty()) testCaseTypeName = typeQualifier % QLatin1Char('.') % testCaseTypeName; - testCaseType = compilationUnit->typeNameCache->query(testCaseTypeName).type; + testCaseType = compilationUnit->typeNameCache->query( + testCaseTypeName, QQmlTypeLoader::get(m_engine)).type; if (testCaseType.isValid()) break; } @@ -298,11 +366,11 @@ private: if (!object) // Start at root of compilation unit if not enumerating a specific child object = compilationUnit->objectAt(0); - if (object->flags & Object::IsInlineComponentRoot) + if (object->hasFlag(QV4::CompiledData::Object::IsInlineComponentRoot)) return result; - if (const auto superTypeUnit = compilationUnit->resolvedTypes.value( - object->inheritedTypeNameIndex)->compilationUnit()) { + if (const auto superTypeUnit = compilationUnit->resolvedType(object->inheritedTypeNameIndex) + ->compilationUnit()) { // We have a non-C++ super type, which could indicate we're a subtype of a TestCase if (testCaseType.isValid() && superTypeUnit->url() == testCaseType.sourceUrl()) result.isTestCase = true; @@ -314,13 +382,13 @@ private: // Look for override of name in this type for (auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) { if (compilationUnit->stringAt(binding->propertyNameIndex) == QLatin1String("name")) { - if (binding->type == QV4::CompiledData::Binding::Type_String) { + if (binding->type() == QV4::CompiledData::Binding::Type_String) { result.testCaseName = compilationUnit->stringAt(binding->stringIndex); } else { QQmlError error; error.setUrl(compilationUnit->url()); - error.setLine(binding->location.line); - error.setColumn(binding->location.column); + error.setLine(binding->location.line()); + error.setColumn(binding->location.column()); error.setDescription(QStringLiteral("the 'name' property of a TestCase must be a literal string")); result.errors << error; } @@ -344,8 +412,8 @@ private: } for (auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) { - if (binding->type == QV4::CompiledData::Binding::Type_Object) { - const Object *child = compilationUnit->objectAt(binding->value.objectIndex); + if (binding->type() == QV4::CompiledData::Binding::Type_Object) { + const QV4::CompiledData::Object *child = compilationUnit->objectAt(binding->value.objectIndex); result << enumerateTestCases(compilationUnit, child); } } @@ -361,9 +429,9 @@ int quick_test_main(int argc, char **argv, const char *name, const char *sourceD int quick_test_main_with_setup(int argc, char **argv, const char *name, const char *sourceDir, QObject *setup) { - QCoreApplication *app = nullptr; + QScopedPointer<QCoreApplication> app; if (!QCoreApplication::instance()) - app = new QGuiApplication(argc, argv); + app.reset(new QGuiApplication(argc, argv)); if (setup) maybeInvokeSetupMethod(setup, "applicationAvailable()"); @@ -432,7 +500,7 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch testPath = s; } -#if defined(Q_OS_ANDROID) +#if defined(Q_OS_ANDROID) || defined(Q_OS_INTEGRITY) if (testPath.isEmpty()) testPath = QLatin1String(":/"); #endif @@ -500,6 +568,11 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch return 1; } + std::optional<QTest::CrashHandler::FatalSignalHandler> handler; + QTest::CrashHandler::prepareStackTrace(); + if (!QTest::Internal::noCrashHandler) + handler.emplace(); + qputenv("QT_QTESTLIB_RUNNING", "1"); QSet<QString> commandLineTestFunctions(QTest::testFunctions.cbegin(), QTest::testFunctions.cend()); @@ -507,15 +580,15 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch // Scan through all of the "tst_*.qml" files and run each of them // in turn with a separate QQuickView (for test isolation). - for (const QString &file : qAsConst(files)) { + for (const QString &file : std::as_const(files)) { const QFileInfo fi(file); if (!fi.exists()) continue; QQmlEngine engine; - for (const QString &path : qAsConst(imports)) + for (const QString &path : std::as_const(imports)) engine.addImportPath(path); - for (const QString &path : qAsConst(pluginPaths)) + for (const QString &path : std::as_const(pluginPaths)) engine.addPluginPath(path); if (!fileSelectors.isEmpty()) { @@ -532,9 +605,8 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch TestCaseCollector testCaseCollector(fi, &engine); if (!testCaseCollector.errors().isEmpty()) { - for (const QQmlError &error : testCaseCollector.errors()) - qWarning() << error; - exit(1); + handleCompileErrors(fi, testCaseCollector.errors(), &engine); + continue; } TestCaseCollector::TestCaseList availableTestFunctions = testCaseCollector.testCases(); @@ -573,42 +645,36 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch while (view.status() == QQuickView::Loading) QTest::qWait(10); if (view.status() == QQuickView::Error) { - handleCompileErrors(fi, &view); + handleCompileErrors(fi, view.errors(), view.engine(), &view); continue; } - if (!QTestRootObject::instance()->hasQuit) { - // If the test already quit, then it was performed - // synchronously during setSource(). Otherwise it is - // an asynchronous test and we need to show the window - // and wait for the first frame to be rendered - // and then wait for quit indication. - view.setFramePosition(QPoint(50, 50)); - if (view.size().isEmpty()) { // Avoid hangs with empty windows. - view.resize(200, 200); - } - view.show(); - if (!QTest::qWaitForWindowExposed(&view)) { - qWarning().nospace() - << "Test '" << QDir::toNativeSeparators(path) << "' window not exposed after show()."; - } - view.requestActivate(); - if (!QTest::qWaitForWindowActive(&view)) { - qWarning().nospace() - << "Test '" << QDir::toNativeSeparators(path) << "' window not active after requestActivate()."; - } - if (view.isExposed()) { - // Defer property update until event loop has started - QTimer::singleShot(0, []() { - QTestRootObject::instance()->setWindowShown(true); - }); - } else { - qWarning().nospace() - << "Test '" << QDir::toNativeSeparators(path) << "' window was never exposed! " - << "If the test case was expecting windowShown, it will hang."; - } - if (!QTestRootObject::instance()->hasQuit && QTestRootObject::instance()->hasTestCase()) - eventLoop.exec(); + + view.setFramePosition(QPoint(50, 50)); + if (view.size().isEmpty()) { // Avoid hangs with empty windows. + view.resize(200, 200); + } + view.show(); + if (!QTest::qWaitForWindowExposed(&view)) { + qWarning().nospace() + << "Test '" << QDir::toNativeSeparators(path) << "' window not exposed after show()."; } + view.requestActivate(); + if (!QTest::qWaitForWindowActive(&view)) { + qWarning().nospace() + << "Test '" << QDir::toNativeSeparators(path) << "' window not active after requestActivate()."; + } + if (view.isExposed()) { + // Defer property update until event loop has started + QTimer::singleShot(0, []() { + QTestRootObject::instance()->setWindowShown(true); + }); + } else { + qWarning().nospace() + << "Test '" << QDir::toNativeSeparators(path) << "' window was never exposed! " + << "If the test case was expecting windowShown, it will hang."; + } + if (!QTestRootObject::instance()->hasQuit && QTestRootObject::instance()->hasTestCase()) + eventLoop.exec(); } if (setup) @@ -616,14 +682,14 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch // Flush the current logging stream. QuickTestResult::setProgramName(nullptr); - delete app; + app.reset(); // Check that all test functions passed on the command line were found if (!commandLineTestFunctions.isEmpty()) { qWarning() << "Could not find the following test functions:"; - for (const QString &functionName : qAsConst(commandLineTestFunctions)) + for (const QString &functionName : std::as_const(commandLineTestFunctions)) qWarning(" %s()", qUtf8Printable(functionName)); - return commandLineTestFunctions.count(); + return commandLineTestFunctions.size(); } // Return the number of failures as the exit code. @@ -631,3 +697,6 @@ int quick_test_main_with_setup(int argc, char **argv, const char *name, const ch } QT_END_NAMESPACE + +#include "moc_quicktest_p.cpp" +#include "quicktest.moc" |