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