1 /*
2 * Copyright © 2020 Benjamin Otte
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This 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 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <locale.h>
19
20 #include <gtk/gtk.h>
21
22 #define ensure_updated() G_STMT_START{ \
23 while (g_main_context_pending (NULL)) \
24 g_main_context_iteration (NULL, TRUE); \
25 }G_STMT_END
26
27 #define assert_model_equal(model1, model2) G_STMT_START{ \
28 guint _i, _n; \
29 g_assert_cmpint (g_list_model_get_n_items (model1), ==, g_list_model_get_n_items (model2)); \
30 _n = g_list_model_get_n_items (model1); \
31 for (_i = 0; _i < _n; _i++) \
32 { \
33 gpointer o1 = g_list_model_get_item (model1, _i); \
34 gpointer o2 = g_list_model_get_item (model2, _i); \
35 if (o1 != o2) \
36 { \
37 char *_s = g_strdup_printf ("Objects differ at index %u out of %u", _i, _n); \
38 g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, _s); \
39 g_free (_s); \
40 } \
41 g_object_unref (o1); \
42 g_object_unref (o2); \
43 } \
44 }G_STMT_END
45
46 G_GNUC_UNUSED static char *
model_to_string(GListModel * model)47 model_to_string (GListModel *model)
48 {
49 GString *string;
50 guint i, n;
51
52 n = g_list_model_get_n_items (model);
53 string = g_string_new (NULL);
54
55 /* Check that all unchanged items are indeed unchanged */
56 for (i = 0; i < n; i++)
57 {
58 gpointer item = g_list_model_get_item (model, i);
59
60 if (i > 0)
61 g_string_append (string, ", ");
62 g_string_append (string, gtk_string_object_get_string (item));
63 g_object_unref (item);
64 }
65
66 return g_string_free (string, FALSE);
67 }
68
69 static void
assert_items_changed_correctly(GListModel * model,guint position,guint removed,guint added,GListModel * compare)70 assert_items_changed_correctly (GListModel *model,
71 guint position,
72 guint removed,
73 guint added,
74 GListModel *compare)
75 {
76 guint i, n_items;
77
78 //g_print ("%s => %u -%u +%u => %s\n", model_to_string (compare), position, removed, added, model_to_string (model));
79
80 g_assert_cmpint (g_list_model_get_n_items (model), ==, g_list_model_get_n_items (compare) - removed + added);
81 n_items = g_list_model_get_n_items (model);
82
83 /* Check that all unchanged items are indeed unchanged */
84 for (i = 0; i < position; i++)
85 {
86 gpointer o1 = g_list_model_get_item (model, i);
87 gpointer o2 = g_list_model_get_item (compare, i);
88 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
89 g_object_unref (o1);
90 g_object_unref (o2);
91 }
92 for (i = position + added; i < n_items; i++)
93 {
94 gpointer o1 = g_list_model_get_item (model, i);
95 gpointer o2 = g_list_model_get_item (compare, i - added + removed);
96 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
97 g_object_unref (o1);
98 g_object_unref (o2);
99 }
100
101 /* Check that the first and last added item are different from
102 * first and last removed item.
103 * Otherwise we could have kept them as-is
104 */
105 if (removed > 0 && added > 0)
106 {
107 gpointer o1 = g_list_model_get_item (model, position);
108 gpointer o2 = g_list_model_get_item (compare, position);
109 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
110 g_object_unref (o1);
111 g_object_unref (o2);
112
113 o1 = g_list_model_get_item (model, position + added - 1);
114 o2 = g_list_model_get_item (compare, position + removed - 1);
115 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
116 g_object_unref (o1);
117 g_object_unref (o2);
118 }
119
120 /* Finally, perform the same change as the signal indicates */
121 g_list_store_splice (G_LIST_STORE (compare), position, removed, NULL, 0);
122 for (i = position; i < position + added; i++)
123 {
124 gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i);
125 g_list_store_insert (G_LIST_STORE (compare), i, item);
126 g_object_unref (item);
127 }
128 }
129
130 static GtkFilterListModel *
filter_list_model_new(GListModel * source,GtkFilter * filter)131 filter_list_model_new (GListModel *source,
132 GtkFilter *filter)
133 {
134 GtkFilterListModel *model;
135 GListStore *check;
136 guint i;
137
138 if (source)
139 g_object_ref (source);
140 if (filter)
141 g_object_ref (filter);
142 model = gtk_filter_list_model_new (source, filter);
143 check = g_list_store_new (G_TYPE_OBJECT);
144 for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
145 {
146 gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i);
147 g_list_store_append (check, item);
148 g_object_unref (item);
149 }
150 g_signal_connect_data (model,
151 "items-changed",
152 G_CALLBACK (assert_items_changed_correctly),
153 check,
154 (GClosureNotify) g_object_unref,
155 0);
156
157 return model;
158 }
159
160 #define N_MODELS 8
161
162 static GtkFilterListModel *
create_filter_list_model(gconstpointer model_id,GListModel * source,GtkFilter * filter)163 create_filter_list_model (gconstpointer model_id,
164 GListModel *source,
165 GtkFilter *filter)
166 {
167 GtkFilterListModel *model;
168 guint id = GPOINTER_TO_UINT (model_id);
169
170 model = filter_list_model_new (id & 1 ? NULL : source, id & 2 ? NULL : filter);
171
172 switch (id >> 2)
173 {
174 case 0:
175 break;
176
177 case 1:
178 gtk_filter_list_model_set_incremental (model, TRUE);
179 break;
180
181 default:
182 g_assert_not_reached ();
183 break;
184 }
185
186 if (id & 1)
187 gtk_filter_list_model_set_model (model, source);
188 if (id & 2)
189 gtk_filter_list_model_set_filter (model, filter);
190
191 return model;
192 }
193
194 static GListModel *
create_source_model(guint min_size,guint max_size)195 create_source_model (guint min_size, guint max_size)
196 {
197 GtkStringList *list;
198 guint i, size;
199
200 size = g_test_rand_int_range (min_size, max_size + 1);
201 list = gtk_string_list_new (NULL);
202
203 for (i = 0; i < size; i++)
204 gtk_string_list_append (list, g_test_rand_bit () ? "A" : "B");
205
206 return G_LIST_MODEL (list);
207 }
208
209 #define N_FILTERS 5
210
211 static GtkFilter *
create_filter(gsize id)212 create_filter (gsize id)
213 {
214 GtkFilter *filter;
215
216 switch (id)
217 {
218 case 0:
219 /* GTK_FILTER_MATCH_ALL */
220 return GTK_FILTER (gtk_string_filter_new (NULL));
221
222 case 1:
223 /* GTK_FILTER_MATCH_NONE */
224 filter = GTK_FILTER (gtk_string_filter_new (NULL));
225 gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "does not matter, because no expression");
226 return filter;
227
228 case 2:
229 case 3:
230 case 4:
231 /* match all As, Bs and nothing */
232 filter = GTK_FILTER (gtk_string_filter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string")));
233 if (id == 2)
234 gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "A");
235 else if (id == 3)
236 gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "B");
237 else
238 gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "does-not-match");
239 return filter;
240
241 default:
242 g_assert_not_reached ();
243 return NULL;
244 }
245 }
246
247 static GtkFilter *
create_random_filter(gboolean allow_null)248 create_random_filter (gboolean allow_null)
249 {
250 guint n;
251
252 if (allow_null)
253 n = g_test_rand_int_range (0, N_FILTERS + 1);
254 else
255 n = g_test_rand_int_range (0, N_FILTERS);
256
257 if (n >= N_FILTERS)
258 return NULL;
259
260 return create_filter (n);
261 }
262
263 static void
test_no_filter(gconstpointer model_id)264 test_no_filter (gconstpointer model_id)
265 {
266 GtkFilterListModel *model;
267 GListModel *source;
268 GtkFilter *filter;
269
270 source = create_source_model (10, 10);
271 model = create_filter_list_model (model_id, source, NULL);
272 ensure_updated ();
273 assert_model_equal (G_LIST_MODEL (model), source);
274
275 filter = create_random_filter (FALSE);
276 gtk_filter_list_model_set_filter (model, filter);
277 g_object_unref (filter);
278 gtk_filter_list_model_set_filter (model, NULL);
279 ensure_updated ();
280 assert_model_equal (G_LIST_MODEL (model), source);
281
282 g_object_unref (model);
283 g_object_unref (source);
284 }
285
286 /* Compare this:
287 * source => filter1 => filter2
288 * with:
289 * source => multifilter(filter1, filter2)
290 * and randomly change the source and filters and see if the
291 * two continue agreeing.
292 */
293 static void
test_two_filters(gconstpointer model_id)294 test_two_filters (gconstpointer model_id)
295 {
296 GtkFilterListModel *compare;
297 GtkFilterListModel *model1, *model2;
298 GListModel *source;
299 GtkFilter *every, *filter;
300 guint i, j, k;
301
302 source = create_source_model (10, 10);
303 model1 = create_filter_list_model (model_id, source, NULL);
304 model2 = create_filter_list_model (model_id, G_LIST_MODEL (model1), NULL);
305 every = GTK_FILTER (gtk_every_filter_new ());
306 compare = create_filter_list_model (model_id, source, every);
307 g_object_unref (every);
308 g_object_unref (source);
309
310 for (i = 0; i < N_FILTERS; i++)
311 {
312 filter = create_filter (i);
313 gtk_filter_list_model_set_filter (model1, filter);
314 gtk_multi_filter_append (GTK_MULTI_FILTER (every), filter);
315
316 for (j = 0; j < N_FILTERS; j++)
317 {
318 filter = create_filter (i);
319 gtk_filter_list_model_set_filter (model2, filter);
320 gtk_multi_filter_append (GTK_MULTI_FILTER (every), filter);
321
322 ensure_updated ();
323 assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare));
324
325 for (k = 0; k < 10; k++)
326 {
327 source = create_source_model (0, 1000);
328 gtk_filter_list_model_set_model (compare, source);
329 gtk_filter_list_model_set_model (model1, source);
330 g_object_unref (source);
331
332 ensure_updated ();
333 assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare));
334 }
335
336 gtk_multi_filter_remove (GTK_MULTI_FILTER (every), 1);
337 }
338
339 gtk_multi_filter_remove (GTK_MULTI_FILTER (every), 0);
340 }
341
342 g_object_unref (compare);
343 g_object_unref (model2);
344 g_object_unref (model1);
345 }
346
347 /* Compare this:
348 * (source => filter) * => flatten
349 * with:
350 * source * => flatten => filter
351 * and randomly add/remove sources and change the filters and
352 * see if the two agree.
353 *
354 * We use a multifilter for the top chain so that changing the filter
355 * is easy.
356 */
357 static void
test_model_changes(gconstpointer model_id)358 test_model_changes (gconstpointer model_id)
359 {
360 GListStore *store1, *store2;
361 GtkFlattenListModel *flatten1, *flatten2;
362 GtkFilterListModel *model2;
363 GtkFilter *multi, *filter;
364 gsize i;
365
366 filter = create_random_filter (TRUE);
367 multi = GTK_FILTER (gtk_every_filter_new ());
368 if (filter)
369 gtk_multi_filter_append (GTK_MULTI_FILTER (multi), filter);
370
371 store1 = g_list_store_new (G_TYPE_OBJECT);
372 store2 = g_list_store_new (G_TYPE_OBJECT);
373 flatten1 = gtk_flatten_list_model_new (G_LIST_MODEL (store1));
374 flatten2 = gtk_flatten_list_model_new (G_LIST_MODEL (store2));
375 model2 = create_filter_list_model (model_id, G_LIST_MODEL (flatten2), filter);
376
377 for (i = 0; i < 500; i++)
378 {
379 gboolean add = FALSE, remove = FALSE;
380 guint position;
381
382 switch (g_test_rand_int_range (0, 4))
383 {
384 case 0:
385 /* change the filter */
386 filter = create_random_filter (TRUE);
387 gtk_multi_filter_remove (GTK_MULTI_FILTER (multi), 0); /* no-op if no filter */
388 if (filter)
389 gtk_multi_filter_append (GTK_MULTI_FILTER (multi), filter);
390 gtk_filter_list_model_set_filter (model2, filter);
391 break;
392
393 case 1:
394 /* remove a model */
395 remove = TRUE;
396 break;
397
398 case 2:
399 /* add a model */
400 add = TRUE;
401 break;
402
403 case 3:
404 /* replace a model */
405 remove = TRUE;
406 add = TRUE;
407 break;
408
409 default:
410 g_assert_not_reached ();
411 break;
412 }
413
414 position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store1)) + 1);
415 if (g_list_model_get_n_items (G_LIST_MODEL (store1)) == position)
416 remove = FALSE;
417
418 if (add)
419 {
420 /* We want at least one element, otherwise the filters will see no changes */
421 GListModel *source = create_source_model (1, 50);
422 GtkFilterListModel *model1 = create_filter_list_model (model_id, source, multi);
423 g_list_store_splice (store1,
424 position,
425 remove ? 1 : 0,
426 (gpointer *) &model1, 1);
427 g_list_store_splice (store2,
428 position,
429 remove ? 1 : 0,
430 (gpointer *) &source, 1);
431 g_object_unref (model1);
432 g_object_unref (source);
433 }
434 else if (remove)
435 {
436 g_list_store_remove (store1, position);
437 g_list_store_remove (store2, position);
438 }
439
440 if (g_test_rand_bit ())
441 {
442 ensure_updated ();
443 assert_model_equal (G_LIST_MODEL (flatten1), G_LIST_MODEL (model2));
444 }
445 }
446
447 g_object_unref (model2);
448 g_object_unref (flatten2);
449 g_object_unref (flatten1);
450 g_object_unref (multi);
451 }
452
453 static void
add_test_for_all_models(const char * name,GTestDataFunc test_func)454 add_test_for_all_models (const char *name,
455 GTestDataFunc test_func)
456 {
457 guint i;
458
459 for (i = 0; i < N_MODELS; i++)
460 {
461 char *path = g_strdup_printf ("/filterlistmodel/model%u/%s", i, name);
462 g_test_add_data_func (path, GUINT_TO_POINTER (i), test_func);
463 g_free (path);
464 }
465 }
466
467 int
main(int argc,char * argv[])468 main (int argc, char *argv[])
469 {
470 (g_test_init) (&argc, &argv, NULL);
471 setlocale (LC_ALL, "C");
472
473 add_test_for_all_models ("no-filter", test_no_filter);
474 add_test_for_all_models ("two-filters", test_two_filters);
475 add_test_for_all_models ("model-changes", test_model_changes);
476
477 return g_test_run ();
478 }
479