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 * |
49 | model_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 | |
78 | static void |
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 (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 | |
142 | static GtkSortListModel * |
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 (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 | |
170 | static char * |
171 | create_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 | |
193 | static GtkSortListModel * |
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 (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 | |
229 | static GListModel * |
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 (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 | |
247 | static GtkSorter * |
248 | create_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 | |
271 | static GtkSorter * |
272 | create_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 | */ |
294 | static void |
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 (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 | */ |
354 | static void |
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 (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 | |
438 | static void |
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 (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 | |
455 | int |
456 | main (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 | |