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