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



commit 088243fe2725150c551d602e21da5d1ff4a3c243
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/Makefile.am |    2 +-
 gdata/tests/common.c    |  205 +++++++++++++++++++++++++++++++++++++++++++++++
 gdata/tests/common.h    |   12 +++
 gdata/tests/general.c   |  172 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 390 insertions(+), 1 deletions(-)
---
diff --git a/gdata/tests/Makefile.am b/gdata/tests/Makefile.am
index 4bbc6f4..f58bfc5 100644
--- a/gdata/tests/Makefile.am
+++ b/gdata/tests/Makefile.am
@@ -16,7 +16,7 @@ LIBS = \
 
 noinst_PROGRAMS = $(TEST_PROGS)
 
-TEST_SRCS = common.h
+TEST_SRCS = common.c common.h
 
 TEST_PROGS			+= general
 general_SOURCES			 = general.c $(TEST_SRCS)
diff --git a/gdata/tests/common.c b/gdata/tests/common.c
new file mode 100644
index 0000000..90f57cf
--- /dev/null
+++ b/gdata/tests/common.c
@@ -0,0 +1,205 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <philip tecnocode co uk>
+ *
+ * GData Client is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+#include <glib-object.h>
+#include <stdio.h>
+#include <string.h>
+#include <libxml/parser.h>
+#include <libxml/xmlsave.h>
+
+#include "common.h"
+
+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 745b9e8..43ce74b 100644
--- a/gdata/tests/common.h
+++ b/gdata/tests/common.h
@@ -17,6 +17,9 @@
  * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <glib.h>
+#include <gdata/gdata.h>
+
 #ifndef GDATA_TEST_COMMON_H
 #define GDATA_TEST_COMMON_H
 
@@ -27,6 +30,15 @@ G_BEGIN_DECLS
 #define DOCUMENTS_USERNAME "libgdata documents gmail com"
 #define PASSWORD "gdata-libgdata"
 
+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 dbccdf0..9c58c4e 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, "none");
+	gdata_access_rule_set_scope (rule, "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)
 {
 	GTimeVal updated, published, updated2, published2, *updated3, *published3;
@@ -2552,6 +2722,8 @@ main (int argc, char *argv[])
 	g_test_init (&argc, &argv, NULL);
 	g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=";);
 
+	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 ("/entry/get_xml", test_entry_get_xml);



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