/* -*- 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 <locale.h>
#include <stdio.h>
#include <string.h>
#include <libxml/parser.h>
#include <libxml/xmlsave.h>
#include "common.h"
/* %TRUE if interactive tests should be skipped because we're running automatically (for example) */
static gboolean no_interactive = TRUE;
/* declaration of debug handler */
static void gdata_test_debug_handler (const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data);
/* Directory to output network trace files to, if trace output is enabled. (NULL otherwise.) */
static GFile *trace_dir = NULL;
/* TRUE if tests should be run online and a trace file written for each; FALSE if tests should run offline against existing trace files. */
static gboolean write_traces = FALSE;
/* TRUE if tests should be run online and the server's responses compared to the existing trace file for each; FALSE if tests should run offline without comparison. */
static gboolean compare_traces = FALSE;
/* Global mock server instance used by all tests. */
static UhmServer *mock_server = NULL;
void
gdata_test_init (int argc, char **argv)
{
GTlsCertificate *cert;
GError *child_error = NULL;
gchar *cert_path = NULL, *key_path = NULL;
gint i;
setlocale (LC_ALL, "");
/* Parse the custom options */
for (i = 1; i < argc; i++) {
if (strcmp ("--no-interactive", argv[i]) == 0 || strcmp ("-ni", argv[i]) == 0) {
no_interactive = TRUE;
argv[i] = (char*) "";
} else if (strcmp ("--interactive", argv[i]) == 0 || strcmp ("-i", argv[i]) == 0) {
no_interactive = FALSE;
argv[i] = (char*) "";
} else if (strcmp ("--trace-dir", argv[i]) == 0 || strcmp ("-t", argv[i]) == 0) {
if (i >= argc - 1) {
fprintf (stderr, "Error: Missing directory for --trace-dir option.\n");
exit (1);
}
trace_dir = g_file_new_for_path (argv[i + 1]);
argv[i] = (char*) "";
argv[i + 1] = (char*) "";
i++;
} else if (strcmp ("--write-traces", argv[i]) == 0 || strcmp ("-w", argv[i]) == 0) {
write_traces = TRUE;
argv[i] = (char*) "";
} else if (strcmp ("--compare-traces", argv[i]) == 0 || strcmp ("-c", argv[i]) == 0) {
compare_traces = TRUE;
argv[i] = (char*) "";
} else if (strcmp ("-?", argv[i]) == 0 || strcmp ("--help", argv[i]) == 0 || strcmp ("-h" , argv[i]) == 0) {
/* We have to override --help in order to document --no-interactive and the trace flags. */
printf ("Usage:\n"
" %s [OPTION...]\n\n"
"Help Options:\n"
" -?, --help Show help options\n"
"Test Options:\n"
" -l List test cases available in a test executable\n"
" -seed=RANDOMSEED Provide a random seed to reproduce test\n"
" runs using random numbers\n"
" --verbose Run tests verbosely\n"
" -q, --quiet Run tests quietly\n"
" -p TESTPATH Execute all tests matching TESTPATH\n"
" -m {perf|slow|thorough|quick} Execute tests according modes\n"
" --debug-log Debug test logging output\n"
" -ni, --no-interactive Only execute tests which don't require user interaction\n"
" -i, --interactive Execute tests including those requiring user interaction\n"
" -t, --trace-dir [directory] Read/Write trace files in the specified directory\n"
" -w, --write-traces Work online and write trace files to --trace-dir\n"
" -c, --compare-traces Work online and compare with existing trace files in --trace-dir\n",
argv[0]);
exit (0);
}
}
/* --[write|compare]-traces are mutually exclusive. */
if (write_traces == TRUE && compare_traces == TRUE) {
fprintf (stderr, "Error: --write-traces and --compare-traces are mutually exclusive.\n");
exit (1);
}
g_test_init (&argc, &argv, NULL);
g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=");
/* Set handler of debug information */
g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, (GLogFunc) gdata_test_debug_handler, NULL);
/* Enable full debugging. These options are seriously unsafe, but we don't care for test cases. */
g_setenv ("LIBGDATA_DEBUG", "4" /* GDATA_LOG_FULL_UNREDACTED */, FALSE);
g_setenv ("G_MESSAGES_DEBUG", "libgdata", FALSE);
g_setenv ("LIBGDATA_LAX_SSL_CERTIFICATES", "1", FALSE);
mock_server = uhm_server_new ();
uhm_server_set_enable_logging (mock_server, write_traces);
uhm_server_set_enable_online (mock_server, write_traces || compare_traces);
/* Build the certificate. */
cert_path = g_test_build_filename (G_TEST_DIST, "cert.pem", NULL);
key_path = g_test_build_filename (G_TEST_DIST, "key.pem", NULL);
cert = g_tls_certificate_new_from_files (cert_path, key_path, &child_error);
g_assert_no_error (child_error);
g_free (key_path);
g_free (cert_path);
/* Set it as the property. */
uhm_server_set_tls_certificate (mock_server, cert);
g_object_unref (cert);
}
/*
* gdata_test_get_mock_server:
*
* Returns the singleton #UhmServer instance used throughout the test suite.
*
* Return value: (transfer none): the mock server
*
* Since: 0.13.4
*/
UhmServer *
gdata_test_get_mock_server (void)
{
return mock_server;
}
/*
* gdata_test_interactive:
*
* Returns whether tests which require interactivity should be run.
*
* Return value: %TRUE if interactive tests should be run, %FALSE otherwise
*
* Since: 0.9.0
*/
gboolean
gdata_test_interactive (void)
{
return (no_interactive == FALSE) ? TRUE : FALSE;
}
typedef struct {
GDataBatchOperation *operation;
guint op_id;
GDataBatchOperationType operation_type;
GDataEntry *entry;
GDataEntry **returned_entry;
gchar *id;
GType entry_type;
GError **error;
} BatchOperationData;
static void
batch_operation_data_free (BatchOperationData *data)
{
if (data->operation != NULL)
g_object_unref (data->operation);
if (data->entry != NULL)
g_object_unref (data->entry);
g_free (data->id);
/* We don't free data->error, as it's owned by the calling code */
g_slice_free (BatchOperationData, data);
}
static void
test_batch_operation_query_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
{
BatchOperationData *data = user_data;
/* Mark the callback as having been run */
g_object_set_data (G_OBJECT (data->operation), "test::called-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (data->operation), "test::called-callbacks")) + 1));
/* Check that the @operation_type and @operation_id matches those stored in @data */
g_assert_cmpuint (operation_id, ==, data->op_id);
g_assert_cmpuint (operation_type, ==, data->operation_type);
/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
if (data->error != NULL) {
g_assert (error != NULL);
*(data->error) = g_error_copy (error);
g_assert (entry == NULL);
if (data->returned_entry != NULL)
*(data->returned_entry) = NULL;
} else {
g_assert_no_error (error);
g_assert (entry != NULL);
g_assert (entry != data->entry); /* check that the pointers aren't the same */
g_assert (gdata_entry_is_inserted (entry) == TRUE);
/* Check the ID and type of the returned entry */
/* TODO: We can't check this, because the Contacts service is stupid with IDs
* g_assert_cmpstr (gdata_entry_get_id (entry), ==, data->id); */
g_assert (G_TYPE_CHECK_INSTANCE_TYPE (entry, data->entry_type));
/* Check the entries match */
if (data->entry != NULL) {
g_assert_cmpstr (gdata_entry_get_title (entry), ==, gdata_entry_get_title (data->entry));
g_assert_cmpstr (gdata_entry_get_summary (entry), ==, gdata_entry_get_summary (data->entry));
g_assert_cmpstr (gdata_entry_get_content (entry), ==, gdata_entry_get_content (data->entry));
g_assert_cmpstr (gdata_entry_get_content_uri (entry), ==, gdata_entry_get_content_uri (data->entry));
g_assert_cmpstr (gdata_entry_get_rights (entry), ==, gdata_entry_get_rights (data->entry));
}
/* Copy the returned entry for the calling test code to prod later */
if (data->returned_entry != NULL)
*(data->returned_entry) = g_object_ref (entry);
}
/* Free the data */
batch_operation_data_free (data);
}
guint
gdata_test_batch_operation_query (GDataBatchOperation *operation, const gchar *id, GType entry_type, GDataEntry *entry, GDataEntry **returned_entry,
GError **error)
{
guint op_id;
BatchOperationData *data;
data = g_slice_new (BatchOperationData);
data->operation = g_object_ref (operation);
data->op_id = 0;
data->operation_type = GDATA_BATCH_OPERATION_QUERY;
data->entry = g_object_ref (entry);
data->returned_entry = returned_entry;
data->id = g_strdup (id);
data->entry_type = entry_type;
data->error = error;
op_id = gdata_batch_operation_add_query (operation, id, entry_type, test_batch_operation_query_cb, data);
data->op_id = op_id;
/* We expect a callback to be called when the operation is run */
g_object_set_data (G_OBJECT (operation), "test::expected-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")) + 1));
return op_id;
}
static void
test_batch_operation_insertion_update_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error,
gpointer user_data)
{
BatchOperationData *data = user_data;
/* Mark the callback as having been run */
g_object_set_data (G_OBJECT (data->operation), "test::called-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (data->operation), "test::called-callbacks")) + 1));
/* Check that the @operation_type and @operation_id matches those stored in @data */
g_assert_cmpuint (operation_id, ==, data->op_id);
g_assert_cmpuint (operation_type, ==, data->operation_type);
/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
if (data->error != NULL) {
g_assert (error != NULL);
*(data->error) = g_error_copy (error);
g_assert (entry == NULL);
if (data->returned_entry != NULL)
*(data->returned_entry) = NULL;
} else {
g_assert_no_error (error);
g_assert (entry != NULL);
g_assert (entry != data->entry); /* check that the pointers aren't the same */
g_assert (gdata_entry_is_inserted (entry) == TRUE);
/* Check the entries match */
g_assert_cmpstr (gdata_entry_get_title (entry), ==, gdata_entry_get_title (data->entry));
g_assert_cmpstr (gdata_entry_get_summary (entry), ==, gdata_entry_get_summary (data->entry));
g_assert_cmpstr (gdata_entry_get_content (entry), ==, gdata_entry_get_content (data->entry));
g_assert_cmpstr (gdata_entry_get_rights (entry), ==, gdata_entry_get_rights (data->entry));
/* Only test for differences in content URI if we had one to begin with, since the inserted entry could feasibly generate and return
* new content. */
if (gdata_entry_get_content_uri (data->entry) != NULL)
g_assert_cmpstr (gdata_entry_get_content_uri (entry), ==, gdata_entry_get_content_uri (data->entry));
/* Copy the inserted entry for the calling test code to prod later */
if (data->returned_entry != NULL)
*(data->returned_entry) = g_object_ref (entry);
}
/* Free the data */
batch_operation_data_free (data);
}
guint
gdata_test_batch_operation_insertion (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **inserted_entry, GError **error)
{
guint op_id;
BatchOperationData *data;
data = g_slice_new (BatchOperationData);
data->operation = g_object_ref (operation);
data->op_id = 0;
data->operation_type = GDATA_BATCH_OPERATION_INSERTION;
data->entry = g_object_ref (entry);
data->returned_entry = inserted_entry;
data->id = NULL;
data->entry_type = G_TYPE_INVALID;
data->error = error;
op_id = gdata_batch_operation_add_insertion (operation, entry, test_batch_operation_insertion_update_cb, data);
data->op_id = op_id;
/* We expect a callback to be called when the operation is run */
g_object_set_data (G_OBJECT (operation), "test::expected-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")) + 1));
return op_id;
}
guint
gdata_test_batch_operation_update (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **updated_entry, GError **error)
{
guint op_id;
BatchOperationData *data;
data = g_slice_new (BatchOperationData);
data->operation = g_object_ref (operation);
data->op_id = 0;
data->operation_type = GDATA_BATCH_OPERATION_UPDATE;
data->entry = g_object_ref (entry);
data->returned_entry = updated_entry;
data->id = NULL;
data->entry_type = G_TYPE_INVALID;
data->error = error;
op_id = gdata_batch_operation_add_update (operation, entry, test_batch_operation_insertion_update_cb, data);
data->op_id = op_id;
/* We expect a callback to be called when the operation is run */
g_object_set_data (G_OBJECT (operation), "test::expected-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")) + 1));
return op_id;
}
static void
test_batch_operation_deletion_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
{
BatchOperationData *data = user_data;
/* Mark the callback as having been run */
g_object_set_data (G_OBJECT (data->operation), "test::called-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (data->operation), "test::called-callbacks")) + 1));
/* Check that the @operation_type and @operation_id matches those stored in @data */
g_assert_cmpuint (operation_id, ==, data->op_id);
g_assert_cmpuint (operation_type, ==, data->operation_type);
g_assert (entry == NULL);
/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
if (data->error != NULL) {
g_assert (error != NULL);
*(data->error) = g_error_copy (error);
} else {
g_assert_no_error (error);
}
/* Free the data */
batch_operation_data_free (data);
}
guint
gdata_test_batch_operation_deletion (GDataBatchOperation *operation, GDataEntry *entry, GError **error)
{
guint op_id;
BatchOperationData *data;
data = g_slice_new (BatchOperationData);
data->operation = g_object_ref (operation);
data->op_id = 0;
data->operation_type = GDATA_BATCH_OPERATION_DELETION;
data->entry = g_object_ref (entry);
data->returned_entry = NULL;
data->id = NULL;
data->entry_type = G_TYPE_INVALID;
data->error = error;
op_id = gdata_batch_operation_add_deletion (operation, entry, test_batch_operation_deletion_cb, data);
data->op_id = op_id;
/* We expect a callback to be called when the operation is run */
g_object_set_data (G_OBJECT (operation), "test::expected-callbacks",
GUINT_TO_POINTER (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")) + 1));
return op_id;
}
gboolean
gdata_test_batch_operation_run (GDataBatchOperation *operation, GCancellable *cancellable, GError **error)
{
gboolean success = gdata_batch_operation_run (operation, cancellable, error);
/* Assert that callbacks were called exactly once for each operation in the batch operation */
g_assert_cmpuint (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")), ==,
GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::called-callbacks")));
return success;
}
gboolean
gdata_test_batch_operation_run_finish (GDataBatchOperation *operation, GAsyncResult *async_result, GError **error)
{
gboolean success = gdata_batch_operation_run_finish (operation, async_result, error);
/* Assert that callbacks were called exactly once for each operation in the batch operation */
g_assert_cmpuint (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::expected-callbacks")), ==,
GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (operation), "test::called-callbacks")));
return success;
}
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 ||
compare_xml_namespaces (attr1->ns, attr2->ns) == FALSE ||
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_strings (const gchar *parsable_xml, const gchar *expected_xml, gboolean print_error)
{
gboolean success;
xmlDoc *parsable_doc, *expected_doc;
/* 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);
return success;
}
gboolean
gdata_test_compare_xml (GDataParsable *parsable, const gchar *expected_xml, gboolean print_error)
{
gboolean success;
gchar *parsable_xml;
/* Get an XML string for the GDataParsable */
parsable_xml = gdata_parsable_get_xml (parsable);
success = gdata_test_compare_xml_strings (parsable_xml, expected_xml, print_error);
g_free (parsable_xml);
return success;
}
static gboolean
compare_json_nodes (JsonNode *node1, JsonNode *node2)
{
if (node1 == node2)
return TRUE;
if (JSON_NODE_TYPE (node1) != JSON_NODE_TYPE (node2))
return FALSE;
switch (JSON_NODE_TYPE (node1)) {
case JSON_NODE_OBJECT: {
JsonObject *object1, *object2;
guint size1, size2;
GList *members, *i;
object1 = json_node_get_object (node1);
object2 = json_node_get_object (node2);
size1 = json_object_get_size (object1);
size2 = json_object_get_size (object2);
if (size1 != size2)
return FALSE;
/* Iterate over the first object, checking that every member is also present in the second object. */
members = json_object_get_members (object1);
for (i = members; i != NULL; i = i->next) {
JsonNode *child_node1, *child_node2;
child_node1 = json_object_get_member (object1, i->data);
child_node2 = json_object_get_member (object2, i->data);
g_assert (child_node1 != NULL);
if (child_node2 == NULL) {
g_list_free (members);
return FALSE;
}
if (compare_json_nodes (child_node1, child_node2) == FALSE) {
g_list_free (members);
return FALSE;
}
}
g_list_free (members);
return TRUE;
}
case JSON_NODE_ARRAY: {
JsonArray *array1, *array2;
guint length1, length2, i;
array1 = json_node_get_array (node1);
array2 = json_node_get_array (node2);
length1 = json_array_get_length (array1);
length2 = json_array_get_length (array2);
if (length1 != length2)
return FALSE;
/* Iterate over both arrays, checking the elements at each index are identical. */
for (i = 0; i < length1; i++) {
JsonNode *child_node1, *child_node2;
child_node1 = json_array_get_element (array1, i);
child_node2 = json_array_get_element (array2, i);
if (compare_json_nodes (child_node1, child_node2) == FALSE)
return FALSE;
}
return TRUE;
}
case JSON_NODE_VALUE: {
GType type1, type2;
type1 = json_node_get_value_type (node1);
type2 = json_node_get_value_type (node2);
if (type1 != type2)
return FALSE;
switch (type1) {
case G_TYPE_BOOLEAN:
return (json_node_get_boolean (node1) == json_node_get_boolean (node2)) ? TRUE : FALSE;
case G_TYPE_DOUBLE:
/* Note: This doesn't need an epsilon-based comparison because we only want to return
* true if the string representation of the two values is equal — and if it is, their
* parsed values should be binary identical too. */
return (json_node_get_double (node1) == json_node_get_double (node2)) ? TRUE : FALSE;
case G_TYPE_INT64:
return (json_node_get_int (node1) == json_node_get_int (node2)) ? TRUE : FALSE;
case G_TYPE_STRING:
return (g_strcmp0 (json_node_get_string (node1), json_node_get_string (node2)) == 0) ? TRUE : FALSE;
default:
/* JSON doesn't support any other types. */
g_assert_not_reached ();
}
return TRUE;
}
case JSON_NODE_NULL:
return TRUE;
default:
g_assert_not_reached ();
}
}
gboolean
gdata_test_compare_json_strings (const gchar *parsable_json, const gchar *expected_json, gboolean print_error)
{
gboolean success;
JsonParser *parsable_parser, *expected_parser;
GError *child_error = NULL;
/* Parse both strings. */
parsable_parser = json_parser_new ();
expected_parser = json_parser_new ();
json_parser_load_from_data (parsable_parser, parsable_json, -1, &child_error);
if (child_error != NULL) {
if (print_error == TRUE) {
g_message ("\n\nParsable: %s\n\nNot valid JSON: %s", parsable_json, child_error->message);
}
g_error_free (child_error);
return FALSE;
}
json_parser_load_from_data (expected_parser, expected_json, -1, &child_error);
g_assert_no_error (child_error); /* this really should never fail; or the test has encoded bad JSON */
/* Recursively compare the two JSON nodes. */
success = compare_json_nodes (json_parser_get_root (parsable_parser), json_parser_get_root (expected_parser));
if (success == FALSE && print_error == TRUE) {
/* The comparison has failed, so print out the two JSON strings for ease of debugging */
g_message ("\n\nParsable: %s\n\nExpected: %s\n\n", parsable_json, expected_json);
}
g_object_unref (expected_parser);
g_object_unref (parsable_parser);
return success;
}
gboolean
gdata_test_compare_json (GDataParsable *parsable, const gchar *expected_json, gboolean print_error)
{
gboolean success;
gchar *parsable_json;
/* Get a JSON string for the GDataParsable. */
parsable_json = gdata_parsable_get_json (parsable);
success = gdata_test_compare_json_strings (parsable_json, expected_json, print_error);
g_free (parsable_json);
return success;
}
gboolean
gdata_test_compare_kind (GDataEntry *entry, const gchar *expected_term, const gchar *expected_label)
{
GList *list;
/* Check the entry's kind category is present and correct */
for (list = gdata_entry_get_categories (entry); list != NULL; list = list->next) {
GDataCategory *category = GDATA_CATEGORY (list->data);
if (g_strcmp0 (gdata_category_get_scheme (category), "http://schemas.google.com/g/2005#kind") == 0) {
/* Found the kind category; check its term and label. */
return (g_strcmp0 (gdata_category_get_term (category), expected_term) == 0) &&
(g_strcmp0 (gdata_category_get_label (category), expected_label) == 0);
}
}
/* No kind! */
return FALSE;
}
/* Common code for tests of async query functions that have progress callbacks */
void
gdata_test_async_progress_callback (GDataEntry *entry, guint entry_key, guint entry_count, GDataAsyncProgressClosure *data)
{
/* No-op */
}
void
gdata_test_async_progress_closure_free (GDataAsyncProgressClosure *data)
{
/* Check that this callback is called first */
g_assert_cmpuint (data->async_ready_notify_count, ==, 0);
data->progress_destroy_notify_count++;
}
void
gdata_test_async_progress_finish_callback (GObject *service, GAsyncResult *res, GDataAsyncProgressClosure *data)
{
/* Check that this callback is called second */
g_assert_cmpuint (data->progress_destroy_notify_count, ==, 1);
data->async_ready_notify_count++;
g_main_loop_quit (data->main_loop);
}
gboolean
gdata_async_test_cancellation_cb (GDataAsyncTestData *async_data)
{
g_cancellable_cancel (async_data->cancellable);
async_data->cancellation_timeout_id = 0;
return FALSE;
}
void
gdata_set_up_async_test_data (GDataAsyncTestData *async_data, gconstpointer test_data)
{
async_data->main_loop = g_main_loop_new (NULL, FALSE);
async_data->cancellable = g_cancellable_new ();
async_data->cancellation_timeout = 0;
async_data->cancellation_successful = FALSE;
async_data->test_data = test_data;
}
void
gdata_tear_down_async_test_data (GDataAsyncTestData *async_data, gconstpointer test_data)
{
g_assert (async_data->test_data == test_data); /* sanity check */
g_object_unref (async_data->cancellable);
g_main_loop_unref (async_data->main_loop);
}
/* Output a log message. Note the output is prefixed with ‘# ’ so that it
* doesn’t interfere with TAP output. */
static void
output_commented_lines (const gchar *message)
{
const gchar *i, *next_newline;
for (i = message; i != NULL && *i != '\0'; i = next_newline) {
gchar *line;
next_newline = strchr (i, '\n');
if (next_newline != NULL) {
line = g_strndup (i, next_newline - i);
next_newline++;
} else {
line = g_strdup (i);
}
printf ("# %s\n", line);
g_free (line);
}
}
static void
output_log_message (const gchar *message)
{
if (strlen (message) > 2 && message[2] == '<') {
/* As debug string starts with direction indicator and space, t.i. "< ", */
/* we need access string starting from third character and see if it's */
/* looks like xml - t.i. it starts with '<' */
xmlChar *xml_buff;
int buffer_size;
xmlDocPtr xml_doc;
/* we need to cut to the begining of XML string */
message = message + 2;
/* create xml document and dump it to string buffer */
xml_doc = xmlParseDoc ((const xmlChar*) message);
xmlDocDumpFormatMemory (xml_doc, &xml_buff, &buffer_size, 1);
/* print out structured xml - if it's not xml, it will get error in output */
output_commented_lines ((gchar*) xml_buff);
/* free xml structs */
xmlFree (xml_buff);
xmlFreeDoc (xml_doc);
} else {
output_commented_lines ((gchar*) message);
}
}
static void
gdata_test_debug_handler (const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data)
{
output_log_message (message);
/* Log to the trace file. */
if ((*message == '<' || *message == '>' || *message == ' ') && *(message + 1) == ' ') {
uhm_server_received_message_chunk (mock_server, message, strlen (message), NULL);
}
}
/**
* gdata_test_set_https_port:
* @server: a #UhmServer
*
* Sets the HTTPS port used for all future libgdata requests to that used by the given mock @server,
* effectively redirecting all client requests to the mock server.
*
* Since: 0.13.4
*/
void
gdata_test_set_https_port (UhmServer *server)
{
gchar *port_string = g_strdup_printf ("%u", uhm_server_get_port (server));
g_setenv ("LIBGDATA_HTTPS_PORT", port_string, TRUE);
g_free (port_string);
}
/**
* gdata_test_mock_server_start_trace:
* @server: a #UhmServer
* @trace_filename: filename of the trace to load
*
* Wrapper around uhm_server_start_trace() which additionally sets the <code class="literal">LIBGDATA_HTTPS_PORT</code>
* environment variable to redirect all libgdata requests to the mock server.
*
* Since: 0.13.4
*/
void
gdata_test_mock_server_start_trace (UhmServer *server, const gchar *trace_filename)
{
GError *child_error = NULL;
uhm_server_start_trace (server, trace_filename, &child_error);
g_assert_no_error (child_error);
gdata_test_set_https_port (server);
}
/**
* gdata_test_mock_server_handle_message_error:
* @server: a #UhmServer
* @message: the message whose response should be filled
* @client: the currently connected client
* @user_data: user data provided when connecting the signal
*
* Handler for #UhmServer::handle-message which sets the HTTP response for @message to the HTTP error status
* specified in a #GDataTestRequestErrorData structure passed to @user_data.
*
* Since: 0.13.4
*/
gboolean
gdata_test_mock_server_handle_message_error (UhmServer *server, SoupMessage *message, SoupClientContext *client, gpointer user_data)
{
const GDataTestRequestErrorData *data = user_data;
soup_message_set_status_full (message, data->status_code, data->reason_phrase);
soup_message_body_append (message->response_body, SOUP_MEMORY_STATIC, data->message_body, strlen (data->message_body));
return TRUE;
}
/**
* gdata_test_mock_server_handle_message_timeout:
* @server: a #UhmServer
* @message: the message whose response should be filled
* @client: the currently connected client
* @user_data: user data provided when connecting the signal
*
* Handler for #UhmServer::handle-message which waits for 2 seconds before returning a %SOUP_STATUS_REQUEST_TIMEOUT status
* and appropriate error message body. If used in conjunction with a 1 second timeout in the client code under test, this can
* simulate network error conditions and timeouts, in order to test the error handling code for such conditions.
*
* Since: 0.13.4
*/
gboolean
gdata_test_mock_server_handle_message_timeout (UhmServer *server, SoupMessage *message, SoupClientContext *client, gpointer user_data)
{
/* Sleep for longer than the timeout set on the client. */
g_usleep (2 * G_USEC_PER_SEC);
soup_message_set_status_full (message, SOUP_STATUS_REQUEST_TIMEOUT, "Request Timeout");
soup_message_body_append (message->response_body, SOUP_MEMORY_STATIC, "Request timed out.", strlen ("Request timed out."));
return TRUE;
}
/**
* gdata_test_query_user_for_verifier:
* @authentication_uri: authentication URI to present
*
* Given an authentication URI, prompt the user to go to that URI, grant access
* to the test application and enter the resulting verifier. This is to be used
* with interactive OAuth authorisation requests.
*
* Returns: (transfer full): verifier from the web page
*/
gchar *
gdata_test_query_user_for_verifier (const gchar *authentication_uri)
{
char verifier[100];
/* Wait for the user to retrieve and enter the verifier */
g_print ("Please navigate to the following URI and grant access: %s\n", authentication_uri);
g_print ("Enter verifier (EOF to skip test): ");
if (scanf ("%100s", verifier) != 1) {
/* Skip the test */
g_test_message ("Skipping test on user request.");
return NULL;
}
g_test_message ("Proceeding with user-provided verifier “%s”.", verifier);
return g_strdup (verifier);
}