/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* * GData Client * Copyright (C) Philip Withnall 2010 * * 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 . */ #include #include #include #include #include #include #include #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 LIBGDATA_HTTPS_PORT * 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); }