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.cpp1504
1 files changed, 729 insertions, 775 deletions
diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp
index b06c88b6cb..42795fade7 100644
--- a/src/testlib/qtestcase.cpp
+++ b/src/testlib/qtestcase.cpp
@@ -3,13 +3,14 @@
// 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>
@@ -33,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
@@ -60,6 +65,7 @@
#include <memory>
#include <mutex>
#include <numeric>
+#include <optional>
#include <stdarg.h>
#include <stdio.h>
@@ -71,6 +77,7 @@
#endif
#ifdef Q_OS_WIN
+# include <iostream>
# if !defined(Q_CC_MINGW) || (defined(Q_CC_MINGW) && defined(__MINGW64_VERSION_MAJOR))
# include <crtdbg.h>
# endif
@@ -80,14 +87,24 @@
#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
@@ -100,6 +117,10 @@
#include <CoreFoundation/CFPreferences.h>
#endif
+#if defined(Q_OS_WASM)
+#include <emscripten.h>
+#endif
+
#include <vector>
QT_BEGIN_NAMESPACE
@@ -109,231 +130,6 @@ using namespace Qt::StringLiterals;
using QtMiscUtils::toHexUpper;
using QtMiscUtils::fromHex;
-#ifdef Q_OS_UNIX
-namespace {
-static struct iovec IoVec(struct iovec vec)
-{
- return vec;
-}
-static struct iovec IoVec(const char *str)
-{
- struct iovec r = {};
- r.iov_base = const_cast<char *>(str);
- r.iov_len = strlen(str);
- return r;
-}
-
-template <typename... Args> static ssize_t writeToStderr(Args &&... args)
-{
- struct iovec vec[] = { IoVec(std::forward<Args>(args))... };
- return ::writev(STDERR_FILENO, vec, std::size(vec));
-}
-
-// async-signal-safe conversion from int to string
-struct AsyncSafeIntBuffer
-{
- // digits10 + 1 for all possible digits
- // +1 for the sign
- // +1 for the terminating null
- static constexpr int Digits10 = std::numeric_limits<int>::digits10 + 3;
- std::array<char, Digits10> array;
- constexpr AsyncSafeIntBuffer() : array{} {} // initializes array
- AsyncSafeIntBuffer(Qt::Initialization) {} // leaves array uninitialized
-};
-
-static struct iovec asyncSafeToString(int n, AsyncSafeIntBuffer &&result = Qt::Uninitialized)
-{
- char *ptr = result.array.data();
- if (false) {
-#ifdef __cpp_lib_to_chars
- } else if (auto r = std::to_chars(ptr, ptr + result.array.size(), n, 10); r.ec == std::errc{}) {
- ptr = r.ptr;
-#endif
- } else {
- // handle the sign
- if (n < 0) {
- *ptr++ = '-';
- n = -n;
- }
-
- // find the highest power of the base that is less than this number
- static constexpr int StartingDivider = ([]() {
- int divider = 1;
- for (int i = 0; i < std::numeric_limits<int>::digits10; ++i)
- divider *= 10;
- return divider;
- }());
- int divider = StartingDivider;
- while (divider && n < divider)
- divider /= 10;
-
- // now convert to string
- while (divider > 1) {
- int quot = n / divider;
- n = n % divider;
- divider /= 10;
- *ptr++ = quot + '0';
- }
- *ptr++ = n + '0';
- }
-
-#ifndef QT_NO_DEBUG
- // this isn't necessary, it just helps in the debugger
- *ptr = '\0';
-#endif
- struct iovec r;
- r.iov_base = result.array.data();
- r.iov_len = ptr - result.array.data();
- return r;
-};
-}
-#endif // Q_OS_UNIX
-
-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
-}
-
-static bool hasSystemCrashReporter()
-{
-#if defined(Q_OS_MACOS)
- return QTestPrivate::macCrashReporterWillShowDialog();
-#else
- return false;
-#endif
-}
-
-static void disableCoreDump()
-{
-#ifdef RLIMIT_CORE
- bool ok = false;
- const int disableCoreDump = qEnvironmentVariableIntValue("QTEST_DISABLE_CORE_DUMP", &ok);
- if (ok && disableCoreDump) {
- 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 std::array<char, 512> stackTraceCommand = {};
-static void prepareStackTrace()
-{
- stackTraceCommand[0] = '\0';
-
- bool ok = false;
- const int disableStackDump = qEnvironmentVariableIntValue("QTEST_DISABLE_STACK_DUMP", &ok);
- if (ok && disableStackDump)
- return;
-
- if (hasSystemCrashReporter())
- return;
-
-#if defined(Q_OS_MACOS)
- #define CSR_ALLOW_UNRESTRICTED_FS (1 << 1)
- std::optional<uint32_t> sipConfiguration = qt_mac_sipConfiguration();
- if (!sipConfiguration || !(*sipConfiguration & CSR_ALLOW_UNRESTRICTED_FS))
- return; // LLDB will fail to provide a valid stack trace
-#endif
-
- // prepare the command to be run (our PID shouldn't change!)
-# ifdef Q_OS_LINUX
- qsnprintf(stackTraceCommand.data(), stackTraceCommand.size(),
- "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()));
-# elif defined(Q_OS_MACOS)
- qsnprintf(stackTraceCommand.data(), stackTraceCommand.size(),
- "lldb -p %d 1>&2 2>/dev/null <<EOF\n"
- "bt all\n"
- "quit\n"
- "EOF\n",
- static_cast<int>(getpid()));
-# endif
-}
-
-[[maybe_unused]] static void generateStackTrace()
-{
- if (stackTraceCommand[0] == '\0' || debuggerPresent())
- return;
-
-#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
- const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime());
- const int msecsTotalTime = qRound(QTestLog::msecsTotalTime());
- writeToStderr("\n=== Received signal at function time: ", asyncSafeToString(msecsFunctionTime),
- "ms, total time: ", asyncSafeToString(msecsTotalTime),
- "ms, dumping stack ===\n");
-
- // Note: POSIX.1-2001 still has fork() in the list of async-safe functions,
- // but in a future edition, it might be removed. It would be safer to wake
- // up a babysitter thread to launch the debugger.
- pid_t pid = fork();
- if (pid == 0) {
- // child process
- execl("/bin/sh", "/bin/sh", "-c", stackTraceCommand.data(), nullptr);
- _exit(1);
- } else if (pid < 0) {
- writeToStderr("Failed to start debugger.\n");
- } else {
- int ret;
- EINTR_LOOP(ret, waitpid(pid, nullptr, 0));
- }
- writeToStderr("=== End of stack trace ===\n");
-#endif
-}
-
static bool installCoverageTool(const char * appname, const char * testname)
{
#if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover)
@@ -371,9 +167,193 @@ 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 "QTestLib: This test case check (\"%1\") failed because the requested timeout (%2 ms) "
@@ -388,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;
@@ -406,7 +387,7 @@ public:
static QMetaMethod findMethod(const QObject *obj, const char *signature);
private:
- bool invokeTest(int index, QLatin1StringView tag, 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().
@@ -451,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()
@@ -519,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 || QLatin1StringView(signature).contains(QLatin1StringView(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)
@@ -538,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();
@@ -557,7 +560,7 @@ 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)
@@ -565,7 +568,7 @@ static void qPrintDataTags(FILE *stream)
// 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 {
@@ -577,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",
@@ -614,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
@@ -668,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)
@@ -817,8 +837,26 @@ 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()) {
@@ -960,16 +998,21 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, const char *const argv[], bool
#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
@@ -977,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);
}
@@ -1007,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*/
@@ -1023,27 +1060,30 @@ 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);
+ QTest::inTestFunction = true;
+ invokeTestMethodIfValid(m_initMethod);
const bool initQuit =
QTestResult::skipCurrentTest() || QTestResult::currentTestFailed();
if (!initQuit) {
- QBenchmarkTestMethodData::current->result = QBenchmarkResult();
+ QBenchmarkTestMethodData::current->results.clear();
QBenchmarkTestMethodData::current->resultAccepted = false;
+ QBenchmarkTestMethodData::current->valid = false;
QBenchmarkGlobalData::current->context.tag = QLatin1StringView(
QTestResult::currentDataTag() ? QTestResult::currentDataTag() : "");
- invokeOk = m_methods[index].invoke(QTest::currentTestObject, Qt::DirectConnection);
+ invokeOk = invokeTestMethodIfValid(m_methods[index]);
if (!invokeOk)
QTestResult::addFailure("Unable to execute slot", __FILE__, __LINE__);
@@ -1052,11 +1092,11 @@ void TestMethods::invokeTestOnData(int index) const
invokeOk = false;
}
+ QTest::inTestFunction = false;
QTestResult::finishedCurrentTestData();
if (!initQuit) {
- if (m_cleanupMethod.isValid())
- m_cleanupMethod.invoke(QTest::currentTestObject, Qt::DirectConnection);
+ invokeTestMethodIfValid(m_cleanupMethod);
// Process any deleteLater(), used by event-loop-based apps.
// Fixes memleak reports.
@@ -1079,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
@@ -1111,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));
}
}
@@ -1119,17 +1162,30 @@ void TestMethods::invokeTestOnData(int index) const
class WatchDog : public QThread
{
- enum Expectation {
+ enum Expectation : std::size_t {
+ // bits 0..1: state
ThreadStart,
TestFunctionStart,
TestFunctionEnd,
ThreadEnd,
- };
- bool waitFor(std::unique_lock<QtPrivate::mutex> &m, Expectation e)
+ // bits 2..: generation
+ };
+ static constexpr auto ExpectationMask = Expectation{ThreadStart | TestFunctionStart | TestFunctionEnd | ThreadEnd};
+ static_assert(size_t(ExpectationMask) == 0x3);
+ static constexpr size_t GenerationShift = 2;
+
+ static constexpr Expectation state(Expectation e) noexcept
+ { return Expectation{e & ExpectationMask}; }
+ static constexpr size_t generation(Expectation e) noexcept
+ { return e >> GenerationShift; }
+ static constexpr Expectation combine(Expectation e, size_t gen) noexcept
+ { return Expectation{e | (gen << GenerationShift)}; }
+
+ bool waitFor(std::unique_lock<std::mutex> &m, Expectation e)
{
auto expectationChanged = [this, e] { return expecting.load(std::memory_order_relaxed) != e; };
- switch (e) {
+ switch (state(e)) {
case TestFunctionEnd:
return waitCondition.wait_for(m, defaultTimeout(), expectationChanged);
case ThreadStart:
@@ -1138,8 +1194,20 @@ 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:
@@ -1154,36 +1222,29 @@ public:
~WatchDog()
{
- {
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(ThreadEnd, std::memory_order_relaxed);
- waitCondition.notify_all();
- }
+ setExpectation(ThreadEnd);
wait();
}
void beginTest()
{
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(TestFunctionEnd, std::memory_order_relaxed);
- waitCondition.notify_all();
+ setExpectation(TestFunctionEnd);
}
void testFinished()
{
- const auto locker = qt_scoped_lock(mutex);
- expecting.store(TestFunctionStart, std::memory_order_relaxed);
- waitCondition.notify_all();
+ setExpectation(TestFunctionStart);
}
void run() override
{
+ CrashHandler::blockUnixSignals();
auto locker = qt_unique_lock(mutex);
expecting.store(TestFunctionStart, std::memory_order_release);
waitCondition.notify_all();
while (true) {
Expectation e = expecting.load(std::memory_order_acquire);
- switch (e) {
+ switch (state(e)) {
case ThreadEnd:
return;
case ThreadStart:
@@ -1191,7 +1252,9 @@ public:
case TestFunctionStart:
case TestFunctionEnd:
if (Q_UNLIKELY(!waitFor(locker, e))) {
- generateStackTrace();
+ fflush(stderr);
+ CrashHandler::printTestRunTime();
+ CrashHandler::generateStackTrace();
qFatal("Test function timed out");
}
}
@@ -1199,8 +1262,8 @@ public:
}
private:
- QtPrivate::mutex mutex;
- QtPrivate::condition_variable waitCondition;
+ std::mutex mutex;
+ std::condition_variable waitCondition;
std::atomic<Expectation> expecting;
};
@@ -1213,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
@@ -1225,7 +1308,7 @@ public:
If the function was successfully called, true is returned, otherwise
false.
*/
-bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDog) const
+bool TestMethods::invokeTest(int index, QLatin1StringView tag, std::optional<WatchDog> &watchDog) const
{
QBenchmarkTestMethodData benchmarkData;
QBenchmarkTestMethodData::current = &benchmarkData;
@@ -1257,6 +1340,7 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDo
tag[global.size()] == ':';
};
bool foundFunction = false;
+ bool blacklisted = false;
/* For each entry in the global data table, do: */
do {
@@ -1265,7 +1349,7 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDo
if (curGlobalDataIndex == 0) {
qsnprintf(member, 512, "%s_data()", name.constData());
- invokeMethod(QTest::currentTestObject, member);
+ invokeTestMethodIfExists(member);
if (QTestResult::skipCurrentTest())
break;
}
@@ -1276,13 +1360,6 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDo
return dataCount ? table.testData(index)->dataTag() : nullptr;
};
- // Data tag requested but none available?
- if (!tag.isEmpty() && !dataCount && !globalDataCount) {
- fprintf(stderr, "Unknown test data tag for function %s(): '%s'\n"
- "Function has no testdata.\n", name.constData(), tag.data());
- return false;
- }
-
/* For each entry in this test's data table, do: */
do {
QTestResult::setSkipCurrentTest(false);
@@ -1290,19 +1367,28 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDo
if (dataTagMatches(tag, QLatin1StringView(dataTag(curDataIndex)),
QLatin1StringView(globalDataTag(curGlobalDataIndex)))) {
foundFunction = true;
-
- QTestPrivate::checkBlackLists(name.constData(), dataTag(curDataIndex),
- globalDataTag(curGlobalDataIndex));
-
- 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();
+ 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();
+ }
if (!tag.isEmpty() && !globalDataCount)
break;
@@ -1315,20 +1401,9 @@ bool TestMethods::invokeTest(int index, QLatin1StringView tag, WatchDog *watchDo
} while (curGlobalDataIndex < globalDataCount);
if (!tag.isEmpty() && !foundFunction) {
- fprintf(stderr, "Unknown testdata for function %s(): '%s'\n", name.constData(), tag.data());
- if (table.dataCount()) {
- fputs("Available test-specific data tags:\n", stderr);
- for (int i = 0; i < table.dataCount(); ++i)
- fprintf(stderr, "\t%s\n", table.testData(i)->dataTag());
- }
- 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());
- }
- return false;
+ 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);
@@ -1384,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
@@ -1395,7 +1468,7 @@ char *formatString(const char *prefix, const char *suffix, size_t numArguments,
\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("");
@@ -1407,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;
@@ -1423,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];
@@ -1454,7 +1527,7 @@ 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;
auto buffer = std::make_unique<char[]>(256);
@@ -1533,76 +1606,78 @@ char *toPrettyCString(const char *p, int length)
return buffer.release();
}
-#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));
-}
-#endif
-
/*!
+ \fn char *toPrettyUnicode(QStringView string)
\internal
Returns the same QString but with only the ASCII characters still shown;
everything else is replaced with \c {\uXXXX}.
Similar to QDebug::putString().
*/
+
+constexpr qsizetype PrettyUnicodeMaxOutputSize = 256;
+// escape sequence, closing quote, the three dots and NUL
+constexpr qsizetype PrettyUnicodeMaxIncrement = sizeof(R"(\uXXXX"...)"); // includes NUL
+
+static char *writePrettyUnicodeChar(char16_t ch, char * const buffer)
+{
+ auto dst = buffer;
+ auto first = [&](int n) { Q_ASSERT(dst - buffer == n); return dst; };
+ if (ch < 0x7f && ch >= 0x20 && ch != '\\' && ch != '"') {
+ *dst++ = ch;
+ return first(1);
+ }
+
+ // write as an escape sequence
+ *dst++ = '\\';
+ switch (ch) {
+ case 0x22:
+ case 0x5c:
+ *dst++ = uchar(ch);
+ break;
+ case 0x8:
+ *dst++ = 'b';
+ break;
+ case 0xc:
+ *dst++ = 'f';
+ break;
+ case 0xa:
+ *dst++ = 'n';
+ break;
+ case 0xd:
+ *dst++ = 'r';
+ break;
+ case 0x9:
+ *dst++ = 't';
+ break;
+ default:
+ *dst++ = 'u';
+ *dst++ = toHexUpper(ch >> 12);
+ *dst++ = toHexUpper(ch >> 8);
+ *dst++ = toHexUpper(ch >> 4);
+ *dst++ = toHexUpper(ch);
+ return first(6);
+ }
+ return first(2);
+}
+
char *toPrettyUnicode(QStringView string)
{
auto p = string.utf16();
auto length = string.size();
// keep it simple for the vast majority of cases
bool trimmed = false;
- auto buffer = std::make_unique<char[]>(256);
+ auto buffer = std::make_unique<char[]>(PrettyUnicodeMaxOutputSize);
const auto end = p + length;
char *dst = buffer.get();
*dst++ = '"';
for ( ; p != end; ++p) {
- if (dst - buffer.get() > 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++ = '"';
@@ -1620,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() && !QTestResult::currentTestFailed()) {
- if (m_initTestCaseMethod.isValid())
- m_initTestCaseMethod.invoke(testObject, Qt::DirectConnection);
+ invokeTestMethodIfValid(m_initTestCaseMethod, testObject);
// finishedCurrentTestDataCleanup() resets QTestResult::currentTestFailed(), so use a local copy.
const bool previousFailed = QTestResult::currentTestFailed();
@@ -1649,7 +1722,7 @@ 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, QLatin1StringView(data), watchDog.data());
+ const bool ok = invokeTest(i, QLatin1StringView(data), watchDog);
delete [] data;
if (!ok)
break;
@@ -1660,8 +1733,7 @@ void TestMethods::invokeTests(QObject *testObject) const
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());
@@ -1673,317 +1745,15 @@ void TestMethods::invokeTests(QObject *testObject) const
QSignalDumper::endDump();
}
-} // namespace QTest
-
-namespace {
-#if defined(Q_OS_WIN)
-
-// Helper class for resolving symbol names by dynamically loading "dbghelp.dll".
-class DebugSymbolResolver
+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)
{
- 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
-{
- // 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);
}
-class WindowsFaultHandler
-{
-public:
- WindowsFaultHandler()
- {
-# if !defined(Q_CC_MINGW)
- _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
-# endif
- SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX);
- SetUnhandledExceptionFilter(windowsFaultHandler);
- }
-
-private:
- 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;
- }
-};
-using FatalSignalHandler = WindowsFaultHandler;
-
-#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM)
-class FatalSignalHandler
-{
-public:
- static constexpr std::array fatalSignals = {
- SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGBUS, SIGFPE, SIGSEGV, SIGPIPE, SIGTERM
- };
- static constexpr std::array crashingSignals = {
- // Crash signals are special, because if we return from the handler
- // without adjusting the machine state, the same instruction that
- // originally caused the crash will get re-executed and will thus cause
- // the same crash again. This is useful if our parent process logs the
- // exit result or if core dumps are enabled: the core file will point
- // to the actual instruction that crashed.
- SIGILL, SIGBUS, SIGFPE, SIGSEGV
- };
- using OldActionsArray = std::array<struct sigaction, fatalSignals.size()>;
-
- FatalSignalHandler()
- {
- pauseOnCrash = qEnvironmentVariableIsSet("QTEST_PAUSE_ON_CRASH");
- struct sigaction act;
- memset(&act, 0, sizeof(act));
- act.sa_handler = SIG_DFL;
- oldActions().fill(act);
-
- // Remove the handler after it is invoked.
- act.sa_flags = SA_RESETHAND;
-
-# ifdef SA_SIGINFO
- act.sa_flags |= SA_SIGINFO;
- act.sa_sigaction = FatalSignalHandler::actionHandler;
-# else
- act.sa_handler = FatalSignalHandler::regularHandler;
-# 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 QVarLengthArray<char, 32 * 1024> alternateStack;
- alternateStack.resize(qMax(SIGSTKSZ, alternateStack.size()));
- stack_t stack;
- stack.ss_flags = 0;
- stack.ss_size = alternateStack.size();
- stack.ss_sp = alternateStack.data();
- 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 signal : fatalSignals)
- sigaddset(&act.sa_mask, signal);
-
- for (size_t i = 0; i < fatalSignals.size(); ++i)
- sigaction(fatalSignals[i], &act, &oldActions()[i]);
- }
-
- ~FatalSignalHandler()
- {
- // Restore the default signal handlers in place of ours.
- // If ours has been replaced, leave the replacement alone.
- auto isOurs = [](const struct sigaction &old) {
-# ifdef SA_SIGINFO
- return (old.sa_flags & SA_SIGINFO) && old.sa_sigaction == FatalSignalHandler::actionHandler;
-# else
- return old.sa_handler == FatalSignalHandler::regularHandler;
-# endif
- };
- struct sigaction action;
-
- for (size_t i = 0; i < fatalSignals.size(); ++i) {
- struct sigaction &act = oldActions()[i];
- if (act.sa_flags == 0 && act.sa_handler == SIG_DFL)
- continue; // Already the default
- if (sigaction(fatalSignals[i], nullptr, &action))
- continue; // Failed to query present handler
- if (isOurs(action))
- sigaction(fatalSignals[i], &act, nullptr);
- }
- }
-
-private:
- Q_DISABLE_COPY_MOVE(FatalSignalHandler)
-
- static OldActionsArray &oldActions()
- {
- Q_CONSTINIT static OldActionsArray oldActions {};
- return oldActions;
- }
-
- static void actionHandler(int signum, siginfo_t * /* info */, void * /* ucontext */)
- {
- const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime());
- const int msecsTotalTime = qRound(QTestLog::msecsTotalTime());
- if (signum != SIGINT) {
- generateStackTrace();
- if (pauseOnCrash) {
- writeToStderr("Pausing process ", asyncSafeToString(getpid()),
- " for debugging\n");
- raise(SIGSTOP);
- }
- }
-
- writeToStderr("Received signal ", asyncSafeToString(signum),
- "\n Function time: ", asyncSafeToString(msecsFunctionTime),
- "ms Total time: ", asyncSafeToString(msecsTotalTime), "ms\n");
-
- bool isCrashingSignal =
- std::find(crashingSignals.begin(), crashingSignals.end(), signum) != crashingSignals.end();
-
- // chain back to the previous handler, if any
- for (size_t i = 0; i < fatalSignals.size(); ++i) {
- struct sigaction &act = oldActions()[i];
- if (signum != fatalSignals[i])
- continue;
-
- // restore the handler (if SA_RESETHAND hasn't done the job for us)
- if (SA_RESETHAND == 0 || act.sa_handler != SIG_DFL || act.sa_flags)
- (void) sigaction(signum, &act, nullptr);
-
- if (!isCrashingSignal)
- raise(signum);
-
- // signal is blocked, so it'll be delivered when we return
- return;
- }
-
- // we shouldn't reach here!
- std::abort();
- }
-
- [[maybe_unused]] static void regularHandler(int signum)
- {
- actionHandler(signum, nullptr, nullptr);
- }
- static bool pauseOnCrash;
-};
-bool FatalSignalHandler::pauseOnCrash = false;
-#else // Q_OS_WASM or weird systems
-class FatalSignalHandler {};
-#endif // Q_OS_* choice
-
-} // unnamed namespace
+} // namespace QTest
static void initEnvironment()
{
@@ -2021,7 +1791,7 @@ static void initEnvironment()
test that was executed with qExec() can't run another test via qExec() and
threads are not allowed to call qExec() simultaneously.
- If you have programatically created the arguments, as opposed to getting them
+ If you have programmatically created the arguments, as opposed to getting them
from the arguments in \c main(), it is likely of interest to use
QTest::qExec(QObject *, const QStringList &) since it is Unicode safe.
@@ -2037,6 +1807,14 @@ int QTest::qExec(QObject *testObject, int argc, char **argv)
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;
}
@@ -2045,6 +1823,7 @@ int QTest::qExec(QObject *testObject, int argc, char **argv)
void QTest::qInit(QObject *testObject, int argc, char **argv)
{
initEnvironment();
+ CrashHandler::maybeDisableCoreDump();
QBenchmarkGlobalData::current = new QBenchmarkGlobalData;
#if defined(Q_OS_MACOS)
@@ -2080,10 +1859,7 @@ void QTest::qInit(QObject *testObject, int argc, char **argv)
#if QT_CONFIG(valgrind)
if (QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindParentProcess)
#endif
- {
- QTestTable::globalTestTable();
QTestLog::startLogging();
- }
}
/*! \internal
@@ -2114,27 +1890,46 @@ int QTest::qRun()
} else
#endif
{
- std::optional<FatalSignalHandler> handler;
- prepareStackTrace();
- if (!noCrashHandler)
+ 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
@@ -2171,10 +1966,7 @@ void QTest::qCleanup()
#if QT_CONFIG(valgrind)
if (QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindParentProcess)
#endif
- {
QTestLog::stopLogging();
- QTestTable::clearGlobalTestTable();
- }
delete QBenchmarkGlobalData::current;
QBenchmarkGlobalData::current = nullptr;
@@ -2184,6 +1976,34 @@ 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
@@ -2193,7 +2013,7 @@ void QTest::qCleanup()
*/
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;
@@ -2274,6 +2094,39 @@ void QTest::qCaught(const char *expected, const char *what, const char *file, in
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)
/*!
@@ -2433,30 +2286,36 @@ QSharedPointer<QTemporaryDir> QTest::qExtractTestData(const QString &dirName)
return result;
}
- QDirIterator it(resourcePath, QDirIterator::Subdirectories);
- if (!it.hasNext()) {
- qWarning("Resource directory '%s' is empty.", qPrintable(resourcePath));
- return result;
- }
-
- while (it.hasNext()) {
- QFileInfo fileInfo = it.nextFileInfo();
-
- if (!fileInfo.isDir()) {
- const QString destination = dataPath + u'/' + 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;
@@ -2650,9 +2509,13 @@ void QTest::addColumnInternal(int id, const char *name)
}
/*!
- Appends a new row to the current test data. \a dataTag is the name of
- the testdata that will appear in the test output. Returns a QTestData reference
- that can be used to stream in data.
+ Appends a new row to the current test data.
+
+ The test output will identify the test run with this test data using the
+ name \a dataTag.
+
+ Returns a QTestData reference that can be used to stream in data, one value
+ for each column in the table.
Example:
\snippet code/src_qtestlib_qtestcase.cpp 20
@@ -2663,14 +2526,15 @@ void QTest::addColumnInternal(int id, const char *name)
See \l {Chapter 2: Data Driven Testing}{Data Driven Testing} for
a more extensive example.
- \sa addColumn(), QFETCH()
+ \sa addRow(), addColumn(), QFETCH()
*/
QTestData &QTest::newRow(const char *dataTag)
{
QTEST_ASSERT_X(dataTag, "QTest::newRow()", "Data tag cannot be null");
QTestTable *tbl = QTestTable::currentTestTable();
QTEST_ASSERT_X(tbl, "QTest::newRow()", "Cannot add testdata outside of a _data slot.");
- QTEST_ASSERT_X(tbl->elementCount(), "QTest::newRow()", "Must add columns before attempting to add rows.");
+ QTEST_ASSERT_X(tbl->elementCount(), "QTest::newRow()",
+ "Must add columns before attempting to add rows.");
return *tbl->newData(dataTag);
}
@@ -2678,13 +2542,17 @@ QTestData &QTest::newRow(const char *dataTag)
/*!
\since 5.9
- Appends a new row to the current test data. The function's arguments are passed
- to qsnprintf() for formatting according to \a format. See the qvsnprintf()
- documentation for caveats and limitations.
+ Appends a new row to the current test data.
+
+ The 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
@@ -2695,14 +2563,15 @@ QTestData &QTest::newRow(const char *dataTag)
See \l {Chapter 2: Data Driven Testing}{Data Driven Testing} for
a more extensive example.
- \sa addColumn(), QFETCH()
+ \sa newRow(), addColumn(), QFETCH()
*/
QTestData &QTest::addRow(const char *format, ...)
{
QTEST_ASSERT_X(format, "QTest::addRow()", "Format string cannot be null");
QTestTable *tbl = QTestTable::currentTestTable();
QTEST_ASSERT_X(tbl, "QTest::addRow()", "Cannot add testdata outside of a _data slot.");
- QTEST_ASSERT_X(tbl->elementCount(), "QTest::addRow()", "Must add columns before attempting to add rows.");
+ QTEST_ASSERT_X(tbl->elementCount(), "QTest::addRow()",
+ "Must add columns before attempting to add rows.");
char buf[1024];
@@ -2763,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()
{
@@ -2771,13 +2640,46 @@ 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()
@@ -2798,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.
@@ -2806,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>
@@ -2844,7 +2796,8 @@ bool QTest::qCompare(qfloat16 const &t1, qfloat16 const &t2, const char *actual,
{
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)
@@ -3166,7 +3119,8 @@ bool QTest::compare_string_helper(const char *t1, const char *t2, const char *ac
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);
}
/*!