blob: f8ad128f98b244e54dbd6c7838fab196b567c6e6 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QRegExp>
#include <QtDebug>
#include <QUrl>
#include <QXmlAttributes>
#include <QXmlSimpleReader>
#include <private/qxmldebug_p.h>
#include "XMLWriter.h"
#include "TestBaseLine.h"
using namespace QPatternistSDK;
using namespace QPatternist;
Q_GLOBAL_STATIC_WITH_ARGS(QRegExp, errorRegExp, (QLatin1String("[A-Z]{4}[0-9]{4}")))
TestBaseLine::TestBaseLine(const Type t) : m_type(t)
{
Q_ASSERT(errorRegExp()->isValid());
}
TestResult::Status TestBaseLine::scan(const QString &serialized,
const TestBaseLine::List &lines)
{
Q_ASSERT_X(lines.count() >= 1, Q_FUNC_INFO,
"At least one base line must be passed, otherwise there's nothing "
"to compare to.");
const TestBaseLine::List::const_iterator end(lines.constEnd());
TestBaseLine::List::const_iterator it(lines.constBegin());
for(; it != end; ++it)
{
const TestResult::Status retval((*it)->verify(serialized));
if(retval == TestResult::Pass || retval == TestResult::NotTested)
return retval;
}
return TestResult::Fail;
}
TestResult::Status TestBaseLine::scanErrors(const ErrorHandler::Message::List &errors,
const TestBaseLine::List &lines)
{
pDebug() << "TestBaseLine::scanErrors()";
/* 1. Find the first error in @p errors that's a Patternist
* error(not warning and not from Qt) and extract the error code. */
QString errorCode;
const ErrorHandler::Message::List::const_iterator end(errors.constEnd());
ErrorHandler::Message::List::const_iterator it(errors.constBegin());
for(; it != end; ++it)
{
if((*it).type() != QtFatalMsg)
continue;
errorCode = QUrl((*it).identifier()).fragment();
pDebug() << "ERR:" << (*it).description();
/* This is hackish. We have no way of determining whether a Message
* is actually issued from Patternist, so we try to narrow it down like this. */
if(errorRegExp()->exactMatch(errorCode))
break; /* It's an error code. */
else
errorCode.clear();
}
pDebug() << "Got error code: " << errorCode;
/* 2. Loop through @p lines, and for the first base line
* which is of type ExpectedError and which matches @p errorCode
* return Pass, otherwise Fail. */
const TestBaseLine::List::const_iterator blend(lines.constEnd());
TestBaseLine::List::const_iterator blit(lines.constBegin());
for(; blit != blend; ++blit)
{
const Type t = (*blit)->type();
if(t == TestBaseLine::ExpectedError)
{
const QString d((*blit)->details());
if(d == errorCode || d == QChar::fromLatin1('*'))
return TestResult::Pass;
}
}
return TestResult::Fail;
}
void TestBaseLine::toXML(XMLWriter &receiver) const
{
switch(m_type)
{
case XML: /* Fallthrough. */
case Fragment: /* Fallthrough. */
case SchemaIsValid: /* Fallthrough. */
case Text:
{
QXmlStreamAttributes inspectAtts;
inspectAtts.append(QLatin1String("role"), QLatin1String("principal"));
inspectAtts.append(QLatin1String("compare"), displayName(m_type));
receiver.startElement(QLatin1String("output-file"), inspectAtts);
receiver.characters(m_details);
receiver.endElement(QLatin1String("output-file"));
return;
}
case Ignore:
{
Q_ASSERT_X(false, Q_FUNC_INFO, "Serializing 'Ignore' is not implemented.");
return;
}
case Inspect:
{
QXmlStreamAttributes inspectAtts;
inspectAtts.append(QLatin1String("role"), QLatin1String("principal"));
inspectAtts.append(QLatin1String("compare"), QLatin1String("Inspect"));
receiver.startElement(QLatin1String("output-file"), inspectAtts);
receiver.characters(m_details);
receiver.endElement(QLatin1String("output-file"));
return;
}
case ExpectedError:
{
receiver.startElement(QLatin1String("expected-error"));
receiver.characters(m_details);
receiver.endElement(QLatin1String("expected-error"));
return;
}
}
}
bool TestBaseLine::isChildrenDeepEqual(const QDomNodeList &cl1, const QDomNodeList &cl2)
{
const int len = cl1.length();
if(len == cl2.length())
{
for (int i = 0; i < len; ++i) {
if(!isDeepEqual(cl1.at(i), cl2.at(i)))
return false;
}
return true;
}
else
return false;
}
bool TestBaseLine::isAttributesEqual(const QDomNamedNodeMap &cl1, const QDomNamedNodeMap &cl2)
{
const int len = cl1.length();
pDebug() << "LEN:" << len;
if(len == cl2.length())
{
for (int i1 = 0; i1 < len; ++i1) {
const QDomNode attr1(cl1.item(i1));
Q_ASSERT(!attr1.isNull());
/* This is set if attr1 cannot be found at all in cl2. */
bool earlyExit = false;
for (int i2 = 0; i2 < len; ++i2) {
const QDomNode attr2(cl2.item(i2));
Q_ASSERT(!attr2.isNull());
pDebug() << "ATTR1:" << attr1.localName() << attr1.namespaceURI() << attr1.prefix() << attr1.nodeName();
pDebug() << "ATTR2:" << attr2.localName() << attr2.namespaceURI() << attr2.prefix() << attr2.nodeName();
if(attr1.localName() == attr2.localName() &&
attr1.namespaceURI() == attr2.namespaceURI() &&
attr1.prefix() == attr2.prefix() &&
attr1.nodeName() == attr2.nodeName() && /* Yes, needed in addition to all the other. */
attr1.nodeValue() == attr2.nodeValue())
{
earlyExit = true;
break;
}
}
if(!earlyExit)
{
/* An attribute was found that doesn't exist in the other list so exit. */
return false;
}
}
return true;
}
else
return false;
}
bool TestBaseLine::isDeepEqual(const QDomNode &n1, const QDomNode &n2)
{
if(n1.nodeType() != n2.nodeType())
return false;
switch(n1.nodeType())
{
case QDomNode::CommentNode:
/* Fallthrough. */
case QDomNode::TextNode:
{
return static_cast<const QDomCharacterData &>(n1).data() ==
static_cast<const QDomCharacterData &>(n2).data();
}
case QDomNode::ProcessingInstructionNode:
{
return n1.nodeName() == n2.nodeName() &&
n1.nodeValue() == n2.nodeValue();
}
case QDomNode::DocumentNode:
return isChildrenDeepEqual(n1.childNodes(), n2.childNodes());
case QDomNode::ElementNode:
{
return n1.localName() == n2.localName() &&
n1.namespaceURI() == n2.namespaceURI() &&
n1.nodeName() == n2.nodeName() && /* Yes, this one is needed in addition to localName(). */
isAttributesEqual(n1.attributes(), n2.attributes()) &&
isChildrenDeepEqual(n1.childNodes(), n2.childNodes());
}
/* Fallthrough all these. */
case QDomNode::EntityReferenceNode:
case QDomNode::CDATASectionNode:
case QDomNode::EntityNode:
case QDomNode::DocumentTypeNode:
case QDomNode::DocumentFragmentNode:
case QDomNode::NotationNode:
case QDomNode::BaseNode:
case QDomNode::CharacterDataNode:
{
Q_ASSERT_X(false, Q_FUNC_INFO,
"An unsupported node type was encountered.");
return false;
}
case QDomNode::AttributeNode:
{
Q_ASSERT_X(false, Q_FUNC_INFO,
"This should never happen. QDom doesn't allow us to compare DOM attributes "
"properly.");
return false;
}
default:
{
Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled QDom::NodeType value.");
return false;
}
}
}
TestResult::Status TestBaseLine::verify(const QString &serializedInput) const
{
switch(m_type)
{
case SchemaIsValid:
/* Fall through. */
case Text:
{
if(serializedInput == details())
return TestResult::Pass;
else
return TestResult::Fail;
}
case Fragment:
/* Fall through. */
case XML:
{
/* Read the baseline and the serialized input into two QDomDocuments, and compare
* them deeply. We wrap fragments in a root node such that it is well-formed XML.
*/
QDomDocument output;
{
/* The reason we put things into a QByteArray and then parse it through QXmlSimpleReader, is that
* QDomDocument does whitespace stripping when calling setContent(QString). In other words,
* this workarounds a bug. */
const bool success =
output.setContent((m_type == XML ? serializedInput
: QLatin1String("<r>") + serializedInput
+ QLatin1String("</r>"))
.toUtf8());
if(!success)
return TestResult::Fail;
Q_ASSERT(success);
}
QDomDocument baseline;
{
QString baselineReadingError;
const bool success = baseline.setContent(
(m_type == XML ? details()
: QLatin1String("<r>") + details() + QLatin1String("</r>"))
.toUtf8(),
&baselineReadingError);
if(!success)
return TestResult::Fail;
/* This piece of code workaround a bug in QDom, which treats XML prologs as processing
* instructions and make them available in the tree as so. */
if(m_type == XML)
{
/* $doc/r/node() */
const QDomNodeList children(baseline.childNodes());
const int len = children.length();
for(int i = 0; i < len; ++i)
{
const QDomNode &child = children.at(i);
if(child.isProcessingInstruction() && child.nodeName() == QLatin1String("xml"))
{
baseline.removeChild(child);
break;
}
}
}
Q_ASSERT_X(baselineReadingError.isNull(), Q_FUNC_INFO,
qPrintable((QLatin1String("Reading the baseline failed: ") + baselineReadingError)));
}
if(isDeepEqual(output, baseline))
return TestResult::Pass;
else
{
pDebug() << "FAILURE:" << output.toString() << "is NOT IDENTICAL to(baseline):" << baseline.toString();
return TestResult::Fail;
}
}
case Ignore:
return TestResult::Pass;
case Inspect:
return TestResult::NotTested;
case ExpectedError:
{
/* This function is only called for Text/XML/Fragment tests. */
return TestResult::Fail;
}
}
Q_ASSERT(false);
return TestResult::Fail;
}
TestBaseLine::Type TestBaseLine::identifierFromString(const QString &string)
{
/* "html-output: Using an ad hoc tool, it must assert that the document obeys the HTML
* Output Method as defined in the Serialization specification and section
* 20 of the XSLT 2.0 specification." We treat it as XML for now, same with
* xhtml-output. */
if(string.compare(QLatin1String("XML"), Qt::CaseInsensitive) == 0 ||
string == QLatin1String("html-output") ||
string == QLatin1String("xml-output") ||
string == QLatin1String("xhtml-output"))
return XML;
else if(string == QLatin1String("Fragment") || string == QLatin1String("xml-frag"))
return Fragment;
else if(string.compare(QLatin1String("Text"), Qt::CaseInsensitive) == 0)
return Text;
else if(string == QLatin1String("Ignore"))
return Ignore;
else if(string.compare(QLatin1String("Inspect"), Qt::CaseInsensitive) == 0)
return Inspect;
else
{
Q_ASSERT_X(false, Q_FUNC_INFO,
qPrintable(QString::fromLatin1("Invalid string representation for a comparation type: %1").arg(string)));
return Ignore; /* Silence GCC. */
}
}
QString TestBaseLine::displayName(const Type id)
{
switch(id)
{
case XML:
return QLatin1String("XML");
case Fragment:
return QLatin1String("Fragment");
case Text:
return QLatin1String("Text");
case Ignore:
return QLatin1String("Ignore");
case Inspect:
return QLatin1String("Inspect");
case ExpectedError:
return QLatin1String("ExpectedError");
case SchemaIsValid:
return QLatin1String("SchemaIsValid");
}
Q_ASSERT(false);
return QString();
}
QString TestBaseLine::details() const
{
if(m_type == Ignore) /* We're an error code. */
return QString();
if(m_type == ExpectedError) /* We're an error code. */
return m_details;
if(m_type == SchemaIsValid) /* We're a schema validation information . */
return m_details;
if(m_details.isEmpty())
return m_details;
/* m_details is a file name, we open it and return the result. */
QFile file(QUrl(m_details).toLocalFile());
QString retval;
if(!file.exists())
retval = QString::fromLatin1("%1 does not exist.").arg(file.fileName());
else if(!QFileInfo(file.fileName()).isFile())
retval = QString::fromLatin1("%1 is not a file, cannot display it.").arg(file.fileName());
else if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
retval = QString::fromLatin1("Could not open %1. Likely a permission error.").arg(file.fileName());
if(retval.isNull())
{
/* Scary, we assume the query/baseline is in UTF-8. */
return QString::fromUtf8(file.readAll());
}
else
{
/* We had a file error. */
retval.prepend(QLatin1String("Test-suite harness error: "));
qCritical() << retval;
return retval;
}
}
TestBaseLine::Type TestBaseLine::type() const
{
return m_type;
}
void TestBaseLine::setDetails(const QString &detailsP)
{
m_details = detailsP;
}
// vim: et:ts=4:sw=4:sts=4