XML compare function

XML compare function for comparing a subset of MPD or the whole MPD.
The children must appear in the same order but the order of attributes
do not matter.
Change existing tests to use XmlEqual() instead of string comparison.

Change-Id: Ib7f80f52b5bed5b5f7c2517620c8955261a4b6a2
This commit is contained in:
Rintaro Kuroiwa 2014-04-11 18:23:20 -07:00
parent 7fe5b5171a
commit c73c25c1c0
5 changed files with 251 additions and 44 deletions

View File

@ -7,6 +7,7 @@
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "mpd/base/xml/xml_node.h"
#include "mpd/test/xml_compare.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libxml/src/include/libxml/tree.h"
@ -27,49 +28,109 @@ void AddAttribute(const std::string& name,
attribute->set_value(value);
}
std::string GetDocAsFlatString(xmlDocPtr doc) {
static const int kFlatFormat = 0;
int doc_str_size = 0;
xmlChar* doc_str = NULL;
xmlDocDumpFormatMemoryEnc(doc, &doc_str, &doc_str_size, "UTF-8", kFlatFormat);
DCHECK(doc_str);
std::string output(doc_str, doc_str + doc_str_size);
xmlFree(doc_str);
return output;
}
// Ownership transfers, IOW this function will release the resource for |node|.
// Returns |node| in string format.
std::string GetStringFormat(ScopedXmlPtr<xmlNode>::type node) {
ScopedXmlPtr<xmlDoc>::type MakeDoc(ScopedXmlPtr<xmlNode>::type node) {
xml::ScopedXmlPtr<xmlDoc>::type doc(xmlNewDoc(BAD_CAST ""));
// Because you cannot easily get the string format of a xmlNodePtr, it gets
// attached to a temporary xml doc.
xmlDocSetRootElement(doc.get(), node.release());
std::string doc_str = GetDocAsFlatString(doc.get());
// GetDocAsFlatString() adds
// <?xml version="" encoding="UTF-8"?>
// to the first line. So this removes the first line.
const size_t first_newline_char_pos = doc_str.find('\n');
DCHECK_NE(first_newline_char_pos, std::string::npos);
return doc_str.substr(first_newline_char_pos + 1);
return doc.Pass();
}
} // namespace
// Make sure XmlEqual() is functioning correctly.
TEST(MetaTest, XmlEqual) {
static const char kXml1[] =
"<A>\n"
" <B\n"
" c=\"1\""
" e=\"foobar\""
" somelongnameattribute=\"somevalue\">\n"
" <Bchild childvalue=\"3\"\n"
" f=\"4\"/>\n"
" </B>\n"
" <C />\n"
"</A>";
// This is same as kXml1 but the attributes are reordered. Note that the
// children are not reordered.
static const char kXml1AttributeReorder[] =
"<A>\n"
" <B\n"
" c=\"1\""
" somelongnameattribute=\"somevalue\"\n"
" e=\"foobar\">"
" <Bchild childvalue=\"3\"\n"
" f=\"4\"/>\n"
" </B>\n"
" <C />\n"
"</A>";
// <C> is before <B>.
static const char kXml1ChildrenReordered[] =
"<A>\n"
" <C />\n"
" <B\n"
" d=\"2\""
" c=\"1\""
" somelongnameattribute=\"somevalue\"\n"
" e=\"foobar\">"
" <Bchild childvalue=\"3\"\n"
" f=\"4\"/>\n"
" </B>\n"
"</A>";
// <C> is before <B>.
static const char kXml1RemovedAttributes[] =
"<A>\n"
" <B\n"
" d=\"2\"\n>"
" <Bchild f=\"4\"/>\n"
" </B>\n"
" <C />\n"
"</A>";
static const char kXml2[] =
"<A>\n"
" <C />\n"
"</A>";
// In XML <C />, <C></C>, and <C/> mean the same thing.
static const char kXml2DifferentSyntax[] =
"<A>\n"
" <C></C>\n"
"</A>";
static const char kXml2MoreDifferentSyntax[] =
"<A>\n"
" <C/>\n"
"</A>";
// Identity.
ASSERT_TRUE(XmlEqual(kXml1, kXml1));
// Equivalent.
ASSERT_TRUE(XmlEqual(kXml1, kXml1AttributeReorder));
ASSERT_TRUE(XmlEqual(kXml2, kXml2DifferentSyntax));
ASSERT_TRUE(XmlEqual(kXml2, kXml2MoreDifferentSyntax));
// Different.
ASSERT_FALSE(XmlEqual(kXml1, kXml2));
ASSERT_FALSE(XmlEqual(kXml1, kXml1ChildrenReordered));
ASSERT_FALSE(XmlEqual(kXml1, kXml1RemovedAttributes));
ASSERT_FALSE(XmlEqual(kXml1AttributeReorder, kXml1ChildrenReordered));
}
TEST(Representation, AddContentProtectionXml) {
static const char kExpectedRepresentaionString[] =
"<Representation>\n\
<ContentProtection \
a=\"1\" \
b=\"2\" \
schemeIdUri=\"http://www.foo.com/drm\" \
value=\"somevalue\">\n\
<TestSubElement c=\"3\" d=\"4\"/>\n\
</ContentProtection>\n\
</Representation>";
"<Representation>\n"
" <ContentProtection\n"
" a=\"1\"\n"
" b=\"2\"\n"
" schemeIdUri=\"http://www.foo.com/drm\"\n"
" value=\"somevalue\">\n"
" <TestSubElement c=\"3\" d=\"4\"/>\n"
" </ContentProtection>\n"
"</Representation>";
MediaInfo media_info;
MediaInfo::ContentProtectionXml* content_protection_xml =
@ -89,11 +150,9 @@ TEST(Representation, AddContentProtectionXml) {
ASSERT_TRUE(
representation.AddContentProtectionElementsFromMediaInfo(media_info));
std::string representation_xml_string =
GetStringFormat(representation.PassScopedPtr());
// Compare with expected output (both flattened).
ASSERT_EQ(CollapseWhitespaceASCII(kExpectedRepresentaionString, true),
CollapseWhitespaceASCII(representation_xml_string, true));
ScopedXmlPtr<xmlDoc>::type doc(MakeDoc(representation.PassScopedPtr()));
ASSERT_TRUE(
XmlEqual(kExpectedRepresentaionString, doc.get()));
}
} // namespace xml

View File

@ -71,6 +71,8 @@
'base/xml/xml_node_unittest.cc',
'test/mpd_builder_test_helper.cc',
'test/mpd_builder_test_helper.h',
'test/xml_compare.cc',
'test/xml_compare.h',
'util/mpd_writer_unittest.cc',
],
'dependencies': [

View File

@ -11,6 +11,7 @@
#include "mpd/base/media_info.pb.h"
#include "mpd/base/mpd_builder.h"
#include "mpd/base/xml/scoped_xml_ptr.h"
#include "mpd/test/xml_compare.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/protobuf/src/google/protobuf/text_format.h"
@ -104,10 +105,9 @@ void ExpectMpdToEqualExpectedOutputFile(
<< "Failed to read: " << expected_output_file;
// Adding extra << here to get a formatted output.
ASSERT_EQ(expected_mpd, mpd_string) << "Expected:" << std::endl
<< expected_mpd << std::endl
<< "Actual:" << std::endl
<< mpd_string;
ASSERT_TRUE(XmlEqual(expected_mpd, mpd_string))
<< "Expected:" << std::endl << expected_mpd << std::endl
<< "Actual:" << std::endl << mpd_string;
}
} // namespace dash_packager

122
mpd/test/xml_compare.cc Normal file
View File

@ -0,0 +1,122 @@
#include "mpd/test/xml_compare.h"
#include <algorithm>
#include <map>
#include <string>
#include "base/logging.h"
#include "mpd/base/xml/scoped_xml_ptr.h"
#include "third_party/libxml/src/include/libxml/tree.h"
namespace dash_packager {
namespace {
xml::ScopedXmlPtr<xmlDoc>::type GetDocFromString(const std::string& xml_str) {
xml::ScopedXmlPtr<xmlDoc>::type schema_as_doc(
xmlReadMemory(xml_str.data(), xml_str.size(), NULL, NULL, 0));
return schema_as_doc.Pass();
}
// Make a map from attributes of the node.
std::map<std::string, std::string> GetMapOfAttributes(xmlNodePtr node) {
std::map<std::string, std::string> attribute_map;
for (xmlAttr* attribute = node->properties;
attribute && attribute->name && attribute->children;
attribute = attribute->next) {
const char* name = reinterpret_cast<const char*>(attribute->name);
xml::ScopedXmlPtr<xmlChar>::type value(
xmlNodeListGetString(node->doc, attribute->children, 1));
attribute_map[name] = reinterpret_cast<const char*>(value.get());
DLOG(INFO) << "In node " << reinterpret_cast<const char*>(node->name)
<< " found attribute " << name << " with value "
<< reinterpret_cast<const char*>(value.get());
}
return attribute_map;
}
bool MapsEqual(const std::map<std::string, std::string>& map1,
const std::map<std::string, std::string>& map2) {
return map1.size() == map2.size() &&
std::equal(map1.begin(), map1.end(), map2.begin());
}
// Return true if the nodes have the same attributes.
bool CompareAttributes(xmlNodePtr node1, xmlNodePtr node2) {
return MapsEqual(GetMapOfAttributes(node1), GetMapOfAttributes(node2));
}
// Return true if the name of the nodes match.
bool CompareNames(xmlNodePtr node1, xmlNodePtr node2) {
DLOG(INFO) << "Comparing " << reinterpret_cast<const char*>(node1->name)
<< " and " << reinterpret_cast<const char*>(node2->name);
return xmlStrcmp(node1->name, node2->name) == 0;
}
// Recursively check the elements.
// Note that the terminating condition of the recursion is when the children do
// not match (inside the loop).
bool CompareNodes(xmlNodePtr node1, xmlNodePtr node2) {
DCHECK(node1 && node2);
if (!CompareNames(node1, node2)) {
LOG(ERROR) << "Names of the nodes do not match: "
<< reinterpret_cast<const char*>(node1->name) << " "
<< reinterpret_cast<const char*>(node2->name);
return false;
}
if (!CompareAttributes(node1, node2)) {
LOG(ERROR) << "Attributes of element "
<< reinterpret_cast<const char*>(node1->name)
<< " do not match.";
return false;
}
xmlNodePtr node1_child = xmlFirstElementChild(node1);
xmlNodePtr node2_child = xmlFirstElementChild(node2);
do {
if (!node1_child || !node2_child)
return node1_child == node2_child;
DCHECK(node1_child && node2_child);
if (!CompareNodes(node1_child, node2_child))
return false;
node1_child = xmlNextElementSibling(node1_child);
node2_child = xmlNextElementSibling(node2_child);
} while (node1_child || node2_child);
// Both nodes have equivalent descendents.
return true;
}
} // namespace
bool XmlEqual(const std::string& xml1, const std::string& xml2) {
xml::ScopedXmlPtr<xmlDoc>::type xml1_doc(GetDocFromString(xml1));
xml::ScopedXmlPtr<xmlDoc>::type xml2_doc(GetDocFromString(xml2));
return XmlEqual(xml1_doc.get(), xml2_doc.get());
}
bool XmlEqual(const std::string& xml1, xmlDocPtr xml2) {
xml::ScopedXmlPtr<xmlDoc>::type xml1_doc(GetDocFromString(xml1));
return XmlEqual(xml1_doc.get(), xml2);
}
bool XmlEqual(xmlDocPtr xml1, xmlDocPtr xml2) {
if (!xml1|| !xml2) {
LOG(ERROR) << "xml1 and/or xml2 are not valid XML.";
return false;
}
xmlNodePtr xml1_root_element = xmlDocGetRootElement(xml1);
xmlNodePtr xml2_root_element = xmlDocGetRootElement(xml2);
if (!xml1_root_element || !xml2_root_element)
return xml1_root_element == xml2_root_element;
return CompareNodes(xml1_root_element, xml2_root_element);
}
} // namespace dash_packager

24
mpd/test/xml_compare.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef MPD_TEST_XML_COMPARE_H_
#define MPD_TEST_XML_COMPARE_H_
#include <string>
#include "third_party/libxml/src/include/libxml/tree.h"
namespace dash_packager {
/// Checks whether the XMLs are equivalent. An XML schema can specify strict
/// ordering of children and MPD does. These functions are currently tuned to
/// compare subsets of MPD. Therefore this function requires strict ordering of
/// the child elements of an element. Attributes can be in any order since XML
/// cannot enforce ordering of attributes.
/// @param xml1 is compared against @a xml2.
/// @param xml2 is compared against @a xml1.
/// @return true if @a xml1 and @a xml2 are equivalent, false otherwise.
bool XmlEqual(const std::string& xml1, const std::string& xml2);
bool XmlEqual(const std::string& xml1, xmlDocPtr xml2);
bool XmlEqual(xmlDocPtr xml1, xmlDocPtr xml2);
} // namespace dash_packager
#endif // MPD_TEST_XML_COMPARE_H_