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 const char *tag = QTestResult::currentDataTag();
277 const char *gtag = QTestResult::currentGlobalDataTag();
278 const char *filler = (tag && gtag) ? ":" : "";
279 const bool notag = QTest::isEmpty(tag) && QTest::isEmpty(gtag);
280
281 QTestCharBuffer quotedFile;
282 QTestCharBuffer cdataGtag;
283 QTestCharBuffer cdataTag;
284 QTestCharBuffer cdataDescription;
285
286 if (xmlQuote(&quotedFile, file)
287 && xmlCdata(&cdataGtag, gtag)
288 && xmlCdata(&cdataTag, tag)
289 && xmlCdata(&cdataDescription, message.toUtf8().constData())) {
290 QTest::qt_asprintf(&buf,
291 QTest::messageFormatString(message.isEmpty(), notag),
292 QTest::xmlMessageType2String(type),
293 quotedFile.constData(), line,
294 cdataGtag.constData(),
295 filler,
296 cdataTag.constData(),
297 cdataDescription.constData());
298
299 outputString(buf.constData());
300 }
301}
302
303int QXmlTestLogger::xmlQuote(QTestCharBuffer *destBuf, char const *src, qsizetype n)
304{
305 // QTestCharBuffer initially has size 512, with '\0' at the start of its
306 // data; and we only grow it.
307 Q_ASSERT(n >= 512 && destBuf->size() == n);
308 char *dest = destBuf->data();
309
310 if (!src || !*src) {
311 Q_ASSERT(!dest[0]);
312 return 0;
313 }
314
315 char *begin = dest;
316 char *end = dest + n;
317
318 while (dest < end) {
319 switch (*src) {
320
321#define MAP_ENTITY(chr, ent)
322 case chr:
323 if (dest + sizeof(ent) < end) {
324 strcpy(dest, ent);
325 dest += sizeof(ent) - 1;
326 } else {
327 *dest = '\0';
328 return dest + sizeof(ent) - begin;
329 }
330 ++src;
331 break;
332
333 MAP_ENTITY('>', "&gt;");
334 MAP_ENTITY('<', "&lt;");
335 MAP_ENTITY('\'', "&apos;");
336 MAP_ENTITY('"', "&quot;");
337 MAP_ENTITY('&', "&amp;");
338
339 // Not strictly necessary, but allows handling of comments without
340 // having to explicitly look for `--'
341 MAP_ENTITY('-', "&#x002D;");
342
343#undef MAP_ENTITY
344
345 case '\0':
346 *dest = '\0';
347 return dest - begin;
348
349 default:
350 *dest = *src;
351 ++dest;
352 ++src;
353 break;
354 }
355 }
356
357 // If we get here, dest was completely filled:
358 Q_ASSERT(dest == end && end > begin);
359 dest[-1] = '\0'; // hygiene, but it'll be ignored
360 return n;
361}
362
363int QXmlTestLogger::xmlCdata(QTestCharBuffer *destBuf, char const *src, qsizetype n)
364{
365 Q_ASSERT(n >= 512 && destBuf->size() == n);
366 char *dest = destBuf->data();
367
368 if (!src || !*src) {
369 Q_ASSERT(!dest[0]);
370 return 0;
371 }
372
373 static char const CDATA_END[] = "]]>";
374 static char const CDATA_END_ESCAPED[] = "]]]><![CDATA[]>";
375 const size_t CDATA_END_LEN = sizeof(CDATA_END) - 1;
376
377 char *begin = dest;
378 char *end = dest + n;
379 while (dest < end) {
380 if (!*src) {
381 *dest = '\0';
382 return dest - begin;
383 }
384
385 if (!strncmp(src, CDATA_END, CDATA_END_LEN)) {
386 if (dest + sizeof(CDATA_END_ESCAPED) < end) {
387 strcpy(dest, CDATA_END_ESCAPED);
388 src += CDATA_END_LEN;
389 dest += sizeof(CDATA_END_ESCAPED) - 1;
390 } else {
391 *dest = '\0';
392 return dest + sizeof(CDATA_END_ESCAPED) - begin;
393 }
394 continue;
395 }
396
397 *dest = *src;
398 ++src;
399 ++dest;
400 }
401
402 // If we get here, dest was completely filled; caller shall grow and retry:
403 Q_ASSERT(dest == end && end > begin);
404 dest[-1] = '\0'; // hygiene, but it'll be ignored
405 return n;
406}
407
408typedef int (*StringFormatFunction)(QTestCharBuffer *, char const *, qsizetype);
409
410/*
411 A wrapper for string functions written to work with a fixed size buffer so they can be called
412 with a dynamically allocated buffer.
413*/
414static bool allocateStringFn(QTestCharBuffer *str, char const *src, StringFormatFunction func)
415{
416 constexpr int MAXSIZE = 1024 * 1024 * 2;
417 int size = str->size();
418 Q_ASSERT(size >= 512 && !str->data()[0]);
419
420 do {
421 const int res = func(str, src, size);
422 if (res < size) { // Success
423 Q_ASSERT(res > 0 || (!res && (!src || !src[0])));
424 return true;
425 }
426
427 // Buffer wasn't big enough, try again, if not too big:
428 size *= 2;
429 } while (size <= MAXSIZE && str->reset(size));
430
431 return false;
432}
433
434/*
435 Copy from \a src into \a destBuf, escaping any special XML characters as
436 necessary so that destBuf is suitable for use in an XML quoted attribute
437 string. Expands \a destBuf as needed to make room, up to a size of 2
438 MiB. Input requiring more than that much space for output is considered
439 invalid.
440
441 Returns 0 on invalid or empty input, the actual length written on success.
442*/
443bool QXmlTestLogger::xmlQuote(QTestCharBuffer *str, char const *src)
444{
445 return allocateStringFn(str, src, QXmlTestLogger::xmlQuote);
446}
447
448/*
449 Copy from \a src into \a destBuf, escaping any special strings such that
450 destBuf is suitable for use in an XML CDATA section. Expands \a destBuf as
451 needed to make room, up to a size of 2 MiB. Input requiring more than that
452 much space for output is considered invalid.
453
454 Returns 0 on invalid or empty input, the actual length written on success.
455*/
456bool QXmlTestLogger::xmlCdata(QTestCharBuffer *str, char const *src)
457{
458 return allocateStringFn(str, src, QXmlTestLogger::xmlCdata);
459}
460
461QT_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)