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 * |
47 | model_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 | |
69 | static void |
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 (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 | |
130 | static GtkFilterListModel * |
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 (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 | |
162 | static GtkFilterListModel * |
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 (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 | |
194 | static GListModel * |
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 (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 | |
211 | static GtkFilter * |
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 (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 | |
247 | static GtkFilter * |
248 | create_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 | |
263 | static void |
264 | test_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 | */ |
293 | static void |
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 (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 | */ |
357 | static void |
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 (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 | |
453 | static void |
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 (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 | |
467 | int |
468 | main (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 | |