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 | |
43 | using namespace QPatternistSDK; |
44 | using namespace QPatternist; |
45 | |
46 | Q_GLOBAL_STATIC_WITH_ARGS(QRegExp, errorRegExp, (QLatin1String("[A-Z]{4}[0-9]{4}" ))) |
47 | |
48 | TestBaseLine::TestBaseLine(const Type t) : m_type(t) |
49 | { |
50 | Q_ASSERT(errorRegExp()->isValid()); |
51 | } |
52 | |
53 | TestResult::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 | |
73 | TestResult::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 | |
121 | void 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 | |
163 | bool 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 | |
180 | bool 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 | |
224 | bool 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 | |
282 | TestResult::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 | |
377 | TestBaseLine::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 | |
405 | QString 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 | |
429 | QString 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 | |
466 | TestBaseLine::Type TestBaseLine::type() const |
467 | { |
468 | return m_type; |
469 | } |
470 | |
471 | void TestBaseLine::setDetails(const QString &detailsP) |
472 | { |
473 | m_details = detailsP; |
474 | } |
475 | |
476 | // vim: et:ts=4:sw=4:sts=4 |
477 | |