forked from AuroraMiddleware/gtk
bf8b26aa27
With tests!
494 lines
14 KiB
C
494 lines
14 KiB
C
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <locale.h>
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#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;
|
|
|
|
//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_cmpuint (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 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 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);
|
|
|
|
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 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 ();
|
|
}
|