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