/* * 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 = g_list_model_get_item (model, i); if (i > 0) g_string_append (string, ", "); g_string_append (string, gtk_string_object_get_string (item)); g_object_unref (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; //sanity check that we got all notifies g_assert_cmpuint (g_list_model_get_n_items (compare), ==, GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (compare), "last-notified-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); /* 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 void assert_n_items_notified_properly (GListModel *model, GParamSpec *pspec, GListModel *compare) { g_assert_cmpuint (g_list_model_get_n_items (model), !=, GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (compare), "last-notified-n-items"))); /* These should hve been updated in items-changed, which should have been emitted first */ g_assert_cmpuint (g_list_model_get_n_items (model), ==, g_list_model_get_n_items (compare)); g_object_set_data (G_OBJECT (compare), "last-notified-n-items", GUINT_TO_POINTER (g_list_model_get_n_items (model))); } static GtkFilterListModel * filter_list_model_new (GListModel *source, GtkFilter *filter) { GtkFilterListModel *model; GListStore *check; guint i; if (source) g_object_ref (source); if (filter) g_object_ref (filter); model = gtk_filter_list_model_new (source, filter); 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); g_object_set_data (G_OBJECT (check), "last-notified-n-items", GUINT_TO_POINTER (g_list_model_get_n_items (G_LIST_MODEL (check)))); g_signal_connect_data (model, "notify::n-items", G_CALLBACK (assert_n_items_notified_properly), g_object_ref (check), (GClosureNotify) g_object_unref, 0); return model; } #define N_MODELS 8 static GtkFilterListModel * create_filter_list_model (gconstpointer model_id, GListModel *source, GtkFilter *filter) { GtkFilterListModel *model; guint id = GPOINTER_TO_UINT (model_id); model = filter_list_model_new (id & 1 ? NULL : source, id & 2 ? NULL : filter); switch (id >> 2) { case 0: break; case 1: gtk_filter_list_model_set_incremental (model, TRUE); break; default: g_assert_not_reached (); break; } if (id & 1) gtk_filter_list_model_set_model (model, source); if (id & 2) gtk_filter_list_model_set_filter (model, filter); return model; } static GListModel * create_source_model (guint min_size, guint max_size) { 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, g_test_rand_bit () ? "A" : "B"); return G_LIST_MODEL (list); } #define N_FILTERS 5 static GtkFilter * create_filter (gsize id) { GtkFilter *filter; switch (id) { case 0: /* GTK_FILTER_MATCH_ALL */ return GTK_FILTER (gtk_string_filter_new (NULL)); case 1: /* GTK_FILTER_MATCH_NONE */ filter = GTK_FILTER (gtk_string_filter_new (NULL)); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "does not matter, because no expression"); return filter; case 2: case 3: case 4: /* match all As, Bs and nothing */ filter = GTK_FILTER (gtk_string_filter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); if (id == 2) gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "A"); else if (id == 3) gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "B"); else gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "does-not-match"); return filter; default: g_assert_not_reached (); return NULL; } } static GtkFilter * create_random_filter (gboolean allow_null) { guint n; if (allow_null) n = g_test_rand_int_range (0, N_FILTERS + 1); else n = g_test_rand_int_range (0, N_FILTERS); if (n >= N_FILTERS) return NULL; return create_filter (n); } static void test_no_filter (gconstpointer model_id) { GtkFilterListModel *model; GListModel *source; GtkFilter *filter; source = create_source_model (10, 10); model = create_filter_list_model (model_id, source, NULL); ensure_updated (); assert_model_equal (G_LIST_MODEL (model), source); filter = create_random_filter (FALSE); gtk_filter_list_model_set_filter (model, filter); g_object_unref (filter); gtk_filter_list_model_set_filter (model, NULL); ensure_updated (); assert_model_equal (G_LIST_MODEL (model), source); g_object_unref (model); g_object_unref (source); } /* Compare this: * source => filter1 => filter2 * with: * source => multifilter(filter1, filter2) * and randomly change the source and filters and see if the * two continue agreeing. */ static void test_two_filters (gconstpointer model_id) { GtkFilterListModel *compare; GtkFilterListModel *model1, *model2; GListModel *source; GtkFilter *every, *filter; guint i, j, k; source = create_source_model (10, 10); model1 = create_filter_list_model (model_id, source, NULL); model2 = create_filter_list_model (model_id, G_LIST_MODEL (model1), NULL); every = GTK_FILTER (gtk_every_filter_new ()); compare = create_filter_list_model (model_id, source, every); g_object_unref (every); g_object_unref (source); for (i = 0; i < N_FILTERS; i++) { filter = create_filter (i); gtk_filter_list_model_set_filter (model1, filter); gtk_multi_filter_append (GTK_MULTI_FILTER (every), filter); for (j = 0; j < N_FILTERS; j++) { filter = create_filter (i); gtk_filter_list_model_set_filter (model2, filter); gtk_multi_filter_append (GTK_MULTI_FILTER (every), filter); 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_filter_list_model_set_model (compare, source); gtk_filter_list_model_set_model (model1, source); g_object_unref (source); ensure_updated (); assert_model_equal (G_LIST_MODEL (model2), G_LIST_MODEL (compare)); } gtk_multi_filter_remove (GTK_MULTI_FILTER (every), 1); } gtk_multi_filter_remove (GTK_MULTI_FILTER (every), 0); } g_object_unref (compare); g_object_unref (model2); g_object_unref (model1); } /* Compare this: * (source => filter) * => flatten * with: * source * => flatten => filter * and randomly add/remove sources and change the filters and * see if the two agree. * * We use a multifilter for the top chain so that changing the filter * is easy. */ static void test_model_changes (gconstpointer model_id) { GListStore *store1, *store2; GtkFlattenListModel *flatten1, *flatten2; GtkFilterListModel *model2; GtkFilter *multi, *filter; gsize i; filter = create_random_filter (TRUE); multi = GTK_FILTER (gtk_every_filter_new ()); if (filter) gtk_multi_filter_append (GTK_MULTI_FILTER (multi), filter); store1 = g_list_store_new (G_TYPE_OBJECT); store2 = g_list_store_new (G_TYPE_OBJECT); flatten1 = gtk_flatten_list_model_new (G_LIST_MODEL (store1)); flatten2 = gtk_flatten_list_model_new (G_LIST_MODEL (store2)); model2 = create_filter_list_model (model_id, G_LIST_MODEL (flatten2), filter); 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 filter */ filter = create_random_filter (TRUE); gtk_multi_filter_remove (GTK_MULTI_FILTER (multi), 0); /* no-op if no filter */ if (filter) gtk_multi_filter_append (GTK_MULTI_FILTER (multi), filter); gtk_filter_list_model_set_filter (model2, filter); 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 (store1)) + 1); if (g_list_model_get_n_items (G_LIST_MODEL (store1)) == position) remove = FALSE; if (add) { /* We want at least one element, otherwise the filters will see no changes */ GListModel *source = create_source_model (1, 50); GtkFilterListModel *model1 = create_filter_list_model (model_id, source, multi); g_list_store_splice (store1, position, remove ? 1 : 0, (gpointer *) &model1, 1); g_list_store_splice (store2, position, remove ? 1 : 0, (gpointer *) &source, 1); g_object_unref (model1); g_object_unref (source); } else if (remove) { g_list_store_remove (store1, position); g_list_store_remove (store2, position); } if (g_test_rand_bit ()) { ensure_updated (); assert_model_equal (G_LIST_MODEL (flatten1), G_LIST_MODEL (model2)); } } g_object_unref (model2); g_object_unref (flatten2); g_object_unref (flatten1); g_object_unref (multi); } static void add_test_for_all_models (const char *name, GTestDataFunc test_func) { guint i; for (i = 0; i < N_MODELS; i++) { char *path = g_strdup_printf ("/filterlistmodel/model%u/%s", i, name); g_test_add_data_func (path, GUINT_TO_POINTER (i), test_func); g_free (path); } } int main (int argc, char *argv[]) { (g_test_init) (&argc, &argv, NULL); setlocale (LC_ALL, "C"); add_test_for_all_models ("no-filter", test_no_filter); add_test_for_all_models ("two-filters", test_two_filters); add_test_for_all_models ("model-changes", test_model_changes); return g_test_run (); }