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.cpp1458
1 files changed, 578 insertions, 880 deletions
diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp
index 89530f823f..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>
@@ -111,6 +117,10 @@
#include <CoreFoundation/CFPreferences.h>
#endif
+#if defined(Q_OS_WASM)
+#include <emscripten.h>
+#endif
+
#include <vector>
QT_BEGIN_NAMESPACE
@@ -120,335 +130,229 @@ using namespace Qt::StringLiterals;
using QtMiscUtils::toHexUpper;
using QtMiscUtils::fromHex;
-namespace {
-enum DebuggerProgram { None, Gdb, Lldb };
-
-#if defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread))
-static struct iovec IoVec(struct iovec vec)
+static bool installCoverageTool(const char * appname, const char * testname)
{
- return vec;
+#if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover)
+ if (!qEnvironmentVariableIsEmpty("QT_TESTCOCOON_ACTIVE"))
+ return false;
+ // Set environment variable QT_TESTCOCOON_ACTIVE to prevent an eventual subtest from
+ // being considered as a stand-alone test regarding the coverage analysis.
+ qputenv("QT_TESTCOCOON_ACTIVE", "1");
+
+ // Install Coverage Tool
+ __coveragescanner_install(appname);
+ __coveragescanner_testname(testname);
+ __coveragescanner_clear();
+ return true;
+#else
+ Q_UNUSED(appname);
+ Q_UNUSED(testname);
+ return false;
+#endif
}
-static struct iovec IoVec(const char *str)
+
+static bool isValidSlot(const QMetaMethod &sl)
{
- struct iovec r = {};
- r.iov_base = const_cast<char *>(str);
- r.iov_len = strlen(str);
- return r;
+ if (sl.access() != QMetaMethod::Private || sl.parameterCount() != 0
+ || sl.returnType() != QMetaType::Void || sl.methodType() != QMetaMethod::Slot)
+ return false;
+ const QByteArray name = sl.name();
+ return !(name.isEmpty() || name.endsWith("_data")
+ || name == "initTestCase" || name == "cleanupTestCase"
+ || name == "init" || name == "cleanup");
}
-template <typename... Args> static ssize_t writeToStderr(Args &&... args)
+namespace QTestPrivate
{
- struct iovec vec[] = { IoVec(std::forward<Args>(args))... };
- return ::writev(STDERR_FILENO, vec, std::size(vec));
+ Q_TESTLIB_EXPORT Qt::MouseButtons qtestMouseButtons = Qt::NoButton;
}
-// async-signal-safe conversion from int to string
-struct AsyncSafeIntBuffer
+namespace {
+
+class TestFailedException : public std::exception // clazy:exclude=copyable-polymorphic
{
- // 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
+public:
+ TestFailedException() = default;
+ ~TestFailedException() override = default;
+
+ const char *what() const noexcept override { return "QtTest: test failed"; }
};
-static struct iovec asyncSafeToString(int n, AsyncSafeIntBuffer &&result = Qt::Uninitialized)
+class TestSkippedException : public std::exception // clazy:exclude=copyable-polymorphic
{
- 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';
- }
+public:
+ TestSkippedException() = default;
+ ~TestSkippedException() override = default;
-#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;
+ const char *what() const noexcept override { return "QtTest: test was skipped"; }
};
-#elif defined(Q_OS_WIN)
-// Windows doesn't need to be async-safe
-template <typename... Args> static void writeToStderr(Args &&... args)
-{
- (std::cerr << ... << args);
-}
-static std::string asyncSafeToString(int n)
-{
- return std::to_string(n);
-}
-#endif // defined(Q_OS_UNIX)
} // unnamed namespace
-static bool alreadyDebugging()
+namespace QTest
{
-#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()
+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 defined(Q_OS_MACOS)
- return QTestPrivate::macCrashReporterWillShowDialog();
-#else
- return false;
-#endif
+ if (g_throwOnFail.loadRelaxed() > 0)
+ Internal::throwOnFail();
}
-static void maybeDisableCoreDump()
+void Internal::maybeThrowOnSkip()
{
-#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
+ if (g_throwOnSkip.loadRelaxed() > 0)
+ Internal::throwOnSkip();
}
-static DebuggerProgram debugger = None;
-static void prepareStackTrace()
-{
+/*!
+ \since 6.8
+ \macro QTEST_THROW_ON_FAIL
+ \relates <QTest>
- bool ok = false;
- const int disableStackDump = qEnvironmentVariableIntValue("QTEST_DISABLE_STACK_DUMP", &ok);
- if (ok && disableStackDump)
- return;
+ When defined, QCOMPARE()/QVERIFY() etc always throw on failure.
+ QTest::throwOnFail() then no longer has any effect.
+*/
- if (hasSystemCrashReporter())
- return;
+/*!
+ \since 6.8
+ \macro QTEST_THROW_ON_SKIP
+ \relates <QTest>
-#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
+ When defined, QSKIP() always throws. QTest::throwOnSkip() then no longer
+ has any effect.
+*/
-#ifdef Q_OS_UNIX
- // like QStandardPaths::findExecutable(), but simpler
- auto hasExecutable = [](const char *execname) {
- std::string candidate;
- std::string path;
- if (const char *p = getenv("PATH"); p && *p)
- path = p;
- else
- path = _PATH_DEFPATH;
- for (const char *p = std::strtok(&path[0], ":'"); p; p = std::strtok(nullptr, ":")) {
- candidate = p;
- candidate += '/';
- candidate += execname;
- if (QT_ACCESS(candidate.data(), X_OK) == 0)
- return true;
- }
- return false;
- };
+/*!
+ \since 6.8
+ \class QTest::ThrowOnFailEnabler
+ \inmodule QtTestLib
- static constexpr DebuggerProgram debuggerSearchOrder[] = {
-# if defined(Q_OS_QNX) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
- Gdb, Lldb
-# else
- Lldb, Gdb
-# endif
- };
- for (DebuggerProgram candidate : debuggerSearchOrder) {
- switch (candidate) {
- case None:
- Q_UNREACHABLE();
- break;
- case Gdb:
- if (hasExecutable("gdb")) {
- debugger = Gdb;
- return;
- }
- break;
- case Lldb:
- if (hasExecutable("lldb")) {
- debugger = Lldb;
- return;
- }
- break;
- }
- }
-#endif // Q_OS_UNIX
-}
+ RAII class around setThrowOnFail().
+*/
+/*!
+ \fn QTest::ThrowOnFailEnabler::ThrowOnFailEnabler()
-#if !defined(Q_OS_WASM) || QT_CONFIG(thread)
-static void printTestRunTime()
-{
- const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime());
- const int msecsTotalTime = qRound(QTestLog::msecsTotalTime());
- const char *const name = QTest::currentTestFunction();
- writeToStderr("\n ", name ? name : "[Non-test]",
- " function time: ", asyncSafeToString(msecsFunctionTime),
- "ms, total time: ", asyncSafeToString(msecsTotalTime), "ms\n");
-}
+ Constructor. Calls \c{setThrowOnFail(true)}.
+*/
+/*!
+ \fn QTest::ThrowOnFailEnabler::~ThrowOnFailEnabler()
-static void generateStackTrace()
-{
- if (debugger == None || alreadyDebugging())
- return;
+ Destructor. Calls \c{setThrowOnFail(false)}.
+*/
-# if defined(Q_OS_UNIX) && !defined(Q_OS_WASM) && !defined(Q_OS_INTEGRITY)
- writeToStderr("\n=== Stack trace ===\n");
+/*!
+ \since 6.8
+ \class QTest::ThrowOnFailDisabler
+ \inmodule QtTestLib
- // execlp() requires null-termination, so call the default constructor
- AsyncSafeIntBuffer pidbuffer;
- asyncSafeToString(getpid(), std::move(pidbuffer));
+ RAII class around setThrowOnFail().
+*/
+/*!
+ \fn QTest::ThrowOnFailDisabler::ThrowOnFailDisabler()
- // 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
- (void) dup2(STDERR_FILENO, STDOUT_FILENO); // redirect stdout to stderr
+ Constructor. Calls \c{setThrowOnFail(false)}.
+*/
+/*!
+ \fn QTest::ThrowOnFailDisabler::~ThrowOnFailDisabler()
- switch (debugger) {
- case None:
- Q_UNREACHABLE();
- break;
- case Gdb:
- execlp("gdb", "gdb", "--nx", "--batch", "-ex", "thread apply all bt",
- "--pid", pidbuffer.array.data(), nullptr);
- break;
- case Lldb:
- execlp("lldb", "lldb", "--no-lldbinit", "--batch", "-o", "bt all",
- "--attach-pid", pidbuffer.array.data(), nullptr);
- break;
- }
- _exit(1);
- } else if (pid < 0) {
- writeToStderr("Failed to start debugger.\n");
- } else {
- int ret;
- EINTR_LOOP(ret, waitpid(pid, nullptr, 0));
- }
+ Destructor. Calls \c{setThrowOnFail(true)}.
+*/
- writeToStderr("=== End of stack trace ===\n");
-# endif // Q_OS_UNIX && !Q_OS_WASM
-}
-#endif // !defined(Q_OS_WASM) || QT_CONFIG(thread)
+/*!
+ \since 6.8
+ \class QTest::ThrowOnSkipEnabler
+ \inmodule QtTestLib
-static bool installCoverageTool(const char * appname, const char * testname)
-{
-#if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover)
- if (!qEnvironmentVariableIsEmpty("QT_TESTCOCOON_ACTIVE"))
- return false;
- // Set environment variable QT_TESTCOCOON_ACTIVE to prevent an eventual subtest from
- // being considered as a stand-alone test regarding the coverage analysis.
- qputenv("QT_TESTCOCOON_ACTIVE", "1");
+ RAII class around setThrowOnSkip().
+*/
+/*!
+ \fn QTest::ThrowOnSkipEnabler::ThrowOnSkipEnabler()
- // Install Coverage Tool
- __coveragescanner_install(appname);
- __coveragescanner_testname(testname);
- __coveragescanner_clear();
- return true;
-#else
- Q_UNUSED(appname);
- Q_UNUSED(testname);
- return false;
-#endif
-}
+ Constructor. Calls \c{setThrowOnSkip(true)}.
+*/
+/*!
+ \fn QTest::ThrowOnSkipEnabler::~ThrowOnSkipEnabler()
-static bool isValidSlot(const QMetaMethod &sl)
-{
- if (sl.access() != QMetaMethod::Private || sl.parameterCount() != 0
- || sl.returnType() != QMetaType::Void || sl.methodType() != QMetaMethod::Slot)
- return false;
- const QByteArray name = sl.name();
- return !(name.isEmpty() || name.endsWith("_data")
- || name == "initTestCase" || name == "cleanupTestCase"
- || name == "init" || name == "cleanup");
-}
+ Destructor. Calls \c{setThrowOnSkip(false)}.
+*/
-namespace QTestPrivate
+/*!
+ \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
{
- Q_TESTLIB_EXPORT Qt::MouseButtons qtestMouseButtons = Qt::NoButton;
+ g_throwOnFail.fetchAndAddRelaxed(enable ? 1 : -1);
}
-namespace QTest
+/*!
+ \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)
{
@@ -464,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;
@@ -482,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().
@@ -527,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()
@@ -623,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();
@@ -642,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)
@@ -650,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 {
@@ -662,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",
@@ -699,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
@@ -753,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)
@@ -902,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()) {
@@ -1045,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
@@ -1062,17 +1020,20 @@ 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;
@@ -1092,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*/
@@ -1108,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__);
@@ -1137,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.
@@ -1164,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
@@ -1196,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));
}
}
@@ -1204,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:
@@ -1223,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:
@@ -1239,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:
@@ -1277,8 +1253,8 @@ public:
case TestFunctionEnd:
if (Q_UNLIKELY(!waitFor(locker, e))) {
fflush(stderr);
- printTestRunTime();
- generateStackTrace();
+ CrashHandler::printTestRunTime();
+ CrashHandler::generateStackTrace();
qFatal("Test function timed out");
}
}
@@ -1286,8 +1262,8 @@ public:
}
private:
- QtPrivate::mutex mutex;
- QtPrivate::condition_variable waitCondition;
+ std::mutex mutex;
+ std::condition_variable waitCondition;
std::atomic<Expectation> expecting;
};
@@ -1332,7 +1308,7 @@ static void printUnknownDataTagError(QLatin1StringView name, QLatin1StringView t
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;
@@ -1364,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 {
@@ -1372,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;
}
@@ -1390,18 +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;
@@ -1620,65 +1607,77 @@ char *toPrettyCString(const char *p, qsizetype length)
}
/*!
+ \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++ = '"';
@@ -1696,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 (!alreadyDebugging()
+ 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();
@@ -1725,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;
@@ -1736,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());
@@ -1759,421 +1755,6 @@ bool reportResult(bool success, qxp::function_ref<const char *()> lhs,
} // namespace QTest
-namespace {
-#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
-{
- // 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;
-}
-
-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;
- fprintf(stderr, "A crash occurred in %s.\n", appName);
- if (const char *name = QTest::currentTestFunction())
- fprintf(stderr, "While testing %s\n", name);
- fprintf(stderr, "Function time: %dms Total time: %dms\n\n"
- "Exception address: 0x%p\n"
- "Exception code : 0x%lx\n",
- msecsFunctionTime, msecsTotalTime, exceptionAddress,
- exInfo->ExceptionRecord->ExceptionCode);
-
- DebugSymbolResolver resolver(GetCurrentProcess());
- if (resolver.isValid()) {
- DebugSymbolResolver::Symbol exceptionSymbol = resolver.resolveSymbol(DWORD64(exceptionAddress));
- if (exceptionSymbol.name) {
- fprintf(stderr, "Nearby symbol : %s\n", exceptionSymbol.name);
- delete [] exceptionSymbol.name;
- }
- void *stack[maxStackFrames];
- fputs("\nStack:\n", stderr);
- 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) {
- fprintf(stderr, "#%3u: %s() - 0x%p\n", f + 1, symbol.name, (const void *)symbol.address);
- delete [] symbol.name;
- } else {
- fprintf(stderr, "#%3u: Unable to obtain symbol\n", f + 1);
- }
- }
- }
-
- fputc('\n', stderr);
-
- return EXCEPTION_EXECUTE_HANDLER;
- }
-};
-using FatalSignalHandler = WindowsFaultHandler;
-
-#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM)
-class FatalSignalHandler
-{
-public:
-# define OUR_SIGNALS(F) \
- F(HUP) \
- F(INT) \
- F(QUIT) \
- F(ABRT) \
- F(ILL) \
- F(BUS) \
- F(FPE) \
- F(SEGV) \
- F(PIPE) \
- F(TERM) \
- /**/
-# define CASE_LABEL(S) case SIG ## S: return QT_STRINGIFY(S);
-# define ENUMERATE_SIGNALS(S) SIG ## S,
- static const char *signalName(int signum) noexcept
- {
- switch (signum) {
- OUR_SIGNALS(CASE_LABEL)
- }
-
-# if defined(__GLIBC_MINOR__) && (__GLIBC_MINOR__ >= 32 || __GLIBC__ > 2)
- // get the other signal names from glibc 2.32
- // (accessing the sys_sigabbrev variable causes linker warnings)
- if (const char *p = sigabbrev_np(signum))
- return p;
-# endif
- return "???";
- }
- static constexpr std::array fatalSignals = {
- OUR_SIGNALS(ENUMERATE_SIGNALS)
- };
-# undef CASE_LABEL
-# undef ENUMERATE_SIGNALS
-
- 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 | setupAlternateStack();
-
-# ifdef SA_SIGINFO
- act.sa_flags |= SA_SIGINFO;
- act.sa_sigaction = FatalSignalHandler::actionHandler;
-# else
- act.sa_handler = FatalSignalHandler::regularHandler;
-# 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);
- }
-
- freeAlternateStack();
- }
-
-private:
- Q_DISABLE_COPY_MOVE(FatalSignalHandler)
-
- static OldActionsArray &oldActions()
- {
- Q_CONSTINIT static OldActionsArray oldActions {};
- return oldActions;
- }
-
- auto alternateStackSize()
- {
- struct R { size_t size, pageSize; };
- static constexpr size_t MinStackSize = 32 * 1024;
- size_t pageSize = sysconf(_SC_PAGESIZE);
- size_t size = SIGSTKSZ;
- if (size < MinStackSize) {
- size = MinStackSize;
- } else {
- // round up to a page
- size = (size + pageSize - 1) & -pageSize;
- }
-
- return R{ size + pageSize, pageSize };
- }
-
- int setupAlternateStack()
- {
- // 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
- auto r = alternateStackSize();
- int flags = MAP_PRIVATE | MAP_ANONYMOUS;
-# ifdef MAP_STACK
- flags |= MAP_STACK;
-# endif
- alternateStackBase = mmap(nullptr, r.size, PROT_READ | PROT_WRITE, flags, -1, 0);
- if (alternateStackBase == MAP_FAILED)
- return 0;
-
- // mark the bottom page inaccessible, to catch a handler stack overflow
- (void) mprotect(alternateStackBase, r.pageSize, PROT_NONE);
-
- stack_t stack;
- stack.ss_flags = 0;
- stack.ss_size = r.size - r.pageSize;
- stack.ss_sp = static_cast<char *>(alternateStackBase) + r.pageSize;
- sigaltstack(&stack, nullptr);
- return SA_ONSTACK;
-# else
- return 0;
-# endif
- }
-
- void freeAlternateStack()
- {
-# if defined(SA_ONSTACK) && !defined(Q_OS_TVOS) && !defined(Q_OS_WATCHOS)
- if (alternateStackBase != MAP_FAILED) {
- stack_t stack = {};
- stack.ss_flags = SS_DISABLE;
- sigaltstack(&stack, nullptr);
- munmap(alternateStackBase, alternateStackSize().size);
- }
-# endif
- }
-
- template <typename T> static
- std::enable_if_t<sizeof(std::declval<T>().si_pid) + sizeof(std::declval<T>().si_uid) >= 1>
- printSentSignalInfo(T *info)
- {
- writeToStderr(" sent by PID ", asyncSafeToString(info->si_pid),
- " UID ", asyncSafeToString(info->si_uid));
- }
- static void printSentSignalInfo(...) {}
-
- template <typename T> static
- std::enable_if_t<sizeof(std::declval<T>().si_addr) >= 1> printCrashingSignalInfo(T *info)
- {
- using HexString = std::array<char, sizeof(quintptr) * 2>;
- auto toHexString = [](quintptr u, HexString &&r = {}) {
- int shift = sizeof(quintptr) * 8 - 4;
- for (size_t i = 0; i < sizeof(quintptr) * 2; ++i, shift -= 4)
- r[i] = QtMiscUtils::toHexLower(u >> shift);
- struct iovec vec;
- vec.iov_base = r.data();
- vec.iov_len = r.size();
- return vec;
- };
- writeToStderr(", code ", asyncSafeToString(info->si_code),
- ", for address 0x", toHexString(quintptr(info->si_addr)));
- }
- static void printCrashingSignalInfo(...) {}
-
- static void actionHandler(int signum, siginfo_t *info, void * /* ucontext */)
- {
- writeToStderr("Received signal ", asyncSafeToString(signum),
- " (SIG", signalName(signum), ")");
-
- bool isCrashingSignal =
- std::find(crashingSignals.begin(), crashingSignals.end(), signum) != crashingSignals.end();
- if (isCrashingSignal && (!info || info->si_code <= 0))
- isCrashingSignal = false; // wasn't sent by the kernel, so it's not really a crash
- if (isCrashingSignal)
- printCrashingSignalInfo(info);
- else if (info && (info->si_code == SI_USER || info->si_code == SI_QUEUE))
- printSentSignalInfo(info);
-
- printTestRunTime();
- if (signum != SIGINT) {
- generateStackTrace();
- if (pauseOnCrash) {
- writeToStderr("Pausing process ", asyncSafeToString(getpid()),
- " for debugging\n");
- raise(SIGSTOP);
- }
- }
-
- // 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);
- }
-
- void *alternateStackBase = MAP_FAILED;
- static bool pauseOnCrash;
-};
-bool FatalSignalHandler::pauseOnCrash = false;
-#else // Q_OS_WASM or weird systems
-class FatalSignalHandler {};
-#endif // Q_OS_* choice
-
-} // unnamed namespace
-
static void initEnvironment()
{
qputenv("QT_QTESTLIB_RUNNING", "1");
@@ -2226,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;
}
@@ -2234,7 +1823,7 @@ int QTest::qExec(QObject *testObject, int argc, char **argv)
void QTest::qInit(QObject *testObject, int argc, char **argv)
{
initEnvironment();
- maybeDisableCoreDump();
+ CrashHandler::maybeDisableCoreDump();
QBenchmarkGlobalData::current = new QBenchmarkGlobalData;
#if defined(Q_OS_MACOS)
@@ -2270,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
@@ -2304,15 +1890,15 @@ 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)) {
+ 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());
@@ -2338,7 +1924,12 @@ int QTest::qRun()
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
@@ -2375,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;
@@ -2388,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
@@ -2397,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;
@@ -2478,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)
/*!
@@ -2637,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;
@@ -2854,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
@@ -2867,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);
}
@@ -2882,13 +2542,17 @@ QTestData &QTest::newRow(const char *dataTag)
/*!
\since 5.9
- Appends a new row to the current test data. The function's arguments are passed
- to qsnprintf() for formatting according to \a format. See the qvsnprintf()
- documentation for caveats and limitations.
+ Appends a new row to the current test data.
- The formatted string will appear as the name of this test data in the test output.
+ The function's arguments are passed to qsnprintf() for formatting according
+ to \a format. See the qvsnprintf() documentation for caveats and
+ limitations.
- Returns a QTestData reference that can be used to stream in data.
+ The test output will identify the test run with this test data using the
+ name that results from this formatting.
+
+ Returns a QTestData reference that can be used to stream in data, one value
+ for each column in the table.
Example:
\snippet code/src_qtestlib_qtestcase.cpp addRow
@@ -2899,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];
@@ -2975,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()