1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2017 Borgar Ovsthus
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include <QtTest/private/qtestresult_p.h>
6#include <QtTest/qtestassert.h>
7#include <QtTest/private/qtestlog_p.h>
8#include <QtTest/private/qteamcitylogger_p.h>
9#include <QtCore/qbytearray.h>
10#include <private/qlocale_p.h>
11
12#include <limits.h>
13#include <stdarg.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17
18QT_BEGIN_NAMESPACE
19
20using namespace Qt::StringLiterals;
21
22namespace QTest {
23
24 static const char *tcIncidentType2String(QAbstractTestLogger::IncidentTypes type)
25 {
26 switch (type) {
27 case QAbstractTestLogger::Skip:
28 return "SKIP";
29 case QAbstractTestLogger::Pass:
30 return "PASS";
31 case QAbstractTestLogger::XFail:
32 return "XFAIL";
33 case QAbstractTestLogger::Fail:
34 return "FAIL!";
35 case QAbstractTestLogger::XPass:
36 return "XPASS";
37 case QAbstractTestLogger::BlacklistedPass:
38 return "BPASS";
39 case QAbstractTestLogger::BlacklistedFail:
40 return "BFAIL";
41 case QAbstractTestLogger::BlacklistedXPass:
42 return "BXPASS";
43 case QAbstractTestLogger::BlacklistedXFail:
44 return "BXFAIL";
45 }
46 return "??????";
47 }
48
49 static const char *tcMessageType2String(QAbstractTestLogger::MessageTypes type)
50 {
51 switch (type) {
52 case QAbstractTestLogger::QDebug:
53 return "QDEBUG";
54 case QAbstractTestLogger::QInfo:
55 return "QINFO";
56 case QAbstractTestLogger::QWarning:
57 return "QWARN";
58 case QAbstractTestLogger::QCritical:
59 return "QCRITICAL";
60 case QAbstractTestLogger::QFatal:
61 return "QFATAL";
62 case QAbstractTestLogger::Info:
63 return "INFO";
64 case QAbstractTestLogger::Warn:
65 return "WARNING";
66 }
67 return "??????";
68 }
69}
70
71/*! \internal
72 \class QTeamCityLogger
73 \inmodule QtTest
74
75 QTeamCityLogger implements logging in the \l{TeamCity} format.
76*/
77
78QTeamCityLogger::QTeamCityLogger(const char *filename)
79 : QAbstractTestLogger(filename)
80{
81}
82
83QTeamCityLogger::~QTeamCityLogger() = default;
84
85void QTeamCityLogger::startLogging()
86{
87 QAbstractTestLogger::startLogging();
88
89 tcEscapedString(buf: &flowID, str: QTestResult::currentTestObjectName());
90
91 QTestCharBuffer buf;
92 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testSuiteStarted name='%s' flowId='%s']\n",
93 flowID.constData(), flowID.constData());
94 outputString(msg: buf.constData());
95}
96
97void QTeamCityLogger::stopLogging()
98{
99 QTestCharBuffer buf;
100 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testSuiteFinished name='%s' flowId='%s']\n",
101 flowID.constData(), flowID.constData());
102 outputString(msg: buf.constData());
103
104 QAbstractTestLogger::stopLogging();
105}
106
107void QTeamCityLogger::enterTestFunction(const char * /*function*/)
108{
109 // don't print anything
110}
111
112void QTeamCityLogger::leaveTestFunction()
113{
114 // don't print anything
115}
116
117void QTeamCityLogger::addIncident(IncidentTypes type, const char *description,
118 const char *file, int line)
119{
120 // suppress B?PASS and B?XFAIL in silent mode
121 if ((type == Pass || type == XFail || type == BlacklistedPass || type == BlacklistedXFail) && QTestLog::verboseLevel() < 0)
122 return;
123
124 QTestCharBuffer buf;
125 QTestCharBuffer tmpFuncName;
126 escapedTestFuncName(buf: &tmpFuncName);
127
128 if (qstrcmp(str1: tmpFuncName.constData(), str2: currTestFuncName.constData()) != 0) {
129 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testStarted name='%s' flowId='%s']\n",
130 tmpFuncName.constData(), flowID.constData());
131 outputString(msg: buf.constData());
132
133 currTestFuncName.clear();
134 QTestPrivate::appendCharBuffer(accumulator: &currTestFuncName, more: tmpFuncName);
135 }
136
137 if (type == QAbstractTestLogger::XFail) {
138 addPendingMessage(type: QTest::tcIncidentType2String(type), msg: description, file, line);
139 return;
140 }
141
142 QTestCharBuffer detailedText;
143 tcEscapedString(buf: &detailedText, str: description);
144
145 // Test failed
146 if (type == Fail || type == XPass) {
147 QTestCharBuffer messageText;
148 if (file)
149 QTest::qt_asprintf(buf: &messageText, format: "Failure! |[Loc: %s(%d)|]", file, line);
150 else
151 QTest::qt_asprintf(buf: &messageText, format: "Failure!");
152
153 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testFailed name='%s' message='%s' details='%s'"
154 " flowId='%s']\n", tmpFuncName.constData(), messageText.constData(),
155 detailedText.constData(), flowID.constData());
156
157 outputString(msg: buf.constData());
158 } else if (type == Skip) {
159 if (file) {
160 QTestCharBuffer detail;
161 QTest::qt_asprintf(buf: &detail, format: " |[Loc: %s(%d)|]", file, line);
162 QTestPrivate::appendCharBuffer(accumulator: &detailedText, more: detail);
163 }
164
165 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testIgnored name='%s' message='%s' flowId='%s']\n",
166 currTestFuncName.constData(), detailedText.constData(),
167 flowID.constData());
168
169 outputString(msg: buf.constData());
170 }
171
172 if (!pendingMessages.isEmpty()) {
173 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testStdOut name='%s' out='%s' flowId='%s']\n",
174 tmpFuncName.constData(), pendingMessages.constData(),
175 flowID.constData());
176
177 outputString(msg: buf.constData());
178 pendingMessages.clear();
179 }
180
181 QTest::qt_asprintf(buf: &buf, format: "##teamcity[testFinished name='%s' flowId='%s']\n",
182 tmpFuncName.constData(), flowID.constData());
183 outputString(msg: buf.constData());
184}
185
186void QTeamCityLogger::addBenchmarkResult(const QBenchmarkResult &)
187{
188 // don't print anything
189}
190
191void QTeamCityLogger::addMessage(MessageTypes type, const QString &message,
192 const char *file, int line)
193{
194 // suppress non-fatal messages in silent mode
195 if (type != QFatal && QTestLog::verboseLevel() < 0)
196 return;
197
198 QTestCharBuffer escapedMessage;
199 tcEscapedString(buf: &escapedMessage, qUtf8Printable(message));
200 addPendingMessage(type: QTest::tcMessageType2String(type), msg: escapedMessage.constData(), file, line);
201}
202
203void QTeamCityLogger::tcEscapedString(QTestCharBuffer *buf, const char *str) const
204{
205 {
206 size_t size = qstrlen(str) + 1;
207 for (const char *p = str; *p; ++p) {
208 if (strchr(s: "\n\r|[]'", c: *p))
209 ++size;
210 }
211 Q_ASSERT(size <= size_t(INT_MAX));
212 buf->resize(newSize: int(size));
213 }
214
215 bool swallowSpace = true;
216 char *p = buf->data();
217 for (; *str; ++str) {
218 char ch = *str;
219 switch (ch) {
220 case '\n':
221 p++[0] = '|';
222 ch = 'n';
223 swallowSpace = false;
224 break;
225 case '\r':
226 p++[0] = '|';
227 ch = 'r';
228 swallowSpace = false;
229 break;
230 case '|':
231 case '[':
232 case ']':
233 case '\'':
234 p++[0] = '|';
235 swallowSpace = false;
236 break;
237 default:
238 if (ascii_isspace(c: ch)) {
239 if (swallowSpace)
240 continue;
241 swallowSpace = true;
242 ch = ' ';
243 } else {
244 swallowSpace = false;
245 }
246 break;
247 }
248 p++[0] = ch;
249 }
250 Q_ASSERT(p < buf->data() + buf->size());
251 if (swallowSpace && p > buf->data()) {
252 Q_ASSERT(p[-1] == ' ');
253 --p;
254 }
255 Q_ASSERT(p == buf->data() || !ascii_isspace(p[-1]));
256 *p = '\0';
257}
258
259void QTeamCityLogger::escapedTestFuncName(QTestCharBuffer *buf) const
260{
261 constexpr int TestTag = QTestPrivate::TestFunction | QTestPrivate::TestDataTag;
262 QTestPrivate::generateTestIdentifier(identifier: buf, parts: TestTag);
263}
264
265void QTeamCityLogger::addPendingMessage(const char *type, const char *msg,
266 const char *file, int line)
267{
268 const char *pad = pendingMessages.isEmpty() ? "" : "|n";
269
270 QTestCharBuffer newMessage;
271 if (file)
272 QTest::qt_asprintf(buf: &newMessage, format: "%s%s |[Loc: %s(%d)|]: %s", pad, type, file, line, msg);
273 else
274 QTest::qt_asprintf(buf: &newMessage, format: "%s%s: %s", pad, type, msg);
275
276 QTestPrivate::appendCharBuffer(accumulator: &pendingMessages, more: newMessage);
277}
278
279QT_END_NAMESPACE
280

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