/* * Copyright © 2020 Benjamin Otte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include #include #define ensure_updated() G_STMT_START{ \ while (g_main_context_pending (NULL)) \ g_main_context_iteration (NULL, TRUE); \ }G_STMT_END #define assert_model_equal(model1, model2) G_STMT_START{ \ guint _i, _n; \ g_assert_cmpint (g_list_model_get_n_items (model1), ==, g_list_model_get_n_items (model2)); \ _n = g_list_model_get_n_items (model1); \ for (_i = 0; _i < _n; _i++) \ { \ gpointer o1 = g_list_model_get_item (model1, _i); \ gpointer o2 = g_list_model_get_item (model2, _i); \ \ if (o1 != o2) \ { \ char *_s = g_strdup_printf ("Objects differ at index %u out of %u", _i, _n); \ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, _s); \ g_free (_s); \ } \ \ g_object_unref (o1); \ g_object_unref (o2); \ } \ }G_STMT_END G_GNUC_UNUSED static char * model_to_string (GListModel *model) { GString *string; guint i, n; n = g_list_model_get_n_items (model); string = g_string_new (NULL); /* Check that all unchanged items are indeed unchanged */ for (i = 0; i < n; i++) { gpointer item, model_item = g_list_model_get_item (model, i); if (GTK_IS_TREE_LIST_ROW (model_item)) item = gtk_tree_list_row_get_item (model_item); else item = model_item; if (i > 0) g_string_append (string, ", "); if (G_IS_LIST_MODEL (item)) g_string_append (string, "*"); else g_string_append (string, gtk_string_object_get_string (item)); g_object_unref (model_item); } return g_string_free (string, FALSE); } static void assert_items_changed_correctly (GListModel *model, guint position, guint removed, guint added, GListModel *compare) { guint i, n_items; //g_print ("%s => %u -%u +%u => %s\n", model_to_string (compare), position, removed, added, model_to_string (model)); g_assert_cmpint (g_list_model_get_n_items (model), ==, g_list_model_get_n_items (compare) - removed + added); n_items = g_list_model_get_n_items (model); if (position != 0 || removed != n_items) { /* Check that all unchanged items are indeed unchanged */ for (i = 0; i < position; i++) { gpointer o1 = g_list_model_get_item (model, i); gpointer o2 = g_list_model_get_item (compare, i); g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2)); g_object_unref (o1); g_object_unref (o2); } for (i = position + added; i < n_items; i++) { gpointer o1 = g_list_model_get_item (model, i); gpointer o2 = g_list_model_get_item (compare, i - added + removed); g_assert_cmphex (GPOINTER_TO_SIZE (o1), ==, GPOINTER_TO_SIZE (o2)); g_object_unref (o1); g_object_unref (o2); } /* Check that the first and last added item are different from * first and last removed item. * Otherwise we could have kept them as-is */ if (removed > 0 && added > 0) { gpointer o1 = g_list_model_get_item (model, position); gpointer o2 = g_list_model_get_item (compare, position); g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2)); g_object_unref (o1); g_object_unref (o2); o1 = g_list_model_get_item (model, position + added - 1); o2 = g_list_model_get_item (compare, position + removed - 1); g_assert_cmphex (GPOINTER_TO_SIZE (o1), !=, GPOINTER_TO_SIZE (o2)); g_object_unref (o1); g_object_unref (o2); } } /* Finally, perform the same change as the signal indicates */ g_list_store_splice (G_LIST_STORE (compare), position, removed, NULL, 0); for (i = position; i < position + added; i++) { gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i); g_list_store_insert (G_LIST_STORE (compare), i, item); g_object_unref (item); } } static GtkSortListModel * sort_list_model_new (GListModel *source, GtkSorter *sorter) { GtkSortListModel *model; GListStore *check; guint i; model = gtk_sort_list_model_new (source, sorter); check = g_list_store_new (G_TYPE_OBJECT); for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++) { gpointer item = g_list_model_get_item (G_LIST_MODEL (model), i); g_list_store_append (check, item); g_object_unref (item); } g_signal_connect_data (model, "items-changed", G_CALLBACK (assert_items_changed_correctly), check, (GClosureNotify) g_object_unref, 0); return model; } #define N_MODELS 8 static char * create_test_name (guint id) { GString *s = g_string_new (""); if (id & (1 << 0)) g_string_append (s, "set-model"); else g_string_append (s, "construct-with-model"); if (id & (1 << 1)) g_string_append (s, "/set-sorter"); else g_string_append (s, "/construct-with-sorter"); if (id & (1 << 2)) g_string_append (s, "/incremental"); else g_string_append (s, "/non-incremental"); return g_string_free (s, FALSE); } static GtkSortListModel * create_sort_list_model (gconstpointer model_id, gboolean track_changes, GListModel *source, GtkSorter *sorter) { GtkSortListModel *model; guint id = GPOINTER_TO_UINT (model_id); if (track_changes) model = sort_list_model_new (((id & 1) || !source) ? NULL : g_object_ref (source), ((id & 2) || !sorter) ? NULL : g_object_ref (sorter)); else model = gtk_sort_list_model_new (((id & 1) || !source) ? NULL : g_object_ref (source), ((id & 2) || !sorter) ? NULL : g_object_ref (sorter)); switch (id >> 2) { case 0: break; case 1: gtk_sort_list_model_set_incremental (model, TRUE); break; default: g_assert_not_reached (); break; } if (id & 1) gtk_sort_list_model_set_model (model, source); if (id & 2) gtk_sort_list_model_set_sorter (model, sorter); return model; } static GListModel * create_source_model (guint min_size, guint max_size) { const char *strings[] = { "A", "a", "B", "b" }; GtkStringList *list; guint i, size; size = g_test_rand_int_range (min_size, max_size + 1); list = gtk_string_list_new (NULL); for (i = 0; i < size; i++) gtk_string_list_append (list, strings[g_test_rand_int_range (0, G_N_ELEMENTS (strings))]); return G_LIST_MODEL (list); } #define N_SORTERS 3 static GtkSorter * create_sorter (gsize id) { GtkSorter *sorter; switch (id) { case 0: return GTK_SORTER (gtk_string_sorter_new (NULL)); case 1: case 2: /* match all As, Bs and nothing */ sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); if (id == 1) gtk_string_sorter_set_ignore_case (GTK_STRING_SORTER (sorter), TRUE); return sorter; default: g_assert_not_reached (); return NULL; } } static GtkSorter * create_random_sorter (gboolean allow_null) { guint n; if (allow_null) n = g_test_rand_int_range (0, N_SORTERS + 1); else n = g_test_rand_int_range (0, N_SORTERS); if (n >= N_SORTERS) return NULL; return create_sorter (n); } /* Compare this: * source => sorter1 => sorter2 * with: * source => multisorter(sorter1, sorter2) * and randomly change the source and sorters and see if the * two continue agreeing. */ static void test_two_sorters (gconstpointer model_id) { GtkSortListModel *compare; GtkSortListModel *model1, *model2; GListModel *source; GtkSorter *every, *sorter; guint i, j, k; source = create_source_model (10, 10); model2 = create_sort_list_model (model_id, TRUE, source, NULL); /* can't track changes from a sortmodel, where the same items get reordered */ model1 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (model2), NULL); every = GTK_SORTER (gtk_multi_sorter_new ()); compare = create_sort_list_model (model_id, TRUE, source, every); g_object_unref (every); g_object_unref (source); for (i = 0; i < N_SORTERS; i++) { sorter = create_sorter (i); gtk_sort_list_model_set_sorter (model1, sorter); gtk_multi_sorter_append (GTK_MULTI_SORTER (every), sorter); for (j = 0; j < N_SORTERS; j++) { sorter = create_sorter (i); gtk_sort_list_model_set_sorter (model2, sorter); gtk_multi_sorter_append (GTK_MULTI_SORTER (every), sorter); ensure_updated (); assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare)); for (k = 0; k < 10; k++) { source = create_source_model (0, 1000); gtk_sort_list_model_set_model (compare, source); gtk_sort_list_model_set_model (model2, source); g_object_unref (source); ensure_updated (); assert_model_equal (G_LIST_MODEL (model1), G_LIST_MODEL (compare)); } gtk_multi_sorter_remove (GTK_MULTI_SORTER (every), 1); } gtk_multi_sorter_remove (GTK_MULTI_SORTER (every), 0); } g_object_unref (compare); g_object_unref (model2); g_object_unref (model1); } /* Run: * source => sorter1 => sorter2 * and randomly add/remove sources and change the sorters and * see if the two sorters stay identical */ static void test_stability (gconstpointer model_id) { GListStore *store; GtkFlattenListModel *flatten; GtkSortListModel *sort1, *sort2; GtkSorter *sorter; gsize i; sorter = create_random_sorter (TRUE); store = g_list_store_new (G_TYPE_OBJECT); flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store)); sort1 = create_sort_list_model (model_id, TRUE, G_LIST_MODEL (flatten), sorter); sort2 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (sort1), sorter); g_clear_object (&sorter); for (i = 0; i < 500; i++) { gboolean add = FALSE, remove = FALSE; guint position; switch (g_test_rand_int_range (0, 4)) { case 0: /* change the sorter */ sorter = create_random_sorter (TRUE); gtk_sort_list_model_set_sorter (sort1, sorter); gtk_sort_list_model_set_sorter (sort2, sorter); g_clear_object (&sorter); break; case 1: /* remove a model */ remove = TRUE; break; case 2: /* add a model */ add = TRUE; break; case 3: /* replace a model */ remove = TRUE; add = TRUE; break; default: g_assert_not_reached (); break; } position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1); if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position) remove = FALSE; if (add) { /* We want at least one element, otherwise the sorters will see no changes */ GListModel *source = create_source_model (1, 50); g_list_store_splice (store, position, remove ? 1 : 0, (gpointer *) &source, 1); g_object_unref (source); } else if (remove) { g_list_store_remove (store, position); } if (g_test_rand_bit ()) { ensure_updated (); assert_model_equal (G_LIST_MODEL (sort1), G_LIST_MODEL (sort2)); } } g_object_unref (sort2); g_object_unref (sort1); g_object_unref (flatten); } static void add_test_for_all_models (const char *name, GTestDataFunc test_func) { guint i; char *test; for (i = 0; i < N_MODELS; i++) { test = create_test_name (i); char *path = g_strdup_printf ("/sorterlistmodel/%s/%s", test, name); g_test_add_data_func (path, GUINT_TO_POINTER (i), test_func); g_free (path); g_free (test); } } int main (int argc, char *argv[]) { g_test_init (&argc, &argv, NULL); setlocale (LC_ALL, "C"); add_test_for_all_models ("two-sorters", test_two_sorters); add_test_for_all_models ("stability", test_stability); return g_test_run (); }