[libgdata/libgdata-0-7] tests: Add an XML tree comparison function for the test suite



commit 7de1ab4b5536a62634637c11f975c1168655d5d9
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sat Dec 11 11:53:55 2010 +0000

    tests: Add an XML tree comparison function for the test suite
    
    Due to a recent change in GLib master which affects the ordering of entries
    in hash tables (g_str_hash()'s implementation has been changed), many of the
    XML comparisons in libgdata's test suite have been broken. A tidy solution is
    to introduce an XML-aware comparison function, rather than just using a
    simple string comparison. This commit does that.
    
    This commit only adds the comparison function; a further one will port all
    the test suites to use it.

 gdata/tests/common.c  |  180 +++++++++++++++++++++++++++++++++++++++++++++++++
 gdata/tests/common.h  |    9 +++
 gdata/tests/general.c |  172 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 361 insertions(+), 0 deletions(-)
---
diff --git a/gdata/tests/common.c b/gdata/tests/common.c
index 37d3e7d..3659680 100644
--- a/gdata/tests/common.c
+++ b/gdata/tests/common.c
@@ -21,6 +21,8 @@
 #include <glib-object.h>
 #include <stdio.h>
 #include <string.h>
+#include <libxml/parser.h>
+#include <libxml/xmlsave.h>
 
 #include "common.h"
 
@@ -305,3 +307,181 @@ gdata_test_batch_operation_deletion (GDataBatchOperation *operation, GDataEntry
 
 	return op_id;
 }
+
+static gboolean
+compare_xml_namespaces (xmlNs *ns1, xmlNs *ns2)
+{
+	if (ns1 == ns2)
+		return TRUE;
+
+	/* Compare various simple properties */
+	if (ns1->type != ns2->type ||
+	    xmlStrcmp (ns1->href, ns2->href) != 0 ||
+	    xmlStrcmp (ns1->prefix, ns2->prefix) != 0 ||
+	    ns1->context != ns2->context) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static gboolean compare_xml_nodes (xmlNode *node1, xmlNode *node2);
+
+static gboolean
+compare_xml_node_lists (xmlNode *list1, xmlNode *list2)
+{
+	GHashTable *table;
+	xmlNode *child;
+
+	/* Compare their child elements. We iterate through the first linked list and, for each child node, iterate through the second linked list
+	 * comparing it against each node there. We keep a hashed set of nodes in the second linked list which have already been visited and compared
+	 * successfully, both for speed and to guarantee that one element in the second linked list doesn't match more than one in the first linked
+	 * list. We take this approach because we can't modify the second linked list in place to remove matched nodes.
+	 * Finally, we iterate through the second node list and check that all its elements are in the hash table (i.e. they've all been visited
+	 * exactly once).
+	 * This approach is O(n^2) in the number of nodes in the linked lists, but since we should be dealing with fairly narrow XML trees this should
+	 * be OK. */
+	table = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+	for (child = list1; child != NULL; child = child->next) {
+		xmlNode *other_child;
+		gboolean matched = FALSE;
+
+		for (other_child = list2; other_child != NULL; other_child = other_child->next) {
+			if (g_hash_table_lookup (table, other_child) != NULL)
+				continue;
+
+			if (compare_xml_nodes (child, other_child) == TRUE) {
+				g_hash_table_insert (table, other_child, other_child);
+				matched = TRUE;
+				break;
+			}
+		}
+
+		if (matched == FALSE) {
+			g_hash_table_destroy (table);
+			return FALSE;
+		}
+	}
+
+	for (child = list2; child != NULL; child = child->next) {
+		if (g_hash_table_lookup (table, child) == NULL) {
+			g_hash_table_destroy (table);
+			return FALSE;
+		}
+	}
+
+	g_hash_table_destroy (table);
+
+	return TRUE;
+}
+
+static gboolean
+compare_xml_nodes (xmlNode *node1, xmlNode *node2)
+{
+	GHashTable *table;
+	xmlAttr *attr1, *attr2;
+	xmlNs *ns;
+
+	if (node1 == node2)
+		return TRUE;
+
+	/* Compare various simple properties */
+	if (node1->type != node2->type ||
+	    xmlStrcmp (node1->name, node2->name) != 0 ||
+	    compare_xml_namespaces (node1->ns, node2->ns) == FALSE ||
+	    xmlStrcmp (node1->content, node2->content) != 0) {
+		return FALSE;
+	}
+
+	/* Compare their attributes. This is done in document order, which isn't strictly correct, since XML specifically does not apply an ordering
+	 * over attributes. However, it suffices for our needs. */
+	for (attr1 = node1->properties, attr2 = node2->properties; attr1 != NULL && attr2 != NULL; attr1 = attr1->next, attr2 = attr2->next) {
+		/* Compare various simple properties */
+		if (attr1->type != attr2->type ||
+		    xmlStrcmp (attr1->name, attr2->name) != 0 ||
+		    attr1->ns != attr2->ns ||
+		    attr1->atype != attr2->atype) {
+			return FALSE;
+		}
+
+		/* Compare their child nodes (values represented as text and entity nodes) */
+		if (compare_xml_node_lists (attr1->children, attr2->children) == FALSE)
+			return FALSE;
+	}
+
+	/* Stragglers? */
+	if (attr1 != NULL || attr2 != NULL)
+		return FALSE;
+
+	/* Compare their namespace definitions regardless of order. Do this by inserting all the definitions from node1 into a hash table, then running
+	 * through the  definitions in node2 and ensuring they exist in the hash table, removing each one from the table as we go. Check there aren't
+	 * any left in the hash table afterwards. */
+	table = g_hash_table_new (g_str_hash, g_str_equal);
+
+	for (ns = node1->nsDef; ns != NULL; ns = ns->next) {
+		/* Prefixes should be unique, but I trust libxml about as far as I can throw it. */
+		if (g_hash_table_lookup (table, ns->prefix ? ns->prefix : (gpointer) "") != NULL) {
+			g_hash_table_destroy (table);
+			return FALSE;
+		}
+
+		g_hash_table_insert (table, ns->prefix ? (gpointer) ns->prefix : (gpointer) "", ns);
+	}
+
+	for (ns = node2->nsDef; ns != NULL; ns = ns->next) {
+		xmlNs *original_ns = g_hash_table_lookup (table, ns->prefix ? ns->prefix : (gpointer) "");
+
+		if (original_ns == NULL ||
+		    compare_xml_namespaces (original_ns, ns) == FALSE) {
+			g_hash_table_destroy (table);
+			return FALSE;
+		}
+
+		g_hash_table_remove (table, ns->prefix ? ns->prefix : (gpointer) "");
+	}
+
+	if (g_hash_table_size (table) != 0) {
+		g_hash_table_destroy (table);
+		return FALSE;
+	}
+
+	g_hash_table_destroy (table);
+
+	/* Compare their child nodes */
+	if (compare_xml_node_lists (node1->children, node2->children) == FALSE)
+		return FALSE;
+
+	/* Success! */
+	return TRUE;
+}
+
+gboolean
+gdata_test_compare_xml (GDataParsable *parsable, const gchar *expected_xml, gboolean print_error)
+{
+	gboolean success;
+	gchar *parsable_xml;
+	xmlDoc *parsable_doc, *expected_doc;
+
+	/* Get an XML string for the GDataParsable */
+	parsable_xml = gdata_parsable_get_xml (parsable);
+
+	/* Parse both the XML strings */
+	parsable_doc = xmlReadMemory (parsable_xml, strlen (parsable_xml), "/dev/null", NULL, 0);
+	expected_doc = xmlReadMemory (expected_xml, strlen (expected_xml), "/dev/null", NULL, 0);
+
+	g_assert (parsable_doc != NULL && expected_doc != NULL);
+
+	/* Recursively compare the two XML trees */
+	success = compare_xml_nodes (xmlDocGetRootElement (parsable_doc), xmlDocGetRootElement (expected_doc));
+	if (success == FALSE && print_error == TRUE) {
+		/* The comparison has failed, so print out the two XML strings for ease of debugging */
+		g_message ("\n\nParsable: %s\n\nExpected: %s\n\n", parsable_xml, expected_xml);
+	}
+
+	xmlFreeDoc (expected_doc);
+	xmlFreeDoc (parsable_doc);
+	g_free (parsable_xml);
+
+	return success;
+}
diff --git a/gdata/tests/common.h b/gdata/tests/common.h
index 4c9492d..e8fe566 100644
--- a/gdata/tests/common.h
+++ b/gdata/tests/common.h
@@ -40,6 +40,15 @@ guint gdata_test_batch_operation_insertion (GDataBatchOperation *operation, GDat
 guint gdata_test_batch_operation_update (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **updated_entry, GError **error);
 guint gdata_test_batch_operation_deletion (GDataBatchOperation *operation, GDataEntry *entry, GError **error);
 
+gboolean gdata_test_compare_xml (GDataParsable *parsable, const gchar *expected_xml, gboolean print_error);
+
+/* Convenience macro */
+#define gdata_test_assert_xml(Parsable, XML) \
+	G_STMT_START { \
+		gboolean _test_success = gdata_test_compare_xml (GDATA_PARSABLE (Parsable), XML, TRUE); \
+		g_assert (_test_success == TRUE); \
+	} G_STMT_END
+
 G_END_DECLS
 
 #endif /* !GDATA_TEST_COMMON_H */
diff --git a/gdata/tests/general.c b/gdata/tests/general.c
index 7990e47..d527886 100644
--- a/gdata/tests/general.c
+++ b/gdata/tests/general.c
@@ -24,6 +24,176 @@
 #include "common.h"
 
 static void
+test_xml_comparison (void)
+{
+	GDataAccessRule *rule;
+
+	/* Since we've written the XML comparison function used across all the test suites, it's necessary to test that it actually works before we
+	 * blindly assert that its results are correct. */
+	rule = gdata_access_rule_new ("an-id");
+	gdata_access_rule_set_role (rule, GDATA_ACCESS_ROLE_NONE);
+	gdata_access_rule_set_scope (rule, GDATA_ACCESS_SCOPE_USER, "foo example com");
+
+	/* Check a valid comparison */
+	gdata_test_assert_xml (rule,
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>");
+
+	/* Check a valid comparison with namespaces swapped */
+	gdata_test_assert_xml (rule,
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007' "
+		       "xmlns:gd='http://schemas.google.com/g/2005'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>");
+
+	/* Check a valid comparison with elements swapped */
+	gdata_test_assert_xml (rule,
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<id>an-id</id>"
+			"<title type='text'>none</title>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>");
+
+	/* Missing namespace (still valid XML, just not what's expected) */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Extra namespace */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007' "
+		       "xmlns:foo='http://foo.com/'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Missing element */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Extra element */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<foo>bar</foo>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Incorrect namespace on element */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<gAcl:title type='text'>none</gAcl:title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Mis-valued content */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-other-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Missing attribute */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Extra attribute */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='none' other-value='foo'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	/* Mis-valued attribute */
+	g_assert (gdata_test_compare_xml (GDATA_PARSABLE (rule),
+		"<?xml version='1.0' encoding='UTF-8'?>"
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+		       "xmlns:gd='http://schemas.google.com/g/2005' "
+		       "xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+			"<title type='text'>none</title>"
+			"<id>an-id</id>"
+			"<category term='http://schemas.google.com/acl/2007#accessRule' scheme='http://schemas.google.com/g/2005#kind'/>"
+			"<gAcl:role value='other'/>"
+			"<gAcl:scope type='user' value='foo example com'/>"
+		"</entry>", FALSE) == FALSE);
+
+	g_object_unref (rule);
+}
+
+static void
 test_entry_get_xml (void)
 {
 	gint64 updated, published, updated2, published2, updated3, published3;
@@ -3254,6 +3424,8 @@ main (int argc, char *argv[])
 {
 	gdata_test_init (argc, argv);
 
+	g_test_add_func ("/tests/xml_comparison", test_xml_comparison);
+
 	g_test_add_func ("/service/network_error", test_service_network_error);
 	g_test_add_func ("/service/locale", test_service_locale);
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]