/*
 *  $Id: dict.c 29645 2026-03-24 17:18:23Z yeti-dn $
 *  Copyright (C) 2009-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "tests/testlibgwy.h"

static void
dict_item_changed(G_GNUC_UNUSED GwyDict *dict,
                  gpointer arg1,
                  const gchar **changed_item_name)
{
    *changed_item_name = g_quark_to_string(GPOINTER_TO_UINT(arg1));
}

void
record_signal(guint *counter)
{
    (*counter)++;
}

void
record_finalisation(gpointer pcanary, GObject *where_the_object_was)
{
    gsize *canary = (gsize*)pcanary;
    g_assert_cmpuint(*canary, ==, 0);
    *canary = GPOINTER_TO_SIZE(where_the_object_was);
}

void
test_dict_in_construction(void)
{
    GwyDict *dict;

    dict = gwy_dict_new();
    g_assert_true(GWY_IS_DICT(dict));
    g_assert_false(gwy_dict_is_being_constructed(dict));
    g_assert_finalize_object(dict);

    dict = gwy_dict_new_in_construction();
    g_assert_true(GWY_IS_DICT(dict));
    g_assert_true(gwy_dict_is_being_constructed(dict));
    gwy_dict_finish_construction(dict);
    g_assert_false(gwy_dict_is_being_constructed(dict));
    g_assert_finalize_object(dict);
}

void
test_dict_data_boolean(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark quark;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    gwy_dict_set_boolean_by_name(dict, "/pfx/boolean", 12345);
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/boolean"));
    /* The dict should forget the actual value and return plain TRUE. */
    g_assert_cmpint(gwy_dict_get_boolean_by_name(dict, "/pfx/boolean"), ==, TRUE);

    quark = g_quark_try_string("/pfx/boolean");
    g_assert_true(quark);
    g_assert_true(gwy_dict_contains(dict, quark));
    g_assert_true(gwy_dict_get_boolean(dict, quark) == TRUE);

    gboolean b;
    g_assert_true(gwy_dict_gis_boolean_by_name(dict, "/pfx/boolean", &b));
    g_assert_cmpuint(b, ==, TRUE);

    g_assert_finalize_object(dict);
}

void
test_dict_data_int32(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark quark;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    gwy_dict_set_int32_by_name(dict, "/pfx/int", 42);
    quark = g_quark_try_string("/pfx/int");
    g_assert_true(quark);

    g_assert_true(gwy_dict_contains(dict, quark));
    g_assert_true(gwy_dict_get_int32(dict, quark) == 42);

    gwy_dict_set_int32(dict, quark, -3);
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/int"));
    g_assert_cmpint(gwy_dict_get_int32_by_name(dict, "/pfx/int"), ==, -3);

    gint32 i32;
    g_assert_true(gwy_dict_gis_int32_by_name(dict, "/pfx/int", &i32));
    g_assert_cmpint(i32, ==, -3);

    g_assert_finalize_object(dict);
}

void
test_dict_data_int64(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark quark;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    gwy_dict_set_int64_by_name(dict, "/pfx/int64", G_GUINT64_CONSTANT(0xdeadbeefdeadbeef));
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/int64"));
    g_assert_true((guint64)gwy_dict_get_int64_by_name(dict, "/pfx/int64") == G_GUINT64_CONSTANT(0xdeadbeefdeadbeef));

    quark = g_quark_try_string("/pfx/int64");
    g_assert_true(quark);
    g_assert_true(gwy_dict_contains(dict, quark));
    g_assert_true(gwy_dict_get_int64(dict, quark) == G_GUINT64_CONSTANT(0xdeadbeefdeadbeef));

    guint64 i64;
    g_assert_true(gwy_dict_gis_int64_by_name(dict, "/pfx/int64", (gint64*)&i64));
    g_assert_cmpuint(i64, ==, G_GUINT64_CONSTANT(0xdeadbeefdeadbeef));

    g_assert_finalize_object(dict);
}

void
test_dict_data_double(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark quark;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    gwy_dict_set_double_by_name(dict, "/pfx/double", G_LN2);
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/double"));
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/pfx/double"), ==, G_LN2);

    quark = g_quark_try_string("/pfx/double");
    g_assert_true(quark);
    g_assert_true(gwy_dict_contains(dict, quark));
    g_assert_true(gwy_dict_get_double(dict, quark) == G_LN2);

    gdouble flt;
    g_assert_true(gwy_dict_gis_double_by_name(dict, "/pfx/double", &flt));
    g_assert_cmpfloat(flt, ==, G_LN2);

    g_assert_finalize_object(dict);
}

void
test_dict_data_string(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark quark;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    gwy_dict_pass_string_by_name(dict, "/pfx/string", g_strdup("Test Test"));
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/string"));
    g_assert_cmpstr(gwy_dict_get_string_by_name(dict, "/pfx/string"), ==, "Test Test");

    const gchar *s = "";
    g_assert_true(gwy_dict_gis_string_by_name(dict, "/pfx/string", &s));
    g_assert_cmpstr(s, ==, "Test Test");

    quark = g_quark_try_string("/pfx/string");
    g_assert_true(quark);
    g_assert_true(gwy_dict_contains(dict, quark));
    g_assert_cmpstr(gwy_dict_get_string(dict, quark), ==, "Test Test");

    /* No value change */
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Test Test");
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/string"));
    g_assert_cmpstr(gwy_dict_get_string_by_name(dict, "/pfx/string"), ==, "Test Test");

    gwy_dict_set_string_by_name(dict, "/pfx/string", "Test Test Test");
    g_assert_true(gwy_dict_contains_by_name(dict, "/pfx/string"));
    g_assert_cmpstr(gwy_dict_get_string_by_name(dict, "/pfx/string"), ==, "Test Test Test");

    g_assert_finalize_object(dict);
}

void
test_dict_gvalue(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/val", G_CALLBACK(record_signal), &item_changed);

    const gdouble number = 1.175;
    GValue value;
    gwy_clear1(value);
    g_value_init(&value, G_TYPE_DOUBLE);
    g_value_set_double(&value, number);

    changed_item_name = "";
    gwy_dict_set_value_by_name(dict, "/val", &value);
    g_assert_true(gwy_dict_contains_by_name(dict, "/val"));
    g_assert_cmpuint(gwy_dict_value_type_by_name(dict, "/val"), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/val"), ==, number);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/val");

    changed_item_name = "";
    gwy_dict_set_double_by_name(dict, "/val", number);
    g_assert_cmpuint(gwy_dict_value_type_by_name(dict, "/val"), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/val"), ==, number);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_double_by_name(dict, "/val", 2.0);
    g_assert_cmpuint(gwy_dict_value_type_by_name(dict, "/val"), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/val"), ==, 2.0);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/val");
    GValue dictval = gwy_dict_get_value_by_name(dict, "/val");
    g_assert_cmpuint(G_VALUE_TYPE(&dictval), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(g_value_get_double(&dictval), ==, 2.0);
    g_value_unset(&dictval);

    changed_item_name = "";
    g_value_set_double(&value, 2.0);
    gwy_dict_set_value_by_name(dict, "/val", &value);
    g_assert_cmpuint(gwy_dict_value_type_by_name(dict, "/val"), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/val"), ==, 2.0);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    g_value_set_double(&value, number);
    gwy_dict_set_value_by_name(dict, "/val", &value);
    g_assert_cmpuint(gwy_dict_value_type_by_name(dict, "/val"), ==, G_TYPE_DOUBLE);
    g_assert_cmpfloat(gwy_dict_get_double_by_name(dict, "/val"), ==, number);
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "/val");

    g_value_unset(&value);
    g_assert_finalize_object(dict);
}

void
test_dict_notify_boolean(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/boolean", G_CALLBACK(record_signal), &item_changed);

    changed_item_name = "";
    gwy_dict_set_boolean_by_name(dict, "/elsewhere/boolean", FALSE);
    g_assert_cmpuint(item_changed, ==, 0);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/boolean");

    changed_item_name = "";
    gwy_dict_set_boolean_by_name(dict, "/pfx/boolean", TRUE);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boolean");

    /* All TRUEs are the same. */
    changed_item_name = "";
    gwy_dict_set_boolean_by_name(dict, "/pfx/boolean", 1000);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_boolean_by_name(dict, "/pfx/boolean", FALSE);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boolean");

    g_assert_finalize_object(dict);
}

void
test_dict_notify_int32(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint int32_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/int32", G_CALLBACK(record_signal), &int32_changed);

    changed_item_name = "";
    gwy_dict_set_int32_by_name(dict, "/elsewhere/int32", 66);
    g_assert_cmpuint(int32_changed, ==, 0);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/int32");

    changed_item_name = "";
    gwy_dict_set_int32_by_name(dict, "/pfx/int32", 123);
    g_assert_cmpuint(int32_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/int32");

    changed_item_name = "";
    gwy_dict_set_int32_by_name(dict, "/pfx/int32", 123);
    g_assert_cmpuint(int32_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_int32_by_name(dict, "/pfx/int32", -1);
    g_assert_cmpuint(int32_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/int32");

    g_assert_finalize_object(dict);
}

void
test_dict_notify_string(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/string", G_CALLBACK(record_signal), &item_changed);

    changed_item_name = "";
    gwy_dict_set_string_by_name(dict, "/elsewhere/string", "One");
    g_assert_cmpuint(item_changed, ==, 0);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/string");

    changed_item_name = "";
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Two");
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/string");

    changed_item_name = "";
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Two");
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Three");
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/string");

    changed_item_name = "";
    gwy_dict_pass_string_by_name(dict, "/pfx/string", g_strdup("Three"));
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_pass_string_by_name(dict, "/pfx/string", g_strdup("Four"));
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/string");

    changed_item_name = "";
    gwy_dict_pass_string_by_name(dict, "/pfx/string", g_strdup("Four"));
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Four");
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "");

    g_assert_finalize_object(dict);
}

void
test_dict_notify_object(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/object", G_CALLBACK(record_signal), &item_changed);

    gdouble data[2] = { G_LN2, -1.4 }, another_data[3] = { G_PI, 0.0, -600.453453 };
    GwySerTest *object = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Balrog", 0xdeadbeefu);
    GwySerTest *another_object = gwy_ser_test_new_filled(FALSE, another_data, G_N_ELEMENTS(another_data), "X", 0xff);

    gsize canary = 0, another_canary = 0;
    g_object_weak_ref(G_OBJECT(object), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(another_object), record_finalisation, &another_canary);

    changed_item_name = "";
    gwy_dict_set_object_by_name(dict, "/elsewhere/object", object);
    g_assert_cmpuint(item_changed, ==, 0);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");

    changed_item_name = "";
    gwy_dict_set_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");

    changed_item_name = "";
    gwy_dict_set_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");
    g_assert_cmpuint(canary, ==, 0);

    changed_item_name = "";
    gwy_dict_set_object_by_name(dict, "/pfx/object", another_object);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");
    g_assert_cmpuint(another_canary, ==, 0);

    /* This is a silly case! We are only passing the reference while the object is already there.
     * Nothing has to change as we are only moving some references around. */
    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", another_object);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "");
    g_assert_cmpuint(another_canary, ==, 0);

    g_object_ref(object);
    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");
    g_assert_cmpuint(another_canary, !=, 0);
    g_assert_cmpuint(canary, ==, 0);

    /* This one is even sillier! We are creating extra references just to have some to give up.
     * Nothing has to change as we are only moving some references around. */
    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "");
    g_assert_cmpuint(canary, ==, 0);

    changed_item_name = "";
    gwy_dict_remove_by_name(dict, "/pfx/object");
    g_assert_cmpuint(item_changed, ==, 4);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");
    g_assert_cmpuint(canary, ==, 0);

    g_assert_finalize_object(dict);

    g_assert_cmpuint(canary, !=, 0);
}

void
test_dict_notify_boxed(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/boxed", G_CALLBACK(record_signal), &item_changed);

    GwyXY xy = { 1.5, -1e7 }, another_xy = { 0.0006, G_PI };

    changed_item_name = "";
    gwy_dict_set_boxed_by_name(dict, GWY_TYPE_XY, "/elsewhere/boxed", &xy);
    g_assert_cmpuint(item_changed, ==, 0);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/boxed");

    changed_item_name = "";
    gwy_dict_set_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", &xy);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boxed");

    changed_item_name = "";
    gwy_dict_set_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", &xy);
    g_assert_cmpuint(item_changed, ==, 1);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_set_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", &another_xy);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boxed");

    changed_item_name = "";
    gwy_dict_pass_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", g_boxed_copy(GWY_TYPE_XY, &another_xy));
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_pass_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", g_boxed_copy(GWY_TYPE_XY, &xy));
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boxed");

    changed_item_name = "";
    gwy_dict_pass_boxed_by_name(dict, GWY_TYPE_XY, "/pfx/boxed", g_boxed_copy(GWY_TYPE_XY, &xy));
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    gwy_dict_remove_by_name(dict, "/pfx/boxed");
    g_assert_cmpuint(item_changed, ==, 4);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/boxed");

    g_assert_finalize_object(dict);
}

void
test_dict_prefix(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;
    guint item_changed = 0;
    gboolean ok;
    guint n;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);
    g_signal_connect_swapped(dict, "item-changed::/pfx/int", G_CALLBACK(record_signal), &item_changed);

    gwy_dict_set_boolean_by_name(dict, "/pfx/boolean", TRUE);
    gwy_dict_set_int32_by_name(dict, "/pfx/int", 11);
    gwy_dict_set_int64_by_name(dict, "/pfx/int64", G_GUINT64_CONSTANT(0xdeadbeefdeadbeef));
    gwy_dict_set_double_by_name(dict, "/pfx/double", G_PI);
    gwy_dict_set_string_by_name(dict, "/pfx/string", "Test Test");

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 5);

    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", TRUE, TRUE);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 10);

    ok = gwy_dict_remove_by_name(dict, "/pfx/string/ble");
    g_assert_true(!ok);

    n = gwy_dict_remove_by_prefix(dict, "/pfx/string/ble");
    g_assert_cmpuint(n, ==, 0);

    changed_item_name = "";
    ok = gwy_dict_remove_by_name(dict, "/pfx/string");
    g_assert_true(ok);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 9);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/string");

    changed_item_name = "";
    n = gwy_dict_remove_by_prefix(dict, "/pfx/int");
    g_assert_cmpuint(n, ==, 1);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 8);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/int");

    changed_item_name = "";
    ok = gwy_dict_rename(dict, g_quark_try_string("/pfx/int64"), g_quark_try_string("/pfx/int"), TRUE);
    g_assert_true(ok);
    g_assert_cmpuint(item_changed, ==, 3);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/int");

    n = gwy_dict_remove_by_prefix(dict, "/pfx");
    g_assert_cmpuint(n, ==, 3);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 5);
    g_assert_cmpuint(item_changed, ==, 4);

    gwy_dict_remove_by_prefix(dict, NULL);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);

    g_assert_finalize_object(dict);
}

void
test_dict_refcount(void)
{
    GwyDict *dict = gwy_dict_new();

    GwySerTest *st1 = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 1);
    gwy_dict_set_object_by_name(dict, "/pfx/object", st1);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 2);

    GwySerTest *st2 = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    g_assert_cmpuint(G_OBJECT(st2)->ref_count, ==, 1);
    gwy_dict_set_object_by_name(dict, "/pfx/object", st2);
    g_assert_cmpuint(G_OBJECT(st2)->ref_count, ==, 2);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 1);
    g_object_unref(st1);

    GwySerTest *st3 = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    g_assert_cmpuint(G_OBJECT(st3)->ref_count, ==, 1);
    g_object_ref(st3);
    gwy_dict_pass_object_by_name(dict, "/pfx/taken", st3);
    g_assert_cmpuint(G_OBJECT(st3)->ref_count, ==, 2);

    g_object_unref(dict);
    g_assert_cmpuint(G_OBJECT(st2)->ref_count, ==, 1);
    g_object_unref(st2);
    g_assert_cmpuint(G_OBJECT(st3)->ref_count, ==, 1);
    g_object_unref(st3);

    dict = gwy_dict_new();
    st1 = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    gwy_dict_set_object_by_name(dict, "/pfx/object", st1);
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", FALSE, TRUE);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 3);
    st2 = gwy_dict_get_object_by_name(dict, "/elsewhere/object");
    g_assert_true(st2 == st1);

    gwy_dict_transfer(dict, dict, "/pfx", "/faraway", TRUE, TRUE);
    st2 = gwy_dict_get_object_by_name(dict, "/faraway/object");
    g_assert_true(GWY_IS_SER_TEST(st2));
    g_assert_true(st2 != st1);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 3);
    g_assert_cmpuint(G_OBJECT(st2)->ref_count, ==, 1);
    g_object_ref(st2);

    g_assert_finalize_object(dict);
    g_assert_cmpuint(G_OBJECT(st1)->ref_count, ==, 1);
    g_assert_cmpuint(G_OBJECT(st2)->ref_count, ==, 1);
    g_object_unref(st1);
    g_object_unref(st2);
}

void
test_dict_transfer_object_default(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);

    gdouble data[2] = { G_LN2, -1.4 }, another_data[3] = { G_PI, 0.0, -600.453453 };
    GwySerTest *object = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Balrog", 0xdeadbeefu);
    GwySerTest *another_object = gwy_ser_test_new_filled(FALSE, another_data, G_N_ELEMENTS(another_data), "X", 0xff);

    gsize canary = 0, another_canary = 0;
    g_object_weak_ref(G_OBJECT(object), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(another_object), record_finalisation, &another_canary);

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/elsewhere/object", another_object);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");

    changed_item_name = "";
    /* The object at "/elsewhere" must not be changed. */
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", FALSE, FALSE);
    g_assert_cmpstr(changed_item_name, ==, "");
    g_assert_false(gwy_dict_get_object_by_name(dict, "/elsewhere/object") == object);

    changed_item_name = "";
    /* The same object must appear also at "/different/place". */
    gwy_dict_transfer(dict, dict, "/pfx", "/different/place", FALSE, FALSE);
    g_assert_cmpstr(changed_item_name, ==, "/different/place/object");
    g_assert_true(gwy_dict_get_object_by_name(dict, "/different/place/object") == object);

    g_assert_cmpuint(canary, ==, 0);
    g_assert_cmpuint(another_canary, ==, 0);

    g_assert_finalize_object(dict);

    g_assert_cmpuint(canary, !=, 0);
    g_assert_cmpuint(another_canary, !=, 0);
}

void
test_dict_transfer_object_force(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);

    gdouble data[2] = { G_LN2, -1.4 }, another_data[3] = { G_PI, 0.0, -600.453453 };
    GwySerTest *object = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Balrog", 0xdeadbeefu);
    GwySerTest *another_object = gwy_ser_test_new_filled(FALSE, another_data, G_N_ELEMENTS(another_data), "X", 0xff);

    gsize canary = 0, another_canary = 0;
    g_object_weak_ref(G_OBJECT(object), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(another_object), record_finalisation, &another_canary);

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/elsewhere/object", another_object);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");

    changed_item_name = "";
    /* The object at "/elsewhere" must be replaced with object. */
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", FALSE, TRUE);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");
    g_assert_true(gwy_dict_get_object_by_name(dict, "/elsewhere/object") == object);
    g_assert_cmpuint(another_canary, !=, 0);

    changed_item_name = "";
    /* If we do it again nothing must change. */
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", FALSE, TRUE);
    g_assert_true(gwy_dict_get_object_by_name(dict, "/elsewhere/object") == object);
    g_assert_cmpstr(changed_item_name, ==, "");

    changed_item_name = "";
    /* The same object must appear also at "/different/place". */
    gwy_dict_transfer(dict, dict, "/pfx", "/different/place", FALSE, TRUE);
    g_assert_cmpstr(changed_item_name, ==, "/different/place/object");
    g_assert_true(gwy_dict_get_object_by_name(dict, "/different/place/object") == object);

    g_assert_cmpuint(canary, ==, 0);

    g_assert_finalize_object(dict);

    g_assert_cmpuint(canary, !=, 0);
}

void
test_dict_transfer_object_deep(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);

    gdouble data[2] = { G_LN2, -1.4 }, another_data[3] = { G_PI, 0.0, -600.453453 };
    GwySerTest *object = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Balrog", 0xdeadbeefu);
    GwySerTest *another_object = gwy_ser_test_new_filled(FALSE, another_data, G_N_ELEMENTS(another_data), "X", 0xff);

    gsize canary = 0, another_canary = 0;
    g_object_weak_ref(G_OBJECT(object), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(another_object), record_finalisation, &another_canary);

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/elsewhere/object", another_object);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");

    changed_item_name = "";
    /* The object at "/elsewhere" must not be changed. */
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", TRUE, FALSE);
    g_assert_cmpstr(changed_item_name, ==, "");
    g_assert_cmpuint(another_canary, ==, 0);

    /* A copy of the object (i.e. a different object) must replace the current object at "/elsewhere".
     * We must neither leak nor free prematurely anything. */
    changed_item_name = "";
    gwy_dict_transfer(dict, dict, "/pfx", "/different/place", TRUE, FALSE);
    g_assert_cmpstr(changed_item_name, ==, "/different/place/object");

    g_assert_cmpuint(canary, ==, 0);
    g_assert_cmpuint(another_canary, ==, 0);

    g_assert_nonnull(gwy_dict_get_object_by_name(dict, "/different/place/object"));
    GwySerTest *copied = gwy_dict_get_object_by_name(dict, "/different/place/object");
    g_assert_false(copied == object);
    g_assert_false(copied == another_object);

    gsize copied_canary = 0;
    g_object_weak_ref(G_OBJECT(copied), record_finalisation, &copied_canary);

    g_assert_finalize_object(dict);

    g_assert_cmpuint(canary, !=, 0);
    g_assert_cmpuint(another_canary, !=, 0);
    g_assert_cmpuint(copied_canary, !=, 0);
}

void
test_dict_transfer_object_deep_force(void)
{
    GwyDict *dict = gwy_dict_new();
    const gchar *changed_item_name;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect(dict, "item-changed", G_CALLBACK(dict_item_changed), &changed_item_name);

    gdouble data[2] = { G_LN2, -1.4 }, another_data[3] = { G_PI, 0.0, -600.453453 };
    GwySerTest *object = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Balrog", 0xdeadbeefu);
    GwySerTest *another_object = gwy_ser_test_new_filled(FALSE, another_data, G_N_ELEMENTS(another_data), "X", 0xff);

    gsize canary = 0, another_canary = 0;
    g_object_weak_ref(G_OBJECT(object), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(another_object), record_finalisation, &another_canary);

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/elsewhere/object", another_object);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");

    changed_item_name = "";
    gwy_dict_pass_object_by_name(dict, "/pfx/object", object);
    g_assert_cmpstr(changed_item_name, ==, "/pfx/object");

    g_object_ref(another_object);
    changed_item_name = "";
    /* The object at "/elsewhere" must be replaced with a copy of object. */
    gwy_dict_transfer(dict, dict, "/pfx", "/elsewhere", TRUE, TRUE);
    g_assert_cmpstr(changed_item_name, ==, "/elsewhere/object");
    g_assert_cmpuint(another_canary, ==, 0);

    g_assert_nonnull(gwy_dict_get_object_by_name(dict, "/elsewhere/object"));
    GwySerTest *copied = gwy_dict_get_object_by_name(dict, "/elsewhere/object");
    g_assert_false(copied == object);
    g_assert_false(copied == another_object);

    gsize copied_canary = 0;
    g_object_weak_ref(G_OBJECT(copied), record_finalisation, &copied_canary);

    changed_item_name = "";
    /* A copy of object must appear at "/different/place". */
    gwy_dict_transfer(dict, dict, "/pfx", "/different/place", TRUE, TRUE);
    g_assert_cmpstr(changed_item_name, ==, "/different/place/object");

    g_assert_cmpuint(canary, ==, 0);

    g_assert_nonnull(gwy_dict_get_object_by_name(dict, "/different/place/object"));
    GwySerTest *copied2 = gwy_dict_get_object_by_name(dict, "/different/place/object");
    g_assert_false(copied2 == object);
    g_assert_false(copied2 == another_object);

    gsize copied2_canary = 0;
    g_object_weak_ref(G_OBJECT(copied2), record_finalisation, &copied2_canary);

    g_assert_finalize_object(another_object);
    g_assert_cmpuint(another_canary, !=, 0);

    g_assert_finalize_object(dict);

    g_assert_cmpuint(canary, !=, 0);
    g_assert_cmpuint(copied_canary, !=, 0);
    g_assert_cmpuint(copied2_canary, !=, 0);
}

void
test_dict_transfer_in_construction(void)
{
    GwyDict *dict = gwy_dict_new();
    guint src_changed_count = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 0);
    g_signal_connect_swapped(dict, "item-changed", G_CALLBACK(record_signal), &src_changed_count);

    gwy_dict_set_int32_by_name(dict, "/loc/int", -4);
    gwy_dict_set_boolean_by_name(dict, "/loc/bool", TRUE);
    gwy_dict_set_string_by_name(dict, "/loc/str", "striiiing");
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 3);
    g_assert_cmpuint(src_changed_count, ==, 3);
    src_changed_count = 0;

    GwyDict *dest = gwy_dict_new_in_construction();
    guint dest_changed_count = 0;

    g_assert_cmpuint(gwy_dict_get_n_items(dest), ==, 0);
    g_signal_connect_swapped(dest, "item-changed", G_CALLBACK(record_signal), &dest_changed_count);

    // We can use transfer on in-construction objects and it does not emit signals.
    guint nchanged = gwy_dict_transfer(dict, dest, "/loc", "/different/place", TRUE, TRUE);
    g_assert_cmpuint(nchanged, ==, 3);
    g_assert_cmpuint(gwy_dict_get_n_items(dest), ==, 3);
    g_assert_cmpuint(src_changed_count, ==, 0);
    g_assert_cmpuint(dest_changed_count, ==, 0);

    g_assert_finalize_object(dest);
    g_assert_finalize_object(dict);
}

void
test_dict_keys(void)
{
    GwyDict *dict = gwy_dict_new();
    GQuark *qkeys = gwy_dict_keys(dict);
    g_assert_nonnull(qkeys);
    g_assert_true(!qkeys[0]);
    g_free(qkeys);
    const gchar **skeys = gwy_dict_keys_by_name(dict);
    g_assert_nonnull(skeys);
    g_assert_null(skeys[0]);
    g_free(skeys);

    gwy_dict_set_enum_by_name(dict, "test", GWY_RAW_DATA_REAL);
    gwy_dict_set_enum_by_name(dict, "real", GWY_RAW_DATA_REAL);
    gwy_dict_set_enum_by_name(dict, "extended", GWY_RAW_DATA_EXTENDED);
    gwy_dict_set_enum_by_name(dict, "sint12", GWY_RAW_DATA_SINT12);
    gwy_dict_remove_by_name(dict, "test");
    gwy_dict_set_enum_by_name(dict, "extended", GWY_RAW_DATA_EXTENDED);

    const gchar *expected_keys[] = { "real", "extended", "sint12" };
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, G_N_ELEMENTS(expected_keys));

    qkeys = gwy_dict_keys(dict);
    g_assert_nonnull(qkeys);
    for (guint i = 0; i < G_N_ELEMENTS(expected_keys); i++) {
        g_assert_cmpuint(qkeys[i], !=, 0);
        for (guint j = 0; j < i; j++)
            g_assert_cmpuint(qkeys[j], !=, qkeys[i]);
        gboolean found = FALSE;
        for (guint j = 0; j < G_N_ELEMENTS(expected_keys); j++) {
            if (qkeys[i] == g_quark_from_string(expected_keys[j])) {
                found = TRUE;
                break;
            }
        }
        g_assert_true(found);
    }
    g_assert_true(!qkeys[G_N_ELEMENTS(expected_keys)]);
    g_free(qkeys);

    skeys = gwy_dict_keys_by_name(dict);
    g_assert_nonnull(skeys);
    for (guint i = 0; i < G_N_ELEMENTS(expected_keys); i++) {
        g_assert_nonnull(skeys[i]);
        for (guint j = 0; j < i; j++)
            g_assert_cmpstr(skeys[j], !=, skeys[i]);
        gboolean found = FALSE;
        for (guint j = 0; j < G_N_ELEMENTS(expected_keys); j++) {
            if (gwy_strequal(skeys[i], expected_keys[j])) {
                found = TRUE;
                break;
            }
        }
        g_assert_true(found);
    }
    g_assert_true(!skeys[G_N_ELEMENTS(expected_keys)]);
    g_free(skeys);

    g_assert_finalize_object(dict);
}

static void
check_value(GQuark key, GValue *value, gpointer user_data)
{
    GwyDict *reference = (GwyDict*)user_data;
    g_assert_true(gwy_dict_contains(reference, key));

    GValue refvalue = gwy_dict_get_value(reference, key);
    g_assert_true(values_are_equal(value, &refvalue));
    g_value_unset(&refvalue);
}

void
dict_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_DICT(object));
    g_assert_true(GWY_IS_DICT(reference));

    GwyDict *dict = GWY_DICT(object), *ref_dict = GWY_DICT(reference);
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, gwy_dict_get_n_items(ref_dict));
    gwy_dict_foreach(dict, NULL, check_value, ref_dict);
}

static void
compare_values(GQuark key, GValue *value, gpointer user_data)
{
    GwyDict *reference = (GwyDict*)user_data;
    if (!gwy_dict_contains(reference, key)) {
        g_object_set_data(G_OBJECT(reference), "different", GINT_TO_POINTER(TRUE));
        return;
    }

    GValue refvalue = gwy_dict_get_value(reference, key);
    if (!values_are_equal(value, &refvalue))
        g_object_set_data(G_OBJECT(reference), "different", GINT_TO_POINTER(TRUE));
    g_value_unset(&refvalue);
}

gboolean
compare_dicts(GwyDict *dict, GwyDict *reference)
{
    if (gwy_dict_get_n_items(dict) != gwy_dict_get_n_items(reference))
        return FALSE;
    gwy_dict_foreach(dict, NULL, compare_values, reference);
    gboolean equal = !g_object_get_data(G_OBJECT(reference), "different");
    g_object_steal_data(G_OBJECT(reference), "different");
    return equal;
}

static GwyDict*
create_dict_for_serialisation(gboolean with_non_atomic)
{
    GwyDict *dict = gwy_dict_new();
    gwy_dict_set_uchar_by_name(dict, "char", '\xfe');
    gwy_dict_set_boolean_by_name(dict, "bool", TRUE);
    gwy_dict_set_int32_by_name(dict, "int32", -123456);
    gwy_dict_set_int64_by_name(dict, "int64", G_GINT64_CONSTANT(-1234567890));
    gwy_dict_set_string_by_name(dict, "string", "Mud");
    gwy_dict_set_double_by_name(dict, "double", G_E);
    if (with_non_atomic)
        gwy_dict_pass_object_by_name(dict, "unit", gwy_unit_new("uPa"));

    return dict;
}

void
test_dict_copy(void)
{
    GwyDict *dict = create_dict_for_serialisation(TRUE);
    serializable_test_copy(GWY_SERIALIZABLE(dict), dict_assert_equal);
    g_assert_finalize_object(dict);
}

void
test_dict_assign(void)
{
    GwyDict *dict = create_dict_for_serialisation(TRUE);
    serializable_test_assign(GWY_SERIALIZABLE(dict), NULL, dict_assert_equal);
    g_assert_finalize_object(dict);
}

void
test_dict_serialize_simple(void)
{
    GwyDict *dict = create_dict_for_serialisation(TRUE);
    serialize_object_and_back(G_OBJECT(dict), dict_assert_equal, FALSE, NULL);
    g_assert_finalize_object(dict);
}

void
test_dict_serialize_nested(void)
{
    GwyDict *dict = gwy_dict_new();
    GwyDict *dict2 = gwy_dict_new();
    GwyDict *dict3 = gwy_dict_new();
    GwyDict *dict4 = gwy_dict_new();
    gwy_dict_set_string_by_name(dict4, "ssss", "Very deep");
    gwy_dict_set_int32_by_name(dict3, "i3", -123456);
    gwy_dict_set_double_by_name(dict2, "d", G_PI);
    gwy_dict_set_string_by_name(dict, "s", "Mud");
    gwy_dict_pass_object_by_name(dict3, "dict4", dict4);
    gwy_dict_pass_object_by_name(dict2, "dict3", dict3);
    gwy_dict_pass_object_by_name(dict, "dict2", dict2);

    serialize_object_and_back(G_OBJECT(dict), dict_assert_equal, FALSE, NULL);

    g_assert_finalize_object(dict);
}

void
test_dict_serialize_text(void)
{
    GwyDict *dict = create_dict_for_serialisation(FALSE);

    GPtrArray *lines = gwy_dict_serialize_to_text(dict);
    g_assert_nonnull(lines);
    /* Currently the function does not add any sentinel. Add it ourselves. */
    g_ptr_array_add(lines, NULL);
    gchar *text = g_strjoinv("\n", (gchar**)lines->pdata);
    g_ptr_array_free(lines, TRUE);
    GwyDict *copy = gwy_dict_deserialize_from_text(text);
    g_free(text);
    g_assert_true(GWY_IS_DICT(copy));
    g_assert_cmpuint(gwy_dict_get_n_items(dict), ==, 6);

    guint change_count = 0;
    g_signal_connect_swapped(dict, "item-changed", G_CALLBACK(record_signal), &change_count);
    gwy_dict_transfer(copy, dict, "", "", FALSE, TRUE);
    /* Atomic types must be detected as same-value. */
    g_assert_cmpuint(change_count, ==, 0);

    g_assert_finalize_object(copy);
    g_assert_finalize_object(dict);
}

/* This is kind of strange. We need an object to excersise the full serialisation machinery, so put the boxed pointer
 * to a Container and serialise that.
 *
 * It can be seen as a Container boxed serialisation test, but it is run by various boxed types. */
gpointer
serialize_boxed_and_back(gpointer boxed, GType type, gboolean return_reconstructed)
{
    g_assert_true(gwy_boxed_type_is_serializable(type));
    GwyDict *dict = gwy_dict_new();
    gwy_dict_set_boxed_by_name(dict, type, "boxed", boxed);
    GwyDict *copy = (GwyDict*)serialize_object_and_back(G_OBJECT(dict), dict_assert_equal, TRUE, NULL);
    g_assert_true(GWY_IS_DICT(copy));
    g_assert_finalize_object(dict);
    g_assert_cmpuint(gwy_dict_value_type_by_name(copy, "boxed"), ==, type);
    gconstpointer cboxed = gwy_dict_get_boxed_by_name(copy, type, "boxed");
    g_assert_nonnull(cboxed);
    g_assert_true(gwy_serializable_boxed_equal(type, cboxed, boxed));
    gpointer newboxed = g_boxed_copy(type, cboxed);
    g_assert_true(gwy_serializable_boxed_equal(type, newboxed, boxed));
    g_assert_finalize_object(copy);
    if (return_reconstructed)
        return newboxed;
    g_boxed_free(type, newboxed);
    return NULL;
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
