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