1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
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 General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QDomDocument>
30#include <QFile>
31#include <QFileInfo>
32#include <QRegExp>
33#include <QtDebug>
34#include <QUrl>
35#include <QXmlAttributes>
36#include <QXmlSimpleReader>
37
38#include <private/qxmldebug_p.h>
39#include "XMLWriter.h"
40
41#include "TestBaseLine.h"
42
43using namespace QPatternistSDK;
44using namespace QPatternist;
45
46Q_GLOBAL_STATIC_WITH_ARGS(QRegExp, errorRegExp, (QLatin1String("[A-Z]{4}[0-9]{4}")))
47
48TestBaseLine::TestBaseLine(const Type t) : m_type(t)
49{
50 Q_ASSERT(errorRegExp()->isValid());
51}
52
53TestResult::Status TestBaseLine::scan(const QString &serialized,
54 const TestBaseLine::List &lines)
55{
56 Q_ASSERT_X(lines.count() >= 1, Q_FUNC_INFO,
57 "At least one base line must be passed, otherwise there's nothing "
58 "to compare to.");
59
60 const TestBaseLine::List::const_iterator end(lines.constEnd());
61 TestBaseLine::List::const_iterator it(lines.constBegin());
62 for(; it != end; ++it)
63 {
64 const TestResult::Status retval((*it)->verify(serializedInput: serialized));
65
66 if(retval == TestResult::Pass || retval == TestResult::NotTested)
67 return retval;
68 }
69
70 return TestResult::Fail;
71}
72
73TestResult::Status TestBaseLine::scanErrors(const ErrorHandler::Message::List &errors,
74 const TestBaseLine::List &lines)
75{
76 pDebug() << "TestBaseLine::scanErrors()";
77
78 /* 1. Find the first error in @p errors that's a Patternist
79 * error(not warning and not from Qt) and extract the error code. */
80 QString errorCode;
81
82 const ErrorHandler::Message::List::const_iterator end(errors.constEnd());
83 ErrorHandler::Message::List::const_iterator it(errors.constBegin());
84 for(; it != end; ++it)
85 {
86 if((*it).type() != QtFatalMsg)
87 continue;
88
89 errorCode = QUrl((*it).identifier()).fragment();
90
91 pDebug() << "ERR:" << (*it).description();
92 /* This is hackish. We have no way of determining whether a Message
93 * is actually issued from Patternist, so we try to narrow it down like this. */
94 if(errorRegExp()->exactMatch(str: errorCode))
95 break; /* It's an error code. */
96 else
97 errorCode.clear();
98 }
99
100 pDebug() << "Got error code: " << errorCode;
101 /* 2. Loop through @p lines, and for the first base line
102 * which is of type ExpectedError and which matches @p errorCode
103 * return Pass, otherwise Fail. */
104 const TestBaseLine::List::const_iterator blend(lines.constEnd());
105 TestBaseLine::List::const_iterator blit(lines.constBegin());
106 for(; blit != blend; ++blit)
107 {
108 const Type t = (*blit)->type();
109
110 if(t == TestBaseLine::ExpectedError)
111 {
112 const QString d((*blit)->details());
113 if(d == errorCode || d == QChar::fromLatin1(c: '*'))
114 return TestResult::Pass;
115 }
116 }
117
118 return TestResult::Fail;
119}
120
121void TestBaseLine::toXML(XMLWriter &receiver) const
122{
123 switch(m_type)
124 {
125 case XML: /* Fallthrough. */
126 case Fragment: /* Fallthrough. */
127 case SchemaIsValid: /* Fallthrough. */
128 case Text:
129 {
130 QXmlStreamAttributes inspectAtts;
131 inspectAtts.append(qualifiedName: QLatin1String("role"), value: QLatin1String("principal"));
132 inspectAtts.append(qualifiedName: QLatin1String("compare"), value: displayName(id: m_type));
133 receiver.startElement(qName: QLatin1String("output-file"), atts: inspectAtts);
134 receiver.characters(ch: m_details);
135 receiver.endElement(qName: QLatin1String("output-file"));
136 return;
137 }
138 case Ignore:
139 {
140 Q_ASSERT_X(false, Q_FUNC_INFO, "Serializing 'Ignore' is not implemented.");
141 return;
142 }
143 case Inspect:
144 {
145 QXmlStreamAttributes inspectAtts;
146 inspectAtts.append(qualifiedName: QLatin1String("role"), value: QLatin1String("principal"));
147 inspectAtts.append(qualifiedName: QLatin1String("compare"), value: QLatin1String("Inspect"));
148 receiver.startElement(qName: QLatin1String("output-file"), atts: inspectAtts);
149 receiver.characters(ch: m_details);
150 receiver.endElement(qName: QLatin1String("output-file"));
151 return;
152 }
153 case ExpectedError:
154 {
155 receiver.startElement(qName: QLatin1String("expected-error"));
156 receiver.characters(ch: m_details);
157 receiver.endElement(qName: QLatin1String("expected-error"));
158 return;
159 }
160 }
161}
162
163bool TestBaseLine::isChildrenDeepEqual(const QDomNodeList &cl1, const QDomNodeList &cl2)
164{
165 const int len = cl1.length();
166
167 if(len == cl2.length())
168 {
169 for (int i = 0; i < len; ++i) {
170 if(!isDeepEqual(n1: cl1.at(index: i), n2: cl2.at(index: i)))
171 return false;
172 }
173
174 return true;
175 }
176 else
177 return false;
178}
179
180bool TestBaseLine::isAttributesEqual(const QDomNamedNodeMap &cl1, const QDomNamedNodeMap &cl2)
181{
182 const int len = cl1.length();
183 pDebug() << "LEN:" << len;
184
185 if(len == cl2.length())
186 {
187 for (int i1 = 0; i1 < len; ++i1) {
188 const QDomNode attr1(cl1.item(index: i1));
189 Q_ASSERT(!attr1.isNull());
190
191 /* This is set if attr1 cannot be found at all in cl2. */
192 bool earlyExit = false;
193
194 for (int i2 = 0; i2 < len; ++i2) {
195 const QDomNode attr2(cl2.item(index: i2));
196 Q_ASSERT(!attr2.isNull());
197 pDebug() << "ATTR1:" << attr1.localName() << attr1.namespaceURI() << attr1.prefix() << attr1.nodeName();
198 pDebug() << "ATTR2:" << attr2.localName() << attr2.namespaceURI() << attr2.prefix() << attr2.nodeName();
199
200 if(attr1.localName() == attr2.localName() &&
201 attr1.namespaceURI() == attr2.namespaceURI() &&
202 attr1.prefix() == attr2.prefix() &&
203 attr1.nodeName() == attr2.nodeName() && /* Yes, needed in addition to all the other. */
204 attr1.nodeValue() == attr2.nodeValue())
205 {
206 earlyExit = true;
207 break;
208 }
209 }
210
211 if(!earlyExit)
212 {
213 /* An attribute was found that doesn't exist in the other list so exit. */
214 return false;
215 }
216 }
217
218 return true;
219 }
220 else
221 return false;
222}
223
224bool TestBaseLine::isDeepEqual(const QDomNode &n1, const QDomNode &n2)
225{
226 if(n1.nodeType() != n2.nodeType())
227 return false;
228
229 switch(n1.nodeType())
230 {
231 case QDomNode::CommentNode:
232 /* Fallthrough. */
233 case QDomNode::TextNode:
234 {
235 return static_cast<const QDomCharacterData &>(n1).data() ==
236 static_cast<const QDomCharacterData &>(n2).data();
237 }
238 case QDomNode::ProcessingInstructionNode:
239 {
240 return n1.nodeName() == n2.nodeName() &&
241 n1.nodeValue() == n2.nodeValue();
242 }
243 case QDomNode::DocumentNode:
244 return isChildrenDeepEqual(cl1: n1.childNodes(), cl2: n2.childNodes());
245 case QDomNode::ElementNode:
246 {
247 return n1.localName() == n2.localName() &&
248 n1.namespaceURI() == n2.namespaceURI() &&
249 n1.nodeName() == n2.nodeName() && /* Yes, this one is needed in addition to localName(). */
250 isAttributesEqual(cl1: n1.attributes(), cl2: n2.attributes()) &&
251 isChildrenDeepEqual(cl1: n1.childNodes(), cl2: n2.childNodes());
252 }
253 /* Fallthrough all these. */
254 case QDomNode::EntityReferenceNode:
255 case QDomNode::CDATASectionNode:
256 case QDomNode::EntityNode:
257 case QDomNode::DocumentTypeNode:
258 case QDomNode::DocumentFragmentNode:
259 case QDomNode::NotationNode:
260 case QDomNode::BaseNode:
261 case QDomNode::CharacterDataNode:
262 {
263 Q_ASSERT_X(false, Q_FUNC_INFO,
264 "An unsupported node type was encountered.");
265 return false;
266 }
267 case QDomNode::AttributeNode:
268 {
269 Q_ASSERT_X(false, Q_FUNC_INFO,
270 "This should never happen. QDom doesn't allow us to compare DOM attributes "
271 "properly.");
272 return false;
273 }
274 default:
275 {
276 Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled QDom::NodeType value.");
277 return false;
278 }
279 }
280}
281
282TestResult::Status TestBaseLine::verify(const QString &serializedInput) const
283{
284 switch(m_type)
285 {
286 case SchemaIsValid:
287 /* Fall through. */
288 case Text:
289 {
290 if(serializedInput == details())
291 return TestResult::Pass;
292 else
293 return TestResult::Fail;
294 }
295 case Fragment:
296 /* Fall through. */
297 case XML:
298 {
299 /* Read the baseline and the serialized input into two QDomDocuments, and compare
300 * them deeply. We wrap fragments in a root node such that it is well-formed XML.
301 */
302
303 QDomDocument output;
304 {
305 /* The reason we put things into a QByteArray and then parse it through QXmlSimpleReader, is that
306 * QDomDocument does whitespace stripping when calling setContent(QString). In other words,
307 * this workarounds a bug. */
308
309 const bool success =
310 output.setContent(text: (m_type == XML ? serializedInput
311 : QLatin1String("<r>") + serializedInput
312 + QLatin1String("</r>"))
313 .toUtf8());
314
315 if(!success)
316 return TestResult::Fail;
317
318 Q_ASSERT(success);
319 }
320
321 QDomDocument baseline;
322 {
323 QString baselineReadingError;
324 const bool success = baseline.setContent(
325 text: (m_type == XML ? details()
326 : QLatin1String("<r>") + details() + QLatin1String("</r>"))
327 .toUtf8(),
328 errorMsg: &baselineReadingError);
329 if(!success)
330 return TestResult::Fail;
331
332 /* This piece of code workaround a bug in QDom, which treats XML prologs as processing
333 * instructions and make them available in the tree as so. */
334 if(m_type == XML)
335 {
336 /* $doc/r/node() */
337 const QDomNodeList children(baseline.childNodes());
338 const int len = children.length();
339
340 for(int i = 0; i < len; ++i)
341 {
342 const QDomNode &child = children.at(index: i);
343 if(child.isProcessingInstruction() && child.nodeName() == QLatin1String("xml"))
344 {
345 baseline.removeChild(oldChild: child);
346 break;
347 }
348 }
349 }
350
351 Q_ASSERT_X(baselineReadingError.isNull(), Q_FUNC_INFO,
352 qPrintable((QLatin1String("Reading the baseline failed: ") + baselineReadingError)));
353 }
354
355 if(isDeepEqual(n1: output, n2: baseline))
356 return TestResult::Pass;
357 else
358 {
359 pDebug() << "FAILURE:" << output.toString() << "is NOT IDENTICAL to(baseline):" << baseline.toString();
360 return TestResult::Fail;
361 }
362 }
363 case Ignore:
364 return TestResult::Pass;
365 case Inspect:
366 return TestResult::NotTested;
367 case ExpectedError:
368 {
369 /* This function is only called for Text/XML/Fragment tests. */
370 return TestResult::Fail;
371 }
372 }
373 Q_ASSERT(false);
374 return TestResult::Fail;
375}
376
377TestBaseLine::Type TestBaseLine::identifierFromString(const QString &string)
378{
379 /* "html-output: Using an ad hoc tool, it must assert that the document obeys the HTML
380 * Output Method as defined in the Serialization specification and section
381 * 20 of the XSLT 2.0 specification." We treat it as XML for now, same with
382 * xhtml-output. */
383 if(string.compare(other: QLatin1String("XML"), cs: Qt::CaseInsensitive) == 0 ||
384 string == QLatin1String("html-output") ||
385 string == QLatin1String("xml-output") ||
386 string == QLatin1String("xhtml-output"))
387 return XML;
388 else if(string == QLatin1String("Fragment") || string == QLatin1String("xml-frag"))
389 return Fragment;
390 else if(string.compare(other: QLatin1String("Text"), cs: Qt::CaseInsensitive) == 0)
391 return Text;
392 else if(string == QLatin1String("Ignore"))
393 return Ignore;
394 else if(string.compare(other: QLatin1String("Inspect"), cs: Qt::CaseInsensitive) == 0)
395 return Inspect;
396 else
397 {
398 Q_ASSERT_X(false, Q_FUNC_INFO,
399 qPrintable(QString::fromLatin1("Invalid string representation for a comparation type: %1").arg(string)));
400
401 return Ignore; /* Silence GCC. */
402 }
403}
404
405QString TestBaseLine::displayName(const Type id)
406{
407 switch(id)
408 {
409 case XML:
410 return QLatin1String("XML");
411 case Fragment:
412 return QLatin1String("Fragment");
413 case Text:
414 return QLatin1String("Text");
415 case Ignore:
416 return QLatin1String("Ignore");
417 case Inspect:
418 return QLatin1String("Inspect");
419 case ExpectedError:
420 return QLatin1String("ExpectedError");
421 case SchemaIsValid:
422 return QLatin1String("SchemaIsValid");
423 }
424
425 Q_ASSERT(false);
426 return QString();
427}
428
429QString TestBaseLine::details() const
430{
431 if(m_type == Ignore) /* We're an error code. */
432 return QString();
433 if(m_type == ExpectedError) /* We're an error code. */
434 return m_details;
435 if(m_type == SchemaIsValid) /* We're a schema validation information . */
436 return m_details;
437
438 if(m_details.isEmpty())
439 return m_details;
440
441 /* m_details is a file name, we open it and return the result. */
442 QFile file(QUrl(m_details).toLocalFile());
443
444 QString retval;
445 if(!file.exists())
446 retval = QString::fromLatin1(str: "%1 does not exist.").arg(a: file.fileName());
447 else if(!QFileInfo(file.fileName()).isFile())
448 retval = QString::fromLatin1(str: "%1 is not a file, cannot display it.").arg(a: file.fileName());
449 else if(!file.open(flags: QIODevice::ReadOnly | QIODevice::Text))
450 retval = QString::fromLatin1(str: "Could not open %1. Likely a permission error.").arg(a: file.fileName());
451
452 if(retval.isNull())
453 {
454 /* Scary, we assume the query/baseline is in UTF-8. */
455 return QString::fromUtf8(str: file.readAll());
456 }
457 else
458 {
459 /* We had a file error. */
460 retval.prepend(s: QLatin1String("Test-suite harness error: "));
461 qCritical() << retval;
462 return retval;
463 }
464}
465
466TestBaseLine::Type TestBaseLine::type() const
467{
468 return m_type;
469}
470
471void TestBaseLine::setDetails(const QString &detailsP)
472{
473 m_details = detailsP;
474}
475
476// vim: et:ts=4:sw=4:sts=4
477

source code of qtxmlpatterns/tests/auto/xmlpatternssdk/TestBaseLine.cpp