1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2013 Intel Corporation
4 *
5 * This program is free software: you can redistribute it and/or modify it
6 * under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12 * for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Tristan Van Berkom <tristanvb@openismus.com>
18 */
19
20 #include <stdlib.h>
21 #include <locale.h>
22
23 #include "client-test-utils.h"
24 #include "e-test-server-utils.h"
25
26 static void setup_custom_book (ESource *scratch,
27 ETestServerClosure *closure);
28
29 static ETestServerClosure book_closure = { E_TEST_SERVER_ADDRESS_BOOK, setup_custom_book, 0 };
30
31 static void
setup_custom_book(ESource * scratch,ETestServerClosure * closure)32 setup_custom_book (ESource *scratch,
33 ETestServerClosure *closure)
34 {
35 ESourceRevisionGuards *guards;
36
37 g_type_ensure (E_TYPE_SOURCE_REVISION_GUARDS);
38 guards = e_source_get_extension (scratch, E_SOURCE_EXTENSION_REVISION_GUARDS);
39 e_source_revision_guards_set_enabled (guards, TRUE);
40 }
41
42 typedef struct {
43 EContactField field;
44 const gchar *value;
45 } TestData;
46
47 typedef struct {
48 ETestServerFixture *fixture;
49 GThread *thread;
50 const gchar *book_uid;
51 const gchar *contact_uid;
52 EContactField field;
53 const gchar *value;
54 EBookClient *client;
55 EFlag *flag;
56 } ThreadData;
57
58 /* Special attention needed for this array:
59 *
60 * Some contact fields cannot be used together, for instance
61 * E_CONTACT_PHONE_OTHER will conflict with E_CONTACT_PHONE_HOME and others,
62 * E_CONTACT_EMAIL_[1-4] can get mixed up if not set in proper sequence.
63 *
64 * For this test case to work properly, all fields must not conflict with eachother.
65 */
66 static const TestData field_tests[] = {
67 { E_CONTACT_GIVEN_NAME, "Elvis" },
68 { E_CONTACT_FAMILY_NAME, "Presley" },
69 { E_CONTACT_NICKNAME, "The King" },
70 { E_CONTACT_EMAIL_1, "elvis@presley.com" },
71 { E_CONTACT_ADDRESS_LABEL_HOME, "3764 Elvis Presley Boulevard, Graceland" },
72 { E_CONTACT_ADDRESS_LABEL_WORK, "Workin on the road again..." },
73 { E_CONTACT_ADDRESS_LABEL_OTHER, "Another address to reach the king" },
74 { E_CONTACT_PHONE_ASSISTANT, "+1234567890" },
75 { E_CONTACT_PHONE_BUSINESS, "+99-123-4352-9943" },
76 { E_CONTACT_PHONE_BUSINESS_FAX, "+44-123456789" },
77 { E_CONTACT_PHONE_CALLBACK, "+11-222-3333-4444" },
78 { E_CONTACT_PHONE_CAR, "555-123-4567" },
79 { E_CONTACT_PHONE_COMPANY, "666-666-6666" },
80 { E_CONTACT_PHONE_HOME, "333-4444-5678" },
81 { E_CONTACT_PHONE_HOME_FAX, "+993355556666" },
82 { E_CONTACT_PHONE_ISDN, "+88-777-6666-5555" },
83 { E_CONTACT_PHONE_MOBILE, "333-3333" }
84 };
85
86 static gboolean try_write_field_thread_idle (ThreadData *data);
87
88 static void
test_write_thread_contact_modified(GObject * source_object,GAsyncResult * res,ThreadData * data)89 test_write_thread_contact_modified (GObject *source_object,
90 GAsyncResult *res,
91 ThreadData *data)
92 {
93 GError *error = NULL;
94 gboolean retry = FALSE;
95
96 if (!e_book_client_modify_contact_finish (E_BOOK_CLIENT (source_object), res, &error)) {
97
98 /* For bad revision errors, retry the transaction after fetching the
99 * contact again first: The backend is telling us that this commit would have
100 * caused some data loss since we dont have the right contact in the first place.
101 */
102 if (g_error_matches (error, E_CLIENT_ERROR,
103 E_CLIENT_ERROR_OUT_OF_SYNC))
104 retry = TRUE;
105 else
106 g_error (
107 "Error updating '%s' field: %s\n",
108 e_contact_field_name (data->field),
109 error->message);
110
111 g_error_free (error);
112 }
113
114 if (retry)
115 try_write_field_thread_idle (data);
116 else
117 e_flag_set (data->flag);
118 }
119
120 static void
test_write_thread_contact_fetched(GObject * source_object,GAsyncResult * res,ThreadData * data)121 test_write_thread_contact_fetched (GObject *source_object,
122 GAsyncResult *res,
123 ThreadData *data)
124 {
125 EContact *contact = NULL;
126 GError *error = NULL;
127
128 if (!e_book_client_get_contact_finish (E_BOOK_CLIENT (source_object), res, &contact, &error))
129 g_error (
130 "Failed to fetch contact in thread '%s': %s",
131 e_contact_field_name (data->field), error->message);
132
133 e_contact_set (contact, data->field, data->value);
134
135 e_book_client_modify_contact (
136 data->client, contact, E_BOOK_OPERATION_FLAG_NONE, NULL,
137 (GAsyncReadyCallback) test_write_thread_contact_modified, data);
138
139 g_object_unref (contact);
140 }
141
142 static gboolean
try_write_field_thread_idle(ThreadData * data)143 try_write_field_thread_idle (ThreadData *data)
144 {
145 e_book_client_get_contact (
146 data->client, data->contact_uid, NULL,
147 (GAsyncReadyCallback) test_write_thread_contact_fetched, data);
148
149 return FALSE;
150 }
151
152 static void
test_write_thread_client_opened(GObject * source_object,GAsyncResult * res,ThreadData * data)153 test_write_thread_client_opened (GObject *source_object,
154 GAsyncResult *res,
155 ThreadData *data)
156 {
157 GError *error = NULL;
158
159 if (!e_client_open_finish (E_CLIENT (source_object), res, &error))
160 g_error (
161 "Error opening client for thread '%s': %s",
162 e_contact_field_name (data->field),
163 error->message);
164
165 g_idle_add ((GSourceFunc) try_write_field_thread_idle, data);
166 }
167
168 static gboolean
test_write_thread_open_idle(ThreadData * data)169 test_write_thread_open_idle (ThreadData *data)
170 {
171 /* Open the book client, only if it exists, it should be the same book created by the main thread */
172 e_client_open (E_CLIENT (data->client), TRUE, NULL, (GAsyncReadyCallback) test_write_thread_client_opened, data);
173
174 return FALSE;
175 }
176
177 static gpointer
test_write_thread(ThreadData * data)178 test_write_thread (ThreadData *data)
179 {
180 ESource *source;
181 GError *error = NULL;
182
183 /* Open the test book client in this thread */
184 source = e_source_registry_ref_source (data->fixture->registry, data->book_uid);
185 if (!source)
186 g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
187
188 data->client = e_book_client_new (source, &error);
189 if (!data->client)
190 g_error ("Unable to create EBookClient for uid '%s': %s", data->book_uid, error->message);
191
192 /* Retry setting the contact field until we succeed setting the field
193 */
194 g_idle_add ((GSourceFunc) test_write_thread_open_idle, data);
195
196 e_flag_wait (data->flag);
197
198 g_object_unref (source);
199 g_object_unref (data->client);
200
201 return NULL;
202 }
203
204 static ThreadData *
create_test_thread(ETestServerFixture * fixture,const gchar * book_uid,const gchar * contact_uid,EContactField field,const gchar * value)205 create_test_thread (ETestServerFixture *fixture,
206 const gchar *book_uid,
207 const gchar *contact_uid,
208 EContactField field,
209 const gchar *value)
210 {
211 ThreadData *data = g_slice_new0 (ThreadData);
212 const gchar *name = e_contact_field_name (field);
213
214 g_assert_nonnull (fixture);
215
216 data->fixture = fixture;
217 data->book_uid = book_uid;
218 data->contact_uid = contact_uid;
219 data->field = field;
220 data->value = value;
221 data->flag = e_flag_new ();
222
223 data->thread = g_thread_new (name, (GThreadFunc) test_write_thread, data);
224
225 return data;
226 }
227
228 static void
wait_thread_test(ThreadData * data)229 wait_thread_test (ThreadData *data)
230 {
231 g_thread_join (data->thread);
232 e_flag_free (data->flag);
233 g_slice_free (ThreadData, data);
234 }
235
236 typedef struct _WaitForThreadsData {
237 ETestServerFixture *fixture;
238 ThreadData **tests;
239 guint n_tests;
240 } WaitForThreadsData;
241
242 static gpointer
wait_for_tests_thread(gpointer user_data)243 wait_for_tests_thread (gpointer user_data)
244 {
245 WaitForThreadsData *data = user_data;
246 gint ii;
247
248 /* Wait for all threads to complete */
249 for (ii = 0; ii < data->n_tests; ii++)
250 wait_thread_test (data->tests[ii]);
251
252 g_main_loop_quit (data->fixture->loop);
253
254 return NULL;
255 }
256
257 static void
test_concurrent_writes(ETestServerFixture * fixture,gconstpointer user_data)258 test_concurrent_writes (ETestServerFixture *fixture,
259 gconstpointer user_data)
260 {
261 EBookClient *main_client;
262 ESource *source;
263 EContact *contact;
264 GError *error = NULL;
265 const gchar *book_uid = NULL;
266 gchar *contact_uid = NULL;
267 WaitForThreadsData wait_data;
268 GThread *wait_thread;
269 ThreadData **tests;
270 gint i;
271
272 main_client = E_TEST_SERVER_UTILS_SERVICE (fixture, EBookClient);
273 source = e_client_get_source (E_CLIENT (main_client));
274 book_uid = e_source_get_uid (source);
275
276 /* Create out test contact */
277 if (!add_contact_from_test_case_verify (main_client, "simple-1", &contact))
278 g_error ("Failed to add the test contact");
279
280 contact_uid = e_contact_get (contact, E_CONTACT_UID);
281 g_object_unref (contact);
282
283 /* Create all concurrent threads accessing the same addressbook */
284 tests = g_new0 (ThreadData *, G_N_ELEMENTS (field_tests));
285 for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
286 tests[i] = create_test_thread (
287 fixture,
288 book_uid, contact_uid,
289 field_tests[i].field,
290 field_tests[i].value);
291
292 wait_data.fixture = fixture;
293 wait_data.tests = tests;
294 wait_data.n_tests = G_N_ELEMENTS (field_tests);
295
296 wait_thread = g_thread_new ("wait-tests", wait_for_tests_thread, &wait_data);
297 g_thread_unref (wait_thread);
298
299 g_main_loop_run (fixture->loop);
300
301 /* Fetch the updated contact */
302 if (!e_book_client_get_contact_sync (main_client, contact_uid, &contact, NULL, &error))
303 g_error ("Failed to fetch test contact after updates: %s", error->message);
304
305 /* Ensure that every value written to the contact concurrently was actually updated in
306 * the final contact
307 */
308 for (i = 0; i < G_N_ELEMENTS (field_tests); i++) {
309 gchar *value = e_contact_get (contact, field_tests[i].field);
310
311 if (g_strcmp0 (field_tests[i].value, value) != 0) {
312 gchar *vcard;
313
314 vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
315
316 g_error (
317 "Lost data in concurrent writes, expected "
318 "value for field '%s' was '%s', actual value "
319 "is '%s', vcard:\n%s\n",
320 e_contact_field_name (field_tests[i].field),
321 field_tests[i].value, value, vcard);
322 }
323
324 g_free (value);
325 }
326 g_object_unref (contact);
327
328 g_free (contact_uid);
329 g_free (tests);
330 }
331
332 gint
main(gint argc,gchar ** argv)333 main (gint argc,
334 gchar **argv)
335 {
336 g_test_init (&argc, &argv, NULL);
337 g_test_bug_base ("https://gitlab.gnome.org/GNOME/evolution-data-server/");
338
339 client_test_utils_read_args (argc, argv);
340
341 setlocale (LC_ALL, "en_US.UTF-8");
342
343 g_test_add (
344 "/EBookClient/ConcurrentWrites",
345 ETestServerFixture,
346 &book_closure,
347 e_test_server_utils_setup,
348 test_concurrent_writes,
349 e_test_server_utils_teardown);
350
351 return e_test_server_utils_run (argc, argv);
352 }
353