[evolution-data-server/openismus-work-master: 1/18] Adding test case proving there is a write-write race condition to fix



commit 169f8e13d3ca74c9c8c32c9e1519cef351554498
Author: Tristan Van Berkom <tristanvb openismus com>
Date:   Thu Oct 25 16:51:32 2012 +0900

    Adding test case proving there is a write-write race condition to fix
    
    The test case creates a number of threads all trying to modify the
    same contact in the same book each concurrently with a different field,
    if the modification fails it retries. If all the fields (one for each
    thread) are not properly set at the end of all thread execution, then
    the test is a failure.

 tests/libebook/client/Makefile.am               |    3 +
 tests/libebook/client/test-client-write-write.c |  297 +++++++++++++++++++++++
 2 files changed, 300 insertions(+), 0 deletions(-)
---
diff --git a/tests/libebook/client/Makefile.am b/tests/libebook/client/Makefile.am
index a7568fa..6aa6f97 100644
--- a/tests/libebook/client/Makefile.am
+++ b/tests/libebook/client/Makefile.am
@@ -38,6 +38,7 @@ TESTS =								\
 	test-client-remove-contact-by-uid			\
 	test-client-remove-contacts				\
 	test-client-photo-is-uri				\
+	test-client-write-write					\
 	test-client-stress-factory--serial			\
 	test-client-stress-factory--fifo			\
 	test-client-stress-factory--single-book			\
@@ -110,6 +111,8 @@ test_client_remove_contacts_LDADD=$(TEST_LIBS)
 test_client_remove_contacts_CPPFLAGS=$(TEST_CPPFLAGS)
 test_client_photo_is_uri_LDADD=$(TEST_LIBS)
 test_client_photo_is_uri_CPPFLAGS=$(TEST_CPPFLAGS)
+test_client_write_write_LDADD=$(TEST_LIBS) $(top_builddir)/addressbook/libedata-book/libedata-book-1.2.la
+test_client_write_write_CPPFLAGS=$(TEST_CPPFLAGS)
 test_client_stress_factory__fifo_LDADD=$(TEST_LIBS)
 test_client_stress_factory__fifo_CPPFLAGS=$(TEST_CPPFLAGS)
 test_client_stress_factory__serial_LDADD=$(TEST_LIBS)
diff --git a/tests/libebook/client/test-client-write-write.c b/tests/libebook/client/test-client-write-write.c
new file mode 100644
index 0000000..1a708d7
--- /dev/null
+++ b/tests/libebook/client/test-client-write-write.c
@@ -0,0 +1,297 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+#include <stdlib.h>
+#include <libebook/libebook.h>
+#include <libedata-book/libedata-book.h>
+
+#include "client-test-utils.h"
+
+
+typedef struct {
+	EContactField field;
+	const gchar  *value;
+} TestData;
+
+typedef struct {
+	GThread       *thread;
+	const gchar   *book_uid;
+	const gchar   *contact_uid;
+	EContactField  field;
+	const gchar   *value;
+	EBookClient   *client;
+	GMainLoop     *loop;
+} ThreadData;
+
+/* Special attention needed for this array:
+ *
+ * Some contact fields cannot be used together, for instance
+ * E_CONTACT_PHONE_OTHER will conflict with E_CONTACT_PHONE_HOME and others,
+ * E_CONTACT_EMAIL_[1-4] can get mixed up if not set in proper sequence.
+ *
+ * For this test case to work properly, all fields must not conflict with eachother.
+ */
+static const TestData field_tests[] = {
+	{ E_CONTACT_GIVEN_NAME,          "Elvis" },
+	{ E_CONTACT_FAMILY_NAME,         "Presley" },
+	{ E_CONTACT_NICKNAME,            "The King" },
+	{ E_CONTACT_EMAIL_1,             "elvis presley com" },
+	{ E_CONTACT_ADDRESS_LABEL_HOME,  "3764 Elvis Presley Boulevard, Graceland" },
+	{ E_CONTACT_ADDRESS_LABEL_WORK,  "Workin on the road again..." },
+	{ E_CONTACT_ADDRESS_LABEL_OTHER, "Another address to reach the king" },
+	{ E_CONTACT_PHONE_ASSISTANT,     "+1234567890" },
+	{ E_CONTACT_PHONE_BUSINESS,      "+99-123-4352-9943" },
+	{ E_CONTACT_PHONE_BUSINESS_FAX,  "+44-123456789" },
+	{ E_CONTACT_PHONE_CALLBACK,      "+11-222-3333-4444" },
+	{ E_CONTACT_PHONE_CAR,           "555-123-4567" },
+	{ E_CONTACT_PHONE_COMPANY,       "666-666-6666" },
+	{ E_CONTACT_PHONE_HOME,          "333-4444-5678" },
+	{ E_CONTACT_PHONE_HOME_FAX,      "+993355556666" },
+	{ E_CONTACT_PHONE_ISDN,          "+88-777-6666-5555" },
+	{ E_CONTACT_PHONE_MOBILE,        "333-3333" }
+};
+
+static gboolean try_write_field_thread_idle (ThreadData *data);
+
+
+static void
+test_write_thread_contact_modified (GObject *source_object,
+				    GAsyncResult *res,
+				    ThreadData *data)
+{
+	GError   *error = NULL;
+	gboolean  retry = FALSE;
+
+	if (!e_book_client_modify_contact_finish (E_BOOK_CLIENT (source_object), res, &error)) {
+
+		/* For bad revision errors, retry the transaction after fetching the
+		 * contact again first: The backend is telling us that this commit would have
+		 * caused some data loss since we dont have the right contact in the first place.
+		 */
+		if (error->domain == E_DATA_BOOK_ERROR &&
+		    error->code == E_DATA_BOOK_STATUS_BAD_REVISION)
+			retry = TRUE;
+		else 
+			g_error ("Error updating '%s' field: %s\n",
+				 e_contact_field_name (data->field),
+				 error->message);
+
+		g_error_free (error);
+	}
+
+	if (retry)
+		try_write_field_thread_idle (data);
+	else
+		g_main_loop_quit (data->loop);
+}
+
+static void
+test_write_thread_contact_fetched (GObject *source_object,
+				   GAsyncResult *res,
+				   ThreadData *data)
+{
+	EContact *contact = NULL;
+	GError   *error = NULL;
+
+	if (!e_book_client_get_contact_finish (E_BOOK_CLIENT (source_object), res, &contact, &error))
+		g_error ("Failed to fetch contact in thread '%s': %s",
+			 e_contact_field_name (data->field), error->message);
+
+	e_contact_set (contact, data->field, data->value);
+
+	e_book_client_modify_contact (data->client, contact, NULL, 
+				      (GAsyncReadyCallback)test_write_thread_contact_modified, data);
+
+	g_object_unref (contact);
+}
+
+static gboolean
+try_write_field_thread_idle (ThreadData *data)
+{
+	e_book_client_get_contact (data->client, data->contact_uid, NULL,
+				   (GAsyncReadyCallback)test_write_thread_contact_fetched, data);
+
+	return FALSE;
+}
+
+static void
+test_write_thread_client_opened (GObject *source_object,
+				 GAsyncResult *res,
+				 ThreadData *data)
+{
+	GMainContext *context;
+	GSource      *gsource;
+	GError       *error = NULL;
+
+	if (!e_client_open_finish (E_CLIENT (source_object), res, &error))
+		g_error ("Error opening client for thread '%s': %s",
+			 e_contact_field_name (data->field),
+			 error->message);
+
+	context = g_main_loop_get_context (data->loop);
+	gsource = g_idle_source_new ();
+	g_source_set_callback (gsource, (GSourceFunc)try_write_field_thread_idle, data, NULL);
+	g_source_attach (gsource, context);
+}
+
+static gboolean
+test_write_thread_open_idle (ThreadData *data)
+{
+	/* Open the book client, only if it exists, it should be the same book created by the main thread */
+	e_client_open (E_CLIENT (data->client), TRUE, NULL, (GAsyncReadyCallback)test_write_thread_client_opened, data);
+
+	return FALSE;
+}
+
+static gpointer
+test_write_thread (ThreadData *data)
+{
+	GMainContext    *context;
+	ESourceRegistry *registry;
+	GSource         *gsource;
+	ESource         *source;
+	GError          *error = NULL;
+
+	context    = g_main_context_new ();
+	data->loop = g_main_loop_new (context, FALSE);
+	g_main_context_push_thread_default (context);
+
+	/* Open the test book client in this thread */
+	registry = e_source_registry_new_sync (NULL, &error);
+	if (!registry)
+		g_error ("Unable to create the registry: %s", error->message);
+
+	source = e_source_registry_ref_source (registry, data->book_uid);
+	if (!source)
+		g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
+
+	data->client = e_book_client_new (source, &error);
+	if (!data->client)
+		g_error ("Unable to create EBookClient for uid '%s': %s", data->book_uid, error->message);
+
+	/* Retry setting the contact field until we succeed setting the field
+	 */
+	gsource = g_idle_source_new ();
+	g_source_set_callback (gsource, (GSourceFunc)test_write_thread_open_idle, data, NULL);
+	g_source_attach (gsource, context);
+	g_main_loop_run (data->loop);
+
+	g_object_unref (source);
+	g_object_unref (registry);
+
+	g_object_unref (data->client);
+	g_main_context_pop_thread_default (context);
+	g_main_loop_unref (data->loop);
+	g_main_context_unref (context);
+
+	return NULL;
+}
+
+static ThreadData *
+create_test_thread (const gchar   *book_uid,
+		    const gchar   *contact_uid,
+		    EContactField  field,
+		    const gchar   *value)
+{
+	ThreadData  *data = g_slice_new0 (ThreadData);
+	const gchar *name = e_contact_field_name (field);
+
+	data->book_uid    = book_uid;
+	data->contact_uid = contact_uid;
+	data->field       = field;
+	data->value       = value;
+
+	data->thread = g_thread_new (name, (GThreadFunc)test_write_thread, data);
+
+	return data;
+}
+
+static void
+wait_thread_test (ThreadData *data)
+{
+	g_thread_join (data->thread);
+	g_slice_free (ThreadData, data);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+	EBookClient *main_client;
+	EContact *contact;
+	GError *error = NULL;
+	gchar *book_uid = NULL;
+	gchar *contact_uid = NULL;
+	ThreadData **tests;
+	gint i;
+
+	main_initialize ();
+
+	/* Open the book */
+	main_client = new_temp_client (&book_uid);
+	g_return_val_if_fail (main_client != NULL, 1);
+
+	if (!e_client_open_sync (E_CLIENT (main_client), FALSE, NULL, &error)) {
+		report_error ("client open sync", &error);
+		g_object_unref (main_client);
+		return 1;
+	}
+
+	/* Create out test contact */
+	if (!add_contact_from_test_case_verify (main_client, "simple-1", &contact)) {
+		g_object_unref (main_client);
+		return 1;
+	}
+
+	contact_uid = e_contact_get (contact, E_CONTACT_UID);
+	g_object_unref (contact);
+
+
+	/* Create all concurrent threads accessing the same addressbook */
+	tests = g_new0 (ThreadData *, G_N_ELEMENTS (field_tests));
+	for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
+		tests[i] = create_test_thread (book_uid, contact_uid,
+					       field_tests[i].field,
+					       field_tests[i].value);
+
+	/* Wait for all threads to complete */
+	for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
+		wait_thread_test (tests[i]);
+
+	/* Fetch the updated contact */
+	if (!e_book_client_get_contact_sync (main_client, contact_uid, &contact, NULL, &error))
+		g_error ("Failed to fetch test contact after updates: %s", error->message);
+
+	/* Ensure that every value written to the contact concurrently was actually updated in
+	 * the final contact
+	 */
+	for (i = 0; i < G_N_ELEMENTS (field_tests); i++) {
+		gchar *value = e_contact_get (contact, field_tests[i].field);
+
+		if (g_strcmp0 (field_tests[i].value, value) != 0) {
+			gchar *vcard;
+
+			vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+			g_error ("Lost data in concurrent writes, expected value for field '%s' was '%s', "
+				 "actual value is '%s', vcard:\n%s\n",
+				 e_contact_field_name (field_tests[i].field), field_tests[i].value, value, vcard);
+		}
+
+		g_free (value);
+	}
+	g_object_unref (contact);
+
+	g_free (book_uid);
+	g_free (contact_uid);
+
+	/* Remove the book, test complete */
+	if (!e_client_remove_sync (E_CLIENT (main_client), NULL, &error)) {
+		report_error ("client remove sync", &error);
+		g_object_unref (main_client);
+		return 1;
+	}
+
+	g_object_unref (main_client);
+
+	return 0;
+}



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