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
46G_GNUC_UNUSED static char *
47model_to_string (GListModel *model)
48{
49 GString *string;
50 guint i, n;
51
52 n = g_list_model_get_n_items (list: 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 (list: model, position: i);
59
60 if (i > 0)
61 g_string_append (string, val: ", ");
62 g_string_append (string, val: gtk_string_object_get_string (self: item));
63 g_object_unref (object: item);
64 }
65
66 return g_string_free (string, FALSE);
67}
68
69static void
70assert_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 (list: 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 (list: model, position: i);
87 gpointer o2 = g_list_model_get_item (list: compare, position: i);
88 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
89 g_object_unref (object: o1);
90 g_object_unref (object: o2);
91 }
92 for (i = position + added; i < n_items; i++)
93 {
94 gpointer o1 = g_list_model_get_item (list: model, position: i);
95 gpointer o2 = g_list_model_get_item (list: compare, position: i - added + removed);
96 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
97 g_object_unref (object: o1);
98 g_object_unref (object: 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 (list: model, position);
108 gpointer o2 = g_list_model_get_item (list: compare, position);
109 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
110 g_object_unref (object: o1);
111 g_object_unref (object: o2);
112
113 o1 = g_list_model_get_item (list: model, position: position + added - 1);
114 o2 = g_list_model_get_item (list: compare, position: position + removed - 1);
115 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
116 g_object_unref (object: o1);
117 g_object_unref (object: o2);
118 }
119
120 /* Finally, perform the same change as the signal indicates */
121 g_list_store_splice (store: G_LIST_STORE (ptr: compare), position, n_removals: removed, NULL, n_additions: 0);
122 for (i = position; i < position + added; i++)
123 {
124 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: model), position: i);
125 g_list_store_insert (store: G_LIST_STORE (ptr: compare), position: i, item);
126 g_object_unref (object: item);
127 }
128}
129
130static GtkFilterListModel *
131filter_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 (model: source, filter);
143 check = g_list_store_new (G_TYPE_OBJECT);
144 for (i = 0; i < g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model)); i++)
145 {
146 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: model), position: i);
147 g_list_store_append (store: check, item);
148 g_object_unref (object: item);
149 }
150 g_signal_connect_data (instance: model,
151 detailed_signal: "items-changed",
152 G_CALLBACK (assert_items_changed_correctly),
153 data: check,
154 destroy_data: (GClosureNotify) g_object_unref,
155 connect_flags: 0);
156
157 return model;
158}
159
160#define N_MODELS 8
161
162static GtkFilterListModel *
163create_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 (source: id & 1 ? NULL : source, filter: 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 (self: 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 (self: model, model: source);
188 if (id & 2)
189 gtk_filter_list_model_set_filter (self: model, filter);
190
191 return model;
192}
193
194static GListModel *
195create_source_model (guint min_size, guint max_size)
196{
197 GtkStringList *list;
198 guint i, size;
199
200 size = g_test_rand_int_range (begin: min_size, end: max_size + 1);
201 list = gtk_string_list_new (NULL);
202
203 for (i = 0; i < size; i++)
204 gtk_string_list_append (self: list, g_test_rand_bit () ? "A" : "B");
205
206 return G_LIST_MODEL (ptr: list);
207}
208
209#define N_FILTERS 5
210
211static GtkFilter *
212create_filter (gsize id)
213{
214 GtkFilter *filter;
215
216 switch (id)
217 {
218 case 0:
219 /* GTK_FILTER_MATCH_ALL */
220 return GTK_FILTER (ptr: gtk_string_filter_new (NULL));
221
222 case 1:
223 /* GTK_FILTER_MATCH_NONE */
224 filter = GTK_FILTER (ptr: gtk_string_filter_new (NULL));
225 gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "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 (ptr: gtk_string_filter_new (expression: gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, property_name: "string")));
233 if (id == 2)
234 gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "A");
235 else if (id == 3)
236 gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "B");
237 else
238 gtk_string_filter_set_search (self: GTK_STRING_FILTER (ptr: filter), search: "does-not-match");
239 return filter;
240
241 default:
242 g_assert_not_reached ();
243 return NULL;
244 }
245}
246
247static GtkFilter *
248create_random_filter (gboolean allow_null)
249{
250 guint n;
251
252 if (allow_null)
253 n = g_test_rand_int_range (begin: 0, N_FILTERS + 1);
254 else
255 n = g_test_rand_int_range (begin: 0, N_FILTERS);
256
257 if (n >= N_FILTERS)
258 return NULL;
259
260 return create_filter (id: n);
261}
262
263static void
264test_no_filter (gconstpointer model_id)
265{
266 GtkFilterListModel *model;
267 GListModel *source;
268 GtkFilter *filter;
269
270 source = create_source_model (min_size: 10, max_size: 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 (self: model, filter);
277 g_object_unref (object: filter);
278 gtk_filter_list_model_set_filter (self: model, NULL);
279 ensure_updated ();
280 assert_model_equal (G_LIST_MODEL (model), source);
281
282 g_object_unref (object: model);
283 g_object_unref (object: 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 */
293static void
294test_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 (min_size: 10, max_size: 10);
303 model1 = create_filter_list_model (model_id, source, NULL);
304 model2 = create_filter_list_model (model_id, source: G_LIST_MODEL (ptr: model1), NULL);
305 every = GTK_FILTER (ptr: gtk_every_filter_new ());
306 compare = create_filter_list_model (model_id, source, filter: every);
307 g_object_unref (object: every);
308 g_object_unref (object: source);
309
310 for (i = 0; i < N_FILTERS; i++)
311 {
312 filter = create_filter (id: i);
313 gtk_filter_list_model_set_filter (self: model1, filter);
314 gtk_multi_filter_append (self: GTK_MULTI_FILTER (ptr: every), filter);
315
316 for (j = 0; j < N_FILTERS; j++)
317 {
318 filter = create_filter (id: i);
319 gtk_filter_list_model_set_filter (self: model2, filter);
320 gtk_multi_filter_append (self: GTK_MULTI_FILTER (ptr: 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 (min_size: 0, max_size: 1000);
328 gtk_filter_list_model_set_model (self: compare, model: source);
329 gtk_filter_list_model_set_model (self: model1, model: source);
330 g_object_unref (object: source);
331
332 ensure_updated ();
333 assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare));
334 }
335
336 gtk_multi_filter_remove (self: GTK_MULTI_FILTER (ptr: every), position: 1);
337 }
338
339 gtk_multi_filter_remove (self: GTK_MULTI_FILTER (ptr: every), position: 0);
340 }
341
342 g_object_unref (object: compare);
343 g_object_unref (object: model2);
344 g_object_unref (object: 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 */
357static void
358test_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 (ptr: gtk_every_filter_new ());
368 if (filter)
369 gtk_multi_filter_append (self: GTK_MULTI_FILTER (ptr: 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 (model: G_LIST_MODEL (ptr: store1));
374 flatten2 = gtk_flatten_list_model_new (model: G_LIST_MODEL (ptr: store2));
375 model2 = create_filter_list_model (model_id, source: G_LIST_MODEL (ptr: 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 (begin: 0, end: 4))
383 {
384 case 0:
385 /* change the filter */
386 filter = create_random_filter (TRUE);
387 gtk_multi_filter_remove (self: GTK_MULTI_FILTER (ptr: multi), position: 0); /* no-op if no filter */
388 if (filter)
389 gtk_multi_filter_append (self: GTK_MULTI_FILTER (ptr: multi), filter);
390 gtk_filter_list_model_set_filter (self: 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 (begin: 0, end: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: store1)) + 1);
415 if (g_list_model_get_n_items (list: G_LIST_MODEL (ptr: 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 (min_size: 1, max_size: 50);
422 GtkFilterListModel *model1 = create_filter_list_model (model_id, source, filter: multi);
423 g_list_store_splice (store: store1,
424 position,
425 n_removals: remove ? 1 : 0,
426 additions: (gpointer *) &model1, n_additions: 1);
427 g_list_store_splice (store: store2,
428 position,
429 n_removals: remove ? 1 : 0,
430 additions: (gpointer *) &source, n_additions: 1);
431 g_object_unref (object: model1);
432 g_object_unref (object: source);
433 }
434 else if (remove)
435 {
436 g_list_store_remove (store: store1, position);
437 g_list_store_remove (store: 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 (object: model2);
448 g_object_unref (object: flatten2);
449 g_object_unref (object: flatten1);
450 g_object_unref (object: multi);
451}
452
453static void
454add_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 (format: "/filterlistmodel/model%u/%s", i, name);
462 g_test_add_data_func (testpath: path, GUINT_TO_POINTER (i), test_func);
463 g_free (mem: path);
464 }
465}
466
467int
468main (int argc, char *argv[])
469{
470 (g_test_init) (argc: &argc, argv: &argv, NULL);
471 setlocale (LC_ALL, locale: "C");
472
473 add_test_for_all_models (name: "no-filter", test_func: test_no_filter);
474 add_test_for_all_models (name: "two-filters", test_func: test_two_filters);
475 add_test_for_all_models (name: "model-changes", test_func: test_model_changes);
476
477 return g_test_run ();
478}
479

source code of gtk/testsuite/gtk/filterlistmodel-exhaustive.c