| 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 | |