1 /*
2 * Copyright 2016 Collabora Ltd.
3 *
4 * The geocode-glib library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public License as
6 * published by the Free Software Foundation; either version 2 of the
7 * License, or (at your option) any later version.
8 *
9 * The geocode-glib library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with the Gnome Library; see the file COPYING.LIB. If not,
16 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301 USA.
18 *
19 * Authors: Philip Withnall <philip.withnall@collabora.co.uk>
20 */
21
22 #include <gio/gio.h>
23 #include <json-glib/json-glib.h>
24 #include <libsoup/soup.h>
25 #include <stdlib.h>
26 #include <string.h>
27
28 #include "geocode-glib-private.h"
29 #include "geocode-glib.h"
30 #include "geocode-mock-backend.h"
31
32 /**
33 * SECTION:geocode-mock-backend
34 * @short_description: Geocode mock backend implementation
35 * @include: geocode-glib/geocode-glib.h
36 *
37 * #GeocodeMockBackend is intended to be used in unit tests for applications
38 * which use geocode-glib — it allows them to set the geocode results they
39 * expect their application to query, and check afterwards that the queries
40 * were performed. It works offline, which allows application unit tests to be
41 * run on integration and build machines which are not online. It is not
42 * expected that #GeocodeMockBackend will be used in production code.
43 *
44 * To use it, create the backend instance, add the query results to it which
45 * you want to be returned to your application’s queries, then use it as the
46 * #GeocodeBackend for geocode_forward_set_backend() or
47 * geocode_reverse_set_backend(). After a test has been run, the set of queries
48 * which the code under test actually made on the backend can be checked using
49 * geocode_mock_backend_get_query_log(). The backend can be reset using
50 * geocode_mock_backend_clear() and new queries added for the next test.
51 *
52 * |[<!-- language="C" -->
53 * static void
54 * place_list_free (GList *l)
55 * {
56 * g_list_free_full (l, g_object_unref);
57 * }
58 *
59 * typedef GList PlaceList;
60 * G_DEFINE_AUTOPTR_CLEANUP_FUNC (PlaceList, place_list_free)
61 *
62 * g_autoptr (GeocodeForward) forward = NULL;
63 * g_autoptr (GeocodeMockBackend) backend = NULL;
64 * g_autoptr (GHashTable) params = NULL;
65 * GValue location = G_VALUE_INIT;
66 * g_autoptr (PlaceList) results = NULL;
67 * g_autoptr (PlaceList) expected_results = NULL;
68 * g_autoptr (GError) error = NULL;
69 * g_autoptr (GeocodePlace) expected_place = NULL;
70 * g_autoptr (GeocodeLocation) expected_location = NULL;
71 * GPtrArray *query_log; /<!-- -->* (element-type GeocodeMockBackendQuery) *<!-- -->/
72 *
73 * backend = geocode_mock_backend_new ();
74 *
75 * /<!-- -->* Build the set of parameters the mock backend expects to receive from
76 * * the #GeocodeForward instance. *<!-- -->/
77 * params = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
78 *
79 * g_value_init (&location, G_TYPE_STRING);
80 * g_value_set_static_string (&location, "Bullpot Farm");
81 * g_hash_table_insert (params, (gpointer) "location", &location);
82 *
83 * /<!-- -->* Build the set of results the mock backend should return. *<!-- -->/
84 * expected_location = geocode_location_new_with_description (
85 * 54.22759825, -2.51857179181113, 5.0,
86 * "Bullpot Farm, Fell Road, South Lakeland, Cumbria, "
87 * "North West England, England, United Kingdom");
88 * expected_place = geocode_place_new_with_location (
89 * "Bullpot Farm", GEOCODE_PLACE_TYPE_BUILDING, expected_location);
90 * expected_results = g_list_prepend (expected_results,
91 * g_steal_pointer (&expected_place));
92 *
93 * geocode_mock_backend_add_forward_result (backend, params,
94 * expected_results, NULL);
95 *
96 * /<!-- -->* Do the search. This would typically call the application code
97 * * under test, rather than geocode-glib directly. *<!-- -->/
98 * forward = geocode_forward_new_for_string ("Bullpot Farm");
99 * geocode_forward_set_backend (forward, GEOCODE_BACKEND (backend));
100 * results = geocode_forward_search (forward, &error);
101 *
102 * g_assert_no_error (error);
103 * assert_place_list_equal (results, expected_results);
104 *
105 * /<!-- -->* Check the application made the expected query. *<!-- -->/
106 * query_log = geocode_mock_backend_get_query_log (backend);
107 * g_assert_cmpuint (query_log->len, ==, 1);
108 * ]|
109 *
110 * Since: 3.23.1
111 */
112
113 struct _GeocodeMockBackend {
114 GObject parent;
115
116 GPtrArray *forward_results; /* (owned) (element-type owned GeocodeMockBackendQuery) */
117 GPtrArray *reverse_results; /* (owned) (element-type owned GeocodeMockBackendQuery) */
118 GPtrArray *query_log; /* (owned) (element-type owned GeocodeMockBackendQuery) */
119 };
120
121 static void geocode_backend_iface_init (GeocodeBackendInterface *iface);
122
G_DEFINE_TYPE_WITH_CODE(GeocodeMockBackend,geocode_mock_backend,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (GEOCODE_TYPE_BACKEND,geocode_backend_iface_init))123 G_DEFINE_TYPE_WITH_CODE (GeocodeMockBackend, geocode_mock_backend, G_TYPE_OBJECT,
124 G_IMPLEMENT_INTERFACE (GEOCODE_TYPE_BACKEND,
125 geocode_backend_iface_init))
126
127 /******************************************************************************/
128
129 static void
130 value_free (GValue *value)
131 {
132 g_value_unset (value);
133 g_free (value);
134 }
135
136 static GHashTable *
params_copy_deep(GHashTable * params)137 params_copy_deep (GHashTable *params)
138 {
139 g_autoptr (GHashTable) output = NULL;
140 GHashTableIter iter;
141 const gchar *key;
142 const GValue *value;
143
144 output = g_hash_table_new_full (g_str_hash, g_str_equal,
145 g_free, (GDestroyNotify) value_free);
146
147 g_hash_table_iter_init (&iter, params);
148
149 while (g_hash_table_iter_next (&iter, (gpointer *) &key,
150 (gpointer *) &value)) {
151 GValue *value_copy = NULL;
152
153 value_copy = g_new0 (GValue, 1);
154 g_value_init (value_copy, G_VALUE_TYPE (value));
155 g_value_copy (value, value_copy);
156
157 g_hash_table_insert (output, g_strdup (key),
158 g_steal_pointer (&value_copy));
159 }
160
161 return g_steal_pointer (&output);
162 }
163
164 static GList *
results_copy_deep(GList * results)165 results_copy_deep (GList *results)
166 {
167 return g_list_copy_deep (results, (GCopyFunc) g_object_ref, NULL);
168 }
169
170 /******************************************************************************/
171
172 static void
geocode_mock_backend_query_free(GeocodeMockBackendQuery * query)173 geocode_mock_backend_query_free (GeocodeMockBackendQuery *query)
174 {
175 if (query == NULL)
176 return;
177
178 g_hash_table_unref (query->params);
179 g_list_free_full (query->results, g_object_unref);
180 g_clear_error (&query->error);
181
182 g_free (query);
183 }
184
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GeocodeMockBackendQuery,geocode_mock_backend_query_free)185 G_DEFINE_AUTOPTR_CLEANUP_FUNC (GeocodeMockBackendQuery,
186 geocode_mock_backend_query_free)
187
188 static GeocodeMockBackendQuery *
189 geocode_mock_backend_query_new (GHashTable *params,
190 gboolean is_forward,
191 GList *results,
192 const GError *error)
193 {
194 g_autoptr (GeocodeMockBackendQuery) query = NULL;
195
196 g_return_val_if_fail (params != NULL, NULL);
197 g_return_val_if_fail ((results == NULL) != (error == NULL), NULL);
198
199 query = g_new0 (GeocodeMockBackendQuery, 1);
200
201 query->params = params_copy_deep (params);
202 query->is_forward = is_forward;
203 query->results = results_copy_deep (results);
204 query->error = (error != NULL) ? g_error_copy (error) : NULL;
205
206 return g_steal_pointer (&query);
207 }
208
209 /******************************************************************************/
210
211 static gboolean
value_equal(const GValue * a,const GValue * b)212 value_equal (const GValue *a,
213 const GValue *b)
214 {
215 GValue a_string = G_VALUE_INIT, b_string = G_VALUE_INIT;
216 gboolean equal;
217
218 g_return_val_if_fail (a != NULL, FALSE);
219 g_return_val_if_fail (b != NULL, FALSE);
220
221 if (G_VALUE_TYPE (a) != G_VALUE_TYPE (b))
222 return FALSE;
223
224 /* Doubles can’t be converted to strings, so special-case comparison
225 * of them. */
226 if (G_VALUE_TYPE (a) == G_TYPE_DOUBLE) {
227 return g_value_get_double (a) == g_value_get_double (b);
228 }
229
230 g_value_init (&a_string, G_TYPE_STRING);
231 g_value_init (&b_string, G_TYPE_STRING);
232
233 /* We assume that all GValue types can be converted to strings for the
234 * purpose of comparison. */
235 if (!g_value_transform (a, &a_string) ||
236 !g_value_transform (b, &b_string))
237 return FALSE;
238
239 equal = g_str_equal (g_value_get_string (&a_string),
240 g_value_get_string (&b_string));
241
242 g_value_unset (&b_string);
243 g_value_unset (&a_string);
244
245 return equal;
246 }
247
248 static gboolean
hash_table_equal(GHashTable * a,GHashTable * b)249 hash_table_equal (GHashTable *a,
250 GHashTable *b)
251 {
252 GHashTableIter iter_a;
253 const gchar *key;
254 const GValue *value_a, *value_b;
255
256 if (g_hash_table_size (a) != g_hash_table_size (b))
257 return FALSE;
258
259 g_hash_table_iter_init (&iter_a, a);
260
261 while (g_hash_table_iter_next (&iter_a, (gpointer *) &key,
262 (gpointer *) &value_a)) {
263 if (!g_hash_table_lookup_extended (b, key, NULL,
264 (gpointer *) &value_b) ||
265 !value_equal (value_a, value_b))
266 return FALSE;
267 }
268
269 return TRUE;
270 }
271
272 static const GeocodeMockBackendQuery *
find_query(GPtrArray * queries,GHashTable * params,gsize * index)273 find_query (GPtrArray *queries,
274 GHashTable *params,
275 gsize *index)
276 {
277 gsize i;
278
279 for (i = 0; i < queries->len; i++) {
280 const GeocodeMockBackendQuery *query = queries->pdata[i];
281
282 if (hash_table_equal (query->params, params)) {
283 if (index != NULL)
284 *index = i;
285
286 return query;
287 }
288 }
289
290 return NULL;
291 }
292
293 static void
debug_print_params(GHashTable * params)294 debug_print_params (GHashTable *params)
295 {
296 GHashTableIter iter;
297 const gchar *key;
298 const GValue *value;
299 g_autoptr (GString) output = NULL;
300 g_autofree gchar *output_str = NULL;
301 gboolean non_empty = FALSE;
302
303 g_hash_table_iter_init (&iter, params);
304 output = g_string_new ("");
305
306 while (g_hash_table_iter_next (&iter, (gpointer *) &key,
307 (gpointer *) &value)) {
308 g_autofree gchar *value_str = NULL;
309
310 value_str = g_strdup_value_contents (value);
311 g_string_append_printf (output, " • %s = %s\n", key, value_str);
312
313 non_empty = TRUE;
314 }
315
316 if (non_empty)
317 g_string_prepend (output, "Parameters:\n");
318 else
319 g_string_append (output, "Parameters: (none)\n");
320
321 /* Strip off the trailing newline. */
322 g_string_truncate (output, output->len - 1);
323
324 output_str = g_string_free (g_steal_pointer (&output), FALSE);
325 g_debug ("%s", output_str);
326 }
327
328 static GList *
forward_or_reverse(GeocodeMockBackend * self,GPtrArray * results,GeocodeError no_results_error,GHashTable * params,GCancellable * cancellable,GError ** error)329 forward_or_reverse (GeocodeMockBackend *self,
330 GPtrArray *results,
331 GeocodeError no_results_error,
332 GHashTable *params,
333 GCancellable *cancellable,
334 GError **error)
335 {
336 const GeocodeMockBackendQuery *query;
337 g_autoptr (GeocodeMockBackendQuery) logged_query = NULL;
338 GList *output_results = NULL; /* (element-type GeocodePlace) */
339 g_autoptr (GError) output_error = NULL;
340
341 /* Log the query; helpful during development. */
342 debug_print_params (params);
343
344 /* Do we have a mock result for this query? */
345 query = find_query (results, params, NULL);
346
347 if (query == NULL) {
348 output_error = g_error_new (GEOCODE_ERROR, no_results_error,
349 "No matches found for request");
350 } else if (query->error != NULL) {
351 output_error = g_error_copy (query->error);
352 } else {
353 output_results = results_copy_deep (query->results);
354 }
355
356 /* Log the query. */
357 logged_query = geocode_mock_backend_query_new (params, TRUE,
358 output_results,
359 output_error);
360 g_ptr_array_add (self->query_log, g_steal_pointer (&logged_query));
361
362 /* Output either the results or the error. */
363 g_assert ((output_results == NULL) != (output_error == NULL));
364
365 if (output_error != NULL)
366 g_propagate_error (error, g_steal_pointer (&output_error));
367
368 return g_steal_pointer (&output_results);
369 }
370
371 static GList *
geocode_mock_backend_forward_search(GeocodeBackend * backend,GHashTable * params,GCancellable * cancellable,GError ** error)372 geocode_mock_backend_forward_search (GeocodeBackend *backend,
373 GHashTable *params,
374 GCancellable *cancellable,
375 GError **error)
376 {
377 GeocodeMockBackend *self = GEOCODE_MOCK_BACKEND (backend);
378
379 return forward_or_reverse (self, self->forward_results,
380 GEOCODE_ERROR_NO_MATCHES, params,
381 cancellable, error);
382 }
383
384 static GList *
geocode_mock_backend_reverse_resolve(GeocodeBackend * backend,GHashTable * params,GCancellable * cancellable,GError ** error)385 geocode_mock_backend_reverse_resolve (GeocodeBackend *backend,
386 GHashTable *params,
387 GCancellable *cancellable,
388 GError **error)
389 {
390 GeocodeMockBackend *self = GEOCODE_MOCK_BACKEND (backend);
391
392 return forward_or_reverse (self, self->reverse_results,
393 GEOCODE_ERROR_NOT_SUPPORTED,
394 params, cancellable, error);
395 }
396
397 /******************************************************************************/
398
399 /**
400 * geocode_mock_backend_new:
401 *
402 * Creates a new mock backend implementation with no initial forward or reverse
403 * query results (so it will return an empty result set for all queries).
404 *
405 * Returns: (transfer full): a new #GeocodeMockBackend
406 *
407 * Since: 3.23.1
408 */
409 GeocodeMockBackend *
geocode_mock_backend_new(void)410 geocode_mock_backend_new (void)
411 {
412 return GEOCODE_MOCK_BACKEND (g_object_new (GEOCODE_TYPE_MOCK_BACKEND,
413 NULL));
414 }
415
416 /**
417 * geocode_mock_backend_add_forward_result:
418 * @self: a #GeocodeMockBackend
419 * @params: (transfer none) (element-type utf8 GValue): query parameters to
420 * respond to, in the same format as accepted by geocode_forward_search()
421 * @results: (transfer none) (nullable) (element-type GeocodePlace): result set
422 * to return for the query, or %NULL if @error is non-%NULL; result sets
423 * must be in the same format as returned by geocode_forward_search()
424 * @error: (nullable): error to return for the query, or %NULL if @results
425 * should be returned instead; errors must match those returned by
426 * geocode_forward_search()
427 *
428 * Add a query and corresponding result (or error) to the mock backend, meaning
429 * that if it receives a forward search for @params through
430 * geocode_backend_forward_search() (or its asynchronous variants), the mock
431 * backend will return the given @results or @error to the caller.
432 *
433 * If a set of @params is added to the backend multiple times, the most
434 * recently provided @results and @error will be used.
435 *
436 * Exactly one of @results and @error must be set. Empty result sets are
437 * represented as a %GEOCODE_ERROR_NO_MATCHES error.
438 *
439 * Since: 3.23.1
440 */
441 void
geocode_mock_backend_add_forward_result(GeocodeMockBackend * self,GHashTable * params,GList * results,const GError * error)442 geocode_mock_backend_add_forward_result (GeocodeMockBackend *self,
443 GHashTable *params,
444 GList *results,
445 const GError *error)
446 {
447 g_autoptr (GeocodeMockBackendQuery) query = NULL;
448 gsize idx;
449
450 g_return_if_fail (GEOCODE_IS_MOCK_BACKEND (self));
451 g_return_if_fail (params != NULL);
452 g_return_if_fail (results == NULL || error == NULL);
453
454 if (find_query (self->forward_results, params, &idx))
455 g_ptr_array_remove_index_fast (self->forward_results, idx);
456
457 query = geocode_mock_backend_query_new (params, TRUE, results, error);
458 g_ptr_array_add (self->forward_results, g_steal_pointer (&query));
459 }
460
461 /**
462 * geocode_mock_backend_add_reverse_result:
463 * @self: a #GeocodeMockBackend
464 * @params: (transfer none) (element-type utf8 GValue): query parameters to
465 * respond to, in the same format as accepted by geocode_reverse_resolve()
466 * @results: (transfer none) (nullable) (element-type GeocodePlace): result set
467 * to return for the query, or %NULL if @error is non-%NULL; result sets
468 * must be in the same format as returned by geocode_reverse_resolve()
469 * @error: (nullable): error to return for the query, or %NULL if @results
470 * should be returned instead; errors must match those returned by
471 * geocode_reverse_resolve()
472 *
473 * Add a query and corresponding result (or error) to the mock backend, meaning
474 * that if it receives a reverse search for @params through
475 * geocode_backend_reverse_resolve() (or its asynchronous variants), the mock
476 * backend will return the given @results or @error to the caller.
477 *
478 * If a set of @params is added to the backend multiple times, the most
479 * recently provided @results and @error will be used.
480 *
481 * Exactly one of @results and @error must be set. Empty result sets are
482 * represented as a %GEOCODE_ERROR_NOT_SUPPORTED error.
483 *
484 * Since: 3.23.1
485 */
486 void
geocode_mock_backend_add_reverse_result(GeocodeMockBackend * self,GHashTable * params,GList * results,const GError * error)487 geocode_mock_backend_add_reverse_result (GeocodeMockBackend *self,
488 GHashTable *params,
489 GList *results,
490 const GError *error)
491 {
492 g_autoptr (GeocodeMockBackendQuery) query = NULL;
493 gsize idx;
494
495 g_return_if_fail (GEOCODE_IS_MOCK_BACKEND (self));
496 g_return_if_fail (params != NULL);
497 g_return_if_fail (results == NULL || error == NULL);
498
499 if (find_query (self->reverse_results, params, &idx))
500 g_ptr_array_remove_index_fast (self->reverse_results, idx);
501
502 query = geocode_mock_backend_query_new (params, FALSE, results, error);
503 g_ptr_array_add (self->reverse_results, g_steal_pointer (&query));
504 }
505
506 /**
507 * geocode_mock_backend_clear:
508 * @self: a #GeocodeMockBackend
509 *
510 * Clear the set of stored results in the mock backend which have been added
511 * using geocode_mock_backend_add_forward_result() and
512 * geocode_mock_backend_add_reverse_result(). Additionally, clear the query log
513 * so far (see geocode_mock_backend_get_query_log()).
514 *
515 * This effectively resets the mock backend to its initial state.
516 *
517 * Since: 3.23.1
518 */
519 void
geocode_mock_backend_clear(GeocodeMockBackend * self)520 geocode_mock_backend_clear (GeocodeMockBackend *self)
521 {
522 g_return_if_fail (GEOCODE_MOCK_BACKEND (self));
523
524 g_ptr_array_set_size (self->query_log, 0);
525 g_ptr_array_set_size (self->forward_results, 0);
526 g_ptr_array_set_size (self->reverse_results, 0);
527 }
528
529 /**
530 * geocode_mock_backend_get_query_log:
531 * @self: a #GeocodeMockBackend
532 *
533 * Get the details of the forward and reverse queries which have been requested
534 * of the mock backend since the most recent call to
535 * geocode_mock_backend_clear(). The query details are provided as
536 * #GeocodeMockBackendQuery structures, which map the query parameters to
537 * either the result set or the error which geocode_backend_forward_search()
538 * or geocode_backend_reverse_resolve() (or their asynchronous variants)
539 * returned to the caller.
540 *
541 * The results are provided in the order in which calls were made to
542 * geocode_backend_forward_search() and geocode_backend_reverse_resolve().
543 * Results for forward and reverse queries may be interleaved.
544 *
545 * Returns: (transfer none) (element-type GeocodeMockBackendQuery): potentially
546 * empty sequence of forward and reverse query details
547 * Since: 3.23.1
548 */
549 GPtrArray *
geocode_mock_backend_get_query_log(GeocodeMockBackend * self)550 geocode_mock_backend_get_query_log (GeocodeMockBackend *self)
551 {
552 g_return_val_if_fail (GEOCODE_IS_MOCK_BACKEND (self), NULL);
553
554 return self->query_log;
555 }
556
557 static void
geocode_mock_backend_init(GeocodeMockBackend * self)558 geocode_mock_backend_init (GeocodeMockBackend *self)
559 {
560 self->query_log =
561 g_ptr_array_new_with_free_func ((GDestroyNotify) geocode_mock_backend_query_free);
562 self->forward_results =
563 g_ptr_array_new_with_free_func ((GDestroyNotify) geocode_mock_backend_query_free);
564 self->reverse_results =
565 g_ptr_array_new_with_free_func ((GDestroyNotify) geocode_mock_backend_query_free);
566 }
567
568 static void
geocode_mock_backend_finalize(GObject * object)569 geocode_mock_backend_finalize (GObject *object)
570 {
571 GeocodeMockBackend *self = GEOCODE_MOCK_BACKEND (object);
572
573 g_clear_pointer (&self->forward_results, g_ptr_array_unref);
574 g_clear_pointer (&self->reverse_results, g_ptr_array_unref);
575
576 G_OBJECT_CLASS (geocode_mock_backend_parent_class)->finalize (object);
577 }
578
579 static void
geocode_backend_iface_init(GeocodeBackendInterface * iface)580 geocode_backend_iface_init (GeocodeBackendInterface *iface)
581 {
582 /* We use the default implementation of the asynchronous methods, which
583 * runs the synchronous version in a thread. */
584 iface->forward_search = geocode_mock_backend_forward_search;
585 iface->reverse_resolve = geocode_mock_backend_reverse_resolve;
586 }
587
588 static void
geocode_mock_backend_class_init(GeocodeMockBackendClass * klass)589 geocode_mock_backend_class_init (GeocodeMockBackendClass *klass)
590 {
591 GObjectClass *object_class = G_OBJECT_CLASS (klass);
592
593 object_class->finalize = geocode_mock_backend_finalize;
594 }
595