summaryrefslogtreecommitdiffstats
path: root/src/testlib/qtaptestlogger.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/testlib/qtaptestlogger.cpp')
-rw-r--r--src/testlib/qtaptestlogger.cpp539
1 files changed, 372 insertions, 167 deletions
diff --git a/src/testlib/qtaptestlogger.cpp b/src/testlib/qtaptestlogger.cpp
index 5b5a3c4875..76f0ba0e8b 100644
--- a/src/testlib/qtaptestlogger.cpp
+++ b/src/testlib/qtaptestlogger.cpp
@@ -1,41 +1,5 @@
-/****************************************************************************
-**
-** Copyright (C) 2018 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the QtTest module of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:LGPL$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 3 as published by the Free Software
-** Foundation and appearing in the file LICENSE.LGPL3 included in the
-** packaging of this file. Please review the following information to
-** ensure the GNU Lesser General Public License version 3 requirements
-** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 2.0 or (at your option) the GNU General
-** Public license version 3 or any later version approved by the KDE Free
-** Qt Foundation. The licenses are as published by the Free Software
-** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-2.0.html and
-** https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qtaptestlogger_p.h"
@@ -49,9 +13,87 @@
QT_BEGIN_NAMESPACE
+using namespace Qt::StringLiterals;
+
+/*! \internal
+ \class QTapTestLogger
+ \inmodule QtTest
+
+ QTapTestLogger implements the Test Anything Protocol v13.
+
+ The \l{Test Anything Protocol} (TAP) is a simple plain-text interface
+ between testing code and systems for reporting and analyzing test results.
+ Since QtTest doesn't build the table for a data-driven test until the test
+ is about to be run, we don't typically know how many tests we'll run until
+ we've run them, so we put The Plan at the end, rather than the beginning.
+ We summarise the results in comments following The Plan.
+
+ \section1 YAMLish
+
+ The TAP protocol supports inclusion, after a Test Line, of a "diagnostic
+ block" in YAML, containing supporting information. We use this to package
+ other information from the test, in combination with comments to record
+ information we're unable to deliver at the same time as a test line. By
+ convention, TAP producers limit themselves to a restricted subset of YAML,
+ known as YAMLish, for maximal compatibility with TAP consumers.
+
+ YAML (see \c yaml.org for details) supports three data-types: mapping (hash
+ or dictionary), sequence (list, array or set) and scalar (string or number),
+ much as JSON does. It uses indentation to indicate levels of nesting of
+ mappings and sequences within one another. A line starting with a dash (at
+ the same level of indent as its context) introduces a list entry. A line
+ starting with a key (more indented than its context, aside from any earlier
+ keys at its level) followed by a colon introduces a mapping entry; the key
+ may be either a simple word or a quoted string (within which numeric escapes
+ may be used to indicate unicode characters). The value associated with a
+ given key, or the entry in a list, can appar after the key-colon or dasy
+ either on the same line or on a succession of subsequent lines at higher
+ indent. Thus
+
+ \code
+ - first list item
+ -
+ second: list item is a mapping
+ with:
+ - two keys
+ - the second of which is a list
+ - back in the outer list, a third item
+ \endcode
+
+ In YAMLish, the top-level structure should be a hash. The keys supported for
+ this hash, with the meanings for their values, are:
+
+ \list
+ \li \c message: free text supplying supporting details
+ \li \c severity: how bad is it ?
+ \li \c source: source describing the test, as an URL (compare file, line)
+ \li \c at: identifies the function (with file and line) performing the test
+ \li \c datetime: when the test was run (ISO 8601 or HTTP format)
+ \li \c file: source of the test as a local file-name, when appropriate
+ \li \c line: line number within the source
+ \li \c name: test name
+ \li \c extensions: sub-hash in which to store random other stuff
+ \li \c actual: what happened (a.k.a. found; contrast expected)
+ \li \c expected: what was expected (a.k.a. wanted; contrast actual)
+ \li \c display: description of the result, suitable for display
+ \li \c dump: a sub-hash of variable values when the result arose
+ \li \c error: describes the error
+ \li \c backtrace: describes the call-stack of the error
+ \endlist
+
+ In practice, only \c at, \c expected and \c actual appear to be generally
+ supported.
+
+ We can also have several messages produced within a single test, so the
+ simple \c message / \c severity pair of top-level keys does not serve us
+ well. We therefore use \c extensions with a sub-tag \c messages in which to
+ package a list of messages.
+
+ \sa QAbstractTestLogger
+*/
+
QTapTestLogger::QTapTestLogger(const char *filename)
: QAbstractTestLogger(filename)
- , m_wasExpectedFail(false)
{
}
@@ -76,7 +118,7 @@ void QTapTestLogger::stopLogging()
QTestCharBuffer testPlanAndStats;
QTest::qt_asprintf(&testPlanAndStats,
- "1..%d\n"
+ "1..%d\n" // The plan (last non-diagnostic line)
"# tests %d\n"
"# pass %d\n"
"# fail %d\n",
@@ -88,176 +130,339 @@ void QTapTestLogger::stopLogging()
void QTapTestLogger::enterTestFunction(const char *function)
{
- Q_UNUSED(function);
- m_wasExpectedFail = false;
+ m_firstExpectedFail.clear();
+ Q_ASSERT(!m_gatherMessages);
+ Q_ASSERT(m_comments.isEmpty());
+ Q_ASSERT(m_messages.isEmpty());
+ m_gatherMessages = function != nullptr;
}
void QTapTestLogger::enterTestData(QTestData *data)
{
- Q_UNUSED(data);
- m_wasExpectedFail = false;
+ m_firstExpectedFail.clear();
+ if (!m_messages.isEmpty() || !m_comments.isEmpty())
+ flushMessages();
+ m_gatherMessages = data != nullptr;
}
using namespace QTestPrivate;
-void QTapTestLogger::outputTestLine(bool ok, int testNumber, QTestCharBuffer &directive)
+void QTapTestLogger::outputTestLine(bool ok, int testNumber, const QTestCharBuffer &directive)
{
QTestCharBuffer testIdentifier;
QTestPrivate::generateTestIdentifier(&testIdentifier, TestFunction | TestDataTag);
QTestCharBuffer testLine;
- QTest::qt_asprintf(&testLine, "%s %d - %s%s\n",
- ok ? "ok" : "not ok", testNumber, testIdentifier.data(), directive.data());
+ QTest::qt_asprintf(&testLine, "%s %d - %s%s\n", ok ? "ok" : "not ok",
+ testNumber, testIdentifier.data(), directive.constData());
outputString(testLine.data());
}
-void QTapTestLogger::addIncident(IncidentTypes type, const char *description,
- const char *file, int line)
+// The indent needs to be two spaces for maximum compatibility.
+// This matches the width of the "- " prefix on a list item's first line.
+#define YAML_INDENT " "
+
+void QTapTestLogger::outputBuffer(const QTestCharBuffer &buffer)
{
- if (m_wasExpectedFail && type == Pass) {
- // XFail comes with a corresponding Pass incident, but we only want
- // to emit a single test point for it, so skip the this pass.
- return;
- }
+ auto isComment = [&buffer]() {
+ return buffer.constData()[strlen(YAML_INDENT)] == '#';
+ };
+ if (!m_gatherMessages)
+ outputString(buffer.constData());
+ else
+ QTestPrivate::appendCharBuffer(isComment() ? &m_comments : &m_messages, buffer);
+}
- bool ok = type == Pass || type == XPass || type == BlacklistedPass || type == BlacklistedXPass;
+void QTapTestLogger::beginYamlish()
+{
+ outputString(YAML_INDENT "---\n");
+}
- QTestCharBuffer directive;
- if (type == XFail || type == XPass || type == BlacklistedFail || type == BlacklistedPass
- || type == BlacklistedXFail || type == BlacklistedXPass) {
- // We treat expected or blacklisted failures/passes as TODO-failures/passes,
- // which should be treated as soft issues by consumers. Not all do though :/
- QTest::qt_asprintf(&directive, " # TODO %s", description);
+void QTapTestLogger::endYamlish()
+{
+ // Flush any accumulated messages:
+ if (!m_messages.isEmpty()) {
+ outputString(YAML_INDENT "extensions:\n");
+ outputString(YAML_INDENT YAML_INDENT "messages:\n");
+ outputString(m_messages.constData());
+ m_messages.clear();
}
+ outputString(YAML_INDENT "...\n");
+}
+
+void QTapTestLogger::flushComments()
+{
+ if (!m_comments.isEmpty()) {
+ outputString(m_comments.constData());
+ m_comments.clear();
+ }
+}
- int testNumber = QTestLog::totalCount();
- if (type == XFail) {
- // The global test counter hasn't been updated yet for XFail
- testNumber += 1;
+void QTapTestLogger::flushMessages()
+{
+ /* A _data() function's messages show up here. */
+ QTestCharBuffer dataLine;
+ QTest::qt_asprintf(&dataLine, "ok %d - %s() # Data prepared\n",
+ QTestLog::totalCount(), QTestResult::currentTestFunction());
+ outputString(dataLine.constData());
+ flushComments();
+ if (!m_messages.isEmpty()) {
+ beginYamlish();
+ endYamlish();
}
+}
- outputTestLine(ok, testNumber, directive);
+void QTapTestLogger::addIncident(IncidentTypes type, const char *description,
+ const char *file, int line)
+{
+ const bool isExpectedFail = type == XFail || type == BlacklistedXFail;
+ const bool ok = (m_firstExpectedFail.isEmpty()
+ && (type == Pass || type == BlacklistedPass || type == Skip
+ || type == XPass || type == BlacklistedXPass));
- if (!ok) {
- // All failures need a diagnostics sections to not confuse consumers
+ const char *const incident = [type](const char *priorXFail) {
+ switch (type) {
+ // We treat expected or blacklisted failures/passes as TODO-failures/passes,
+ // which should be treated as soft issues by consumers. Not all do though :/
+ case BlacklistedPass:
+ if (priorXFail[0] != '\0')
+ return priorXFail;
+ Q_FALLTHROUGH();
+ case XFail: case BlacklistedXFail:
+ case XPass: case BlacklistedXPass:
+ case BlacklistedFail:
+ return "TODO";
+ case Skip:
+ return "SKIP";
+ case Pass:
+ if (priorXFail[0] != '\0')
+ return priorXFail;
+ Q_FALLTHROUGH();
+ case Fail:
+ break;
+ }
+ return static_cast<const char *>(nullptr);
+ }(m_firstExpectedFail.constData());
- // The indent needs to be two spaces for maximum compatibility
- #define YAML_INDENT " "
+ QTestCharBuffer directive;
+ if (incident) {
+ QTest::qt_asprintf(&directive, "%s%s%s%s",
+ isExpectedFail ? "" : " # ", incident,
+ description && description[0] ? " " : "", description);
+ }
- outputString(YAML_INDENT "---\n");
+ if (!isExpectedFail) {
+ m_gatherMessages = false;
+ outputTestLine(ok, QTestLog::totalCount(), directive);
+ } else if (m_gatherMessages && m_firstExpectedFail.isEmpty()) {
+ QTestPrivate::appendCharBuffer(&m_firstExpectedFail, directive);
+ }
+ flushComments();
+
+ if (!ok || !m_messages.isEmpty()) {
+ // All failures need a diagnostics section to not confuse consumers.
+ // We also need a diagnostics section when we have messages to report.
+ if (isExpectedFail) {
+ QTestCharBuffer message;
+ if (m_gatherMessages) {
+ QTest::qt_asprintf(&message, YAML_INDENT YAML_INDENT "- severity: xfail\n"
+ YAML_INDENT YAML_INDENT YAML_INDENT "message:%s\n",
+ directive.constData() + 4);
+ } else {
+ QTest::qt_asprintf(&message, YAML_INDENT "# xfail:%s\n", directive.constData() + 4);
+ }
+ outputBuffer(message);
+ } else {
+ beginYamlish();
+ }
- if (type != XFail) {
+ if (!isExpectedFail || m_gatherMessages) {
+ const char *indent = isExpectedFail ? YAML_INDENT YAML_INDENT YAML_INDENT : YAML_INDENT;
+ if (!ok) {
#if QT_CONFIG(regularexpression)
- // This is fragile, but unfortunately testlib doesn't plumb
- // the expected and actual values to the loggers (yet).
- static QRegularExpression verifyRegex(
- QLatin1String("^'(?<actualexpression>.*)' returned (?<actual>\\w+).+\\((?<message>.*)\\)$"));
-
- static QRegularExpression comparRegex(
- QLatin1String("^(?<message>.*)\n"
- "\\s*Actual\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
- "\\s*Expected\\s+\\((?<expectedexpresssion>.*)\\)\\s*: (?<expected>.*)$"));
-
- QString descriptionString = QString::fromUtf8(description);
- QRegularExpressionMatch match = verifyRegex.match(descriptionString);
- if (!match.hasMatch())
- match = comparRegex.match(descriptionString);
-
- if (match.hasMatch()) {
- bool isVerify = match.regularExpression() == verifyRegex;
- QString message = match.captured(QLatin1String("message"));
- QString expected;
- QString actual;
-
- if (isVerify) {
- QString expression = QLatin1String(" (")
- % match.captured(QLatin1String("actualexpression")) % QLatin1Char(')') ;
- actual = match.captured(QLatin1String("actual")).toLower() % expression;
- expected = (actual.startsWith(QLatin1String("true")) ? QLatin1String("false") : QLatin1String("true")) % expression;
- if (message.isEmpty())
- message = QLatin1String("Verification failed");
- } else {
- expected = match.captured(QLatin1String("expected"))
- % QLatin1String(" (") % match.captured(QLatin1String("expectedexpresssion")) % QLatin1Char(')');
- actual = match.captured(QLatin1String("actual"))
- % QLatin1String(" (") % match.captured(QLatin1String("actualexpression")) % QLatin1Char(')');
+ enum class OperationType {
+ Unknown,
+ Compare, /* Plain old QCOMPARE */
+ Verify, /* QVERIFY */
+ CompareOp, /* QCOMPARE_OP */
+ };
+
+ // This is fragile, but unfortunately testlib doesn't plumb
+ // the expected and actual values to the loggers (yet).
+ static const QRegularExpression verifyRegex(
+ u"^'(?<actualexpression>.*)' returned "
+ "(?<actual>\\w+)\\. \\((?<message>.*)\\)$"_s);
+
+ static const QRegularExpression compareRegex(
+ u"^(?<message>.*)\n"
+ "\\s*Actual\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
+ "\\s*Expected\\s+\\((?<expectedexpresssion>.*)\\)\\s*: "
+ "(?<expected>.*)$"_s);
+
+ static const QRegularExpression compareOpRegex(
+ u"^(?<message>.*)\n"
+ "\\s*Computed\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
+ "\\s*Baseline\\s+\\((?<expectedexpresssion>.*)\\)\\s*: "
+ "(?<expected>.*)$"_s);
+
+ const QString descriptionString = QString::fromUtf8(description);
+ QRegularExpressionMatch match = verifyRegex.match(descriptionString);
+
+ OperationType opType = OperationType::Unknown;
+ if (match.hasMatch())
+ opType = OperationType::Verify;
+
+ if (opType == OperationType::Unknown) {
+ match = compareRegex.match(descriptionString);
+ if (match.hasMatch())
+ opType = OperationType::Compare;
}
- QTestCharBuffer diagnosticsYamlish;
- QTest::qt_asprintf(&diagnosticsYamlish,
- YAML_INDENT "type: %s\n"
- YAML_INDENT "message: %s\n"
-
- // Some consumers understand 'wanted/found', while others need
- // 'expected/actual', so we do both for maximum compatibility.
- YAML_INDENT "wanted: %s\n"
- YAML_INDENT "found: %s\n"
- YAML_INDENT "expected: %s\n"
- YAML_INDENT "actual: %s\n",
-
- isVerify ? "QVERIFY" : "QCOMPARE",
- qPrintable(message),
- qPrintable(expected), qPrintable(actual),
- qPrintable(expected), qPrintable(actual)
- );
-
- outputString(diagnosticsYamlish.data());
- } else {
- QTestCharBuffer unparsableDescription;
- QTest::qt_asprintf(&unparsableDescription,
- YAML_INDENT "# %s\n", description);
- outputString(unparsableDescription.data());
- }
-#else
- QTestCharBuffer unparsableDescription;
- QTest::qt_asprintf(&unparsableDescription,
- YAML_INDENT "# %s\n", description);
- outputString(unparsableDescription.data());
+ if (opType == OperationType::Unknown) {
+ match = compareOpRegex.match(descriptionString);
+ if (match.hasMatch())
+ opType = OperationType::CompareOp;
+ }
+
+ if (opType != OperationType::Unknown) {
+ QString message = match.captured(u"message");
+ QLatin1StringView comparisonType;
+ QString expected;
+ QString actual;
+ const auto parenthesize = [&match](QLatin1StringView key) -> QString {
+ return " ("_L1 % match.captured(key) % u')';
+ };
+ const QString actualExpression = parenthesize("actualexpression"_L1);
+
+ if (opType == OperationType::Verify) {
+ comparisonType = "QVERIFY"_L1;
+ actual = match.captured(u"actual").toLower() % actualExpression;
+ expected = (actual.startsWith("true "_L1) ? "false"_L1 : "true"_L1)
+ % actualExpression;
+ if (message.isEmpty())
+ message = u"Verification failed"_s;
+ } else if (opType == OperationType::Compare) {
+ comparisonType = "QCOMPARE"_L1;
+ expected = match.captured(u"expected")
+ % parenthesize("expectedexpresssion"_L1);
+ actual = match.captured(u"actual") % actualExpression;
+ } else {
+ struct ComparisonInfo {
+ const char *comparisonType;
+ const char *comparisonStringOp;
+ };
+ // get a proper comparison type based on the error message
+ const auto info = [](const QString &err) -> ComparisonInfo {
+ if (err.contains("different"_L1))
+ return { "QCOMPARE_NE", "!= " };
+ else if (err.contains("less than or equal to"_L1))
+ return { "QCOMPARE_LE", "<= " };
+ else if (err.contains("greater than or equal to"_L1))
+ return { "QCOMPARE_GE", ">= " };
+ else if (err.contains("less than"_L1))
+ return { "QCOMPARE_LT", "< " };
+ else if (err.contains("greater than"_L1))
+ return { "QCOMPARE_GT", "> " };
+ else if (err.contains("to be equal to"_L1))
+ return { "QCOMPARE_EQ", "== " };
+ else
+ return { "Unknown", "" };
+ }(message);
+ comparisonType = QLatin1StringView(info.comparisonType);
+ expected = QLatin1StringView(info.comparisonStringOp)
+ % match.captured(u"expected")
+ % parenthesize("expectedexpresssion"_L1);
+ actual = match.captured(u"actual") % actualExpression;
+ }
+
+ QTestCharBuffer diagnosticsYamlish;
+ QTest::qt_asprintf(&diagnosticsYamlish,
+ "%stype: %s\n"
+ "%smessage: %s\n"
+ // Some consumers understand 'wanted/found', others need
+ // 'expected/actual', so be compatible with both.
+ "%swanted: %s\n"
+ "%sfound: %s\n"
+ "%sexpected: %s\n"
+ "%sactual: %s\n",
+ indent, comparisonType.latin1(),
+ indent, qPrintable(message),
+ indent, qPrintable(expected), indent, qPrintable(actual),
+ indent, qPrintable(expected), indent, qPrintable(actual)
+ );
+
+ outputBuffer(diagnosticsYamlish);
+ } else
#endif
- }
+ if (description && !incident) {
+ QTestCharBuffer unparsableDescription;
+ QTest::qt_asprintf(&unparsableDescription, YAML_INDENT "# %s\n", description);
+ outputBuffer(unparsableDescription);
+ }
+ }
- if (file) {
- QTestCharBuffer location;
- QTest::qt_asprintf(&location,
- // The generic 'at' key is understood by most consumers.
- YAML_INDENT "at: %s::%s() (%s:%d)\n"
-
- // The file and line keys are for consumers that are able
- // to read more granular location info.
- YAML_INDENT "file: %s\n"
- YAML_INDENT "line: %d\n",
-
- QTestResult::currentTestObjectName(),
- QTestResult::currentTestFunction(),
- file, line, file, line
- );
- outputString(location.data());
+ if (file) {
+ QTestCharBuffer location;
+ QTest::qt_asprintf(&location,
+ // The generic 'at' key is understood by most consumers.
+ "%sat: %s::%s() (%s:%d)\n"
+
+ // The file and line keys are for consumers that are able
+ // to read more granular location info.
+ "%sfile: %s\n"
+ "%sline: %d\n",
+
+ indent, QTestResult::currentTestObjectName(),
+ QTestResult::currentTestFunction(),
+ file, line, indent, file, indent, line
+ );
+ outputBuffer(location);
+ }
}
- outputString(YAML_INDENT "...\n");
+ if (!isExpectedFail)
+ endYamlish();
}
-
- m_wasExpectedFail = type == XFail;
}
void QTapTestLogger::addMessage(MessageTypes type, const QString &message,
- const char *file, int line)
+ const char *file, int line)
{
Q_UNUSED(file);
Q_UNUSED(line);
-
- if (type == Skip) {
- QTestCharBuffer directive;
- QTest::qt_asprintf(&directive, " # SKIP %s", message.toUtf8().constData());
- outputTestLine(/* ok = */ true, QTestLog::totalCount(), directive);
- return;
+ const char *const flavor = [type]() {
+ switch (type) {
+ case QDebug: return "debug";
+ case QInfo: return "info";
+ case QWarning: return "warning";
+ case QCritical: return "critical";
+ case QFatal: return "fatal";
+ // Handle internal messages as comments
+ case Info: return "# inform";
+ case Warn: return "# warn";
+ }
+ return "unrecognised message";
+ }();
+
+ QTestCharBuffer diagnostic;
+ if (!m_gatherMessages) {
+ QTest::qt_asprintf(&diagnostic, "%s%s: %s\n",
+ flavor[0] == '#' ? "" : "# ",
+ flavor, qPrintable(message));
+ outputString(diagnostic.constData());
+ } else if (flavor[0] == '#') {
+ QTest::qt_asprintf(&diagnostic, YAML_INDENT "%s: %s\n",
+ flavor, qPrintable(message));
+ QTestPrivate::appendCharBuffer(&m_comments, diagnostic);
+ } else {
+ // These shall appear in a messages: sub-block of the extensions: block,
+ // so triple-indent.
+ QTest::qt_asprintf(&diagnostic, YAML_INDENT YAML_INDENT "- severity: %s\n"
+ YAML_INDENT YAML_INDENT YAML_INDENT "message: %s\n",
+ flavor, qPrintable(message));
+ QTestPrivate::appendCharBuffer(&m_messages, diagnostic);
}
-
- QTestCharBuffer diagnostics;
- QTest::qt_asprintf(&diagnostics, "# %s\n", qPrintable(message));
- outputString(diagnostics.data());
}
QT_END_NAMESPACE
-