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: &buf, format: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
104 outputString(msg: buf.constData());
105 if (xmlQuote(dest: &quotedTc, src: QTestResult::currentTestObjectName())) {
106 QTest::qt_asprintf(buf: &buf, format: "<TestCase name=\"%s\">\n", quotedTc.constData());
107 outputString(msg: 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(dest: &quotedBuild, src: QLibraryInfo::build())) {
117 QTest::qt_asprintf(buf: &buf,
118 format: " <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(msg: buf.constData());
124 }
125}
126
127void QXmlTestLogger::stopLogging()
128{
129 QTestCharBuffer buf;
130
131 QTest::qt_asprintf(buf: &buf, format: " <Duration msecs=\"%s\"/>\n",
132 QString::number(QTestLog::msecsTotalTime()).toUtf8().constData());
133 outputString(msg: buf.constData());
134 if (xmlmode == QXmlTestLogger::Complete)
135 outputString(msg: "</TestCase>\n");
136
137 QAbstractTestLogger::stopLogging();
138}
139
140void QXmlTestLogger::enterTestFunction(const char *function)
141{
142 QTestCharBuffer quotedFunction;
143 if (xmlQuote(dest: &quotedFunction, src: function)) {
144 QTestCharBuffer buf;
145 QTest::qt_asprintf(buf: &buf, format: " <TestFunction name=\"%s\">\n", quotedFunction.constData());
146 outputString(msg: 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: &buf,
158 format: " <Duration msecs=\"%s\"/>\n"
159 " </TestFunction>\n",
160 QString::number(QTestLog::msecsFunctionTime()).toUtf8().constData());
161
162 outputString(msg: 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\" 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(str: tag) && QTest::isEmpty(str: gtag);
229
230 QTestCharBuffer quotedFile;
231 QTestCharBuffer cdataGtag;
232 QTestCharBuffer cdataTag;
233 QTestCharBuffer cdataDescription;
234
235 if (xmlQuote(dest: &quotedFile, src: file)
236 && xmlCdata(dest: &cdataGtag, src: gtag)
237 && xmlCdata(dest: &cdataTag, src: tag)
238 && xmlCdata(dest: &cdataDescription, src: description)) {
239
240 QTest::qt_asprintf(buf: &buf,
241 format: QTest::incidentFormatString(noDescription: QTest::isEmpty(str: description), noTag: notag),
242 QTest::xmlIncidentType2String(type),
243 quotedFile.constData(), line,
244 cdataGtag.constData(),
245 filler,
246 cdataTag.constData(),
247 cdataDescription.constData());
248
249 outputString(msg: buf.constData());
250 }
251}
252
253void QXmlTestLogger::addBenchmarkResult(const QBenchmarkResult &result)
254{
255 QTestCharBuffer quotedMetric;
256 QTestCharBuffer quotedTag;
257
258 if (xmlQuote(dest: &quotedMetric, src: benchmarkMetricName(metric: result.measurement.metric))
259 && xmlQuote(dest: &quotedTag, src: result.context.tag.toUtf8().constData())) {
260 QTestCharBuffer buf;
261 QTest::qt_asprintf(buf: &buf,
262 format: QTest::benchmarkResultFormatString(),
263 quotedMetric.constData(),
264 quotedTag.constData(),
265 result.measurement.value / double(result.iterations),
266 result.iterations);
267 outputString(msg: buf.constData());
268 }
269}
270
271void QXmlTestLogger::addMessage(MessageTypes type, const QString &message,
272 const char *file, int line)
273{
274 QTestCharBuffer buf;
275 const char *tag = QTestResult::currentDataTag();
276 const char *gtag = QTestResult::currentGlobalDataTag();
277 const char *filler = (tag && gtag) ? ":" : "";
278 const bool notag = QTest::isEmpty(str: tag) && QTest::isEmpty(str: gtag);
279
280 QTestCharBuffer quotedFile;
281 QTestCharBuffer cdataGtag;
282 QTestCharBuffer cdataTag;
283 QTestCharBuffer cdataDescription;
284
285 if (xmlQuote(dest: &quotedFile, src: file)
286 && xmlCdata(dest: &cdataGtag, src: gtag)
287 && xmlCdata(dest: &cdataTag, src: tag)
288 && xmlCdata(dest: &cdataDescription, src: message.toUtf8().constData())) {
289 QTest::qt_asprintf(buf: &buf,
290 format: QTest::messageFormatString(noDescription: message.isEmpty(), noTag: notag),
291 QTest::xmlMessageType2String(type),
292 quotedFile.constData(), line,
293 cdataGtag.constData(),
294 filler,
295 cdataTag.constData(),
296 cdataDescription.constData());
297
298 outputString(msg: buf.constData());
299 }
300}
301
302int QXmlTestLogger::xmlQuote(QTestCharBuffer *destBuf, char const *src, qsizetype n)
303{
304 // QTestCharBuffer initially has size 512, with '\0' at the start of its
305 // data; and we only grow it.
306 Q_ASSERT(n >= 512 && destBuf->size() == n);
307 char *dest = destBuf->data();
308
309 if (!src || !*src) {
310 Q_ASSERT(!dest[0]);
311 return 0;
312 }
313
314 char *begin = dest;
315 char *end = dest + n;
316
317 while (dest < end) {
318 switch (*src) {
319
320#define MAP_ENTITY(chr, ent) \
321 case chr: \
322 if (dest + sizeof(ent) < end) { \
323 strcpy(dest, ent); \
324 dest += sizeof(ent) - 1; \
325 } else { \
326 *dest = '\0'; \
327 return dest + sizeof(ent) - begin; \
328 } \
329 ++src; \
330 break;
331
332 MAP_ENTITY('>', "&gt;");
333 MAP_ENTITY('<', "&lt;");
334 MAP_ENTITY('\'', "&apos;");
335 MAP_ENTITY('"', "&quot;");
336 MAP_ENTITY('&', "&amp;");
337
338 // Not strictly necessary, but allows handling of comments without
339 // having to explicitly look for `--'
340 MAP_ENTITY('-', "&#x002D;");
341
342#undef MAP_ENTITY
343
344 case '\0':
345 *dest = '\0';
346 return dest - begin;
347
348 default:
349 *dest = *src;
350 ++dest;
351 ++src;
352 break;
353 }
354 }
355
356 // If we get here, dest was completely filled:
357 Q_ASSERT(dest == end && end > begin);
358 dest[-1] = '\0'; // hygiene, but it'll be ignored
359 return n;
360}
361
362int QXmlTestLogger::xmlCdata(QTestCharBuffer *destBuf, char const *src, qsizetype n)
363{
364 Q_ASSERT(n >= 512 && destBuf->size() == n);
365 char *dest = destBuf->data();
366
367 if (!src || !*src) {
368 Q_ASSERT(!dest[0]);
369 return 0;
370 }
371
372 static char const CDATA_END[] = "]]>";
373 static char const CDATA_END_ESCAPED[] = "]]]><![CDATA[]>";
374 const size_t CDATA_END_LEN = sizeof(CDATA_END) - 1;
375
376 char *begin = dest;
377 char *end = dest + n;
378 while (dest < end) {
379 if (!*src) {
380 *dest = '\0';
381 return dest - begin;
382 }
383
384 if (!strncmp(s1: src, s2: CDATA_END, n: CDATA_END_LEN)) {
385 if (dest + sizeof(CDATA_END_ESCAPED) < end) {
386 strcpy(dest: dest, src: CDATA_END_ESCAPED);
387 src += CDATA_END_LEN;
388 dest += sizeof(CDATA_END_ESCAPED) - 1;
389 } else {
390 *dest = '\0';
391 return dest + sizeof(CDATA_END_ESCAPED) - begin;
392 }
393 continue;
394 }
395
396 *dest = *src;
397 ++src;
398 ++dest;
399 }
400
401 // If we get here, dest was completely filled; caller shall grow and retry:
402 Q_ASSERT(dest == end && end > begin);
403 dest[-1] = '\0'; // hygiene, but it'll be ignored
404 return n;
405}
406
407typedef int (*StringFormatFunction)(QTestCharBuffer *, char const *, qsizetype);
408
409/*
410 A wrapper for string functions written to work with a fixed size buffer so they can be called
411 with a dynamically allocated buffer.
412*/
413static bool allocateStringFn(QTestCharBuffer *str, char const *src, StringFormatFunction func)
414{
415 constexpr int MAXSIZE = 1024 * 1024 * 2;
416 int size = str->size();
417 Q_ASSERT(size >= 512 && !str->data()[0]);
418
419 do {
420 const int res = func(str, src, size);
421 if (res < size) { // Success
422 Q_ASSERT(res > 0 || (!res && (!src || !src[0])));
423 return true;
424 }
425
426 // Buffer wasn't big enough, try again, if not too big:
427 size *= 2;
428 } while (size <= MAXSIZE && str->reset(newSize: size));
429
430 return false;
431}
432
433/*
434 Copy from \a src into \a destBuf, escaping any special XML characters as
435 necessary so that destBuf is suitable for use in an XML quoted attribute
436 string. Expands \a destBuf as needed to make room, up to a size of 2
437 MiB. Input requiring more than that much space for output is considered
438 invalid.
439
440 Returns 0 on invalid or empty input, the actual length written on success.
441*/
442bool QXmlTestLogger::xmlQuote(QTestCharBuffer *str, char const *src)
443{
444 return allocateStringFn(str, src, func: QXmlTestLogger::xmlQuote);
445}
446
447/*
448 Copy from \a src into \a destBuf, escaping any special strings such that
449 destBuf is suitable for use in an XML CDATA section. Expands \a destBuf as
450 needed to make room, up to a size of 2 MiB. Input requiring more than that
451 much space for output is considered invalid.
452
453 Returns 0 on invalid or empty input, the actual length written on success.
454*/
455bool QXmlTestLogger::xmlCdata(QTestCharBuffer *str, char const *src)
456{
457 return allocateStringFn(str, src, func: QXmlTestLogger::xmlCdata);
458}
459
460QT_END_NAMESPACE
461

source code of qtbase/src/testlib/qxmltestlogger.cpp