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 <QtTest/private/qjunittestlogger_p.h>
5#include <QtTest/private/qtestelement_p.h>
6#include <QtTest/private/qtestjunitstreamer_p.h>
7#include <QtTest/qtestcase.h>
8#include <QtTest/private/qtestresult_p.h>
9#include <QtTest/private/qbenchmark_p.h>
10#include <QtTest/private/qtestlog_p.h>
11
12#include <QtCore/qelapsedtimer.h>
13#include <QtCore/qlibraryinfo.h>
14
15#include <cstdio>
16
17#include <string.h>
18
19QT_BEGIN_NAMESPACE
20/*! \internal
21 \class QJUnitTestLogger
22 \inmodule QtTest
23
24 QJUnitTestLogger implements logging in a JUnit-compatible XML format.
25
26 The \l{JUnit XML} format was originally developed for Java testing.
27 It is supported by \l{Test Center}.
28*/
29// QTBUG-95424 links to further useful documentation.
30
31QJUnitTestLogger::QJUnitTestLogger(const char *filename)
32 : QAbstractTestLogger(filename)
33{
34}
35
36QJUnitTestLogger::~QJUnitTestLogger()
37{
38 Q_ASSERT(!currentTestSuite);
39 delete logFormatter;
40}
41
42// We track test timing per test case, so we
43// need to maintain our own elapsed timer.
44Q_CONSTINIT static QElapsedTimer elapsedTestcaseTime;
45static qreal elapsedTestCaseSeconds()
46{
47 return elapsedTestcaseTime.nsecsElapsed() / 1e9;
48}
49
50static QByteArray toSecondsFormat(qreal ms)
51{
52 return QByteArray::number(ms / 1000, format: 'f', precision: 3);
53}
54
55void QJUnitTestLogger::startLogging()
56{
57 QAbstractTestLogger::startLogging();
58
59 logFormatter = new QTestJUnitStreamer(this);
60
61 Q_ASSERT(!currentTestSuite);
62 currentTestSuite = new QTestElement(QTest::LET_TestSuite);
63 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Name, value: QTestResult::currentTestObjectName());
64
65 auto localTime = QDateTime::currentDateTime();
66 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Timestamp,
67 value: localTime.toString(format: Qt::ISODate).toUtf8().constData());
68
69 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Hostname,
70 value: QSysInfo::machineHostName().toUtf8().constData());
71
72 QTestElement *property;
73 QTestElement *properties = new QTestElement(QTest::LET_Properties);
74
75 property = new QTestElement(QTest::LET_Property);
76 property->addAttribute(attributeIndex: QTest::AI_Name, value: "QTestVersion");
77 property->addAttribute(attributeIndex: QTest::AI_PropertyValue, QTEST_VERSION_STR);
78 properties->addChild(element: property);
79
80 property = new QTestElement(QTest::LET_Property);
81 property->addAttribute(attributeIndex: QTest::AI_Name, value: "QtVersion");
82 property->addAttribute(attributeIndex: QTest::AI_PropertyValue, value: qVersion());
83 properties->addChild(element: property);
84
85 property = new QTestElement(QTest::LET_Property);
86 property->addAttribute(attributeIndex: QTest::AI_Name, value: "QtBuild");
87 property->addAttribute(attributeIndex: QTest::AI_PropertyValue, value: QLibraryInfo::build());
88 properties->addChild(element: property);
89
90 currentTestSuite->addChild(element: properties);
91
92 elapsedTestcaseTime.start();
93}
94
95void QJUnitTestLogger::stopLogging()
96{
97 char buf[10];
98
99 std::snprintf(s: buf, maxlen: sizeof(buf), format: "%i", testCounter);
100 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Tests, value: buf);
101
102 std::snprintf(s: buf, maxlen: sizeof(buf), format: "%i", failureCounter);
103 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Failures, value: buf);
104
105 std::snprintf(s: buf, maxlen: sizeof(buf), format: "%i", errorCounter);
106 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Errors, value: buf);
107
108 std::snprintf(s: buf, maxlen: sizeof(buf), format: "%i", QTestLog::skipCount());
109 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Skipped, value: buf);
110
111 currentTestSuite->addAttribute(attributeIndex: QTest::AI_Time,
112 value: toSecondsFormat(ms: QTestLog::msecsTotalTime()).constData());
113
114 for (auto *testCase : listOfTestcases)
115 currentTestSuite->addChild(element: testCase);
116 listOfTestcases.clear();
117
118 logFormatter->output(element: currentTestSuite);
119
120 delete currentTestSuite;
121 currentTestSuite = nullptr;
122
123 QAbstractTestLogger::stopLogging();
124}
125
126void QJUnitTestLogger::enterTestFunction(const char *function)
127{
128 enterTestCase(name: function);
129}
130
131void QJUnitTestLogger::enterTestCase(const char *name)
132{
133 {
134 QMutexLocker locker(&mutex);
135 currentTestCase = new QTestElement(QTest::LET_TestCase);
136 currentTestCase->addAttribute(attributeIndex: QTest::AI_Name, value: name);
137 currentTestCase->addAttribute(attributeIndex: QTest::AI_Classname, value: QTestResult::currentTestObjectName());
138 listOfTestcases.push_back(x: currentTestCase);
139
140 Q_ASSERT(!systemOutputElement && !systemErrorElement);
141 systemOutputElement = new QTestElement(QTest::LET_SystemOutput);
142 systemErrorElement = new QTestElement(QTest::LET_SystemError);
143 }
144
145 // The element will be deleted when the suite is deleted
146
147 ++testCounter;
148
149 elapsedTestcaseTime.start();
150}
151
152void QJUnitTestLogger::enterTestData(QTestData *)
153{
154 QTestCharBuffer testIdentifier;
155 QTestPrivate::generateTestIdentifier(identifier: &testIdentifier,
156 parts: QTestPrivate::TestFunction | QTestPrivate::TestDataTag);
157
158 static const char *lastTestFunction = nullptr;
159 if (QTestResult::currentTestFunction() != lastTestFunction) {
160 // Adopt existing testcase for the initial test data
161 auto *name = const_cast<QTestElementAttribute*>(
162 currentTestCase->attribute(index: QTest::AI_Name));
163 name->setPair(attributeIndex: QTest::AI_Name, value: testIdentifier.data());
164 lastTestFunction = QTestResult::currentTestFunction();
165 elapsedTestcaseTime.start();
166 } else {
167 // Create new test cases for remaining test data
168 leaveTestCase();
169 enterTestCase(name: testIdentifier.data());
170 }
171}
172
173void QJUnitTestLogger::leaveTestFunction()
174{
175 leaveTestCase();
176}
177
178void QJUnitTestLogger::leaveTestCase()
179{
180 QMutexLocker locker(&mutex);
181 currentTestCase->addAttribute(attributeIndex: QTest::AI_Time,
182 value: toSecondsFormat(ms: elapsedTestCaseSeconds() * 1000).constData());
183
184 if (!systemOutputElement->childElements().empty())
185 currentTestCase->addChild(element: systemOutputElement);
186 else
187 delete systemOutputElement;
188
189 if (!systemErrorElement->childElements().empty())
190 currentTestCase->addChild(element: systemErrorElement);
191 else
192 delete systemErrorElement;
193
194 systemOutputElement = nullptr;
195 systemErrorElement = nullptr;
196}
197
198void QJUnitTestLogger::addIncident(IncidentTypes type, const char *description,
199 const char *file, int line)
200{
201 if (type == Fail || type == XPass) {
202 auto failureType = [&]() {
203 switch (type) {
204 case QAbstractTestLogger::Fail: return "fail";
205 case QAbstractTestLogger::XPass: return "xpass";
206 default: Q_UNREACHABLE();
207 }
208 }();
209
210 addFailure(elementType: QTest::LET_Failure, failureType, failureDescription: QString::fromUtf8(utf8: description));
211 } else if (type == XFail) {
212 // Since XFAIL does not add a failure to the testlog in JUnit XML we add a
213 // message, so we still have some information about the expected failure.
214 addMessage(type: Info, message: QString::fromUtf8(utf8: description), file, line);
215 } else if (type == Skip) {
216 auto skippedElement = new QTestElement(QTest::LET_Skipped);
217 skippedElement->addAttribute(attributeIndex: QTest::AI_Message, value: description);
218 currentTestCase->addChild(element: skippedElement);
219 }
220}
221
222void QJUnitTestLogger::addFailure(QTest::LogElementType elementType,
223 const char *failureType, const QString &failureDescription)
224{
225 if (elementType == QTest::LET_Failure) {
226 // Make sure we're not adding failure when we already have error,
227 // or adding additional failures when we already have a failure.
228 for (auto *childElement : currentTestCase->childElements()) {
229 if (childElement->elementType() == QTest::LET_Error ||
230 childElement->elementType() == QTest::LET_Failure)
231 return;
232 }
233 }
234
235 QTestElement *failureElement = new QTestElement(elementType);
236 failureElement->addAttribute(attributeIndex: QTest::AI_Type, value: failureType);
237
238 // Assume the first line is the message, and the remainder are details
239 QString message = failureDescription.section(asep: u'\n', astart: 0, aend: 0);
240 QString details = failureDescription.section(asep: u'\n', astart: 1);
241
242 failureElement->addAttribute(attributeIndex: QTest::AI_Message, value: message.toUtf8().constData());
243
244 if (!details.isEmpty()) {
245 auto textNode = new QTestElement(QTest::LET_Text);
246 textNode->addAttribute(attributeIndex: QTest::AI_Value, value: details.toUtf8().constData());
247 failureElement->addChild(element: textNode);
248 }
249
250 currentTestCase->addChild(element: failureElement);
251
252 switch (elementType) {
253 case QTest::LET_Failure: ++failureCounter; break;
254 case QTest::LET_Error: ++errorCounter; break;
255 default: Q_UNREACHABLE();
256 }
257}
258
259void QJUnitTestLogger::addMessage(MessageTypes type, const QString &message, const char *file, int line)
260{
261 Q_UNUSED(file);
262 Q_UNUSED(line);
263
264 QMutexLocker locker(&mutex);
265 if (type == QFatal) {
266 addFailure(elementType: QTest::LET_Error, failureType: "qfatal", failureDescription: message);
267 return;
268 }
269
270 auto systemLogElement = [&]() {
271 switch (type) {
272 case QAbstractTestLogger::QDebug:
273 case QAbstractTestLogger::Info:
274 case QAbstractTestLogger::QInfo:
275 return systemOutputElement;
276 case QAbstractTestLogger::Warn:
277 case QAbstractTestLogger::QWarning:
278 case QAbstractTestLogger::QCritical:
279 return systemErrorElement;
280 default:
281 Q_UNREACHABLE();
282 }
283 }();
284
285 if (!systemLogElement)
286 return; // FIXME: Handle messages outside of test functions
287
288 auto textNode = new QTestElement(QTest::LET_Text);
289 textNode->addAttribute(attributeIndex: QTest::AI_Value, value: message.toUtf8().constData());
290 systemLogElement->addChild(element: textNode);
291}
292
293QT_END_NAMESPACE
294
295

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