diff options
Diffstat (limited to 'src/testlib/qtestcase.cpp')
-rw-r--r-- | src/testlib/qtestcase.cpp | 1836 |
1 files changed, 1030 insertions, 806 deletions
diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp index ce10b0a8eb..42795fade7 100644 --- a/src/testlib/qtestcase.cpp +++ b/src/testlib/qtestcase.cpp @@ -1,51 +1,16 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Copyright (C) 2016 Intel Corporation. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtTest module 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) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QtTest/qtestcase.h> +#include <QtTest/private/qtestcase_p.h> #include <QtTest/qtestassert.h> #include <QtCore/qbytearray.h> #include <QtCore/qcoreapplication.h> #include <QtCore/qdebug.h> #include <QtCore/qdir.h> -#include <QtCore/qdiriterator.h> +#include <QtCore/qdirlisting.h> #include <QtCore/qfile.h> #include <QtCore/qfileinfo.h> #include <QtCore/qfloat16.h> @@ -69,8 +34,12 @@ #include <QtTest/private/qtestresult_p.h> #include <QtTest/private/qsignaldumper_p.h> #include <QtTest/private/qbenchmark_p.h> +#if QT_CONFIG(batch_test_support) +#include <QtTest/private/qtestregistry_p.h> +#endif // QT_CONFIG(batch_test_support) #include <QtTest/private/cycle_p.h> #include <QtTest/private/qtestblacklist_p.h> +#include <QtTest/private/qtestcrashhandler_p.h> #if defined(HAVE_XCTEST) #include <QtTest/private/qxctestlogger_p.h> #endif @@ -82,11 +51,21 @@ #include <QtTest/private/qappletestlogger_p.h> #endif -#include <cmath> -#include <numeric> #include <algorithm> -#include <mutex> +#include <array> +#if !defined(Q_OS_INTEGRITY) || __GHS_VERSION_NUMBER > 202014 +# include <charconv> +#else +// Broken implementation, causes link failures just by #include'ing! +# undef __cpp_lib_to_chars // in case <version> was included +#endif #include <chrono> +#include <cmath> +#include <limits> +#include <memory> +#include <mutex> +#include <numeric> +#include <optional> #include <stdarg.h> #include <stdio.h> @@ -98,19 +77,37 @@ #endif #ifdef Q_OS_WIN +# include <iostream> # if !defined(Q_CC_MINGW) || (defined(Q_CC_MINGW) && defined(__MINGW64_VERSION_MAJOR)) # include <crtdbg.h> # endif #include <qt_windows.h> // for Sleep #endif #ifdef Q_OS_UNIX +#include <QtCore/private/qcore_unix_p.h> + #include <errno.h> +#if __has_include(<paths.h>) +# include <paths.h> +#endif #include <signal.h> #include <time.h> +#include <sys/mman.h> +#include <sys/uio.h> +#include <sys/wait.h> #include <unistd.h> # if !defined(Q_OS_INTEGRITY) # include <sys/resource.h> # endif +# ifndef _PATH_DEFPATH +# define _PATH_DEFPATH "/usr/bin:/bin" +# endif +# ifndef SIGSTKSZ +# define SIGSTKSZ 0 /* we have code to set the minimum */ +# endif +# ifndef SA_RESETHAND +# define SA_RESETHAND 0 +# endif #endif #if defined(Q_OS_MACOS) @@ -120,134 +117,19 @@ #include <CoreFoundation/CFPreferences.h> #endif +#if defined(Q_OS_WASM) +#include <emscripten.h> +#endif + #include <vector> QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + using QtMiscUtils::toHexUpper; using QtMiscUtils::fromHex; -static bool debuggerPresent() -{ -#if defined(Q_OS_LINUX) - int fd = open("/proc/self/status", O_RDONLY); - if (fd == -1) - return false; - char buffer[2048]; - ssize_t size = read(fd, buffer, sizeof(buffer) - 1); - if (size == -1) { - close(fd); - return false; - } - buffer[size] = 0; - const char tracerPidToken[] = "\nTracerPid:"; - char *tracerPid = strstr(buffer, tracerPidToken); - if (!tracerPid) { - close(fd); - return false; - } - tracerPid += sizeof(tracerPidToken); - long int pid = strtol(tracerPid, &tracerPid, 10); - close(fd); - return pid != 0; -#elif defined(Q_OS_WIN) - return IsDebuggerPresent(); -#elif defined(Q_OS_MACOS) - // Check if there is an exception handler for the process: - mach_msg_type_number_t portCount = 0; - exception_mask_t masks[EXC_TYPES_COUNT]; - mach_port_t ports[EXC_TYPES_COUNT]; - exception_behavior_t behaviors[EXC_TYPES_COUNT]; - thread_state_flavor_t flavors[EXC_TYPES_COUNT]; - exception_mask_t mask = EXC_MASK_ALL & ~(EXC_MASK_RESOURCE | EXC_MASK_GUARD); - kern_return_t result = task_get_exception_ports(mach_task_self(), mask, masks, &portCount, - ports, behaviors, flavors); - if (result == KERN_SUCCESS) { - for (mach_msg_type_number_t portIndex = 0; portIndex < portCount; ++portIndex) { - if (MACH_PORT_VALID(ports[portIndex])) { - return true; - } - } - } - return false; -#else - // TODO - return false; -#endif -} - -#if !defined(Q_OS_WASM) -static bool hasSystemCrashReporter() -{ -#if defined(Q_OS_MACOS) - return QTestPrivate::macCrashReporterWillShowDialog(); -#else - return false; -#endif -} - -static void disableCoreDump() -{ - bool ok = false; - const int disableCoreDump = qEnvironmentVariableIntValue("QTEST_DISABLE_CORE_DUMP", &ok); - if (ok && disableCoreDump) { -#if defined(Q_OS_UNIX) && !defined(Q_OS_INTEGRITY) - struct rlimit limit; - limit.rlim_cur = 0; - limit.rlim_max = 0; - if (setrlimit(RLIMIT_CORE, &limit) != 0) - qWarning("Failed to disable core dumps: %d", errno); -#endif - } -} -Q_CONSTRUCTOR_FUNCTION(disableCoreDump); - -static void stackTrace() -{ - bool ok = false; - const int disableStackDump = qEnvironmentVariableIntValue("QTEST_DISABLE_STACK_DUMP", &ok); - if (ok && disableStackDump) - return; - - if (debuggerPresent() || hasSystemCrashReporter()) - return; - -#if defined(Q_OS_LINUX) || (defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM_64)) - - const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime()); - const int msecsTotalTime = qRound(QTestLog::msecsTotalTime()); - fprintf(stderr, "\n=== Received signal at function time: %dms, total time: %dms, dumping stack ===\n", - msecsFunctionTime, msecsTotalTime); - -# ifdef Q_OS_LINUX - char cmd[512]; - qsnprintf(cmd, 512, "gdb --pid %d 1>&2 2>/dev/null <<EOF\n" - "set prompt\n" - "set height 0\n" - "thread apply all where full\n" - "detach\n" - "quit\n" - "EOF\n", - static_cast<int>(getpid())); - if (system(cmd) == -1) - fprintf(stderr, "calling gdb failed\n"); - fprintf(stderr, "=== End of stack trace ===\n"); -# elif defined(Q_OS_MACOS) - char cmd[512]; - qsnprintf(cmd, 512, "lldb -p %d 1>&2 2>/dev/null <<EOF\n" - "bt all\n" - "quit\n" - "EOF\n", - static_cast<int>(getpid())); - if (system(cmd) == -1) - fprintf(stderr, "calling lldb failed\n"); - fprintf(stderr, "=== End of stack trace ===\n"); -# endif - -#endif -} -#endif // !Q_OS_WASM - static bool installCoverageTool(const char * appname, const char * testname) { #if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover) @@ -285,12 +167,197 @@ namespace QTestPrivate Q_TESTLIB_EXPORT Qt::MouseButtons qtestMouseButtons = Qt::NoButton; } +namespace { + +class TestFailedException : public std::exception // clazy:exclude=copyable-polymorphic +{ +public: + TestFailedException() = default; + ~TestFailedException() override = default; + + const char *what() const noexcept override { return "QtTest: test failed"; } +}; + +class TestSkippedException : public std::exception // clazy:exclude=copyable-polymorphic +{ +public: + TestSkippedException() = default; + ~TestSkippedException() override = default; + + const char *what() const noexcept override { return "QtTest: test was skipped"; } +}; + +} // unnamed namespace + namespace QTest { +void Internal::throwOnFail() { throw TestFailedException(); } +void Internal::throwOnSkip() { throw TestSkippedException(); } + +Q_CONSTINIT static QBasicAtomicInt g_throwOnFail = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_CONSTINIT static QBasicAtomicInt g_throwOnSkip = Q_BASIC_ATOMIC_INITIALIZER(0); + +void Internal::maybeThrowOnFail() +{ + if (g_throwOnFail.loadRelaxed() > 0) + Internal::throwOnFail(); +} + +void Internal::maybeThrowOnSkip() +{ + if (g_throwOnSkip.loadRelaxed() > 0) + Internal::throwOnSkip(); +} + +/*! + \since 6.8 + \macro QTEST_THROW_ON_FAIL + \relates <QTest> + + When defined, QCOMPARE()/QVERIFY() etc always throw on failure. + QTest::throwOnFail() then no longer has any effect. +*/ + +/*! + \since 6.8 + \macro QTEST_THROW_ON_SKIP + \relates <QTest> + + When defined, QSKIP() always throws. QTest::throwOnSkip() then no longer + has any effect. +*/ + +/*! + \since 6.8 + \class QTest::ThrowOnFailEnabler + \inmodule QtTestLib + + RAII class around setThrowOnFail(). +*/ +/*! + \fn QTest::ThrowOnFailEnabler::ThrowOnFailEnabler() + + Constructor. Calls \c{setThrowOnFail(true)}. +*/ +/*! + \fn QTest::ThrowOnFailEnabler::~ThrowOnFailEnabler() + + Destructor. Calls \c{setThrowOnFail(false)}. +*/ + +/*! + \since 6.8 + \class QTest::ThrowOnFailDisabler + \inmodule QtTestLib + + RAII class around setThrowOnFail(). +*/ +/*! + \fn QTest::ThrowOnFailDisabler::ThrowOnFailDisabler() + + Constructor. Calls \c{setThrowOnFail(false)}. +*/ +/*! + \fn QTest::ThrowOnFailDisabler::~ThrowOnFailDisabler() + + Destructor. Calls \c{setThrowOnFail(true)}. +*/ + +/*! + \since 6.8 + \class QTest::ThrowOnSkipEnabler + \inmodule QtTestLib + + RAII class around setThrowOnSkip(). +*/ +/*! + \fn QTest::ThrowOnSkipEnabler::ThrowOnSkipEnabler() + + Constructor. Calls \c{setThrowOnSkip(true)}. +*/ +/*! + \fn QTest::ThrowOnSkipEnabler::~ThrowOnSkipEnabler() + + Destructor. Calls \c{setThrowOnSkip(false)}. +*/ + +/*! + \since 6.8 + \class QTest::ThrowOnSkipDisabler + \inmodule QtTestLib + + RAII class around setThrowOnSkip(). +*/ +/*! + \fn QTest::ThrowOnSkipDisabler::ThrowOnSkipDisabler() + + Constructor. Calls \c{setThrowOnSkip(false)}. +*/ +/*! + \fn QTest::ThrowOnSkipDisabler::~ThrowOnSkipDisabler() + + Destructor. Calls \c{setThrowOnSkip(true)}. +*/ + +/*! + \since 6.8 + + Enables (\a enable = \c true) or disables (\ enable = \c false) throwing on + QCOMPARE()/QVERIFY() failures (as opposed to just returning from the + immediately-surrounding function context). + + The feature is reference-counted: If you call this function \e{N} times + with \c{true}, you need to call it \e{N} times with \c{false} to get back + to where you started. + + The default is \c{false}, unless the \l{Qt Test Environment Variables} + {QTEST_THROW_ON_FAIL environment variable} is set. + + This call has no effect when the \l{QTEST_THROW_ON_FAIL} C++ macro is + defined. + + \note You must compile your tests with exceptions enabled to use this + feature. + + \sa setThrowOnSkip(), ThrowOnFailEnabler, ThrowOnFailDisabler, QTEST_THROW_ON_FAIL +*/ +void setThrowOnFail(bool enable) noexcept +{ + g_throwOnFail.fetchAndAddRelaxed(enable ? 1 : -1); +} + +/*! + \since 6.8 + + Enables (\a enable = \c true) or disables (\ enable = \c false) throwing on + QSKIP() (as opposed to just returning from the immediately-surrounding + function context). + + The feature is reference-counted: If you call this function \e{N} times + with \c{true}, you need to call it \e{N} times with \c{false} to get back + to where you started. + + The default is \c{false}, unless the \l{Qt Test Environment Variables} + {QTEST_THROW_ON_SKIP environment variable} is set. + + This call has no effect when the \l{QTEST_THROW_ON_SKIP} C++ macro is + defined. + + \note You must compile your tests with exceptions enabled to use this + feature. + + \sa setThrowOnFail(), ThrowOnSkipEnabler, ThrowOnSkipDisabler, QTEST_THROW_ON_SKIP +*/ +void setThrowOnSkip(bool enable) noexcept +{ + g_throwOnSkip.fetchAndAddRelaxed(enable ? 1 : -1); +} + QString Internal::formatTryTimeoutDebugMessage(q_no_char8_t::QUtf8StringView expr, int timeout, int actual) { - return QLatin1String("QTestLib: This test case check (\"%1\") failed because the requested timeout (%2 ms) was too short, %3 ms would have been sufficient this time.") + return "QTestLib: This test case check (\"%1\") failed because the requested timeout (%2 ms) " + "was too short, %3 ms would have been sufficient this time."_L1 // ### Qt 7: remove the toString() (or earlier, when arg() can handle QUtf8StringView), passing the view directly .arg(expr.toString(), QString::number(timeout), QString::number(actual)); } @@ -301,6 +368,7 @@ class WatchDog; static QObject *currentTestObject = nullptr; static QString mainSourcePath; +static bool inTestFunction = false; #if defined(Q_OS_MACOS) static IOPMAssertionID macPowerSavingDisabled = 0; @@ -319,7 +387,7 @@ public: static QMetaMethod findMethod(const QObject *obj, const char *signature); private: - bool invokeTest(int index, const char *data, WatchDog *watchDog) const; + bool invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const; void invokeTestOnData(int index) const; QMetaMethod m_initTestCaseMethod; // might not exist, check isValid(). @@ -364,19 +432,32 @@ static int eventDelay = -1; #if QT_CONFIG(thread) static int timeout = -1; #endif -static bool noCrashHandler = false; +static int repetitions = 1; +static bool repeatForever = false; +static bool skipBlacklisted = false; -/*! \internal - Invoke a method of the object without generating warning if the method does not exist - */ -static void invokeMethod(QObject *obj, const char *methodName) +namespace Internal { +bool noCrashHandler = false; +} + +static bool invokeTestMethodIfValid(QMetaMethod m, QObject *obj = QTest::currentTestObject) +{ + if (!m.isValid()) + return false; + bool ok = true; + try { ok = m.invoke(obj, Qt ::DirectConnection); } + catch (const TestFailedException &) {} // ignore (used for control flow) + catch (const TestSkippedException &) {} // ditto + // every other exception is someone else's problem + return ok; +} + +static void invokeTestMethodIfExists(const char *methodName, QObject *obj = QTest::currentTestObject) { const QMetaObject *metaObject = obj->metaObject(); int funcIndex = metaObject->indexOfMethod(methodName); - if (funcIndex >= 0) { - QMetaMethod method = metaObject->method(funcIndex); - method.invoke(obj, Qt::DirectConnection); - } + // doesn't generate a warning if it doesn't exist: + invokeTestMethodIfValid(metaObject->method(funcIndex), obj); } int defaultEventDelay() @@ -432,16 +513,25 @@ Q_TESTLIB_EXPORT bool printAvailableFunctions = false; Q_TESTLIB_EXPORT QStringList testFunctions; Q_TESTLIB_EXPORT QStringList testTags; -static void qPrintTestSlots(FILE *stream, const char *filter = nullptr) +static bool qPrintTestSlots(FILE *stream, const char *filter = nullptr, const char *preamble = "") { + const auto matches = [filter](const QByteArray &s) { + return !filter || QLatin1StringView(s).contains(QLatin1StringView(filter), + Qt::CaseInsensitive); + }; + bool matched = false; for (int i = 0; i < QTest::currentTestObject->metaObject()->methodCount(); ++i) { QMetaMethod sl = QTest::currentTestObject->metaObject()->method(i); if (isValidSlot(sl)) { const QByteArray signature = sl.methodSignature(); - if (!filter || QString::fromLatin1(signature).contains(QLatin1String(filter), Qt::CaseInsensitive)) - fprintf(stream, "%s\n", signature.constData()); + if (matches(signature)) { + fprintf(stream, "%s%s\n", preamble, signature.constData()); + preamble = ""; + matched = true; + } } } + return matched; } static void qPrintDataTags(FILE *stream) @@ -451,7 +541,7 @@ static void qPrintDataTags(FILE *stream) // Get global data tags: QTestTable::globalTestTable(); - invokeMethod(QTest::currentTestObject, "initTestCase_data()"); + invokeTestMethodIfExists("initTestCase_data()"); const QTestTable *gTable = QTestTable::globalTestTable(); const QMetaObject *currTestMetaObj = QTest::currentTestObject->metaObject(); @@ -470,15 +560,15 @@ static void qPrintDataTags(FILE *stream) QByteArray member; member.resize(qstrlen(slot) + qstrlen("_data()") + 1); qsnprintf(member.data(), member.size(), "%s_data()", slot); - invokeMethod(QTest::currentTestObject, member.constData()); + invokeTestMethodIfExists(member.constData()); const int dataCount = table.dataCount(); localTags.reserve(dataCount); for (int j = 0; j < dataCount; ++j) - localTags << QLatin1String(table.testData(j)->dataTag()); + localTags << QLatin1StringView(table.testData(j)->dataTag()); // Print all tag combinations: if (gTable->dataCount() == 0) { - if (localTags.count() == 0) { + if (localTags.size() == 0) { // No tags at all, so just print the test function: fprintf(stream, "%s %s\n", currTestMetaObj->className(), slot); } else { @@ -490,7 +580,7 @@ static void qPrintDataTags(FILE *stream) } } else { for (int j = 0; j < gTable->dataCount(); ++j) { - if (localTags.count() == 0) { + if (localTags.size() == 0) { // Only global tags, so print the current one: fprintf( stream, "%s %s __global__ %s\n", @@ -527,10 +617,18 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool int logFormat = -1; // Not set const char *logFilename = nullptr; + repetitions = 1; + repeatForever = false; + QTest::testFunctions.clear(); QTest::testTags.clear(); -#if defined(Q_OS_MAC) && defined(HAVE_XCTEST) + if (qEnvironmentVariableIsSet("QTEST_THROW_ON_FAIL")) + QTest::setThrowOnFail(true); + if (qEnvironmentVariableIsSet("QTEST_THROW_ON_SKIP")) + QTest::setThrowOnSkip(true); + +#if defined(Q_OS_DARWIN) && defined(HAVE_XCTEST) if (QXcodeTestLogger::canLogTestProgress()) logFormat = QTestLog::XCTest; #endif @@ -581,6 +679,15 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool " -maxwarnings n : Sets the maximum amount of messages to output.\n" " 0 means unlimited, default: 2000\n" " -nocrashhandler : Disables the crash handler. Useful for debugging crashes.\n" + " -repeat n : Run the testsuite n times or until the test fails.\n" + " Useful for finding flaky tests. If negative, the tests are\n" + " repeated forever. This is intended as a developer tool, and\n" + " is only supported with the plain text logger.\n" + " -skipblacklisted : Skip blacklisted tests. Useful for measuring test coverage.\n" + " -[no]throwonfail : Enables/disables throwing on QCOMPARE()/QVERIFY()/etc.\n" + " Default: off, unless QTEST_THROW_ON_FAIL is set." + " -[no]throwonskip : Enables/disables throwing on QSKIP().\n" + " Default: off, unless QTEST_THROW_ON_SKIP is set." "\n" " Benchmarking options:\n" #if QT_CONFIG(valgrind) @@ -609,14 +716,14 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool "%s", argv[0], testOptions); if (qml) { - printf ("\n" - " QmlTest options:\n" - " -import dir : Specify an import directory.\n" - " -plugins dir : Specify a directory where to search for plugins.\n" - " -input dir/file : Specify the root directory for test cases or a single test case file.\n" - " -translation file : Specify the translation file.\n" - " -file-selector dir : Specify a file selector for the QML engine.\n" - ); + printf("\n" + " QmlTest options:\n" + " -import dir : Specify an import directory.\n" + " -plugins dir : Specify a directory where to search for plugins.\n" + " -input dir/file : Specify the root directory for test cases or a single test case file.\n" + " -translation file : Specify the translation file.\n" + " -file-selector dir : Specify a file selector for the QML engine.\n" + ); } printf("\n" @@ -730,19 +837,40 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool } else { QTestLog::setMaxWarnings(qToInt(argv[++i])); } + } else if (strcmp(argv[i], "-repeat") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "-repeat needs an extra parameter for the number of repetitions\n"); + exit(1); + } else { + repetitions = qToInt(argv[++i]); + repeatForever = repetitions < 0; + } } else if (strcmp(argv[i], "-nocrashhandler") == 0) { - QTest::noCrashHandler = true; + QTest::Internal::noCrashHandler = true; + } else if (strcmp(argv[i], "-skipblacklisted") == 0) { + QTest::skipBlacklisted = true; + } else if (strcmp(argv[i], "-throwonfail") == 0) { + QTest::setThrowOnFail(true); + } else if (strcmp(argv[i], "-nothrowonfail") == 0) { + QTest::setThrowOnFail(false); + } else if (strcmp(argv[i], "-throwonskip") == 0) { + QTest::setThrowOnSkip(true); + } else if (strcmp(argv[i], "-nothrowonskip") == 0) { + QTest::setThrowOnSkip(false); #if QT_CONFIG(valgrind) } else if (strcmp(argv[i], "-callgrind") == 0) { - if (QBenchmarkValgrindUtils::haveValgrind()) - if (QFileInfo(QDir::currentPath()).isWritable()) { - QBenchmarkGlobalData::current->setMode(QBenchmarkGlobalData::CallgrindParentProcess); - } else { - fprintf(stderr, "WARNING: Current directory not writable. Using the walltime measurer.\n"); - } - else { - fprintf(stderr, "WARNING: Valgrind not found or too old. Make sure it is installed and in your path. " - "Using the walltime measurer.\n"); + if (!QBenchmarkValgrindUtils::haveValgrind()) { + fprintf(stderr, + "WARNING: Valgrind not found or too old. " + "Make sure it is installed and in your path. " + "Using the walltime measurer.\n"); + } else if (QFileInfo(QDir::currentPath()).isWritable()) { + QBenchmarkGlobalData::current->setMode( + QBenchmarkGlobalData::CallgrindParentProcess); + } else { + fprintf(stderr, + "WARNING: Current directory not writable. " + "Using the walltime measurer.\n"); } } else if (strcmp(argv[i], "-callgrindchild") == 0) { // "private" option QBenchmarkGlobalData::current->setMode(QBenchmarkGlobalData::CallgrindChildProcess); @@ -863,23 +991,28 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool // If no loggers were created by the long version of the -o command-line // option, but a logger was requested via the old-style option, add it. const bool explicitLoggerRequested = logFormat != -1; - if (QTestLog::loggerCount() == 0 && explicitLoggerRequested) + if (!QTestLog::hasLoggers() && explicitLoggerRequested) QTestLog::addLogger(QTestLog::LogMode(logFormat), logFilename); bool addFallbackLogger = !explicitLoggerRequested; #if defined(QT_USE_APPLE_UNIFIED_LOGGING) // Any explicitly requested loggers will be added by now, so we can check if they use stdout - const bool safeToAddAppleLogger = !AppleUnifiedLogger::willMirrorToStderr() || !QTestLog::loggerUsingStdout(); + const bool safeToAddAppleLogger = !AppleUnifiedLogger::preventsStderrLogging() || !QTestLog::loggerUsingStdout(); if (safeToAddAppleLogger && QAppleTestLogger::debugLoggingEnabled()) { QTestLog::addLogger(QTestLog::Apple, nullptr); - if (AppleUnifiedLogger::willMirrorToStderr() && !logFilename) + if (AppleUnifiedLogger::preventsStderrLogging() && !logFilename) addFallbackLogger = false; // Prevent plain test logger fallback below } #endif if (addFallbackLogger) QTestLog::addLogger(QTestLog::Plain, logFilename); + + if (repetitions != 1 && !QTestLog::isRepeatSupported()) { + fprintf(stderr, "-repeat is only supported with plain text logger\n"); + exit(1); + } } // Temporary, backwards compatibility, until qtdeclarative's use of it is converted @@ -887,21 +1020,24 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, char *argv[], bool qml) { qtest_qParseArgs(argc, const_cast<const char *const *>(argv), qml); } -QBenchmarkResult qMedian(const QList<QBenchmarkResult> &container) +static QList<QBenchmarkResult> qMedian(const QList<QList<QBenchmarkResult>> &container) { - const int count = container.count(); + const int count = container.size(); if (count == 0) - return QBenchmarkResult(); + return {}; if (count == 1) return container.front(); - QList<QBenchmarkResult> containerCopy = container; - std::sort(containerCopy.begin(), containerCopy.end()); + QList<QList<QBenchmarkResult>> containerCopy = container; + std::sort(containerCopy.begin(), containerCopy.end(), + [](const QList<QBenchmarkResult> &a, const QList<QBenchmarkResult> &b) { + return a.first() < b.first(); + }); const int middle = count / 2; - // ### handle even-sized containers here by doing an aritmetic mean of the two middle items. + // ### handle even-sized containers here by doing an arithmetic mean of the two middle items. return containerCopy.at(middle); } @@ -917,15 +1053,6 @@ struct QTestDataSetter } }; -namespace { - -qreal addResult(qreal current, const QBenchmarkResult& r) -{ - return current + r.value; -} - -} - void TestMethods::invokeTestOnData(int index) const { /* Benchmarking: for each median iteration*/ @@ -933,43 +1060,51 @@ void TestMethods::invokeTestOnData(int index) const bool isBenchmark = false; int i = (QBenchmarkGlobalData::current->measurer->needsWarmupIteration()) ? -1 : 0; - QList<QBenchmarkResult> results; + QList<QList<QBenchmarkResult>> resultsList; bool minimumTotalReached = false; do { QBenchmarkTestMethodData::current->beginDataRun(); + if (i < 0) + QBenchmarkTestMethodData::current->iterationCount = 1; /* Benchmarking: for each accumulation iteration*/ bool invokeOk; do { - if (m_initMethod.isValid()) - m_initMethod.invoke(QTest::currentTestObject, Qt::DirectConnection); - if (QTestResult::skipCurrentTest() || QTestResult::currentTestFailed()) - break; + QTest::inTestFunction = true; + invokeTestMethodIfValid(m_initMethod); - QBenchmarkTestMethodData::current->result = QBenchmarkResult(); - QBenchmarkTestMethodData::current->resultAccepted = false; + const bool initQuit = + QTestResult::skipCurrentTest() || QTestResult::currentTestFailed(); + if (!initQuit) { + QBenchmarkTestMethodData::current->results.clear(); + QBenchmarkTestMethodData::current->resultAccepted = false; + QBenchmarkTestMethodData::current->valid = false; - QBenchmarkGlobalData::current->context.tag = - QLatin1String( - QTestResult::currentDataTag() - ? QTestResult::currentDataTag() : ""); + QBenchmarkGlobalData::current->context.tag = QLatin1StringView( + QTestResult::currentDataTag() ? QTestResult::currentDataTag() : ""); - invokeOk = m_methods[index].invoke(QTest::currentTestObject, Qt::DirectConnection); - if (!invokeOk) - QTestResult::addFailure("Unable to execute slot", __FILE__, __LINE__); + invokeOk = invokeTestMethodIfValid(m_methods[index]); + if (!invokeOk) + QTestResult::addFailure("Unable to execute slot", __FILE__, __LINE__); - isBenchmark = QBenchmarkTestMethodData::current->isBenchmark(); + isBenchmark = QBenchmarkTestMethodData::current->isBenchmark(); + } else { + invokeOk = false; + } + QTest::inTestFunction = false; QTestResult::finishedCurrentTestData(); - if (m_cleanupMethod.isValid()) - m_cleanupMethod.invoke(QTest::currentTestObject, Qt::DirectConnection); - - // Process any deleteLater(), like event-loop based apps would do. Fixes memleak reports. - if (QCoreApplication::instance()) - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + if (!initQuit) { + invokeTestMethodIfValid(m_cleanupMethod); - // If the test isn't a benchmark, finalize the result after cleanup() has finished. + // Process any deleteLater(), used by event-loop-based apps. + // Fixes memleak reports. + if (QCoreApplication::instance()) + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + // If the test isn't a benchmark, finalize the result after + // cleanup() has finished (or init has lead us to skip the test). if (!isBenchmark) QTestResult::finishedCurrentTestDataCleanup(); @@ -984,26 +1119,29 @@ void TestMethods::invokeTestOnData(int index) const QBenchmarkTestMethodData::current->endDataRun(); if (!QTestResult::skipCurrentTest() && !QTestResult::currentTestFailed()) { if (i > -1) // iteration -1 is the warmup iteration. - results.append(QBenchmarkTestMethodData::current->result); - - if (isBenchmark && QBenchmarkGlobalData::current->verboseOutput) { - if (i == -1) { - QTestLog::info(qPrintable( - QString::fromLatin1("warmup stage result : %1") - .arg(QBenchmarkTestMethodData::current->result.value)), nullptr, 0); - } else { - QTestLog::info(qPrintable( - QString::fromLatin1("accumulation stage result: %1") - .arg(QBenchmarkTestMethodData::current->result.value)), nullptr, 0); - } + resultsList.append(QBenchmarkTestMethodData::current->results); + + if (isBenchmark && QBenchmarkGlobalData::current->verboseOutput && + !QBenchmarkTestMethodData::current->results.isEmpty()) { + // we only print the first result + const QBenchmarkResult &first = QBenchmarkTestMethodData::current->results.constFirst(); + QString pattern = i < 0 ? "warmup stage result : %1"_L1 + : "accumulation stage result: %1"_L1; + QTestLog::info(qPrintable(pattern.arg(first.measurement.value)), nullptr, 0); } } - // Verify if the minimum total measurement is reached, if it was specified: + // Verify if the minimum total measurement (for the first measurement) + // was reached, if it was specified: if (QBenchmarkGlobalData::current->minimumTotal == -1) { minimumTotalReached = true; } else { - const qreal total = std::accumulate(results.begin(), results.end(), 0.0, addResult); + auto addResult = [](qreal current, const QList<QBenchmarkResult> &r) { + if (!r.isEmpty()) + current += r.first().measurement.value; + return current; + }; + const qreal total = std::accumulate(resultsList.begin(), resultsList.end(), 0.0, addResult); minimumTotalReached = (total >= QBenchmarkGlobalData::current->minimumTotal); } } while (isBenchmark @@ -1016,7 +1154,7 @@ void TestMethods::invokeTestOnData(int index) const QTestResult::finishedCurrentTestDataCleanup(); // Only report benchmark figures if the test passed if (testPassed && QBenchmarkTestMethodData::current->resultsAccepted()) - QTestLog::addBenchmarkResult(qMedian(results)); + QTestLog::addBenchmarkResults(qMedian(resultsList)); } } @@ -1024,17 +1162,30 @@ void TestMethods::invokeTestOnData(int index) const class WatchDog : public QThread { - enum Expectation { + enum Expectation : std::size_t { + // bits 0..1: state ThreadStart, TestFunctionStart, TestFunctionEnd, ThreadEnd, - }; - bool waitFor(std::unique_lock<QtPrivate::mutex> &m, Expectation e) + // bits 2..: generation + }; + static constexpr auto ExpectationMask = Expectation{ThreadStart | TestFunctionStart | TestFunctionEnd | ThreadEnd}; + static_assert(size_t(ExpectationMask) == 0x3); + static constexpr size_t GenerationShift = 2; + + static constexpr Expectation state(Expectation e) noexcept + { return Expectation{e & ExpectationMask}; } + static constexpr size_t generation(Expectation e) noexcept + { return e >> GenerationShift; } + static constexpr Expectation combine(Expectation e, size_t gen) noexcept + { return Expectation{e | (gen << GenerationShift)}; } + + bool waitFor(std::unique_lock<std::mutex> &m, Expectation e) { auto expectationChanged = [this, e] { return expecting.load(std::memory_order_relaxed) != e; }; - switch (e) { + switch (state(e)) { case TestFunctionEnd: return waitCondition.wait_for(m, defaultTimeout(), expectationChanged); case ThreadStart: @@ -1043,14 +1194,26 @@ class WatchDog : public QThread waitCondition.wait(m, expectationChanged); return true; } - Q_UNREACHABLE(); - return false; + Q_UNREACHABLE_RETURN(false); + } + + void setExpectation(Expectation e) + { + Q_ASSERT(generation(e) == 0); // no embedded generation allowed + const auto locker = qt_scoped_lock(mutex); + auto cur = expecting.load(std::memory_order_relaxed); + auto gen = generation(cur); + if (e == TestFunctionStart) + ++gen; + e = combine(e, gen); + expecting.store(e, std::memory_order_relaxed); + waitCondition.notify_all(); } public: WatchDog() { - setObjectName(QLatin1String("QtTest Watchdog")); + setObjectName("QtTest Watchdog"_L1); auto locker = qt_unique_lock(mutex); expecting.store(ThreadStart, std::memory_order_relaxed); start(); @@ -1059,36 +1222,29 @@ public: ~WatchDog() { - { - const auto locker = qt_scoped_lock(mutex); - expecting.store(ThreadEnd, std::memory_order_relaxed); - waitCondition.notify_all(); - } + setExpectation(ThreadEnd); wait(); } void beginTest() { - const auto locker = qt_scoped_lock(mutex); - expecting.store(TestFunctionEnd, std::memory_order_relaxed); - waitCondition.notify_all(); + setExpectation(TestFunctionEnd); } void testFinished() { - const auto locker = qt_scoped_lock(mutex); - expecting.store(TestFunctionStart, std::memory_order_relaxed); - waitCondition.notify_all(); + setExpectation(TestFunctionStart); } void run() override { + CrashHandler::blockUnixSignals(); auto locker = qt_unique_lock(mutex); expecting.store(TestFunctionStart, std::memory_order_release); waitCondition.notify_all(); while (true) { Expectation e = expecting.load(std::memory_order_acquire); - switch (e) { + switch (state(e)) { case ThreadEnd: return; case ThreadStart: @@ -1096,9 +1252,9 @@ public: case TestFunctionStart: case TestFunctionEnd: if (Q_UNLIKELY(!waitFor(locker, e))) { -#ifndef Q_OS_WASM - stackTrace(); -#endif + fflush(stderr); + CrashHandler::printTestRunTime(); + CrashHandler::generateStackTrace(); qFatal("Test function timed out"); } } @@ -1106,8 +1262,8 @@ public: } private: - QtPrivate::mutex mutex; - QtPrivate::condition_variable waitCondition; + std::mutex mutex; + std::condition_variable waitCondition; std::atomic<Expectation> expecting; }; @@ -1120,9 +1276,29 @@ public: void testFinished() {}; }; -#endif +#endif // QT_CONFIG(thread) +static void printUnknownDataTagError(QLatin1StringView name, QLatin1StringView tag, + const QTestTable &lTable, const QTestTable &gTable) +{ + fprintf(stderr, "Unknown testdata for function %s(): '%s'\n", name.constData(), tag.data()); + const int localDataCount = lTable.dataCount(); + if (localDataCount) { + fputs("Available test-specific data tags:\n", stderr); + for (int i = 0; i < localDataCount; ++i) + fprintf(stderr, "\t%s\n", lTable.testData(i)->dataTag()); + } + const int globalDataCount = gTable.dataCount(); + if (globalDataCount) { + fputs("Available global data tags:\n", stderr); + for (int i = 0; i < globalDataCount; ++i) + fprintf(stderr, "\t%s\n", gTable.testData(i)->dataTag()); + } + if (localDataCount == 0 && globalDataCount == 0) + fputs("Function has no data tags\n", stderr); +} + /*! \internal @@ -1131,14 +1307,14 @@ public: If the function was successfully called, true is returned, otherwise false. - */ -bool TestMethods::invokeTest(int index, const char *data, WatchDog *watchDog) const +*/ +bool TestMethods::invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const { QBenchmarkTestMethodData benchmarkData; QBenchmarkTestMethodData::current = &benchmarkData; const QByteArray &name = m_methods[index].name(); - QBenchmarkGlobalData::current->context.slotName = QLatin1String(name) + QLatin1String("()"); + QBenchmarkGlobalData::current->context.slotName = QLatin1StringView(name) + "()"_L1; char member[512]; QTestTable table; @@ -1148,6 +1324,23 @@ bool TestMethods::invokeTest(int index, const char *data, WatchDog *watchDog) co const QTestTable *gTable = QTestTable::globalTestTable(); const int globalDataCount = gTable->dataCount(); int curGlobalDataIndex = 0; + const auto globalDataTag = [gTable, globalDataCount](int index) { + return globalDataCount ? gTable->testData(index)->dataTag() : nullptr; + }; + + const auto dataTagMatches = [](QLatin1StringView tag, QLatin1StringView local, + QLatin1StringView global) { + if (tag.isEmpty()) // No tag specified => run all data sets for this function + return true; + if (tag == local || tag == global) // Equal to either => run it + return true; + // Also allow global:local as a match: + return tag.startsWith(global) && tag.endsWith(local) && + tag.size() == global.size() + 1 + local.size() && + tag[global.size()] == ':'; + }; + bool foundFunction = false; + bool blacklisted = false; /* For each entry in the global data table, do: */ do { @@ -1156,64 +1349,61 @@ bool TestMethods::invokeTest(int index, const char *data, WatchDog *watchDog) co if (curGlobalDataIndex == 0) { qsnprintf(member, 512, "%s_data()", name.constData()); - invokeMethod(QTest::currentTestObject, member); + invokeTestMethodIfExists(member); if (QTestResult::skipCurrentTest()) break; } - bool foundFunction = false; int curDataIndex = 0; const int dataCount = table.dataCount(); - - // Data tag requested but none available? - if (data && !dataCount) { - // Let empty data tag through. - if (!*data) - data = nullptr; - else { - fprintf(stderr, "Unknown testdata for function %s(): '%s'\n", name.constData(), data); - fprintf(stderr, "Function has no testdata.\n"); - return false; - } - } + const auto dataTag = [&table, dataCount](int index) { + return dataCount ? table.testData(index)->dataTag() : nullptr; + }; /* For each entry in this test's data table, do: */ do { QTestResult::setSkipCurrentTest(false); QTestResult::setBlacklistCurrentTest(false); - if (!data || !qstrcmp(data, table.testData(curDataIndex)->dataTag())) { + if (dataTagMatches(tag, QLatin1StringView(dataTag(curDataIndex)), + QLatin1StringView(globalDataTag(curGlobalDataIndex)))) { foundFunction = true; + blacklisted = QTestPrivate::checkBlackLists(name.constData(), dataTag(curDataIndex), + globalDataTag(curGlobalDataIndex)); + if (blacklisted) + QTestResult::setBlacklistCurrentTest(true); + + if (blacklisted && skipBlacklisted) { + QTest::qSkip("Skipping blacklisted test since -skipblacklisted option is set.", + NULL, 0); + QTestResult::finishedCurrentTestData(); + QTestResult::finishedCurrentTestDataCleanup(); + } else { + QTestDataSetter s( + curDataIndex >= dataCount ? nullptr : table.testData(curDataIndex)); + + QTestPrivate::qtestMouseButtons = Qt::NoButton; + if (watchDog) + watchDog->beginTest(); + QTest::lastMouseTimestamp += 500; // Maintain at least 500ms mouse event timestamps between each test function call + invokeTestOnData(index); + if (watchDog) + watchDog->testFinished(); + } - QTestPrivate::checkBlackLists(name.constData(), dataCount ? table.testData(curDataIndex)->dataTag() : nullptr); - - QTestDataSetter s(curDataIndex >= dataCount ? nullptr : table.testData(curDataIndex)); - - QTestPrivate::qtestMouseButtons = Qt::NoButton; - if (watchDog) - watchDog->beginTest(); - QTest::lastMouseTimestamp += 500; // Maintain at least 500ms mouse event timestamps between each test function call - invokeTestOnData(index); - if (watchDog) - watchDog->testFinished(); - - if (data) + if (!tag.isEmpty() && !globalDataCount) break; } ++curDataIndex; } while (curDataIndex < dataCount); - if (data && !foundFunction) { - fprintf(stderr, "Unknown testdata for function %s: '%s()'\n", name.constData(), data); - fprintf(stderr, "Available testdata:\n"); - for (int i = 0; i < table.dataCount(); ++i) - fprintf(stderr, "%s\n", table.testData(i)->dataTag()); - return false; - } - QTestResult::setCurrentGlobalTestData(nullptr); ++curGlobalDataIndex; } while (curGlobalDataIndex < globalDataCount); + if (!tag.isEmpty() && !foundFunction) { + printUnknownDataTagError(QLatin1StringView(name), tag, table, *gTable); + QTestResult::addFailure(qPrintable("Data tag not found: %1"_L1.arg(tag))); + } QTestResult::finishedCurrentTestFunction(); QTestResult::setSkipCurrentTest(false); QTestResult::setBlacklistCurrentTest(false); @@ -1245,7 +1435,7 @@ void *fetchData(QTestData *data, const char *tagName, int typeId) /*! * \internal - */ +*/ char *formatString(const char *prefix, const char *suffix, size_t numArguments, ...) { va_list ap; @@ -1269,8 +1459,6 @@ char *formatString(const char *prefix, const char *suffix, size_t numArguments, } /*! - \fn char* QTest::toHexRepresentation(const char *ba, int length) - Returns a pointer to a string that is the string \a ba represented as a space-separated sequence of hex characters. If the input is considered too long, it is truncated. A trucation is indicated in @@ -1279,8 +1467,8 @@ char *formatString(const char *prefix, const char *suffix, size_t numArguments, to operator delete[]. \a length is the length of the string \a ba. - */ -char *toHexRepresentation(const char *ba, int length) +*/ +char *toHexRepresentation(const char *ba, qsizetype length) { if (length == 0) return qstrdup(""); @@ -1292,12 +1480,12 @@ char *toHexRepresentation(const char *ba, int length) * maxLen can't be for example 200 because Qt Test is sprinkled with fixed * size char arrays. * */ - const int maxLen = 50; - const int len = qMin(maxLen, length); + const qsizetype maxLen = 50; + const qsizetype len = qMin(maxLen, length); char *result = nullptr; if (length > maxLen) { - const int size = len * 3 + 4; + const qsizetype size = len * 3 + 4; result = new char[size]; char *const forElipsis = result + size - 5; @@ -1308,13 +1496,13 @@ char *toHexRepresentation(const char *ba, int length) result[size - 1] = '\0'; } else { - const int size = len * 3; + const qsizetype size = len * 3; result = new char[size]; result[size - 1] = '\0'; } - int i = 0; - int o = 0; + qsizetype i = 0; + qsizetype o = 0; while (true) { const char at = ba[i]; @@ -1339,12 +1527,12 @@ char *toHexRepresentation(const char *ba, int length) Returns the same QByteArray but with only the ASCII characters still shown; everything else is replaced with \c {\xHH}. */ -char *toPrettyCString(const char *p, int length) +char *toPrettyCString(const char *p, qsizetype length) { bool trimmed = false; - QScopedArrayPointer<char> buffer(new char[256]); + auto buffer = std::make_unique<char[]>(256); const char *end = p + length; - char *dst = buffer.data(); + char *dst = buffer.get(); bool lastWasHexEscape = false; *dst++ = '"'; @@ -1354,7 +1542,7 @@ char *toPrettyCString(const char *p, int length) // 2 bytes: a simple escape sequence (\n) // 3 bytes: "" and a character // 4 bytes: an hex escape sequence (\xHH) - if (dst - buffer.data() > 246) { + if (dst - buffer.get() > 246) { // plus the quote, the three dots and NUL, it's 255 in the worst case trimmed = true; break; @@ -1415,79 +1603,81 @@ char *toPrettyCString(const char *p, int length) *dst++ = '.'; } *dst++ = '\0'; - return buffer.take(); -} - -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -// this used to be the signature up to and including Qt 5.9 -// keep it for BC reasons: -Q_TESTLIB_EXPORT -char *toPrettyUnicode(const ushort *p, int length) -{ - return toPrettyUnicode(QStringView(p, length)); + return buffer.release(); } -#endif /*! + \fn char *toPrettyUnicode(QStringView string) \internal Returns the same QString but with only the ASCII characters still shown; everything else is replaced with \c {\uXXXX}. Similar to QDebug::putString(). */ + +constexpr qsizetype PrettyUnicodeMaxOutputSize = 256; +// escape sequence, closing quote, the three dots and NUL +constexpr qsizetype PrettyUnicodeMaxIncrement = sizeof(R"(\uXXXX"...)"); // includes NUL + +static char *writePrettyUnicodeChar(char16_t ch, char * const buffer) +{ + auto dst = buffer; + auto first = [&](int n) { Q_ASSERT(dst - buffer == n); return dst; }; + if (ch < 0x7f && ch >= 0x20 && ch != '\\' && ch != '"') { + *dst++ = ch; + return first(1); + } + + // write as an escape sequence + *dst++ = '\\'; + switch (ch) { + case 0x22: + case 0x5c: + *dst++ = uchar(ch); + break; + case 0x8: + *dst++ = 'b'; + break; + case 0xc: + *dst++ = 'f'; + break; + case 0xa: + *dst++ = 'n'; + break; + case 0xd: + *dst++ = 'r'; + break; + case 0x9: + *dst++ = 't'; + break; + default: + *dst++ = 'u'; + *dst++ = toHexUpper(ch >> 12); + *dst++ = toHexUpper(ch >> 8); + *dst++ = toHexUpper(ch >> 4); + *dst++ = toHexUpper(ch); + return first(6); + } + return first(2); +} + char *toPrettyUnicode(QStringView string) { auto p = string.utf16(); auto length = string.size(); // keep it simple for the vast majority of cases bool trimmed = false; - QScopedArrayPointer<char> buffer(new char[256]); + auto buffer = std::make_unique<char[]>(PrettyUnicodeMaxOutputSize); const auto end = p + length; - char *dst = buffer.data(); + char *dst = buffer.get(); *dst++ = '"'; for ( ; p != end; ++p) { - if (dst - buffer.data() > 245) { - // plus the quote, the three dots and NUL, it's 250, 251 or 255 + if (dst - buffer.get() > PrettyUnicodeMaxOutputSize - PrettyUnicodeMaxIncrement) { trimmed = true; break; } - - if (*p < 0x7f && *p >= 0x20 && *p != '\\' && *p != '"') { - *dst++ = *p; - continue; - } - - // write as an escape sequence - // this means we may advance dst to buffer.data() + 246 or 250 - *dst++ = '\\'; - switch (*p) { - case 0x22: - case 0x5c: - *dst++ = uchar(*p); - break; - case 0x8: - *dst++ = 'b'; - break; - case 0xc: - *dst++ = 'f'; - break; - case 0xa: - *dst++ = 'n'; - break; - case 0xd: - *dst++ = 'r'; - break; - case 0x9: - *dst++ = 't'; - break; - default: - *dst++ = 'u'; - *dst++ = toHexUpper(*p >> 12); - *dst++ = toHexUpper(*p >> 8); - *dst++ = toHexUpper(*p >> 4); - *dst++ = toHexUpper(*p); - } + dst = writePrettyUnicodeChar(*p, dst); } *dst++ = '"'; @@ -1497,7 +1687,7 @@ char *toPrettyUnicode(QStringView string) *dst++ = '.'; } *dst++ = '\0'; - return buffer.take(); + return buffer.release(); } void TestMethods::invokeTests(QObject *testObject) const @@ -1505,23 +1695,21 @@ void TestMethods::invokeTests(QObject *testObject) const const QMetaObject *metaObject = testObject->metaObject(); QTEST_ASSERT(metaObject); QTestResult::setCurrentTestFunction("initTestCase"); - if (m_initTestCaseDataMethod.isValid()) - m_initTestCaseDataMethod.invoke(testObject, Qt::DirectConnection); + invokeTestMethodIfValid(m_initTestCaseDataMethod, testObject); - QScopedPointer<WatchDog> watchDog; - if (!debuggerPresent() + std::optional<WatchDog> watchDog = std::nullopt; + if (!CrashHandler::alreadyDebugging() #if QT_CONFIG(valgrind) && QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindChildProcess #endif ) { - watchDog.reset(new WatchDog); + watchDog.emplace(); } QSignalDumper::startDump(); - if (!QTestResult::skipCurrentTest() && !QTest::currentTestFailed()) { - if (m_initTestCaseMethod.isValid()) - m_initTestCaseMethod.invoke(testObject, Qt::DirectConnection); + if (!QTestResult::skipCurrentTest() && !QTestResult::currentTestFailed()) { + invokeTestMethodIfValid(m_initTestCaseMethod, testObject); // finishedCurrentTestDataCleanup() resets QTestResult::currentTestFailed(), so use a local copy. const bool previousFailed = QTestResult::currentTestFailed(); @@ -1534,19 +1722,21 @@ void TestMethods::invokeTests(QObject *testObject) const const char *data = nullptr; if (i < QTest::testTags.size() && !QTest::testTags.at(i).isEmpty()) data = qstrdup(QTest::testTags.at(i).toLatin1().constData()); - const bool ok = invokeTest(i, data, watchDog.data()); + const bool ok = invokeTest(i, QLatin1StringView(data), watchDog); delete [] data; if (!ok) break; } } + const bool wasSkipped = QTestResult::skipCurrentTest(); QTestResult::setSkipCurrentTest(false); QTestResult::setBlacklistCurrentTest(false); QTestResult::setCurrentTestFunction("cleanupTestCase"); - if (m_cleanupTestCaseMethod.isValid()) - m_cleanupTestCaseMethod.invoke(testObject, Qt::DirectConnection); + invokeTestMethodIfValid(m_cleanupTestCaseMethod, testObject); QTestResult::finishedCurrentTestData(); + // Restore skip state as it affects decision on whether we passed: + QTestResult::setSkipCurrentTest(wasSkipped || QTestResult::skipCurrentTest()); QTestResult::finishedCurrentTestDataCleanup(); } QTestResult::finishedCurrentTestFunction(); @@ -1555,273 +1745,15 @@ void TestMethods::invokeTests(QObject *testObject) const QSignalDumper::endDump(); } -#if defined(Q_OS_WIN) - -// Helper class for resolving symbol names by dynamically loading "dbghelp.dll". -class DebugSymbolResolver -{ - Q_DISABLE_COPY_MOVE(DebugSymbolResolver) -public: - struct Symbol { - Symbol() : name(nullptr), address(0) {} - - const char *name; // Must be freed by caller. - DWORD64 address; - }; - - explicit DebugSymbolResolver(HANDLE process); - ~DebugSymbolResolver() { cleanup(); } - - bool isValid() const { return m_symFromAddr; } - - Symbol resolveSymbol(DWORD64 address) const; - -private: - // typedefs from DbgHelp.h/.dll - struct DBGHELP_SYMBOL_INFO { // SYMBOL_INFO - ULONG SizeOfStruct; - ULONG TypeIndex; // Type Index of symbol - ULONG64 Reserved[2]; - ULONG Index; - ULONG Size; - ULONG64 ModBase; // Base Address of module comtaining this symbol - ULONG Flags; - ULONG64 Value; // Value of symbol, ValuePresent should be 1 - ULONG64 Address; // Address of symbol including base address of module - ULONG Register; // register holding value or pointer to value - ULONG Scope; // scope of the symbol - ULONG Tag; // pdb classification - ULONG NameLen; // Actual length of name - ULONG MaxNameLen; - CHAR Name[1]; // Name of symbol - }; - - typedef BOOL (__stdcall *SymInitializeType)(HANDLE, PCSTR, BOOL); - typedef BOOL (__stdcall *SymFromAddrType)(HANDLE, DWORD64, PDWORD64, DBGHELP_SYMBOL_INFO *); - - void cleanup(); - - const HANDLE m_process; - HMODULE m_dbgHelpLib; - SymFromAddrType m_symFromAddr; -}; - -void DebugSymbolResolver::cleanup() -{ - if (m_dbgHelpLib) - FreeLibrary(m_dbgHelpLib); - m_dbgHelpLib = 0; - m_symFromAddr = nullptr; -} - -DebugSymbolResolver::DebugSymbolResolver(HANDLE process) - : m_process(process), m_dbgHelpLib(0), m_symFromAddr(nullptr) -{ - bool success = false; - m_dbgHelpLib = LoadLibraryW(L"dbghelp.dll"); - if (m_dbgHelpLib) { - SymInitializeType symInitialize = reinterpret_cast<SymInitializeType>( - reinterpret_cast<QFunctionPointer>(GetProcAddress(m_dbgHelpLib, "SymInitialize"))); - m_symFromAddr = reinterpret_cast<SymFromAddrType>( - reinterpret_cast<QFunctionPointer>(GetProcAddress(m_dbgHelpLib, "SymFromAddr"))); - success = symInitialize && m_symFromAddr && symInitialize(process, NULL, TRUE); - } - if (!success) - cleanup(); -} - -DebugSymbolResolver::Symbol DebugSymbolResolver::resolveSymbol(DWORD64 address) const +bool reportResult(bool success, qxp::function_ref<const char *()> lhs, + qxp::function_ref<const char *()> rhs, + const char *lhsExpr, const char *rhsExpr, + ComparisonOperation op, const char *file, int line) { - // reserve additional buffer where SymFromAddr() will store the name - struct NamedSymbolInfo : public DBGHELP_SYMBOL_INFO { - enum { symbolNameLength = 255 }; - - char name[symbolNameLength + 1]; - }; - - Symbol result; - if (!isValid()) - return result; - NamedSymbolInfo symbolBuffer; - memset(&symbolBuffer, 0, sizeof(NamedSymbolInfo)); - symbolBuffer.MaxNameLen = NamedSymbolInfo::symbolNameLength; - symbolBuffer.SizeOfStruct = sizeof(DBGHELP_SYMBOL_INFO); - if (!m_symFromAddr(m_process, address, 0, &symbolBuffer)) - return result; - result.name = qstrdup(symbolBuffer.Name); - result.address = symbolBuffer.Address; - return result; + return QTestResult::reportResult(success, lhs, rhs, lhsExpr, rhsExpr, op, file, line); } -#endif // Q_OS_WIN - -class FatalSignalHandler -{ -public: - FatalSignalHandler() - { -#if defined(Q_OS_WIN) -# if !defined(Q_CC_MINGW) - _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG); -# endif - SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX); - SetUnhandledExceptionFilter(windowsFaultHandler); -#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM) - sigemptyset(&handledSignals); - - const int fatalSignals[] = { - SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGBUS, SIGFPE, SIGSEGV, SIGPIPE, SIGTERM, 0 }; - - struct sigaction act; - memset(&act, 0, sizeof(act)); - act.sa_handler = FatalSignalHandler::signal; - - // Remove the handler after it is invoked. -# if !defined(Q_OS_INTEGRITY) - act.sa_flags = SA_RESETHAND; -# endif - - // tvOS/watchOS both define SA_ONSTACK (in sys/signal.h) but mark sigaltstack() as - // unavailable (__WATCHOS_PROHIBITED __TVOS_PROHIBITED in signal.h) -# if defined(SA_ONSTACK) && !defined(Q_OS_TVOS) && !defined(Q_OS_WATCHOS) - // Let the signal handlers use an alternate stack - // This is necessary if SIGSEGV is to catch a stack overflow -# if defined(Q_CC_GNU) && defined(Q_OF_ELF) - // Put the alternate stack in the .lbss (large BSS) section so that it doesn't - // interfere with normal .bss symbols - __attribute__((section(".lbss.altstack"), aligned(4096))) -# endif - static char alternate_stack[16 * 1024]; - stack_t stack; - stack.ss_flags = 0; - stack.ss_size = sizeof alternate_stack; - stack.ss_sp = alternate_stack; - sigaltstack(&stack, nullptr); - act.sa_flags |= SA_ONSTACK; -# endif - - // Block all fatal signals in our signal handler so we don't try to close - // the testlog twice. - sigemptyset(&act.sa_mask); - for (int i = 0; fatalSignals[i]; ++i) - sigaddset(&act.sa_mask, fatalSignals[i]); - - struct sigaction oldact; - - for (int i = 0; fatalSignals[i]; ++i) { - sigaction(fatalSignals[i], &act, &oldact); - if ( -# ifdef SA_SIGINFO - oldact.sa_flags & SA_SIGINFO || -# endif - oldact.sa_handler != SIG_DFL) { - sigaction(fatalSignals[i], &oldact, nullptr); - } else - { - sigaddset(&handledSignals, fatalSignals[i]); - } - } -#endif // defined(Q_OS_UNIX) && !defined(Q_OS_WASM) - } - - ~FatalSignalHandler() - { -#if defined(Q_OS_UNIX) && !defined(Q_OS_WASM) - // Unregister any of our remaining signal handlers - struct sigaction act; - memset(&act, 0, sizeof(act)); - act.sa_handler = SIG_DFL; - - struct sigaction oldact; - - for (int i = 1; i < 32; ++i) { - if (!sigismember(&handledSignals, i)) - continue; - sigaction(i, &act, &oldact); - - // If someone overwrote it in the mean time, put it back - if (oldact.sa_handler != FatalSignalHandler::signal) - sigaction(i, &oldact, nullptr); - } -#endif - } - -private: -#if defined(Q_OS_WIN) - static LONG WINAPI windowsFaultHandler(struct _EXCEPTION_POINTERS *exInfo) - { - enum { maxStackFrames = 100 }; - char appName[MAX_PATH]; - if (!GetModuleFileNameA(NULL, appName, MAX_PATH)) - appName[0] = 0; - const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime()); - const int msecsTotalTime = qRound(QTestLog::msecsTotalTime()); - const void *exceptionAddress = exInfo->ExceptionRecord->ExceptionAddress; - printf("A crash occurred in %s.\n" - "Function time: %dms Total time: %dms\n\n" - "Exception address: 0x%p\n" - "Exception code : 0x%lx\n", - appName, msecsFunctionTime, msecsTotalTime, - exceptionAddress, exInfo->ExceptionRecord->ExceptionCode); - - DebugSymbolResolver resolver(GetCurrentProcess()); - if (resolver.isValid()) { - DebugSymbolResolver::Symbol exceptionSymbol = resolver.resolveSymbol(DWORD64(exceptionAddress)); - if (exceptionSymbol.name) { - printf("Nearby symbol : %s\n", exceptionSymbol.name); - delete [] exceptionSymbol.name; - } - void *stack[maxStackFrames]; - fputs("\nStack:\n", stdout); - const unsigned frameCount = CaptureStackBackTrace(0, DWORD(maxStackFrames), stack, NULL); - for (unsigned f = 0; f < frameCount; ++f) { - DebugSymbolResolver::Symbol symbol = resolver.resolveSymbol(DWORD64(stack[f])); - if (symbol.name) { - printf("#%3u: %s() - 0x%p\n", f + 1, symbol.name, (const void *)symbol.address); - delete [] symbol.name; - } else { - printf("#%3u: Unable to obtain symbol\n", f + 1); - } - } - } - - fputc('\n', stdout); - fflush(stdout); - - return EXCEPTION_EXECUTE_HANDLER; - } -#endif // defined(Q_OS_WIN) - -#if defined(Q_OS_UNIX) && !defined(Q_OS_WASM) - static void signal(int signum) - { - const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime()); - const int msecsTotalTime = qRound(QTestLog::msecsTotalTime()); - if (signum != SIGINT) { - stackTrace(); - if (qEnvironmentVariableIsSet("QTEST_PAUSE_ON_CRASH")) { - fprintf(stderr, "Pausing process %d for debugging\n", getpid()); - raise(SIGSTOP); - } - } - qFatal("Received signal %d\n" - " Function time: %dms Total time: %dms", - signum, msecsFunctionTime, msecsTotalTime); -# if defined(Q_OS_INTEGRITY) - { - struct sigaction act; - memset(&act, 0, sizeof(struct sigaction)); - act.sa_handler = SIG_DFL; - sigaction(signum, &act, NULL); - } -# endif - } - - sigset_t handledSignals; -#endif // defined(Q_OS_UNIX) && !defined(Q_OS_WASM) -}; - -} // namespace +} // namespace QTest static void initEnvironment() { @@ -1859,26 +1791,39 @@ static void initEnvironment() test that was executed with qExec() can't run another test via qExec() and threads are not allowed to call qExec() simultaneously. - If you have programatically created the arguments, as opposed to getting them + If you have programmatically created the arguments, as opposed to getting them from the arguments in \c main(), it is likely of interest to use QTest::qExec(QObject *, const QStringList &) since it is Unicode safe. - \sa QTEST_MAIN() + \sa QTEST_MAIN(), QTEST_GUILESS_MAIN(), QTEST_APPLESS_MAIN() */ int QTest::qExec(QObject *testObject, int argc, char **argv) { + // NB: QtQuick's testing recombines qInit(), qRun() and qCleanup() to + // provide a replacement for qExec() that calls qRun() once for each + // built-in style. So think twice about moving parts between these three + // functions, as doing so may mess up QtQuick's testing. qInit(testObject, argc, argv); int ret = qRun(); qCleanup(); + +#if defined(Q_OS_WASM) + EM_ASM({ + if (typeof Module != "undefined" && typeof Module.notifyTestFinished != "undefined") + Module.notifyTestFinished($0); + }, ret); +#endif // Q_OS_WASM + return ret; } /*! \internal - */ +*/ void QTest::qInit(QObject *testObject, int argc, char **argv) { initEnvironment(); + CrashHandler::maybeDisableCoreDump(); QBenchmarkGlobalData::current = new QBenchmarkGlobalData; #if defined(Q_OS_MACOS) @@ -1911,12 +1856,14 @@ void QTest::qInit(QObject *testObject, int argc, char **argv) qtest_qParseArgs(argc, argv, false); - QTestTable::globalTestTable(); - QTestLog::startLogging(); +#if QT_CONFIG(valgrind) + if (QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindParentProcess) +#endif + QTestLog::startLogging(); } /*! \internal - */ +*/ int QTest::qRun() { QTEST_ASSERT(currentTestObject); @@ -1943,26 +1890,46 @@ int QTest::qRun() } else #endif { - QScopedPointer<FatalSignalHandler> handler; - if (!noCrashHandler) - handler.reset(new FatalSignalHandler); + std::optional<CrashHandler::FatalSignalHandler> handler; + CrashHandler::prepareStackTrace(); + if (!Internal::noCrashHandler) + handler.emplace(); + bool seenBad = false; TestMethods::MetaMethods commandLineMethods; commandLineMethods.reserve(static_cast<size_t>(QTest::testFunctions.size())); - for (const QString &tf : qAsConst(QTest::testFunctions)) { - const QByteArray tfB = tf.toLatin1(); - const QByteArray signature = tfB + QByteArrayLiteral("()"); - QMetaMethod m = TestMethods::findMethod(currentTestObject, signature.constData()); - if (!m.isValid() || !isValidSlot(m)) { - fprintf(stderr, "Unknown test function: '%s'. Possible matches:\n", tfB.constData()); - qPrintTestSlots(stderr, tfB.constData()); - fprintf(stderr, "\n%s -functions\nlists all available test functions.\n", QTestResult::currentAppName()); - exit(1); - } + for (const QString &tf : std::as_const(QTest::testFunctions)) { + const QByteArray tfB = tf.toLatin1(); + const QByteArray signature = tfB + QByteArrayLiteral("()"); + QMetaMethod m = TestMethods::findMethod(currentTestObject, signature.constData()); + if (m.isValid() && isValidSlot(m)) { commandLineMethods.push_back(m); + } else { + fprintf(stderr, "Unknown test function: '%s'.", tfB.constData()); + if (!qPrintTestSlots(stderr, tfB.constData(), " Possible matches:\n")) + fputc('\n', stderr); + QTestResult::setCurrentTestFunction(tfB.constData()); + QTestResult::addFailure(qPrintable("Function not found: %1"_L1.arg(tf))); + QTestResult::finishedCurrentTestFunction(); + // Ditch the tag that came with tf as test function: + QTest::testTags.remove(commandLineMethods.size()); + seenBad = true; + } + } + if (seenBad) { + // Provide relevant help to do better next time: + fprintf(stderr, "\n%s -functions\nlists all available test functions.\n\n", + QTestResult::currentAppName()); + if (commandLineMethods.empty()) // All requested functions missing. + return 1; } TestMethods test(currentTestObject, std::move(commandLineMethods)); - test.invokeTests(currentTestObject); + + while (QTestLog::failCount() == 0 && (repeatForever || repetitions-- > 0)) { + QTestTable::globalTestTable(); + test.invokeTests(currentTestObject); + QTestTable::clearGlobalTestTable(); + } } #ifndef QT_NO_EXCEPTIONS @@ -1991,13 +1958,15 @@ int QTest::qRun() } /*! \internal - */ +*/ void QTest::qCleanup() { currentTestObject = nullptr; - QTestTable::clearGlobalTestTable(); - QTestLog::stopLogging(); +#if QT_CONFIG(valgrind) + if (QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindParentProcess) +#endif + QTestLog::stopLogging(); delete QBenchmarkGlobalData::current; QBenchmarkGlobalData::current = nullptr; @@ -2007,16 +1976,44 @@ void QTest::qCleanup() #endif } +#if QT_CONFIG(batch_test_support) || defined(Q_QDOC) +/*! + Registers the test \a name, with entry function \a entryFunction, in a + central test case registry for the current binary. + + The \a name will be listed when running the batch test binary with no + parameters. Running the test binary with the argv[1] of \a name will result + in \a entryFunction being called. + + \since 6.5 +*/ +void QTest::qRegisterTestCase(const QString &name, TestEntryFunction entryFunction) +{ + QTest::TestRegistry::instance()->registerTest(name, entryFunction); +} + +QList<QString> QTest::qGetTestCaseNames() +{ + return QTest::TestRegistry::instance()->getAllTestNames(); +} + +QTest::TestEntryFunction QTest::qGetTestCaseEntryFunction(const QString& name) +{ + return QTest::TestRegistry::instance()->getTestEntryFunction(name); +} + +#endif // QT_CONFIG(batch_test_support) + /*! \overload \since 4.4 Behaves identically to qExec(QObject *, int, char**) but takes a QStringList of \a arguments instead of a \c char** list. - */ +*/ int QTest::qExec(QObject *testObject, const QStringList &arguments) { - const int argc = arguments.count(); + const int argc = arguments.size(); QVarLengthArray<char *> argv(argc); QList<QByteArray> args; @@ -2032,14 +2029,14 @@ int QTest::qExec(QObject *testObject, const QStringList &arguments) } /*! \internal - */ +*/ void QTest::qFail(const char *message, const char *file, int line) { QTestResult::fail(message, file, line); } /*! \internal - */ +*/ bool QTest::qVerify(bool statement, const char *statementStr, const char *description, const char *file, int line) { @@ -2047,8 +2044,8 @@ bool QTest::qVerify(bool statement, const char *statementStr, const char *descri } /*! \fn void QTest::qSkip(const char *message, const char *file, int line) -\internal - */ + \internal +*/ void QTest::qSkip(const char *message, const char *file, int line) { QTestResult::addSkip(message, file, line); @@ -2056,19 +2053,86 @@ void QTest::qSkip(const char *message, const char *file, int line) } /*! \fn bool QTest::qExpectFail(const char *dataIndex, const char *comment, TestFailMode mode, const char *file, int line) -\internal - */ + \internal +*/ bool QTest::qExpectFail(const char *dataIndex, const char *comment, QTest::TestFailMode mode, const char *file, int line) { return QTestResult::expectFail(dataIndex, qstrdup(comment), mode, file, line); } +/*! + \internal + + Executes qFail() following a failed QVERIFY_THROWS_EXCEPTION or + QVERIFY_THROWS_NO_EXCEPTION, passing a suitable message created from \a expected, + \a what, along with \a file and \a line. + + The \a expected parameter contains the type of the exception that is expected to + be thrown, or \nullptr, if no exception was expected. + + The \a what parameter contains the result of \c{std::exception::what()}, or nullptr, + if a non-\c{std::exception}-derived exception was caught. + + The \a file and \a line parameters hold expansions of the \c{__FILE__} and \c{__LINE__} + macros, respectively. +*/ +void QTest::qCaught(const char *expected, const char *what, const char *file, int line) +{ + auto message = [&] { + const auto exType = what ? "std::" : "unknown "; + const auto ofType = expected ? " of type " : ""; + const auto no = expected ? "an" : "no"; + const auto withMsg = what ? " with message " : ""; + const auto protect = [](const char *s) { return s ? s : ""; }; + + return QString::asprintf("Expected %s exception%s%s to be thrown, " + "but caught %sexception%s%s", + no, ofType, protect(expected), + exType, withMsg, protect(what)); + }; + qFail(message().toUtf8().constData(), file, line); +} + +/*! + \internal + + Contains the implementation of the catch(...) block of + QVERIFY_THROWS_EXCEPTION. + + The function inspects std::current_exception() by rethrowing it using + std::rethrow_exception(). + + The function must be called from a catch handler. + + If the exception inherits std::exception, its what() message is logged and + this function returns normally. The caller of this function must then + execute a \c{QTEST_FAIL_ACTION} to exit from the test function. + + Otherwise, a message saying an unknown exception was caught is logged and + this function rethrows the exception, skipping the \c{QTEST_FAIL_ACTION} + that follows this function call in the caller. +*/ +void QTest::qCaught(const char *expected, const char *file, int line) +{ + try { + // let's see what the cat brought us: + std::rethrow_exception(std::current_exception()); + } catch (const std::exception &e) { + qCaught(expected, e.what(), file, line); + } catch (...) { + qCaught(expected, nullptr, file, line); + throw; + } + // caller shall invoke `QTEST_FAIL_ACTION` if control reached here +} + + #if QT_DEPRECATED_SINCE(6, 3) /*! \internal \deprecated [6.3] Use qWarning() instead - */ +*/ void QTest::qWarn(const char *message, const char *file, int line) { QTestLog::warn(message, file, line); @@ -2118,14 +2182,76 @@ void QTest::ignoreMessage(QtMsgType type, const QRegularExpression &messagePatte } #endif // QT_CONFIG(regularexpression) +/*! + \since 6.3 + \overload failOnWarning() + + Appends a test failure to the test log if the \a message is output. + + \sa failOnWarning() +*/ +void QTest::failOnWarning(const char *message) +{ + return QTestLog::failOnWarning(message); +} + +#if QT_CONFIG(regularexpression) +/*! + \since 6.3 + + Appends a test failure to the test log for each warning that matches + \a messagePattern. + + The test function will continue execution when a failure is added. To abort + the test instead, you can check \l currentTestFailed() and return early if + it's \c true. + + For each warning, the first pattern that matches will cause a failure, + and the remaining patterns will be ignored. + + All patterns are cleared at the end of each test function. + + \code + void FileTest::loadFiles() + { + QTest::failOnWarning(QRegularExpression("^Failed to load")); + + // Each of these will cause a test failure: + qWarning() << "Failed to load image"; + qWarning() << "Failed to load video"; + } + \endcode + + To fail every test that triggers a given warning, pass a suitable regular + expression to this function in \l {Creating a Test}{init()}: + + \code + void FileTest::init() + { + QTest::failOnWarning(QRegularExpression(".?")); + } + \endcode + + \note \l ignoreMessage() takes precedence over this function, so any + warnings that match a pattern given to both \c ignoreMessage() and + \c failOnWarning() will be ignored. + + \sa {Qt Test Environment Variables}{QTEST_FATAL_FAIL} +*/ +void QTest::failOnWarning(const QRegularExpression &messagePattern) +{ + QTestLog::failOnWarning(messagePattern); +} +#endif // QT_CONFIG(regularexpression) + /*! \internal - */ +*/ #ifdef Q_OS_WIN static inline bool isWindowsBuildDirectory(const QString &dirName) { - return dirName.compare(QLatin1String("Debug"), Qt::CaseInsensitive) == 0 - || dirName.compare(QLatin1String("Release"), Qt::CaseInsensitive) == 0; + return dirName.compare("Debug"_L1, Qt::CaseInsensitive) == 0 + || dirName.compare("Release"_L1, Qt::CaseInsensitive) == 0; } #endif @@ -2139,7 +2265,7 @@ static inline bool isWindowsBuildDirectory(const QString &dirName) Returns the temporary directory where the data was extracted or null in case of errors. - */ +*/ QSharedPointer<QTemporaryDir> QTest::qExtractTestData(const QString &dirName) { QSharedPointer<QTemporaryDir> result; // null until success, then == tempDir @@ -2152,7 +2278,7 @@ QSharedPointer<QTemporaryDir> QTest::qExtractTestData(const QString &dirName) return result; const QString dataPath = tempDir->path(); - const QString resourcePath = QLatin1Char(':') + dirName; + const QString resourcePath = u':' + dirName; const QFileInfo fileInfo(resourcePath); if (!fileInfo.isDir()) { @@ -2160,30 +2286,36 @@ QSharedPointer<QTemporaryDir> QTest::qExtractTestData(const QString &dirName) return result; } - QDirIterator it(resourcePath, QDirIterator::Subdirectories); - if (!it.hasNext()) { - qWarning("Resource directory '%s' is empty.", qPrintable(resourcePath)); - return result; - } - - while (it.hasNext()) { - QFileInfo fileInfo = it.nextFileInfo(); - - if (!fileInfo.isDir()) { - const QString destination = dataPath + QLatin1Char('/') + QStringView{fileInfo.filePath()}.mid(resourcePath.length()); + bool isResourceDirEmpty = true; + for (const auto &dirEntry : QDirListing(resourcePath, QDirListing::IteratorFlag::Recursive)) { + isResourceDirEmpty = false; + if (!dirEntry.isDir()) { + const QString &filePath = dirEntry.filePath(); + const QString destination = + dataPath + u'/' + QStringView{filePath}.sliced(resourcePath.size()); QFileInfo destinationFileInfo(destination); QDir().mkpath(destinationFileInfo.path()); - if (!QFile::copy(fileInfo.filePath(), destination)) { - qWarning("Failed to copy '%s'.", qPrintable(fileInfo.filePath())); + QFile file(filePath); + if (!file.copy(destination)) { + qWarning("Failed to copy '%ls': %ls.", qUtf16Printable(filePath), + qUtf16Printable(file.errorString())); return result; } - if (!QFile::setPermissions(destination, QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup)) { - qWarning("Failed to set permissions on '%s'.", qPrintable(destination)); + + file.setFileName(destination); + if (!file.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup)) { + qWarning("Failed to set permissions on '%ls': %ls.", qUtf16Printable(destination), + qUtf16Printable(file.errorString())); return result; } } } + if (isResourceDirEmpty) { + qWarning("Resource directory '%s' is empty.", qPrintable(resourcePath)); + return result; + } + result = std::move(tempDir); return result; @@ -2191,7 +2323,7 @@ QSharedPointer<QTemporaryDir> QTest::qExtractTestData(const QString &dirName) #endif // QT_CONFIG(temporaryfile) /*! \internal - */ +*/ QString QTest::qFindTestData(const QString& base, const char *file, int line, const char *builddir, const char *sourcedir) @@ -2215,11 +2347,10 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co } #endif // Q_OS_WIN else if (QTestLog::verboseLevel() >= 2) { - const QString candidate = QDir::toNativeSeparators(QCoreApplication::applicationDirPath() + QLatin1Char('/') + base); - QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found relative to test binary [%2]; " - "checking next location").arg(base, candidate)), - file, line); + const QString candidate = QDir::toNativeSeparators(QCoreApplication::applicationDirPath() + u'/' + base); + QTestLog::info(qPrintable("testdata %1 not found relative to test binary [%2]; " + "checking next location"_L1.arg(base, candidate)), + file, line); } } @@ -2228,16 +2359,15 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co const char *testObjectName = QTestResult::currentTestObjectName(); if (testObjectName) { const QString testsPath = QLibraryInfo::path(QLibraryInfo::TestsPath); - const QString candidate = QString::fromLatin1("%1/%2/%3") + const QString candidate = "%1/%2/%3"_L1 .arg(testsPath, QFile::decodeName(testObjectName).toLower(), base); if (QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { - QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found in tests install path [%2]; " - "checking next location") - .arg(base, QDir::toNativeSeparators(candidate))), - file, line); + QTestLog::info(qPrintable("testdata %1 not found in tests install path [%2]; " + "checking next location"_L1 + .arg(base, QDir::toNativeSeparators(candidate))), + file, line); } } } @@ -2249,17 +2379,16 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co // If the srcdir is relative, that means it is relative to the current working // directory of the compiler at compile time, which should be passed in as `builddir'. - if (!srcdir.isAbsolute() && builddir) { - srcdir.setFile(QFile::decodeName(builddir) + QLatin1String("/") + srcdir.filePath()); - } + if (!srcdir.isAbsolute() && builddir) + srcdir.setFile(QFile::decodeName(builddir) + u'/' + srcdir.filePath()); const QString canonicalPath = srcdir.canonicalFilePath(); - const QString candidate = QString::fromLatin1("%1/%2").arg(canonicalPath, base); + const QString candidate = "%1/%2"_L1.arg(canonicalPath, base); if (!canonicalPath.isEmpty() && QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found relative to source path [%2]") + "testdata %1 not found relative to source path [%2]"_L1 .arg(base, QDir::toNativeSeparators(candidate))), file, line); } @@ -2267,12 +2396,12 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co // 4. Try resources if (found.isEmpty()) { - const QString candidate = QString::fromLatin1(":/%1").arg(base); + const QString candidate = ":/%1"_L1.arg(base); if (QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found in resources [%2]") + "testdata %1 not found in resources [%2]"_L1 .arg(base, QDir::toNativeSeparators(candidate))), file, line); } @@ -2280,12 +2409,12 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co // 5. Try current directory if (found.isEmpty()) { - const QString candidate = QDir::currentPath() + QLatin1Char('/') + base; + const QString candidate = QDir::currentPath() + u'/' + base; if (QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found in current directory [%2]") + "testdata %1 not found in current directory [%2]"_L1 .arg(base, QDir::toNativeSeparators(candidate))), file, line); } @@ -2293,12 +2422,12 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co // 6. Try main source directory if (found.isEmpty()) { - const QString candidate = QTest::mainSourcePath % QLatin1Char('/') % base; + const QString candidate = QTest::mainSourcePath % u'/' % base; if (QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found in main source directory [%2]") + "testdata %1 not found in main source directory [%2]"_L1 .arg(base, QDir::toNativeSeparators(candidate))), file, line); } @@ -2306,12 +2435,12 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co // 7. Try the supplied source directory if (found.isEmpty() && sourcedir) { - const QString candidate = QFile::decodeName(sourcedir) % QLatin1Char('/') % base; + const QString candidate = QFile::decodeName(sourcedir) % u'/' % base; if (QFileInfo::exists(candidate)) { found = candidate; } else if (QTestLog::verboseLevel() >= 2) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 not found in supplied source directory [%2]") + "testdata %1 not found in supplied source directory [%2]"_L1 .arg(base, QDir::toNativeSeparators(candidate))), file, line); } @@ -2320,11 +2449,11 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co if (found.isEmpty()) { QTestLog::warn(qPrintable( - QString::fromLatin1("testdata %1 could not be located!").arg(base)), + "testdata %1 could not be located!"_L1.arg(base)), file, line); } else if (QTestLog::verboseLevel() >= 1) { QTestLog::info(qPrintable( - QString::fromLatin1("testdata %1 was located at %2").arg(base, QDir::toNativeSeparators(found))), + "testdata %1 was located at %2"_L1.arg(base, QDir::toNativeSeparators(found))), file, line); } @@ -2332,7 +2461,7 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co } /*! \internal - */ +*/ QString QTest::qFindTestData(const char *base, const char *file, int line, const char *builddir, const char *sourcedir) { @@ -2340,21 +2469,21 @@ QString QTest::qFindTestData(const char *base, const char *file, int line, const } /*! \internal - */ +*/ void *QTest::qData(const char *tagName, int typeId) { return fetchData(QTestResult::currentTestData(), tagName, typeId); } /*! \internal - */ +*/ void *QTest::qGlobalData(const char *tagName, int typeId) { return fetchData(QTestResult::currentGlobalTestData(), tagName, typeId); } /*! \internal - */ +*/ void *QTest::qElementData(const char *tagName, int metaTypeId) { QTEST_ASSERT(tagName); @@ -2370,7 +2499,7 @@ void *QTest::qElementData(const char *tagName, int metaTypeId) } /*! \internal - */ +*/ void QTest::addColumnInternal(int id, const char *name) { QTestTable *tbl = QTestTable::currentTestTable(); @@ -2380,9 +2509,13 @@ void QTest::addColumnInternal(int id, const char *name) } /*! - Appends a new row to the current test data. \a dataTag is the name of - the testdata that will appear in the test output. Returns a QTestData reference - that can be used to stream in data. + Appends a new row to the current test data. + + The test output will identify the test run with this test data using the + name \a dataTag. + + Returns a QTestData reference that can be used to stream in data, one value + for each column in the table. Example: \snippet code/src_qtestlib_qtestcase.cpp 20 @@ -2393,14 +2526,15 @@ void QTest::addColumnInternal(int id, const char *name) See \l {Chapter 2: Data Driven Testing}{Data Driven Testing} for a more extensive example. - \sa addColumn(), QFETCH() + \sa addRow(), addColumn(), QFETCH() */ QTestData &QTest::newRow(const char *dataTag) { QTEST_ASSERT_X(dataTag, "QTest::newRow()", "Data tag cannot be null"); QTestTable *tbl = QTestTable::currentTestTable(); QTEST_ASSERT_X(tbl, "QTest::newRow()", "Cannot add testdata outside of a _data slot."); - QTEST_ASSERT_X(tbl->elementCount(), "QTest::newRow()", "Must add columns before attempting to add rows."); + QTEST_ASSERT_X(tbl->elementCount(), "QTest::newRow()", + "Must add columns before attempting to add rows."); return *tbl->newData(dataTag); } @@ -2408,13 +2542,17 @@ QTestData &QTest::newRow(const char *dataTag) /*! \since 5.9 - Appends a new row to the current test data. The function's arguments are passed - to qsnprintf() for formatting according to \a format. See the qvsnprintf() - documentation for caveats and limitations. + Appends a new row to the current test data. - The formatted string will appear as the name of this test data in the test output. + The function's arguments are passed to qsnprintf() for formatting according + to \a format. See the qvsnprintf() documentation for caveats and + limitations. - Returns a QTestData reference that can be used to stream in data. + The test output will identify the test run with this test data using the + name that results from this formatting. + + Returns a QTestData reference that can be used to stream in data, one value + for each column in the table. Example: \snippet code/src_qtestlib_qtestcase.cpp addRow @@ -2425,14 +2563,15 @@ QTestData &QTest::newRow(const char *dataTag) See \l {Chapter 2: Data Driven Testing}{Data Driven Testing} for a more extensive example. - \sa addColumn(), QFETCH() + \sa newRow(), addColumn(), QFETCH() */ QTestData &QTest::addRow(const char *format, ...) { QTEST_ASSERT_X(format, "QTest::addRow()", "Format string cannot be null"); QTestTable *tbl = QTestTable::currentTestTable(); QTEST_ASSERT_X(tbl, "QTest::addRow()", "Cannot add testdata outside of a _data slot."); - QTEST_ASSERT_X(tbl->elementCount(), "QTest::addRow()", "Must add columns before attempting to add rows."); + QTEST_ASSERT_X(tbl->elementCount(), "QTest::addRow()", + "Must add columns before attempting to add rows."); char buf[1024]; @@ -2493,7 +2632,7 @@ const char *QTest::currentTestFunction() /*! Returns the name of the current test data. If the test doesn't - have any assigned testdata, the function returns 0. + have any assigned testdata, the function returns \nullptr. */ const char *QTest::currentDataTag() { @@ -2501,22 +2640,55 @@ const char *QTest::currentDataTag() } /*! - Returns \c true if the current test function failed, otherwise false. + Returns \c true if the current test function has failed, otherwise false. + + \sa QTest::currentTestResolved() */ bool QTest::currentTestFailed() { return QTestResult::currentTestFailed(); } +/*! + \since 6.5 + Returns \c true if the current test function has failed or skipped. + + This applies if the test has failed or exercised a skip. When it is true, + the test function should return early. In particular, the \c{QTRY_*} macros + and the test event loop terminate their loops early if executed during the + test function (but not its cleanup()). After a test has called a helper + function that uses this module's macros, it can use this function to test + whether to return early. + + \sa QTest::currentTestFailed() +*/ +bool QTest::currentTestResolved() +{ + return QTestResult::currentTestFailed() || QTestResult::skipCurrentTest(); +} + +/*! + \internal + \since 6.4 + Returns \c true during the run of the test-function and its set-up. + + Used by the \c{QTRY_*} macros and \l QTestEventLoop to check whether to + return when QTest::currentTestResolved() is true. +*/ +bool QTest::runningTest() +{ + return QTest::inTestFunction; +} + /*! \internal - */ +*/ QObject *QTest::testObject() { return currentTestObject; } /*! \internal - */ +*/ void QTest::setMainSourcePath(const char *file, const char *builddir) { QString mainSourceFile = QFile::decodeName(file); @@ -2528,7 +2700,9 @@ void QTest::setMainSourcePath(const char *file, const char *builddir) QTest::mainSourcePath = fi.absolutePath(); } +#if QT_DEPRECATED_SINCE(6, 4) /*! \internal + \deprecated [6.4] This function is called by various specializations of QTest::qCompare to decide whether to report a failure and to produce verbose test output. @@ -2536,15 +2710,63 @@ void QTest::setMainSourcePath(const char *file, const char *builddir) will be output if the compare fails. If the compare succeeds, failureMsg will not be output. - If the caller has already passed a failure message showing the compared - values, or if those values cannot be stringified, val1 and val2 can be null. - */ + Using this function is not optimal, because it requires the string + representations of \a actualVal and \a expectedVal to be pre-calculated, + even though they will be used only if the comparison fails. Prefer using the + \l compare_helper() overload that takes qxp::function_ref() for such cases. + + If the caller creates a custom failure message showing the compared values, + or if those values cannot be stringified, use the overload of the function + that takes no \a actualVal and \a expecetedVal parameters. +*/ bool QTest::compare_helper(bool success, const char *failureMsg, - char *val1, char *val2, + char *actualVal, char *expectedVal, const char *actual, const char *expected, const char *file, int line) { - return QTestResult::compare(success, failureMsg, val1, val2, actual, expected, file, line); + return QTestResult::compare(success, failureMsg, actualVal, expectedVal, + actual, expected, file, line); +} +#endif // QT_DEPRECATED_SINCE(6, 4) + +/*! \internal + \since 6.4 + This function is called by various specializations of QTest::qCompare + to decide whether to report a failure and to produce verbose test output. + + The \a failureMsg parameter can be \c {nullptr}, in which case a default + message will be output if the compare fails. If the comparison succeeds, + \a failureMsg will not be output. + + This overload of the function uses qxp::function_ref to defer conversion of + \a actualVal and \a expectedVal to strings until that is really needed + (when the comparison fails). This speeds up test case execution on success. +*/ +bool QTest::compare_helper(bool success, const char *failureMsg, + qxp::function_ref<const char *()> actualVal, + qxp::function_ref<const char *()> expectedVal, + const char *actual, const char *expected, + const char *file, int line) +{ + return QTestResult::reportResult(success, actualVal, expectedVal, actual, expected, + QTest::ComparisonOperation::CustomCompare, + file, line, failureMsg); +} + +/*! \internal + \since 6.4 + This function is called by various specializations of QTest::qCompare + to decide whether to report a failure and to produce verbose test output. + + This overload should be used when there is no string representation of + actual and expected values, so only the \a failureMsg is shown when the + comparison fails. Because of that, \a failureMsg can't be \c {nullptr}. + If the comparison succeeds, \a failureMsg will not be output. +*/ +bool QTest::compare_helper(bool success, const char *failureMsg, const char *actual, + const char *expected, const char *file, int line) +{ + return QTestResult::compare(success, failureMsg, actual, expected, file, line); } template <typename T> @@ -2568,20 +2790,21 @@ static bool floatingCompare(const T &actual, const T &expected) /*! \fn bool QTest::qCompare(const qfloat16 &t1, const qfloat16 &t2, const char *actual, const char *expected, const char *file, int line) \internal - */ +*/ bool QTest::qCompare(qfloat16 const &t1, qfloat16 const &t2, const char *actual, const char *expected, const char *file, int line) { return compare_helper(floatingCompare(t1, t2), "Compared qfloat16s are not the same (fuzzy compare)", - toString(t1), toString(t2), actual, expected, file, line); + [&t1] { return toString(t1); }, [&t2] { return toString(t2); }, + actual, expected, file, line); } /*! \fn bool QTest::qCompare(const float &t1, const float &t2, const char *actual, const char *expected, const char *file, int line) \internal - */ +*/ bool QTest::qCompare(float const &t1, float const &t2, const char *actual, const char *expected, - const char *file, int line) + const char *file, int line) { return QTestResult::compare(floatingCompare(t1, t2), "Compared floats are not the same (fuzzy compare)", @@ -2590,9 +2813,9 @@ bool QTest::qCompare(float const &t1, float const &t2, const char *actual, const /*! \fn bool QTest::qCompare(const double &t1, const double &t2, const char *actual, const char *expected, const char *file, int line) \internal - */ +*/ bool QTest::qCompare(double const &t1, double const &t2, const char *actual, const char *expected, - const char *file, int line) + const char *file, int line) { return QTestResult::compare(floatingCompare(t1, t2), "Compared doubles are not the same (fuzzy compare)", @@ -2602,7 +2825,7 @@ bool QTest::qCompare(double const &t1, double const &t2, const char *actual, con /*! \fn bool QTest::qCompare(int t1, int t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ bool QTest::qCompare(int t1, int t2, const char *actual, const char *expected, const char *file, int line) { @@ -2615,7 +2838,7 @@ bool QTest::qCompare(int t1, int t2, const char *actual, const char *expected, /*! \fn bool QTest::qCompare(qsizetype t1, qsizetype t2, const char *actual, const char *expected, const char *file, int line) \internal \since 6.0 - */ +*/ bool QTest::qCompare(qsizetype t1, qsizetype t2, const char *actual, const char *expected, const char *file, int line) @@ -2629,9 +2852,9 @@ bool QTest::qCompare(qsizetype t1, qsizetype t2, const char *actual, const char /*! \fn bool QTest::qCompare(unsigned t1, unsigned t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ bool QTest::qCompare(unsigned t1, unsigned t2, const char *actual, const char *expected, - const char *file, int line) + const char *file, int line) { return QTestResult::compare(t1 == t2, "Compared values are not the same", @@ -2641,7 +2864,7 @@ bool QTest::qCompare(unsigned t1, unsigned t2, const char *actual, const char *e /*! \fn bool QTest::qCompare(QStringView t1, QStringView t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ bool QTest::qCompare(QStringView t1, QStringView t2, const char *actual, const char *expected, const char *file, int line) { @@ -2650,11 +2873,11 @@ bool QTest::qCompare(QStringView t1, QStringView t2, const char *actual, const c t1, t2, actual, expected, file, line); } -/*! \fn bool QTest::qCompare(QStringView t1, const QLatin1String &t2, const char *actual, const char *expected, const char *file, int line) +/*! \internal \since 5.14 - */ -bool QTest::qCompare(QStringView t1, const QLatin1String &t2, const char *actual, const char *expected, +*/ +bool QTest::qCompare(QStringView t1, const QLatin1StringView &t2, const char *actual, const char *expected, const char *file, int line) { return QTestResult::compare(t1 == t2, @@ -2662,11 +2885,11 @@ bool QTest::qCompare(QStringView t1, const QLatin1String &t2, const char *actual t1, t2, actual, expected, file, line); } -/*! \fn bool QTest::qCompare(const QLatin1String &t1, QStringView t2, const char *actual, const char *expected, const char *file, int line) +/*! \internal \since 5.14 - */ -bool QTest::qCompare(const QLatin1String &t1, QStringView t2, const char *actual, const char *expected, +*/ +bool QTest::qCompare(const QLatin1StringView &t1, QStringView t2, const char *actual, const char *expected, const char *file, int line) { return QTestResult::compare(t1 == t2, @@ -2677,25 +2900,25 @@ bool QTest::qCompare(const QLatin1String &t1, QStringView t2, const char *actual /*! \fn bool QTest::qCompare(const QString &t1, const QString &t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ -/*! \fn bool QTest::qCompare(const QString &t1, const QLatin1String &t2, const char *actual, const char *expected, const char *file, int line) +/*! \fn bool QTest::qCompare(const QString &t1, const QLatin1StringView &t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ -/*! \fn bool QTest::qCompare(const QLatin1String &t1, const QString &t2, const char *actual, const char *expected, const char *file, int line) +/*! \fn bool QTest::qCompare(const QLatin1StringView &t1, const QString &t2, const char *actual, const char *expected, const char *file, int line) \internal \since 5.14 - */ +*/ /*! \fn bool QTest::qCompare(const double &t1, const float &t2, const char *actual, const char *expected, const char *file, int line) \internal - */ +*/ /*! \fn bool QTest::qCompare(const float &t1, const double &t2, const char *actual, const char *expected, const char *file, int line) \internal - */ +*/ #define TO_STRING_IMPL(TYPE, FORMAT) \ template <> Q_TESTLIB_EXPORT char *QTest::toString<TYPE>(const TYPE &t) \ @@ -2731,7 +2954,7 @@ TO_STRING_IMPL(unsigned char, %hhu) the exponent, requiring a leading 0 on single-digit exponents; (at least) MinGW includes a leading zero also on an already-two-digit exponent, e.g. 9e-040, which differs from more usual platforms. So massage that away. - */ +*/ static void massageExponent(char *text) { char *p = strchr(text, 'e'); @@ -2822,7 +3045,7 @@ template <> Q_TESTLIB_EXPORT char *QTest::toString<char>(const char &t) } /*! \internal - */ +*/ char *QTest::toString(const char *str) { if (!str) { @@ -2835,7 +3058,7 @@ char *QTest::toString(const char *str) } /*! \internal - */ +*/ char *QTest::toString(const volatile void *p) // Use volatile to match compare_ptr_helper() { return QTest::toString(const_cast<const void *>(p)); @@ -2849,7 +3072,7 @@ char *QTest::toString(const void *p) } /*! \internal - */ +*/ char *QTest::toString(const volatile QObject *vo) { if (vo == nullptr) @@ -2868,35 +3091,36 @@ char *QTest::toString(const volatile QObject *vo) /*! \fn char *QTest::toString(const QColor &color) \internal - */ +*/ /*! \fn char *QTest::toString(const QRegion ®ion) \internal - */ +*/ /*! \fn char *QTest::toString(const QHostAddress &addr) \internal - */ +*/ /*! \fn char *QTest::toString(QNetworkReply::NetworkError code) \internal - */ +*/ /*! \fn char *QTest::toString(const QNetworkCookie &cookie) \internal - */ +*/ /*! \fn char *QTest::toString(const QList<QNetworkCookie> &list) \internal - */ +*/ /*! \internal - */ +*/ bool QTest::compare_string_helper(const char *t1, const char *t2, const char *actual, const char *expected, const char *file, int line) { return compare_helper(qstrcmp(t1, t2) == 0, "Compared strings are not the same", - toString(t1), toString(t2), actual, expected, file, line); + [t1] { return toString(t1); }, [t2] { return toString(t2); }, + actual, expected, file, line); } /*! @@ -2976,11 +3200,11 @@ bool QTest::compare_string_helper(const char *t1, const char *t2, const char *ac \internal */ -/*! \fn bool QTest::qCompare(const QString &t1, const QLatin1String &t2, const char *actual, const char *expected, const char *file, int line) +/*! \fn bool QTest::qCompare(const QString &t1, const QLatin1StringView &t2, const char *actual, const char *expected, const char *file, int line) \internal */ -/*! \fn bool QTest::qCompare(const QLatin1String &t1, const QString &t2, const char *actual, const char *expected, const char *file, int line) +/*! \fn bool QTest::qCompare(const QLatin1StringView &t1, const QString &t2, const char *actual, const char *expected, const char *file, int line) \internal */ |