summaryrefslogtreecommitdiffstats
path: root/src/testlib/qtestcase.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/testlib/qtestcase.cpp')
-rw-r--r--src/testlib/qtestcase.cpp1897
1 files changed, 1068 insertions, 829 deletions
diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp
index 47f75fcc5b..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,12 +51,21 @@
#include <QtTest/private/qappletestlogger_p.h>
#endif
-#include <cmath>
-#include <numeric>
#include <algorithm>
-#include <condition_variable>
-#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>
@@ -99,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)
@@ -121,131 +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);
-#endif
-#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 // !Q_OS_WASM
-
static bool installCoverageTool(const char * appname, const char * testname)
{
#if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover)
@@ -283,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));
}
@@ -299,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;
@@ -317,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().
@@ -362,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()
@@ -430,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)
@@ -449,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();
@@ -468,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 {
@@ -488,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",
@@ -525,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
@@ -579,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)
@@ -607,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"
@@ -636,7 +745,10 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool
logFormat = QTestLog::Plain;
} else if (strcmp(argv[i], "-csv") == 0) {
logFormat = QTestLog::CSV;
- } else if (strcmp(argv[i], "-junitxml") == 0 || strcmp(argv[i], "-xunitxml") == 0) {
+ } else if (strcmp(argv[i], "-junitxml") == 0) {
+ logFormat = QTestLog::JUnitXML;
+ } else if (strcmp(argv[i], "-xunitxml") == 0) {
+ fprintf(stderr, "WARNING: xunitxml is deprecated. Please use junitxml.\n");
logFormat = QTestLog::JUnitXML;
} else if (strcmp(argv[i], "-xml") == 0) {
logFormat = QTestLog::XML;
@@ -676,9 +788,12 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool
logFormat = QTestLog::LightXML;
else if (strcmp(format, "xml") == 0)
logFormat = QTestLog::XML;
- else if (strcmp(format, "junitxml") == 0 || strcmp(format, "xunitxml") == 0)
+ else if (strcmp(format, "junitxml") == 0)
+ logFormat = QTestLog::JUnitXML;
+ else if (strcmp(format, "xunitxml") == 0) {
+ fprintf(stderr, "WARNING: xunitxml is deprecated. Please use junitxml.\n");
logFormat = QTestLog::JUnitXML;
- else if (strcmp(format, "teamcity") == 0)
+ } else if (strcmp(format, "teamcity") == 0)
logFormat = QTestLog::TeamCity;
else if (strcmp(format, "tap") == 0)
logFormat = QTestLog::TAP;
@@ -722,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);
@@ -855,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
@@ -879,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);
}
@@ -909,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*/
@@ -925,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();
@@ -976,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
@@ -1008,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));
}
}
@@ -1016,16 +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:
@@ -1034,47 +1194,57 @@ 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();
waitFor(locker, ThreadStart);
}
- ~WatchDog() {
- {
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(ThreadEnd, std::memory_order_relaxed);
- waitCondition.notify_all();
- }
+
+ ~WatchDog()
+ {
+ setExpectation(ThreadEnd);
wait();
}
- void beginTest() {
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(TestFunctionEnd, std::memory_order_relaxed);
- waitCondition.notify_all();
+ void beginTest()
+ {
+ setExpectation(TestFunctionEnd);
}
- void testFinished() {
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(TestFunctionStart, std::memory_order_relaxed);
- waitCondition.notify_all();
+ void testFinished()
+ {
+ setExpectation(TestFunctionStart);
}
- void run() override {
+ 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:
@@ -1082,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");
}
}
@@ -1092,8 +1262,8 @@ public:
}
private:
- QtPrivate::mutex mutex;
- QtPrivate::condition_variable waitCondition;
+ std::mutex mutex;
+ std::condition_variable waitCondition;
std::atomic<Expectation> expecting;
};
@@ -1106,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
@@ -1117,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;
@@ -1134,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 {
@@ -1142,68 +1349,64 @@ 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);
- QTestResult::setCurrentTestData(nullptr);
return true;
}
@@ -1232,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;
@@ -1256,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
@@ -1266,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("");
@@ -1279,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;
@@ -1295,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];
@@ -1326,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++ = '"';
@@ -1341,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;
@@ -1402,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 = reinterpret_cast<const ushort *>(string.utf16());
+ 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]);
- const ushort *end = p + length;
- char *dst = buffer.data();
+ auto buffer = std::make_unique<char[]>(PrettyUnicodeMaxOutputSize);
+ const auto end = p + length;
+ 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++ = '"';
@@ -1484,7 +1687,7 @@ char *toPrettyUnicode(QStringView string)
*dst++ = '.';
}
*dst++ = '\0';
- return buffer.take();
+ return buffer.release();
}
void TestMethods::invokeTests(QObject *testObject) const
@@ -1492,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();
@@ -1521,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();
@@ -1542,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()
{
@@ -1842,30 +1787,43 @@ static void initEnvironment()
than once, as command-line options for logging test output to files and
executing individual test functions will not behave correctly.
- Note: This function is not reentrant, only one test can run at a time. A
+ \note This function is not reentrant, only one test can run at a time. A
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)
@@ -1898,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);
@@ -1930,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
@@ -1978,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;
@@ -1994,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;
@@ -2019,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)
{
@@ -2034,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);
@@ -2043,20 +2053,91 @@ 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
- */
+/*!
+ \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);
}
+#endif
/*!
Ignores messages created by qDebug(), qInfo() or qWarning(). If the \a message
@@ -2064,9 +2145,9 @@ void QTest::qWarn(const char *message, const char *file, int line)
test log. If the test finished and the \a message was not outputted,
a test failure is appended to the test log.
- \b {Note:} Invoking this function will only ignore one message.
- If the message you want to ignore is outputted twice, you have to
- call ignoreMessage() twice, too.
+ \note Invoking this function will only ignore one message. If the message
+ you want to ignore is output twice, you have to call ignoreMessage() twice,
+ too.
Example:
\snippet code/src_qtestlib_qtestcase.cpp 19
@@ -2089,9 +2170,9 @@ void QTest::ignoreMessage(QtMsgType type, const char *message)
test log. If the test finished and the message was not outputted,
a test failure is appended to the test log.
- \b {Note:} Invoking this function will only ignore one message.
- If the message you want to ignore is outputted twice, you have to
- call ignoreMessage() twice, too.
+ \note Invoking this function will only ignore one message. If the message
+ you want to ignore is output twice, you have to call ignoreMessage() twice,
+ too.
\since 5.3
*/
@@ -2101,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
@@ -2122,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
@@ -2135,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()) {
@@ -2143,32 +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()) {
- it.next();
-
- QFileInfo fileInfo = it.fileInfo();
-
- 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;
@@ -2176,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)
@@ -2200,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);
}
}
@@ -2213,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);
}
}
}
@@ -2234,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);
}
@@ -2252,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);
}
@@ -2265,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);
}
@@ -2278,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);
}
@@ -2291,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);
}
@@ -2304,12 +2448,12 @@ QString QTest::qFindTestData(const QString& base, const char *file, int line, co
if (found.isEmpty()) {
- QTest::qWarn(qPrintable(
- QString::fromLatin1("testdata %1 could not be located!").arg(base)),
+ QTestLog::warn(qPrintable(
+ "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);
}
@@ -2317,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)
{
@@ -2325,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);
@@ -2355,7 +2499,7 @@ void *QTest::qElementData(const char *tagName, int metaTypeId)
}
/*! \internal
- */
+*/
void QTest::addColumnInternal(int id, const char *name)
{
QTestTable *tbl = QTestTable::currentTestTable();
@@ -2365,27 +2509,32 @@ 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
- \b {Note:} This macro can only be used in a test's data function
+ \note This function can only be called as part of a test's data function
that is invoked by the test framework.
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);
}
@@ -2393,31 +2542,36 @@ 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 function's arguments are passed to qsnprintf() for formatting according
+ to \a format. See the qvsnprintf() documentation for caveats and
+ limitations.
- The formatted string will appear as the name of this test data in the test output.
+ 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.
+ 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
- \b {Note:} This function can only be used in a test's data function
+ \note This function can only be called as part of a test's data function
that is invoked by the test framework.
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];
@@ -2447,8 +2601,8 @@ QTestData &QTest::addRow(const char *format, ...)
To add custom types to the testdata, the type must be registered with
QMetaType via \l Q_DECLARE_METATYPE().
- \b {Note:} This macro can only be used in a test's data function
- that is invoked by the test framework.
+ \note This function can only be used called as part of a test's data
+ function that is invoked by the test framework.
See \l {Chapter 2: Data Driven Testing}{Data Driven Testing} for
a more extensive example.
@@ -2478,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()
{
@@ -2486,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);
@@ -2513,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.
@@ -2521,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 *actualVal, char *expectedVal,
+ const char *actual, const char *expected,
+ const char *file, int 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,
- char *val1, char *val2,
+ 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::compare(success, failureMsg, val1, val2, actual, expected, file, 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>
@@ -2553,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)",
@@ -2575,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)",
@@ -2587,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)
{
@@ -2600,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)
@@ -2614,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",
@@ -2626,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)
{
@@ -2635,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,
@@ -2647,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,
@@ -2662,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) \
@@ -2716,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');
@@ -2807,7 +3045,7 @@ template <> Q_TESTLIB_EXPORT char *QTest::toString<char>(const char &t)
}
/*! \internal
- */
+*/
char *QTest::toString(const char *str)
{
if (!str) {
@@ -2820,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));
@@ -2834,7 +3072,7 @@ char *QTest::toString(const void *p)
}
/*! \internal
- */
+*/
char *QTest::toString(const volatile QObject *vo)
{
if (vo == nullptr)
@@ -2853,35 +3091,36 @@ char *QTest::toString(const volatile QObject *vo)
/*! \fn char *QTest::toString(const QColor &color)
\internal
- */
+*/
/*! \fn char *QTest::toString(const QRegion &region)
\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);
}
/*!
@@ -2961,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
*/