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 \
36       if (o1 != o2) \
37         { \
38           char *_s = g_strdup_printf ("Objects differ at index %u out of %u", _i, _n); \
39          g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, _s); \
40           g_free (_s); \
41         } \
42 \
43       g_object_unref (o1); \
44       g_object_unref (o2); \
45     } \
46 }G_STMT_END
47 
48 G_GNUC_UNUSED static char *
model_to_string(GListModel * model)49 model_to_string (GListModel *model)
50 {
51   GString *string;
52   guint i, n;
53 
54   n = g_list_model_get_n_items (model);
55   string = g_string_new (NULL);
56 
57   /* Check that all unchanged items are indeed unchanged */
58   for (i = 0; i < n; i++)
59     {
60       gpointer item, model_item = g_list_model_get_item (model, i);
61       if (GTK_IS_TREE_LIST_ROW (model_item))
62         item = gtk_tree_list_row_get_item (model_item);
63       else
64         item = model_item;
65 
66       if (i > 0)
67         g_string_append (string, ", ");
68       if (G_IS_LIST_MODEL (item))
69         g_string_append (string, "*");
70       else
71         g_string_append (string, gtk_string_object_get_string (item));
72       g_object_unref (model_item);
73     }
74 
75   return g_string_free (string, FALSE);
76 }
77 
78 static void
assert_items_changed_correctly(GListModel * model,guint position,guint removed,guint added,GListModel * compare)79 assert_items_changed_correctly (GListModel *model,
80                                 guint       position,
81                                 guint       removed,
82                                 guint       added,
83                                 GListModel *compare)
84 {
85   guint i, n_items;
86 
87   //g_print ("%s => %u -%u +%u => %s\n", model_to_string (compare), position, removed, added, model_to_string (model));
88 
89   g_assert_cmpint (g_list_model_get_n_items (model), ==, g_list_model_get_n_items (compare) - removed + added);
90   n_items = g_list_model_get_n_items (model);
91 
92   if (position != 0 || removed != n_items)
93     {
94       /* Check that all unchanged items are indeed unchanged */
95       for (i = 0; i < position; i++)
96         {
97           gpointer o1 = g_list_model_get_item (model, i);
98           gpointer o2 = g_list_model_get_item (compare, i);
99           g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
100           g_object_unref (o1);
101           g_object_unref (o2);
102         }
103       for (i = position + added; i < n_items; i++)
104         {
105           gpointer o1 = g_list_model_get_item (model, i);
106           gpointer o2 = g_list_model_get_item (compare, i - added + removed);
107           g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
108           g_object_unref (o1);
109           g_object_unref (o2);
110         }
111 
112       /* Check that the first and last added item are different from
113        * first and last removed item.
114        * Otherwise we could have kept them as-is
115        */
116       if (removed > 0 && added > 0)
117         {
118           gpointer o1 = g_list_model_get_item (model, position);
119           gpointer o2 = g_list_model_get_item (compare, position);
120           g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
121           g_object_unref (o1);
122           g_object_unref (o2);
123 
124           o1 = g_list_model_get_item (model, position + added - 1);
125           o2 = g_list_model_get_item (compare, position + removed - 1);
126           g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
127           g_object_unref (o1);
128           g_object_unref (o2);
129         }
130     }
131 
132   /* Finally, perform the same change as the signal indicates */
133   g_list_store_splice (G_LIST_STORE (compare), position, removed, NULL, 0);
134   for (i = position; i < position + added; i++)
135     {
136       gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i);
137       g_list_store_insert (G_LIST_STORE (compare), i, item);
138       g_object_unref (item);
139     }
140 }
141 
142 static GtkSortListModel *
sort_list_model_new(GListModel * source,GtkSorter * sorter)143 sort_list_model_new (GListModel *source,
144                      GtkSorter  *sorter)
145 {
146   GtkSortListModel *model;
147   GListStore *check;
148   guint i;
149 
150   model = gtk_sort_list_model_new (source, sorter);
151   check = g_list_store_new (G_TYPE_OBJECT);
152   for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
153     {
154       gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i);
155       g_list_store_append (check, item);
156       g_object_unref (item);
157     }
158   g_signal_connect_data (model,
159                          "items-changed",
160                          G_CALLBACK (assert_items_changed_correctly),
161                          check,
162                          (GClosureNotify) g_object_unref,
163                          0);
164 
165   return model;
166 }
167 
168 #define N_MODELS 8
169 
170 static char *
create_test_name(guint id)171 create_test_name (guint id)
172 {
173   GString *s = g_string_new ("");
174 
175   if (id & (1 << 0))
176     g_string_append (s, "set-model");
177   else
178     g_string_append (s, "construct-with-model");
179 
180   if (id & (1 << 1))
181     g_string_append (s, "/set-sorter");
182   else
183     g_string_append (s, "/construct-with-sorter");
184 
185   if (id & (1 << 2))
186     g_string_append (s, "/incremental");
187   else
188     g_string_append (s, "/non-incremental");
189 
190   return g_string_free (s, FALSE);
191 }
192 
193 static GtkSortListModel *
create_sort_list_model(gconstpointer model_id,gboolean track_changes,GListModel * source,GtkSorter * sorter)194 create_sort_list_model (gconstpointer  model_id,
195                         gboolean       track_changes,
196                         GListModel    *source,
197                         GtkSorter     *sorter)
198 {
199   GtkSortListModel *model;
200   guint id = GPOINTER_TO_UINT (model_id);
201 
202   if (track_changes)
203     model = sort_list_model_new (((id & 1) || !source) ? NULL : g_object_ref (source), ((id & 2) || !sorter) ? NULL : g_object_ref (sorter));
204   else
205     model = gtk_sort_list_model_new (((id & 1) || !source) ? NULL : g_object_ref (source), ((id & 2) || !sorter) ? NULL : g_object_ref (sorter));
206 
207   switch (id >> 2)
208   {
209     case 0:
210       break;
211 
212     case 1:
213       gtk_sort_list_model_set_incremental (model, TRUE);
214       break;
215 
216     default:
217       g_assert_not_reached ();
218       break;
219   }
220 
221   if (id & 1)
222     gtk_sort_list_model_set_model (model, source);
223   if (id & 2)
224     gtk_sort_list_model_set_sorter (model, sorter);
225 
226   return model;
227 }
228 
229 static GListModel *
create_source_model(guint min_size,guint max_size)230 create_source_model (guint min_size, guint max_size)
231 {
232   const char *strings[] = { "A", "a", "B", "b" };
233   GtkStringList *list;
234   guint i, size;
235 
236   size = g_test_rand_int_range (min_size, max_size + 1);
237   list = gtk_string_list_new (NULL);
238 
239   for (i = 0; i < size; i++)
240     gtk_string_list_append (list, strings[g_test_rand_int_range (0, G_N_ELEMENTS (strings))]);
241 
242   return G_LIST_MODEL (list);
243 }
244 
245 #define N_SORTERS 3
246 
247 static GtkSorter *
create_sorter(gsize id)248 create_sorter (gsize id)
249 {
250   GtkSorter *sorter;
251 
252   switch (id)
253   {
254     case 0:
255       return GTK_SORTER (gtk_string_sorter_new (NULL));
256 
257     case 1:
258     case 2:
259       /* match all As, Bs and nothing */
260       sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string")));
261       if (id == 1)
262         gtk_string_sorter_set_ignore_case (GTK_STRING_SORTER (sorter), TRUE);
263       return sorter;
264 
265     default:
266       g_assert_not_reached ();
267       return NULL;
268   }
269 }
270 
271 static GtkSorter *
create_random_sorter(gboolean allow_null)272 create_random_sorter (gboolean allow_null)
273 {
274   guint n;
275 
276   if (allow_null)
277     n = g_test_rand_int_range (0, N_SORTERS + 1);
278   else
279     n = g_test_rand_int_range (0, N_SORTERS);
280 
281   if (n >= N_SORTERS)
282     return NULL;
283 
284   return create_sorter (n);
285 }
286 
287 /* Compare this:
288  *   source => sorter1 => sorter2
289  * with:
290  *   source => multisorter(sorter1, sorter2)
291  * and randomly change the source and sorters and see if the
292  * two continue agreeing.
293  */
294 static void
test_two_sorters(gconstpointer model_id)295 test_two_sorters (gconstpointer model_id)
296 {
297   GtkSortListModel *compare;
298   GtkSortListModel *model1, *model2;
299   GListModel *source;
300   GtkSorter *every, *sorter;
301   guint i, j, k;
302 
303   source = create_source_model (10, 10);
304   model2 = create_sort_list_model (model_id, TRUE, source, NULL);
305   /* can't track changes from a sortmodel, where the same items get reordered */
306   model1 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (model2), NULL);
307   every = GTK_SORTER (gtk_multi_sorter_new ());
308   compare = create_sort_list_model (model_id, TRUE, source, every);
309   g_object_unref (every);
310   g_object_unref (source);
311 
312   for (i = 0; i < N_SORTERS; i++)
313     {
314       sorter = create_sorter (i);
315       gtk_sort_list_model_set_sorter (model1, sorter);
316       gtk_multi_sorter_append (GTK_MULTI_SORTER (every), sorter);
317 
318       for (j = 0; j < N_SORTERS; j++)
319         {
320           sorter = create_sorter (i);
321           gtk_sort_list_model_set_sorter (model2, sorter);
322           gtk_multi_sorter_append (GTK_MULTI_SORTER (every), sorter);
323 
324           ensure_updated ();
325           assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare));
326 
327           for (k = 0; k < 10; k++)
328             {
329               source = create_source_model (0, 1000);
330               gtk_sort_list_model_set_model (compare, source);
331               gtk_sort_list_model_set_model (model2, source);
332               g_object_unref (source);
333 
334               ensure_updated ();
335               assert_model_equal (G_LIST_MODEL (model1), G_LIST_MODEL (compare));
336             }
337 
338           gtk_multi_sorter_remove (GTK_MULTI_SORTER (every), 1);
339         }
340 
341       gtk_multi_sorter_remove (GTK_MULTI_SORTER (every), 0);
342     }
343 
344   g_object_unref (compare);
345   g_object_unref (model2);
346   g_object_unref (model1);
347 }
348 
349 /* Run:
350  *   source => sorter1 => sorter2
351  * and randomly add/remove sources and change the sorters and
352  * see if the two sorters stay identical
353  */
354 static void
test_stability(gconstpointer model_id)355 test_stability (gconstpointer model_id)
356 {
357   GListStore *store;
358   GtkFlattenListModel *flatten;
359   GtkSortListModel *sort1, *sort2;
360   GtkSorter *sorter;
361   gsize i;
362 
363   sorter = create_random_sorter (TRUE);
364 
365   store = g_list_store_new (G_TYPE_OBJECT);
366   flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store));
367   sort1 = create_sort_list_model (model_id, TRUE, G_LIST_MODEL (flatten), sorter);
368   sort2 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (sort1), sorter);
369   g_clear_object (&sorter);
370 
371   for (i = 0; i < 500; i++)
372     {
373       gboolean add = FALSE, remove = FALSE;
374       guint position;
375 
376       switch (g_test_rand_int_range (0, 4))
377       {
378         case 0:
379           /* change the sorter */
380           sorter = create_random_sorter (TRUE);
381           gtk_sort_list_model_set_sorter (sort1, sorter);
382           gtk_sort_list_model_set_sorter (sort2, sorter);
383           g_clear_object (&sorter);
384           break;
385 
386         case 1:
387           /* remove a model */
388           remove = TRUE;
389           break;
390 
391         case 2:
392           /* add a model */
393           add = TRUE;
394           break;
395 
396         case 3:
397           /* replace a model */
398           remove = TRUE;
399           add = TRUE;
400           break;
401 
402         default:
403           g_assert_not_reached ();
404           break;
405       }
406 
407       position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1);
408       if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position)
409         remove = FALSE;
410 
411       if (add)
412         {
413           /* We want at least one element, otherwise the sorters will see no changes */
414           GListModel *source = create_source_model (1, 50);
415           g_list_store_splice (store,
416                                position,
417                                remove ? 1 : 0,
418                                (gpointer *) &source, 1);
419           g_object_unref (source);
420         }
421       else if (remove)
422         {
423           g_list_store_remove (store, position);
424         }
425 
426       if (g_test_rand_bit ())
427         {
428           ensure_updated ();
429           assert_model_equal (G_LIST_MODEL (sort1), G_LIST_MODEL (sort2));
430         }
431     }
432 
433   g_object_unref (sort2);
434   g_object_unref (sort1);
435   g_object_unref (flatten);
436 }
437 
438 static void
add_test_for_all_models(const char * name,GTestDataFunc test_func)439 add_test_for_all_models (const char    *name,
440                          GTestDataFunc  test_func)
441 {
442   guint i;
443   char *test;
444 
445   for (i = 0; i < N_MODELS; i++)
446     {
447       test = create_test_name (i);
448       char *path = g_strdup_printf ("/sorterlistmodel/%s/%s", test, name);
449       g_test_add_data_func (path, GUINT_TO_POINTER (i), test_func);
450       g_free (path);
451       g_free (test);
452     }
453 }
454 
455 int
main(int argc,char * argv[])456 main (int argc, char *argv[])
457 {
458   (g_test_init) (&argc, &argv, NULL);
459   setlocale (LC_ALL, "C");
460 
461   add_test_for_all_models ("two-sorters", test_two_sorters);
462   add_test_for_all_models ("stability", test_stability);
463 
464   return g_test_run ();
465 }
466