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



commit 86a3e6fbc5eba3b142266fd033b4b1f6a8de7139
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 |  310 +++++++++++++++++++++++
 2 files changed, 313 insertions(+), 0 deletions(-)
---
diff --git a/tests/libebook/client/Makefile.am b/tests/libebook/client/Makefile.am
index 9c582c7..d67ad31 100644
--- a/tests/libebook/client/Makefile.am
+++ b/tests/libebook/client/Makefile.am
@@ -34,6 +34,7 @@ TESTS =								\
 	test-client-custom-summary				\
 	test-client-get-revision				\
 	test-client-get-view					\
+	test-client-write-write					\
 	test-client-uid-only-view				\
 	test-client-revision-view				\
 	test-client-suppress-notifications			\
@@ -112,6 +113,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..a2fbaf6
--- /dev/null
+++ b/tests/libebook/client/test-client-write-write.c
@@ -0,0 +1,310 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+#include <libedata-book/libedata-book.h>
+
+#include "client-test-utils.h"
+#include "e-test-server-utils.h"
+
+static void setup_custom_book (ESource            *scratch,
+			       ETestServerClosure *closure);
+
+static ETestServerClosure book_closure = { E_TEST_SERVER_ADDRESS_BOOK, setup_custom_book, 0 };
+
+
+static void
+setup_custom_book (ESource            *scratch,
+		   ETestServerClosure *closure)
+{
+	ESourceRevisionGuards *guards;
+
+	g_type_ensure (E_TYPE_SOURCE_REVISION_GUARDS);
+	guards = e_source_get_extension (scratch, E_SOURCE_EXTENSION_REVISION_GUARDS);
+	e_source_revision_guards_set_guards_enabled (guards, TRUE);
+}
+
+
+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 (g_error_matches (error, E_CLIENT_ERROR,
+				     E_CLIENT_ERROR_OUT_OF_SYNC))
+			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);
+}
+
+static void
+test_concurrent_writes (ETestServerFixture *fixture,
+		       gconstpointer       user_data)
+{
+	EBookClient *main_client;
+	ESource *source;
+	EContact *contact;
+	GError *error = NULL;
+	const gchar *book_uid = NULL;
+	gchar *contact_uid = NULL;
+	ThreadData **tests;
+	gint i;
+
+	main_client = E_TEST_SERVER_UTILS_SERVICE (fixture, EBookClient);
+	source = e_client_get_source (E_CLIENT (main_client));
+	book_uid = e_source_get_uid (source);
+
+	/* Create out test contact */
+	if (!add_contact_from_test_case_verify (main_client, "simple-1", &contact))
+		g_error ("Failed to add the test contact");
+
+	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 (contact_uid);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+	g_type_init ();
+#endif
+	g_test_init (&argc, &argv, NULL);
+	setlocale (LC_ALL, "en_US.UTF-8");
+
+	g_test_add ("/EBookClient/ConcurrentWrites", ETestServerFixture, &book_closure,
+		    e_test_server_utils_setup, test_concurrent_writes, e_test_server_utils_teardown);
+
+	return e_test_server_utils_run ();
+}



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