/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2013 Intel Corporation * * This program 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. * * This program 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 this program. If not, see . * * Authors: Tristan Van Berkom */ #include #include #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_enabled (guards, TRUE); } typedef struct { EContactField field; const gchar *value; } TestData; typedef struct { ETestServerFixture *fixture; GThread *thread; const gchar *book_uid; const gchar *contact_uid; EContactField field; const gchar *value; EBookClient *client; EFlag *flag; } 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 e_flag_set (data->flag); } 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, E_BOOK_OPERATION_FLAG_NONE, 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) { 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); g_idle_add ((GSourceFunc) try_write_field_thread_idle, data); } 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) { ESource *source; GError *error = NULL; /* Open the test book client in this thread */ source = e_source_registry_ref_source (data->fixture->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 */ g_idle_add ((GSourceFunc) test_write_thread_open_idle, data); e_flag_wait (data->flag); g_object_unref (source); g_object_unref (data->client); return NULL; } static ThreadData * create_test_thread (ETestServerFixture *fixture, 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); g_assert_nonnull (fixture); data->fixture = fixture; data->book_uid = book_uid; data->contact_uid = contact_uid; data->field = field; data->value = value; data->flag = e_flag_new (); 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); e_flag_free (data->flag); g_slice_free (ThreadData, data); } typedef struct _WaitForThreadsData { ETestServerFixture *fixture; ThreadData **tests; guint n_tests; } WaitForThreadsData; static gpointer wait_for_tests_thread (gpointer user_data) { WaitForThreadsData *data = user_data; gint ii; /* Wait for all threads to complete */ for (ii = 0; ii < data->n_tests; ii++) wait_thread_test (data->tests[ii]); g_main_loop_quit (data->fixture->loop); return NULL; } 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; WaitForThreadsData wait_data; GThread *wait_thread; 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 ( fixture, book_uid, contact_uid, field_tests[i].field, field_tests[i].value); wait_data.fixture = fixture; wait_data.tests = tests; wait_data.n_tests = G_N_ELEMENTS (field_tests); wait_thread = g_thread_new ("wait-tests", wait_for_tests_thread, &wait_data); g_thread_unref (wait_thread); g_main_loop_run (fixture->loop); /* 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); g_free (tests); } gint main (gint argc, gchar **argv) { g_test_init (&argc, &argv, NULL); g_test_bug_base ("https://gitlab.gnome.org/GNOME/evolution-data-server/"); client_test_utils_read_args (argc, argv); 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 (argc, argv); }