Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qxmltestlogger.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 <stdio.h>
5#include <string.h>
6#include <QtCore/qglobal.h>
7#include <QtCore/qlibraryinfo.h>
8
9#include <QtTest/private/qtestlog_p.h>
10#include <QtTest/private/qxmltestlogger_p.h>
11#include <QtTest/private/qtestresult_p.h>
12#include <QtTest/private/qbenchmark_p.h>
13#include <QtTest/private/qbenchmarkmetric_p.h>
14#include <QtTest/qtestcase.h>
15
16QT_BEGIN_NAMESPACE
17
18namespace QTest {
19
20 static const char *xmlMessageType2String(QAbstractTestLogger::MessageTypes type)
21 {
22 switch (type) {
23 case QAbstractTestLogger::QDebug:
24 return "qdebug";
25 case QAbstractTestLogger::QInfo:
26 return "qinfo";
27 case QAbstractTestLogger::QWarning:
28 return "qwarn";
29 case QAbstractTestLogger::QCritical:
30 return "qcritical";
31 case QAbstractTestLogger::QFatal:
32 return "qfatal";
33 case QAbstractTestLogger::Info:
34 return "info";
35 case QAbstractTestLogger::Warn:
36 return "warn";
37 }
38 return "??????";
39 }
40
41 static const char *xmlIncidentType2String(QAbstractTestLogger::IncidentTypes type)
42 {
43 switch (type) {
44 case QAbstractTestLogger::Skip:
45 return "skip";
46 case QAbstractTestLogger::Pass:
47 return "pass";
48 case QAbstractTestLogger::XFail:
49 return "xfail";
50 case QAbstractTestLogger::Fail:
51 return "fail";
52 case QAbstractTestLogger::XPass:
53 return "xpass";
54 case QAbstractTestLogger::BlacklistedPass:
55 return "bpass";
56 case QAbstractTestLogger::BlacklistedFail:
57 return "bfail";
58 case QAbstractTestLogger::BlacklistedXPass:
59 return "bxpass";
60 case QAbstractTestLogger::BlacklistedXFail:
61 return "bxfail";
62 }
63 return "??????";
64 }
65
66}
67
68/*! \internal
69 \class QXmlTestLogger
70 \inmodule QtTest
71
72 QXmlTestLogger implements two XML formats specific to Qt.
73
74 The two formats are distinguished by the XmlMode enum.
75*/
76/*! \internal
77 \enum QXmlTestLogger::XmlMode
78
79 This enumerated type selects the type of XML output to produce.
80
81 \value Complete A full self-contained XML document
82 \value Light XML content suitable for embedding in an XML document
83
84 The Complete form wraps the Light form in a <TestCase> element whose name
85 attribute identifies the test class whose private slots are to be run. It
86 also includes the usual <?xml ...> preamble.
87*/
88
89QXmlTestLogger::QXmlTestLogger(XmlMode mode, const char *filename)
90 : QAbstractTestLogger(filename), xmlmode(mode)
91{
92}
93
94QXmlTestLogger::~QXmlTestLogger() = default;
95
96void QXmlTestLogger::startLogging()
97{
98 QAbstractTestLogger::startLogging();
99 QTestCharBuffer buf;
100
101 if (xmlmode == QXmlTestLogger::Complete) {
102 QTestCharBuffer quotedTc;
103 QTest::qt_asprintf(&buf, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
104 outputString(buf.constData());
105 if (xmlQuote(&quotedTc, QTestResult::currentTestObjectName())) {
106 QTest::qt_asprintf(&buf, "<TestCase name=\"%s\">\n", quotedTc.constData());
107 outputString(buf.constData());
108 } else {
109 // Unconditional end-tag => omitting the start tag is bad.
110 Q_ASSERT_X(false, "QXmlTestLogger::startLogging",
111 "Insanely long test-case name or OOM issue");
112 }
113 }
114
115 QTestCharBuffer quotedBuild;
116 if (!QLibraryInfo::build() || xmlQuote(&quotedBuild, QLibraryInfo::build())) {
117 QTest::qt_asprintf(&buf,
118 " <Environment>\n"
119 " <QtVersion>%s</QtVersion>\n"
120 " <QtBuild>%s</QtBuild>\n"
121 " <QTestVersion>" QTEST_VERSION_STR "</QTestVersion>\n"
122 " </Environment>\n", qVersion(), quotedBuild.constData());
123 outputString(buf.constData());
124 }
125}
126
127void QXmlTestLogger::stopLogging()
128{
129 QTestCharBuffer buf;
130
131 QTest::qt_asprintf(&buf, " <Duration msecs=\"%s\"/>\n",
132 QString::number(QTestLog::msecsTotalTime()).toUtf8().constData());
133 outputString(buf.constData());
134 if (xmlmode == QXmlTestLogger::Complete)
135 outputString("</TestCase>\n");
136
137 QAbstractTestLogger::stopLogging();
138}
139
140void QXmlTestLogger::enterTestFunction(const char *function)
141{
142 QTestCharBuffer quotedFunction;
143 if (xmlQuote(&quotedFunction, function)) {
144 QTestCharBuffer buf;
145 QTest::qt_asprintf(&buf, " <TestFunction name=\"%s\">\n", quotedFunction.constData());
146 outputString(buf.constData());
147 } else {
148 // Unconditional end-tag => omitting the start tag is bad.
149 Q_ASSERT_X(false, "QXmlTestLogger::enterTestFunction",
150 "Insanely long test-function name or OOM issue");
151 }
152}
153
154void QXmlTestLogger::leaveTestFunction()
155{
156 QTestCharBuffer buf;
157 QTest::qt_asprintf(&buf,
158 " <Duration msecs=\"%s\"/>\n"
159 " </TestFunction>\n",
160 QString::number(QTestLog::msecsFunctionTime()).toUtf8().constData());
161
162 outputString(buf.constData());
163}
164
165namespace QTest
166{
167
168inline static bool isEmpty(const char *str)
169{
170 return !str || !str[0];
171}
172
173static const char *incidentFormatString(bool noDescription, bool noTag)
174{
175 if (noDescription) {
176 return noTag
177 ? " <Incident type=\"%s\" file=\"%s\" line=\"%d\" />\n"
178 : " <Incident type=\"%s\" file=\"%s\" line=\"%d\">\n"
179 " <DataTag><![CDATA[%s%s%s%s]]></DataTag>\n"
180 " </Incident>\n";
181 }
182 return noTag
183 ? " <Incident type=\"%s\" file=\"%s\" line=\"%d\">\n"
184 " <Description><![CDATA[%s%s%s%s]]></Description>\n"
185 " </Incident>\n"
186 : " <Incident type=\"%s\" file=\"%s\" line=\"%d\">\n"
187 " <DataTag><![CDATA[%s%s%s]]></DataTag>\n"
188 " <Description><![CDATA[%s]]></Description>\n"
189 " </Incident>\n";
190}
191
192static const char *benchmarkResultFormatString()
193{
194 return " <BenchmarkResult metric=\"%s\" tag=\"%s\" value=\"%.6g\" variance=\"%.6g\" iterations=\"%d\" />\n";
195}
196
197static const char *messageFormatString(bool noDescription, bool noTag)
198{
199 if (noDescription) {
200 if (noTag)
201 return " <Message type=\"%s\" file=\"%s\" line=\"%d\" />\n";
202 else
203 return " <Message type=\"%s\" file=\"%s\" line=\"%d\">\n"
204 " <DataTag><![CDATA[%s%s%s%s]]></DataTag>\n"
205 " </Message>\n";
206 } else {
207 if (noTag)
208 return " <Message type=\"%s\" file=\"%s\" line=\"%d\">\n"
209 " <Description><![CDATA[%s%s%s%s]]></Description>\n"
210 " </Message>\n";
211 else
212 return " <Message type=\"%s\" file=\"%s\" line=\"%d\">\n"
213 " <DataTag><![CDATA[%s%s%s]]></DataTag>\n"
214 " <Description><![CDATA[%s]]></Description>\n"
215 " </Message>\n";
216 }
217}
218
219} // namespace
220
221void QXmlTestLogger::addIncident(IncidentTypes type, const char *description,
222 const char *file, int line)
223{
224 QTestCharBuffer buf;
225 const char *tag = QTestResult::currentDataTag();
226 const char *gtag = QTestResult::currentGlobalDataTag();
227 const char *filler = (tag && gtag) ? ":" : "";
228 const bool notag = QTest::isEmpty(tag) && QTest::isEmpty(gtag);
229
230 QTestCharBuffer quotedFile;
231 QTestCharBuffer cdataGtag;
232 QTestCharBuffer cdataTag;
233 QTestCharBuffer cdataDescription;
234
235 if (xmlQuote(&quotedFile, file)
236 && xmlCdata(&cdataGtag, gtag)
237 && xmlCdata(&cdataTag, tag)
238 && xmlCdata(&cdataDescription, description)) {
239
240 QTest::qt_asprintf(&buf,
241 QTest::incidentFormatString(QTest::isEmpty(description), notag),
242 QTest::xmlIncidentType2String(type),
243 quotedFile.constData(), line,
244 cdataGtag.constData(),
245 filler,
246 cdataTag.constData(),
247 cdataDescription.constData());
248
249 outputString(buf.constData());
250 }
251}
252
253void QXmlTestLogger::addBenchmarkResult(const QBenchmarkResult &result)
254{
255 QTestCharBuffer quotedMetric;
256 QTestCharBuffer quotedTag;
257
258 if (xmlQuote(&quotedMetric, benchmarkMetricName(result.measurement.metric))
259 && xmlQuote(&quotedTag, result.context.tag.toUtf8().constData())) {
260 QTestCharBuffer buf;
261 QTest::qt_asprintf(&buf,
262 QTest::benchmarkResultFormatString(),
263 quotedMetric.constData(),
264 quotedTag.constData(),
265 result.measurement.value / double(result.iterations),
266 result.measurement.variance,
267 result.iterations);
268 outputString(buf.constData());
269 }
270}
271
272void QXmlTestLogger::addMessage(MessageTypes type, const QString &message,
273 const char *file, int line)
274{
275 QTestCharBuffer buf;
276 QTestCharBuffer quotedFile;
277 QTestCharBuffer cdataGtag;
278 QTestCharBuffer cdataTag;
279 QTestCharBuffer cdataDescription;
280
281 const char *filler;
282 bool notag;
283 bool quoted;
284 {
285 // We may run on any thread, so copy the tags out while holding the lock.
286 const QTestResult::IdentifierLocker locker;
287 const char *tag = locker.dataTag();
288 const char *gtag = locker.globalDataTag();
289 filler = (tag && gtag) ? ":" : "";
290 notag = QTest::isEmpty(tag) && QTest::isEmpty(gtag);
291 quoted = xmlQuote(&quotedFile, file) && xmlCdata(&cdataGtag, gtag)
292 && xmlCdata(&cdataTag, tag);
293 }
294 quoted = quoted && xmlCdata(&cdataDescription, message.toUtf8().constData());
295
296 if (quoted) {
297 QTest::qt_asprintf(&buf,
298 QTest::messageFormatString(message.isEmpty(), notag),
299 QTest::xmlMessageType2String(type),
300 quotedFile.constData(), line,
301 cdataGtag.constData(),
302 filler,
303 cdataTag.constData(),
304 cdataDescription.constData());
305
306 outputString(buf.constData());
307 }
308}
309
310int QXmlTestLogger::xmlQuote(QTestCharBuffer *destBuf, char const *src, qsizetype n)
311{
312 // QTestCharBuffer initially has size 512, with '\0' at the start of its
313 // data; and we only grow it.
314 Q_ASSERT(n >= 512 && destBuf->size() == n);
315 char *dest = destBuf->data();
316
317 if (!src || !*src) {
318 Q_ASSERT(!dest[0]);
319 return 0;
320 }
321
322 char *begin = dest;
323 char *end = dest + n;
324
325 while (dest < end) {
326 switch (*src) {
327
328#define MAP_ENTITY(chr, ent)
329 case chr:
330 if (dest + sizeof(ent) < end) {
331 strcpy(dest, ent);
332 dest += sizeof(ent) - 1;
333 } else {
334 *dest = '\0';
335 return dest + sizeof(ent) - begin;
336 }
337 ++src;
338 break;
339
340 MAP_ENTITY('>', "&gt;");
341 MAP_ENTITY('<', "&lt;");
342 MAP_ENTITY('\'', "&apos;");
343 MAP_ENTITY('"', "&quot;");
344 MAP_ENTITY('&', "&amp;");
345
346 // Not strictly necessary, but allows handling of comments without
347 // having to explicitly look for `--'
348 MAP_ENTITY('-', "&#x002D;");
349
350#undef MAP_ENTITY
351
352 case '\0':
353 *dest = '\0';
354 return dest - begin;
355
356 default:
357 *dest = *src;
358 ++dest;
359 ++src;
360 break;
361 }
362 }
363
364 // If we get here, dest was completely filled:
365 Q_ASSERT(dest == end && end > begin);
366 dest[-1] = '\0'; // hygiene, but it'll be ignored
367 return n;
368}
369
370int QXmlTestLogger::xmlCdata(QTestCharBuffer *destBuf, char const *src, qsizetype n)
371{
372 Q_ASSERT(n >= 512 && destBuf->size() == n);
373 char *dest = destBuf->data();
374
375 if (!src || !*src) {
376 Q_ASSERT(!dest[0]);
377 return 0;
378 }
379
380 static char const CDATA_END[] = "]]>";
381 static char const CDATA_END_ESCAPED[] = "]]]><![CDATA[]>";
382 const size_t CDATA_END_LEN = sizeof(CDATA_END) - 1;
383
384 char *begin = dest;
385 char *end = dest + n;
386 while (dest < end) {
387 if (!*src) {
388 *dest = '\0';
389 return dest - begin;
390 }
391
392 if (!strncmp(src, CDATA_END, CDATA_END_LEN)) {
393 if (dest + sizeof(CDATA_END_ESCAPED) < end) {
394 strcpy(dest, CDATA_END_ESCAPED);
395 src += CDATA_END_LEN;
396 dest += sizeof(CDATA_END_ESCAPED) - 1;
397 } else {
398 *dest = '\0';
399 return dest + sizeof(CDATA_END_ESCAPED) - begin;
400 }
401 continue;
402 }
403
404 *dest = *src;
405 ++src;
406 ++dest;
407 }
408
409 // If we get here, dest was completely filled; caller shall grow and retry:
410 Q_ASSERT(dest == end && end > begin);
411 dest[-1] = '\0'; // hygiene, but it'll be ignored
412 return n;
413}
414
415typedef int (*StringFormatFunction)(QTestCharBuffer *, char const *, qsizetype);
416
417/*
418 A wrapper for string functions written to work with a fixed size buffer so they can be called
419 with a dynamically allocated buffer.
420*/
421static bool allocateStringFn(QTestCharBuffer *str, char const *src, StringFormatFunction func)
422{
423 constexpr int MAXSIZE = 1024 * 1024 * 2;
424 int size = str->size();
425 Q_ASSERT(size >= 512 && !str->data()[0]);
426
427 do {
428 const int res = func(str, src, size);
429 if (res < size) { // Success
430 Q_ASSERT(res > 0 || (!res && (!src || !src[0])));
431 return true;
432 }
433
434 // Buffer wasn't big enough, try again, if not too big:
435 size *= 2;
436 } while (size <= MAXSIZE && str->reset(size));
437
438 return false;
439}
440
441/*
442 Copy from \a src into \a destBuf, escaping any special XML characters as
443 necessary so that destBuf is suitable for use in an XML quoted attribute
444 string. Expands \a destBuf as needed to make room, up to a size of 2
445 MiB. Input requiring more than that much space for output is considered
446 invalid.
447
448 Returns 0 on invalid or empty input, the actual length written on success.
449*/
450bool QXmlTestLogger::xmlQuote(QTestCharBuffer *str, char const *src)
451{
452 return allocateStringFn(str, src, QXmlTestLogger::xmlQuote);
453}
454
455/*
456 Copy from \a src into \a destBuf, escaping any special strings such that
457 destBuf is suitable for use in an XML CDATA section. Expands \a destBuf as
458 needed to make room, up to a size of 2 MiB. Input requiring more than that
459 much space for output is considered invalid.
460
461 Returns 0 on invalid or empty input, the actual length written on success.
462*/
463bool QXmlTestLogger::xmlCdata(QTestCharBuffer *str, char const *src)
464{
465 return allocateStringFn(str, src, QXmlTestLogger::xmlCdata);
466}
467
468QT_END_NAMESPACE
static const char * xmlIncidentType2String(QAbstractTestLogger::IncidentTypes type)
static const char * incidentFormatString(bool noDescription, bool noTag)
static const char * benchmarkResultFormatString()
static bool isEmpty(const char *str)
static const char * xmlMessageType2String(QAbstractTestLogger::MessageTypes type)
static const char * messageFormatString(bool noDescription, bool noTag)
#define MAP_ENTITY(chr, ent)
static bool allocateStringFn(QTestCharBuffer *str, char const *src, StringFormatFunction func)
int(* StringFormatFunction)(QTestCharBuffer *, char const *, qsizetype)