Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qtaptestlogger.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtaptestlogger_p.h"
5
6#include "qtestlog_p.h"
7#include "qtestresult_p.h"
8#include "qtestassert.h"
9
10#if QT_CONFIG(regularexpression)
11# include <QtCore/qregularexpression.h>
12#endif
13
15
16using namespace Qt::StringLiterals;
17
95QTapTestLogger::QTapTestLogger(const char *filename)
96 : QAbstractTestLogger(filename)
97{
98}
99
101
103{
105
107 QTest::qt_asprintf(&preamble, "TAP version 13\n"
108 // By convention, test suite names are output as diagnostics lines
109 // This is a pretty poor convention, as consumers will then treat
110 // actual diagnostics, e.g. qDebug, as test suite names o_O
112 outputString(preamble.data());
113}
114
116{
117 const int total = QTestLog::totalCount();
118
119 QTestCharBuffer testPlanAndStats;
120 QTest::qt_asprintf(&testPlanAndStats,
121 "1..%d\n" // The plan (last non-diagnostic line)
122 "# tests %d\n"
123 "# pass %d\n"
124 "# fail %d\n",
125 total, total, QTestLog::passCount(), QTestLog::failCount());
126 outputString(testPlanAndStats.data());
127
129}
130
131void QTapTestLogger::enterTestFunction(const char *function)
132{
133 m_firstExpectedFail.clear();
134 Q_ASSERT(!m_gatherMessages);
135 Q_ASSERT(m_comments.isEmpty());
136 Q_ASSERT(m_messages.isEmpty());
137 m_gatherMessages = function != nullptr;
138}
139
141{
142 m_firstExpectedFail.clear();
143 if (!m_messages.isEmpty() || !m_comments.isEmpty())
144 flushMessages();
145 m_gatherMessages = data != nullptr;
146}
147
148using namespace QTestPrivate;
149
150void QTapTestLogger::outputTestLine(bool ok, int testNumber, const QTestCharBuffer &directive)
151{
152 QTestCharBuffer testIdentifier;
154
155 QTestCharBuffer testLine;
156 QTest::qt_asprintf(&testLine, "%s %d - %s%s\n", ok ? "ok" : "not ok",
157 testNumber, testIdentifier.data(), directive.constData());
158
159 outputString(testLine.data());
160}
161
162// The indent needs to be two spaces for maximum compatibility.
163// This matches the width of the "- " prefix on a list item's first line.
164#define YAML_INDENT " "
165
166void QTapTestLogger::outputBuffer(const QTestCharBuffer &buffer)
167{
168 auto isComment = [&buffer]() {
169 return buffer.constData()[strlen(YAML_INDENT)] == '#';
170 };
171 if (!m_gatherMessages)
172 outputString(buffer.constData());
173 else
174 QTestPrivate::appendCharBuffer(isComment() ? &m_comments : &m_messages, buffer);
175}
176
177void QTapTestLogger::beginYamlish()
178{
179 outputString(YAML_INDENT "---\n");
180}
181
182void QTapTestLogger::endYamlish()
183{
184 // Flush any accumulated messages:
185 if (!m_messages.isEmpty()) {
186 outputString(YAML_INDENT "extensions:\n");
187 outputString(YAML_INDENT YAML_INDENT "messages:\n");
188 outputString(m_messages.constData());
189 m_messages.clear();
190 }
191 outputString(YAML_INDENT "...\n");
192}
193
194void QTapTestLogger::flushComments()
195{
196 if (!m_comments.isEmpty()) {
197 outputString(m_comments.constData());
198 m_comments.clear();
199 }
200}
201
202void QTapTestLogger::flushMessages()
203{
204 /* A _data() function's messages show up here. */
205 QTestCharBuffer dataLine;
206 QTest::qt_asprintf(&dataLine, "ok %d - %s() # Data prepared\n",
208 outputString(dataLine.constData());
209 flushComments();
210 if (!m_messages.isEmpty()) {
211 beginYamlish();
212 endYamlish();
213 }
214}
215
216void QTapTestLogger::addIncident(IncidentTypes type, const char *description,
217 const char *file, int line)
218{
219 const bool isExpectedFail = type == XFail || type == BlacklistedXFail;
220 const bool ok = (m_firstExpectedFail.isEmpty()
221 && (type == Pass || type == BlacklistedPass || type == Skip
222 || type == XPass || type == BlacklistedXPass));
223
224 const char *const incident = [type](const char *priorXFail) {
225 switch (type) {
226 // We treat expected or blacklisted failures/passes as TODO-failures/passes,
227 // which should be treated as soft issues by consumers. Not all do though :/
228 case BlacklistedPass:
229 if (priorXFail[0] != '\0')
230 return priorXFail;
232 case XFail: case BlacklistedXFail:
233 case XPass: case BlacklistedXPass:
234 case BlacklistedFail:
235 return "TODO";
236 case Skip:
237 return "SKIP";
238 case Pass:
239 if (priorXFail[0] != '\0')
240 return priorXFail;
242 case Fail:
243 break;
244 }
245 return static_cast<const char *>(nullptr);
246 }(m_firstExpectedFail.constData());
247
248 QTestCharBuffer directive;
249 if (incident) {
250 QTest::qt_asprintf(&directive, "%s%s%s%s",
251 isExpectedFail ? "" : " # ", incident,
252 description && description[0] ? " " : "", description);
253 }
254
255 if (!isExpectedFail) {
256 m_gatherMessages = false;
257 outputTestLine(ok, QTestLog::totalCount(), directive);
258 } else if (m_gatherMessages && m_firstExpectedFail.isEmpty()) {
259 QTestPrivate::appendCharBuffer(&m_firstExpectedFail, directive);
260 }
261 flushComments();
262
263 if (!ok || !m_messages.isEmpty()) {
264 // All failures need a diagnostics section to not confuse consumers.
265 // We also need a diagnostics section when we have messages to report.
266 if (isExpectedFail) {
268 if (m_gatherMessages) {
269 QTest::qt_asprintf(&message, YAML_INDENT YAML_INDENT "- severity: xfail\n"
270 YAML_INDENT YAML_INDENT YAML_INDENT "message:%s\n",
271 directive.constData() + 4);
272 } else {
273 QTest::qt_asprintf(&message, YAML_INDENT "# xfail:%s\n", directive.constData() + 4);
274 }
275 outputBuffer(message);
276 } else {
277 beginYamlish();
278 }
279
280 if (!isExpectedFail || m_gatherMessages) {
281 const char *indent = isExpectedFail ? YAML_INDENT YAML_INDENT YAML_INDENT : YAML_INDENT;
282 if (!ok) {
283#if QT_CONFIG(regularexpression)
284 enum class OperationType {
285 Unknown,
286 Compare, /* Plain old QCOMPARE */
287 Verify, /* QVERIFY */
288 CompareOp, /* QCOMPARE_OP */
289 };
290
291 // This is fragile, but unfortunately testlib doesn't plumb
292 // the expected and actual values to the loggers (yet).
293 static const QRegularExpression verifyRegex(
294 u"^'(?<actualexpression>.*)' returned "
295 "(?<actual>\\w+)\\. \\((?<message>.*)\\)$"_s);
296
297 static const QRegularExpression compareRegex(
298 u"^(?<message>.*)\n"
299 "\\s*Actual\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
300 "\\s*Expected\\s+\\((?<expectedexpresssion>.*)\\)\\s*: "
301 "(?<expected>.*)$"_s);
302
303 static const QRegularExpression compareOpRegex(
304 u"^(?<message>.*)\n"
305 "\\s*Computed\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
306 "\\s*Baseline\\s+\\((?<expectedexpresssion>.*)\\)\\s*: "
307 "(?<expected>.*)$"_s);
308
309 const QString descriptionString = QString::fromUtf8(description);
310 QRegularExpressionMatch match = verifyRegex.match(descriptionString);
311
312 OperationType opType = OperationType::Unknown;
313 if (match.hasMatch())
314 opType = OperationType::Verify;
315
316 if (opType == OperationType::Unknown) {
317 match = compareRegex.match(descriptionString);
318 if (match.hasMatch())
319 opType = OperationType::Compare;
320 }
321
322 if (opType == OperationType::Unknown) {
323 match = compareOpRegex.match(descriptionString);
324 if (match.hasMatch())
325 opType = OperationType::CompareOp;
326 }
327
328 if (opType != OperationType::Unknown) {
329 QString message = match.captured(u"message");
330 QLatin1StringView comparisonType;
332 QString actual;
333 const auto parenthesize = [&match](QLatin1StringView key) -> QString {
334 return " ("_L1 % match.captured(key) % u')';
335 };
336 const QString actualExpression = parenthesize("actualexpression"_L1);
337
338 if (opType == OperationType::Verify) {
339 comparisonType = "QVERIFY"_L1;
340 actual = match.captured(u"actual").toLower() % actualExpression;
341 expected = (actual.startsWith("true "_L1) ? "false"_L1 : "true"_L1)
342 % actualExpression;
343 if (message.isEmpty())
344 message = u"Verification failed"_s;
345 } else if (opType == OperationType::Compare) {
346 comparisonType = "QCOMPARE"_L1;
347 expected = match.captured(u"expected")
348 % parenthesize("expectedexpresssion"_L1);
349 actual = match.captured(u"actual") % actualExpression;
350 } else {
351 struct ComparisonInfo {
352 const char *comparisonType;
353 const char *comparisonStringOp;
354 };
355 // get a proper comparison type based on the error message
356 const auto info = [](const QString &err) -> ComparisonInfo {
357 if (err.contains("different"_L1))
358 return { "QCOMPARE_NE", "!= " };
359 else if (err.contains("less than or equal to"_L1))
360 return { "QCOMPARE_LE", "<= " };
361 else if (err.contains("greater than or equal to"_L1))
362 return { "QCOMPARE_GE", ">= " };
363 else if (err.contains("less than"_L1))
364 return { "QCOMPARE_LT", "< " };
365 else if (err.contains("greater than"_L1))
366 return { "QCOMPARE_GT", "> " };
367 else if (err.contains("to be equal to"_L1))
368 return { "QCOMPARE_EQ", "== " };
369 else
370 return { "Unknown", "" };
371 }(message);
372 comparisonType = QLatin1StringView(info.comparisonType);
373 expected = QLatin1StringView(info.comparisonStringOp)
374 % match.captured(u"expected")
375 % parenthesize("expectedexpresssion"_L1);
376 actual = match.captured(u"actual") % actualExpression;
377 }
378
379 QTestCharBuffer diagnosticsYamlish;
380 QTest::qt_asprintf(&diagnosticsYamlish,
381 "%stype: %s\n"
382 "%smessage: %s\n"
383 // Some consumers understand 'wanted/found', others need
384 // 'expected/actual', so be compatible with both.
385 "%swanted: %s\n"
386 "%sfound: %s\n"
387 "%sexpected: %s\n"
388 "%sactual: %s\n",
389 indent, comparisonType.latin1(),
390 indent, qPrintable(message),
391 indent, qPrintable(expected), indent, qPrintable(actual),
392 indent, qPrintable(expected), indent, qPrintable(actual)
393 );
394
395 outputBuffer(diagnosticsYamlish);
396 } else
397#endif
398 if (description && !incident) {
399 QTestCharBuffer unparsableDescription;
400 QTest::qt_asprintf(&unparsableDescription, YAML_INDENT "# %s\n", description);
401 outputBuffer(unparsableDescription);
402 }
403 }
404
405 if (file) {
408 // The generic 'at' key is understood by most consumers.
409 "%sat: %s::%s() (%s:%d)\n"
410
411 // The file and line keys are for consumers that are able
412 // to read more granular location info.
413 "%sfile: %s\n"
414 "%sline: %d\n",
415
418 file, line, indent, file, indent, line
419 );
420 outputBuffer(location);
421 }
422 }
423
424 if (!isExpectedFail)
425 endYamlish();
426 }
427}
428
430 const char *file, int line)
431{
432 Q_UNUSED(file);
433 Q_UNUSED(line);
434 const char *const flavor = [type]() {
435 switch (type) {
436 case QDebug: return "debug";
437 case QInfo: return "info";
438 case QWarning: return "warning";
439 case QCritical: return "critical";
440 case QFatal: return "fatal";
441 // Handle internal messages as comments
442 case Info: return "# inform";
443 case Warn: return "# warn";
444 }
445 return "unrecognised message";
446 }();
447
448 QTestCharBuffer diagnostic;
449 if (!m_gatherMessages) {
450 QTest::qt_asprintf(&diagnostic, "%s%s: %s\n",
451 flavor[0] == '#' ? "" : "# ",
452 flavor, qPrintable(message));
453 outputString(diagnostic.constData());
454 } else if (flavor[0] == '#') {
455 QTest::qt_asprintf(&diagnostic, YAML_INDENT "%s: %s\n",
456 flavor, qPrintable(message));
457 QTestPrivate::appendCharBuffer(&m_comments, diagnostic);
458 } else {
459 // These shall appear in a messages: sub-block of the extensions: block,
460 // so triple-indent.
461 QTest::qt_asprintf(&diagnostic, YAML_INDENT YAML_INDENT "- severity: %s\n"
462 YAML_INDENT YAML_INDENT YAML_INDENT "message: %s\n",
463 flavor, qPrintable(message));
464 QTestPrivate::appendCharBuffer(&m_messages, diagnostic);
465 }
466}
467
Base class for test loggers.
void outputString(const char *msg)
Convenience method to write msg to the output stream.
IncidentTypes
\value Pass The test ran to completion successfully.
virtual void startLogging()
Called before the start of a test run.
MessageTypes
The members whose names begin with Q describe messages that originate in calls, by the test or code u...
virtual void stopLogging()
Called after the end of a test run.
\inmodule QtCore \reentrant
\inmodule QtCore \reentrant
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:6018
void enterTestFunction(const char *) override
This virtual method is called before each test function is invoked.
void addMessage(MessageTypes type, const QString &message, const char *file=nullptr, int line=0) override
This is an overloaded member function, provided for convenience. It differs from the above function o...
void enterTestData(QTestData *data) override
This virtual method is called before and after each call to a test function.
void stopLogging() override
Called after the end of a test run.
QTapTestLogger(const char *filename)
void addIncident(IncidentTypes type, const char *description, const char *file=nullptr, int line=0) override
This virtual method is called when an event occurs that relates to the resolution of the test.
void startLogging() override
Called before the start of a test run.
static int totalCount()
Definition qtestlog.cpp:683
static int passCount()
Definition qtestlog.cpp:663
static int failCount()
Definition qtestlog.cpp:668
static const char * currentTestObjectName()
static const char * currentTestFunction()
QJSValue expected
Definition qjsengine.cpp:12
Combined button and popup list for selecting options.
void generateTestIdentifier(QTestCharBuffer *identifier, int parts)
bool appendCharBuffer(QTestCharBuffer *accumulator, const QTestCharBuffer &more)
int qt_asprintf(QTestCharBuffer *str, const char *format,...)
#define Q_FALLTHROUGH()
GLint location
GLuint64 key
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum GLuint buffer
GLenum type
GLuint GLsizei const GLchar * message
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define qPrintable(string)
Definition qstring.h:1531
#define YAML_INDENT
#define Q_UNUSED(x)
static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
class Preamble preamble
QFile file
[0]
QHostInfo info
[0]
const char * constData() const