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