1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtTest module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qtaptestlogger_p.h"
41 
42 #include "qtestlog_p.h"
43 #include "qtestresult_p.h"
44 #include "qtestassert.h"
45 
46 #if QT_CONFIG(regularexpression)
47 #  include <QtCore/qregularexpression.h>
48 #endif
49 
50 QT_BEGIN_NAMESPACE
51 
QTapTestLogger(const char * filename)52 QTapTestLogger::QTapTestLogger(const char *filename)
53     : QAbstractTestLogger(filename)
54     , m_wasExpectedFail(false)
55 {
56 }
57 
58 QTapTestLogger::~QTapTestLogger() = default;
59 
startLogging()60 void QTapTestLogger::startLogging()
61 {
62     QAbstractTestLogger::startLogging();
63 
64     QTestCharBuffer preamble;
65     QTest::qt_asprintf(&preamble, "TAP version 13\n"
66         // By convention, test suite names are output as diagnostics lines
67         // This is a pretty poor convention, as consumers will then treat
68         // actual diagnostics, e.g. qDebug, as test suite names o_O
69         "# %s\n", QTestResult::currentTestObjectName());
70     outputString(preamble.data());
71 }
72 
stopLogging()73 void QTapTestLogger::stopLogging()
74 {
75     const int total = QTestLog::totalCount();
76 
77     QTestCharBuffer testPlanAndStats;
78     QTest::qt_asprintf(&testPlanAndStats,
79         "1..%d\n"
80         "# tests %d\n"
81         "# pass %d\n"
82         "# fail %d\n",
83     total, total, QTestLog::passCount(), QTestLog::failCount());
84     outputString(testPlanAndStats.data());
85 
86     QAbstractTestLogger::stopLogging();
87 }
88 
enterTestFunction(const char * function)89 void QTapTestLogger::enterTestFunction(const char *function)
90 {
91     Q_UNUSED(function);
92     m_wasExpectedFail = false;
93 }
94 
enterTestData(QTestData * data)95 void QTapTestLogger::enterTestData(QTestData *data)
96 {
97     Q_UNUSED(data);
98     m_wasExpectedFail = false;
99 }
100 
101 using namespace QTestPrivate;
102 
outputTestLine(bool ok,int testNumber,QTestCharBuffer & directive)103 void QTapTestLogger::outputTestLine(bool ok, int testNumber, QTestCharBuffer &directive)
104 {
105     QTestCharBuffer testIdentifier;
106     QTestPrivate::generateTestIdentifier(&testIdentifier, TestFunction | TestDataTag);
107 
108     QTestCharBuffer testLine;
109     QTest::qt_asprintf(&testLine, "%s %d - %s%s\n",
110         ok ? "ok" : "not ok", testNumber, testIdentifier.data(), directive.data());
111 
112     outputString(testLine.data());
113 }
114 
addIncident(IncidentTypes type,const char * description,const char * file,int line)115 void QTapTestLogger::addIncident(IncidentTypes type, const char *description,
116                                    const char *file, int line)
117 {
118     if (m_wasExpectedFail && type == Pass) {
119         // XFail comes with a corresponding Pass incident, but we only want
120         // to emit a single test point for it, so skip the this pass.
121         return;
122     }
123 
124     bool ok = type == Pass || type == XPass || type == BlacklistedPass || type == BlacklistedXPass;
125 
126     QTestCharBuffer directive;
127     if (type == XFail || type == XPass || type == BlacklistedFail || type == BlacklistedPass
128             || type == BlacklistedXFail || type == BlacklistedXPass) {
129         // We treat expected or blacklisted failures/passes as TODO-failures/passes,
130         // which should be treated as soft issues by consumers. Not all do though :/
131         QTest::qt_asprintf(&directive, " # TODO %s", description);
132     }
133 
134     int testNumber = QTestLog::totalCount();
135     if (type == XFail) {
136         // The global test counter hasn't been updated yet for XFail
137         testNumber += 1;
138     }
139 
140     outputTestLine(ok, testNumber, directive);
141 
142     if (!ok) {
143         // All failures need a diagnostics sections to not confuse consumers
144 
145         // The indent needs to be two spaces for maximum compatibility
146         #define YAML_INDENT "  "
147 
148         outputString(YAML_INDENT "---\n");
149 
150         if (type != XFail) {
151 #if QT_CONFIG(regularexpression)
152             // This is fragile, but unfortunately testlib doesn't plumb
153             // the expected and actual values to the loggers (yet).
154             static QRegularExpression verifyRegex(
155                 QLatin1String("^'(?<actualexpression>.*)' returned (?<actual>\\w+).+\\((?<message>.*)\\)$"));
156 
157             static QRegularExpression comparRegex(
158                 QLatin1String("^(?<message>.*)\n"
159                     "\\s*Actual\\s+\\((?<actualexpression>.*)\\)\\s*: (?<actual>.*)\n"
160                     "\\s*Expected\\s+\\((?<expectedexpresssion>.*)\\)\\s*: (?<expected>.*)$"));
161 
162             QString descriptionString = QString::fromUtf8(description);
163             QRegularExpressionMatch match = verifyRegex.match(descriptionString);
164             if (!match.hasMatch())
165                 match = comparRegex.match(descriptionString);
166 
167             if (match.hasMatch()) {
168                 bool isVerify = match.regularExpression() == verifyRegex;
169                 QString message = match.captured(QLatin1String("message"));
170                 QString expected;
171                 QString actual;
172 
173                 if (isVerify) {
174                     QString expression = QLatin1String(" (")
175                         % match.captured(QLatin1String("actualexpression")) % QLatin1Char(')') ;
176                     actual = match.captured(QLatin1String("actual")).toLower() % expression;
177                     expected = (actual.startsWith(QLatin1String("true")) ? QLatin1String("false") : QLatin1String("true")) % expression;
178                     if (message.isEmpty())
179                         message = QLatin1String("Verification failed");
180                 } else {
181                     expected = match.captured(QLatin1String("expected"))
182                         % QLatin1String(" (") % match.captured(QLatin1String("expectedexpresssion")) % QLatin1Char(')');
183                     actual = match.captured(QLatin1String("actual"))
184                         % QLatin1String(" (") % match.captured(QLatin1String("actualexpression")) % QLatin1Char(')');
185                 }
186 
187                 QTestCharBuffer diagnosticsYamlish;
188                 QTest::qt_asprintf(&diagnosticsYamlish,
189                     YAML_INDENT "type: %s\n"
190                     YAML_INDENT "message: %s\n"
191 
192                     // Some consumers understand 'wanted/found', while others need
193                     // 'expected/actual', so we do both for maximum compatibility.
194                     YAML_INDENT "wanted: %s\n"
195                     YAML_INDENT "found: %s\n"
196                     YAML_INDENT "expected: %s\n"
197                     YAML_INDENT "actual: %s\n",
198 
199                     isVerify ? "QVERIFY" : "QCOMPARE",
200                     qPrintable(message),
201                     qPrintable(expected), qPrintable(actual),
202                     qPrintable(expected), qPrintable(actual)
203                 );
204 
205                 outputString(diagnosticsYamlish.data());
206             } else {
207                 QTestCharBuffer unparsableDescription;
208                 QTest::qt_asprintf(&unparsableDescription,
209                     YAML_INDENT "# %s\n", description);
210                 outputString(unparsableDescription.data());
211             }
212 #else
213             QTestCharBuffer unparsableDescription;
214             QTest::qt_asprintf(&unparsableDescription,
215                 YAML_INDENT "# %s\n", description);
216             outputString(unparsableDescription.data());
217 #endif
218         }
219 
220         if (file) {
221             QTestCharBuffer location;
222             QTest::qt_asprintf(&location,
223                 // The generic 'at' key is understood by most consumers.
224                 YAML_INDENT "at: %s::%s() (%s:%d)\n"
225 
226                 // The file and line keys are for consumers that are able
227                 // to read more granular location info.
228                 YAML_INDENT "file: %s\n"
229                 YAML_INDENT "line: %d\n",
230 
231                 QTestResult::currentTestObjectName(),
232                 QTestResult::currentTestFunction(),
233                 file, line, file, line
234             );
235             outputString(location.data());
236         }
237 
238         outputString(YAML_INDENT "...\n");
239     }
240 
241     m_wasExpectedFail = type == XFail;
242 }
243 
addMessage(MessageTypes type,const QString & message,const char * file,int line)244 void QTapTestLogger::addMessage(MessageTypes type, const QString &message,
245                     const char *file, int line)
246 {
247     Q_UNUSED(file);
248     Q_UNUSED(line);
249 
250     if (type == Skip) {
251         QTestCharBuffer directive;
252         QTest::qt_asprintf(&directive, " # SKIP %s", message.toUtf8().constData());
253         outputTestLine(/* ok  = */ true, QTestLog::totalCount(), directive);
254         return;
255     }
256 
257     QTestCharBuffer diagnostics;
258     QTest::qt_asprintf(&diagnostics, "# %s\n", qPrintable(message));
259     outputString(diagnostics.data());
260 }
261 
262 QT_END_NAMESPACE
263 
264