diff --git a/mpd/base/xml/xml_node_unittest.cc b/mpd/base/xml/xml_node_unittest.cc index 1870fc97cf..c7d5756dcf 100644 --- a/mpd/base/xml/xml_node_unittest.cc +++ b/mpd/base/xml/xml_node_unittest.cc @@ -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::type node) { +ScopedXmlPtr::type MakeDoc(ScopedXmlPtr::type node) { xml::ScopedXmlPtr::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 - // - // 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[] = + "\n" + " \n" + " \n" + " \n" + " \n" + ""; + + + // This is same as kXml1 but the attributes are reordered. Note that the + // children are not reordered. + static const char kXml1AttributeReorder[] = + "\n" + " " + " \n" + " \n" + " \n" + ""; + + // is before . + static const char kXml1ChildrenReordered[] = + "\n" + " \n" + " " + " \n" + " \n" + ""; + + // is before . + static const char kXml1RemovedAttributes[] = + "\n" + " " + " \n" + " \n" + " \n" + ""; + + static const char kXml2[] = + "\n" + " \n" + ""; + + // In XML , , and mean the same thing. + static const char kXml2DifferentSyntax[] = + "\n" + " \n" + ""; + + static const char kXml2MoreDifferentSyntax[] = + "\n" + " \n" + ""; + + // 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[] = - "\n\ - \n\ - \n\ - \n\ - "; + "\n" + " \n" + " \n" + " \n" + ""; 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::type doc(MakeDoc(representation.PassScopedPtr())); + ASSERT_TRUE( + XmlEqual(kExpectedRepresentaionString, doc.get())); } } // namespace xml diff --git a/mpd/mpd.gyp b/mpd/mpd.gyp index f3879361bd..98bbabcb7e 100644 --- a/mpd/mpd.gyp +++ b/mpd/mpd.gyp @@ -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': [ diff --git a/mpd/test/mpd_builder_test_helper.cc b/mpd/test/mpd_builder_test_helper.cc index 8f65cdde31..dc6e9c8070 100644 --- a/mpd/test/mpd_builder_test_helper.cc +++ b/mpd/test/mpd_builder_test_helper.cc @@ -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 diff --git a/mpd/test/xml_compare.cc b/mpd/test/xml_compare.cc new file mode 100644 index 0000000000..fea32cb1d5 --- /dev/null +++ b/mpd/test/xml_compare.cc @@ -0,0 +1,122 @@ +#include "mpd/test/xml_compare.h" + +#include +#include +#include + +#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::type GetDocFromString(const std::string& xml_str) { + xml::ScopedXmlPtr::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 GetMapOfAttributes(xmlNodePtr node) { + std::map attribute_map; + for (xmlAttr* attribute = node->properties; + attribute && attribute->name && attribute->children; + attribute = attribute->next) { + const char* name = reinterpret_cast(attribute->name); + xml::ScopedXmlPtr::type value( + xmlNodeListGetString(node->doc, attribute->children, 1)); + + attribute_map[name] = reinterpret_cast(value.get()); + DLOG(INFO) << "In node " << reinterpret_cast(node->name) + << " found attribute " << name << " with value " + << reinterpret_cast(value.get()); + } + + return attribute_map; +} + +bool MapsEqual(const std::map& map1, + const std::map& 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(node1->name) + << " and " << reinterpret_cast(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(node1->name) << " " + << reinterpret_cast(node2->name); + return false; + } + + if (!CompareAttributes(node1, node2)) { + LOG(ERROR) << "Attributes of element " + << reinterpret_cast(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::type xml1_doc(GetDocFromString(xml1)); + xml::ScopedXmlPtr::type xml2_doc(GetDocFromString(xml2)); + return XmlEqual(xml1_doc.get(), xml2_doc.get()); +} + +bool XmlEqual(const std::string& xml1, xmlDocPtr xml2) { + xml::ScopedXmlPtr::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 diff --git a/mpd/test/xml_compare.h b/mpd/test/xml_compare.h new file mode 100644 index 0000000000..76ea4007aa --- /dev/null +++ b/mpd/test/xml_compare.h @@ -0,0 +1,24 @@ +#ifndef MPD_TEST_XML_COMPARE_H_ +#define MPD_TEST_XML_COMPARE_H_ + +#include + +#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_