| 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 | |