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
48G_GNUC_UNUSED static char *
49model_to_string (GListModel *model)
50{
51 GString *string;
52 guint i, n;
53
54 n = g_list_model_get_n_items (list: 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 (list: model, position: i);
61 if (GTK_IS_TREE_LIST_ROW (ptr: model_item))
62 item = gtk_tree_list_row_get_item (self: model_item);
63 else
64 item = model_item;
65
66 if (i > 0)
67 g_string_append (string, val: ", ");
68 if (G_IS_LIST_MODEL (ptr: item))
69 g_string_append (string, val: "*");
70 else
71 g_string_append (string, val: gtk_string_object_get_string (self: item));
72 g_object_unref (object: model_item);
73 }
74
75 return g_string_free (string, FALSE);
76}
77
78static void
79assert_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 (list: 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 (list: model, position: i);
98 gpointer o2 = g_list_model_get_item (list: compare, position: i);
99 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
100 g_object_unref (object: o1);
101 g_object_unref (object: o2);
102 }
103 for (i = position + added; i < n_items; i++)
104 {
105 gpointer o1 = g_list_model_get_item (list: model, position: i);
106 gpointer o2 = g_list_model_get_item (list: compare, position: i - added + removed);
107 g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2));
108 g_object_unref (object: o1);
109 g_object_unref (object: 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 (list: model, position);
119 gpointer o2 = g_list_model_get_item (list: compare, position);
120 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
121 g_object_unref (object: o1);
122 g_object_unref (object: o2);
123
124 o1 = g_list_model_get_item (list: model, position: position + added - 1);
125 o2 = g_list_model_get_item (list: compare, position: position + removed - 1);
126 g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2));
127 g_object_unref (object: o1);
128 g_object_unref (object: o2);
129 }
130 }
131
132 /* Finally, perform the same change as the signal indicates */
133 g_list_store_splice (store: G_LIST_STORE (ptr: compare), position, n_removals: removed, NULL, n_additions: 0);
134 for (i = position; i < position + added; i++)
135 {
136 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: model), position: i);
137 g_list_store_insert (store: G_LIST_STORE (ptr: compare), position: i, item);
138 g_object_unref (object: item);
139 }
140}
141
142static GtkSortListModel *
143sort_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 (model: source, sorter);
151 check = g_list_store_new (G_TYPE_OBJECT);
152 for (i = 0; i < g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model)); i++)
153 {
154 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: model), position: i);
155 g_list_store_append (store: check, item);
156 g_object_unref (object: item);
157 }
158 g_signal_connect_data (instance: model,
159 detailed_signal: "items-changed",
160 G_CALLBACK (assert_items_changed_correctly),
161 data: check,
162 destroy_data: (GClosureNotify) g_object_unref,
163 connect_flags: 0);
164
165 return model;
166}
167
168#define N_MODELS 8
169
170static char *
171create_test_name (guint id)
172{
173 GString *s = g_string_new (init: "");
174
175 if (id & (1 << 0))
176 g_string_append (string: s, val: "set-model");
177 else
178 g_string_append (string: s, val: "construct-with-model");
179
180 if (id & (1 << 1))
181 g_string_append (string: s, val: "/set-sorter");
182 else
183 g_string_append (string: s, val: "/construct-with-sorter");
184
185 if (id & (1 << 2))
186 g_string_append (string: s, val: "/incremental");
187 else
188 g_string_append (string: s, val: "/non-incremental");
189
190 return g_string_free (string: s, FALSE);
191}
192
193static GtkSortListModel *
194create_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 (source: ((id & 1) || !source) ? NULL : g_object_ref (source), sorter: ((id & 2) || !sorter) ? NULL : g_object_ref (sorter));
204 else
205 model = gtk_sort_list_model_new (model: ((id & 1) || !source) ? NULL : g_object_ref (source), sorter: ((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 (self: 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 (self: model, model: source);
223 if (id & 2)
224 gtk_sort_list_model_set_sorter (self: model, sorter);
225
226 return model;
227}
228
229static GListModel *
230create_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 (begin: min_size, end: max_size + 1);
237 list = gtk_string_list_new (NULL);
238
239 for (i = 0; i < size; i++)
240 gtk_string_list_append (self: list, string: strings[g_test_rand_int_range (begin: 0, G_N_ELEMENTS (strings))]);
241
242 return G_LIST_MODEL (ptr: list);
243}
244
245#define N_SORTERS 3
246
247static GtkSorter *
248create_sorter (gsize id)
249{
250 GtkSorter *sorter;
251
252 switch (id)
253 {
254 case 0:
255 return GTK_SORTER (ptr: gtk_string_sorter_new (NULL));
256
257 case 1:
258 case 2:
259 /* match all As, Bs and nothing */
260 sorter = GTK_SORTER (ptr: gtk_string_sorter_new (expression: gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, property_name: "string")));
261 if (id == 1)
262 gtk_string_sorter_set_ignore_case (self: GTK_STRING_SORTER (ptr: sorter), TRUE);
263 return sorter;
264
265 default:
266 g_assert_not_reached ();
267 return NULL;
268 }
269}
270
271static GtkSorter *
272create_random_sorter (gboolean allow_null)
273{
274 guint n;
275
276 if (allow_null)
277 n = g_test_rand_int_range (begin: 0, N_SORTERS + 1);
278 else
279 n = g_test_rand_int_range (begin: 0, N_SORTERS);
280
281 if (n >= N_SORTERS)
282 return NULL;
283
284 return create_sorter (id: 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 */
294static void
295test_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 (min_size: 10, max_size: 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, source: G_LIST_MODEL (ptr: model2), NULL);
307 every = GTK_SORTER (ptr: gtk_multi_sorter_new ());
308 compare = create_sort_list_model (model_id, TRUE, source, sorter: every);
309 g_object_unref (object: every);
310 g_object_unref (object: source);
311
312 for (i = 0; i < N_SORTERS; i++)
313 {
314 sorter = create_sorter (id: i);
315 gtk_sort_list_model_set_sorter (self: model1, sorter);
316 gtk_multi_sorter_append (self: GTK_MULTI_SORTER (ptr: every), sorter);
317
318 for (j = 0; j < N_SORTERS; j++)
319 {
320 sorter = create_sorter (id: i);
321 gtk_sort_list_model_set_sorter (self: model2, sorter);
322 gtk_multi_sorter_append (self: GTK_MULTI_SORTER (ptr: 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 (min_size: 0, max_size: 1000);
330 gtk_sort_list_model_set_model (self: compare, model: source);
331 gtk_sort_list_model_set_model (self: model2, model: source);
332 g_object_unref (object: source);
333
334 ensure_updated ();
335 assert_model_equal (G_LIST_MODEL (model1), G_LIST_MODEL (compare));
336 }
337
338 gtk_multi_sorter_remove (self: GTK_MULTI_SORTER (ptr: every), position: 1);
339 }
340
341 gtk_multi_sorter_remove (self: GTK_MULTI_SORTER (ptr: every), position: 0);
342 }
343
344 g_object_unref (object: compare);
345 g_object_unref (object: model2);
346 g_object_unref (object: 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 */
354static void
355test_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 (model: G_LIST_MODEL (ptr: store));
367 sort1 = create_sort_list_model (model_id, TRUE, source: G_LIST_MODEL (ptr: flatten), sorter);
368 sort2 = create_sort_list_model (model_id, FALSE, source: G_LIST_MODEL (ptr: 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 (begin: 0, end: 4))
377 {
378 case 0:
379 /* change the sorter */
380 sorter = create_random_sorter (TRUE);
381 gtk_sort_list_model_set_sorter (self: sort1, sorter);
382 gtk_sort_list_model_set_sorter (self: 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 (begin: 0, end: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: store)) + 1);
408 if (g_list_model_get_n_items (list: G_LIST_MODEL (ptr: 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 (min_size: 1, max_size: 50);
415 g_list_store_splice (store,
416 position,
417 n_removals: remove ? 1 : 0,
418 additions: (gpointer *) &source, n_additions: 1);
419 g_object_unref (object: 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 (object: sort2);
434 g_object_unref (object: sort1);
435 g_object_unref (object: flatten);
436}
437
438static void
439add_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 (id: i);
448 char *path = g_strdup_printf (format: "/sorterlistmodel/%s/%s", test, name);
449 g_test_add_data_func (testpath: path, GUINT_TO_POINTER (i), test_func);
450 g_free (mem: path);
451 g_free (mem: test);
452 }
453}
454
455int
456main (int argc, char *argv[])
457{
458 (g_test_init) (argc: &argc, argv: &argv, NULL);
459 setlocale (LC_ALL, locale: "C");
460
461 add_test_for_all_models (name: "two-sorters", test_func: test_two_sorters);
462 add_test_for_all_models (name: "stability", test_func: test_stability);
463
464 return g_test_run ();
465}
466

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