/*
 *  $Id: serialization-utils.c 29750 2026-04-07 16:18:14Z yeti-dn $
 *  Copyright (C) 2009-2025 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 inline gsize
group_size(GwySerializableGroup *group)
{
    gsize n;
    gwy_serializable_group_items(group, &n);
    return n;
}

static inline const GwySerializableItem*
group_item(GwySerializableGroup *group, gsize i)
{
    return gwy_serializable_group_items(group, NULL) + i;
}

void
test_serialization_append_int16(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT16;
    model.name = "int16";
    model.value.v_int16 = 7;

    gwy_serializable_group_append_int16(group, &model, -1);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_int16(group, &model, 7);
    g_assert_cmpuint(group_size(group), ==, 2);

    gwy_serializable_group_append_int16(group, &model, 100);
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpint(group_item(group, 0)->value.v_int16, ==, -1);
    g_assert_cmpint(group_item(group, 1)->value.v_int16, ==, 7);
    g_assert_cmpint(group_item(group, 2)->value.v_int16, ==, 100);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int32(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT32;
    model.name = "int32";
    model.value.v_int32 = 7;

    gwy_serializable_group_append_int32(group, &model, -1);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_int32(group, &model, 7);
    g_assert_cmpuint(group_size(group), ==, 2);

    gwy_serializable_group_append_int32(group, &model, 100);
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpint(group_item(group, 0)->value.v_int32, ==, -1);
    g_assert_cmpint(group_item(group, 1)->value.v_int32, ==, 7);
    g_assert_cmpint(group_item(group, 2)->value.v_int32, ==, 100);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int64(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT64;
    model.name = "int64";
    model.value.v_int64 = G_GINT64_CONSTANT(0x12345678deadbeef);

    gwy_serializable_group_append_int64(group, &model, -1);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_int64(group, &model, G_GINT64_CONSTANT(0x12345678deadbeef));
    g_assert_cmpuint(group_size(group), ==, 2);

    gwy_serializable_group_append_int64(group, &model, G_GINT64_CONSTANT(0x92345678deadbeef));
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpint(group_item(group, 0)->value.v_int64, ==, -1);
    g_assert_cmpint(group_item(group, 1)->value.v_int64, ==, G_GINT64_CONSTANT(0x12345678deadbeef));
    g_assert_cmpint(group_item(group, 2)->value.v_int64, ==, G_GINT64_CONSTANT(0x92345678deadbeef));

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_float(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_FLOAT;
    model.name = "float";
    model.value.v_float = G_PI;

    gwy_serializable_group_append_float(group, &model, -1.0);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_float(group, &model, G_PI);
    g_assert_cmpuint(group_size(group), ==, 2);

    gwy_serializable_group_append_float(group, &model, 100.0);
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpfloat(group_item(group, 0)->value.v_float, ==, -1.0);
    g_assert_cmpfloat(group_item(group, 1)->value.v_float, ==, (gfloat)G_PI);
    g_assert_cmpfloat(group_item(group, 2)->value.v_float, ==, 100.0);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_double(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_DOUBLE;
    model.name = "double";
    model.value.v_double = G_PI;

    gwy_serializable_group_append_double(group, &model, -1.0);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_double(group, &model, G_PI);
    g_assert_cmpuint(group_size(group), ==, 2);

    gwy_serializable_group_append_double(group, &model, 100.0);
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpfloat(group_item(group, 0)->value.v_double, ==, -1.0);
    g_assert_cmpfloat(group_item(group, 1)->value.v_double, ==, G_PI);
    g_assert_cmpfloat(group_item(group, 2)->value.v_double, ==, 100.0);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_string(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_STRING;
    model.name = "string";
    /* As specified, the default is not taken into account for strings (it should generally be NULL). */
    model.value.v_string = g_strdup("Test");

    gchar *s1 = g_strdup("Testy");
    gwy_serializable_group_append_string(group, &model, s1);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 0);

    gwy_serializable_group_append_string(group, &model, NULL);
    g_assert_cmpuint(group_size(group), ==, 1);

    gchar *s3 = g_strdup("");
    gwy_serializable_group_append_string(group, &model, s3);
    g_assert_cmpuint(group_size(group), ==, 2);

    gchar *s4 = g_strdup("Test");
    gwy_serializable_group_append_string(group, &model, s4);
    g_assert_cmpuint(group_size(group), ==, 3);
    g_assert_cmpstr(group_item(group, 0)->value.v_string, ==, "Testy");
    g_assert_cmpstr(group_item(group, 1)->value.v_string, ==, "");
    g_assert_cmpstr(group_item(group, 2)->value.v_string, ==, "Test");

    g_free(s1);
    g_free(s3);
    g_free(s4);
    g_free(model.value.v_string);
    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_unit(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 12);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_OBJECT;
    model.name = "unit";

    GwyUnit *u1 = gwy_unit_new("m");
    gwy_serializable_group_append_unit(group, &model, u1);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_unit(group, &model, NULL);
    g_assert_cmpuint(group_size(group), ==, 1);

    GwyUnit *u3 = gwy_unit_new(NULL);
    gwy_serializable_group_append_unit(group, &model, u3);
    g_assert_cmpuint(group_size(group), ==, 2);

    GwyUnit *u4 = gwy_unit_new("A");
    gwy_serializable_group_append_unit(group, &model, u4);
    g_assert_cmpuint(group_size(group), ==, 3);

    g_assert_true(group_item(group, 0)->value.v_object == (GObject*)u1);
    g_assert_true(group_item(group, 1)->value.v_object == (GObject*)u3);
    g_assert_true(group_item(group, 2)->value.v_object == (GObject*)u4);

    g_object_unref(u1);
    g_object_unref(u3);
    g_object_unref(u4);
    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_rgba(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 12);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_BOXED;
    model.name = "rgba";
    model.aux.boxed_type = GWY_TYPE_RGBA;

    GwyRGBA rgba1 = { 1.0, 0.5, 0.6, 0.4 };
    gwy_serializable_group_append_rgba(group, &model, &rgba1);
    g_assert_cmpuint(group_size(group), ==, 1);

    GwyRGBA rgba2 = { 0.3, 0.1, 0.6, 1.0 };
    gwy_serializable_group_append_rgba(group, &model, &rgba2);
    g_assert_cmpuint(group_size(group), ==, 2);

    g_assert_true(group_item(group, 0)->value.v_boxed == &rgba1);
    g_assert_true(group_item(group, 1)->value.v_boxed == &rgba2);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int8_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT8_ARRAY;
    model.name = "int8array";

    gint8 data1[2] = { 1, 5 };
    gwy_serializable_group_append_int8_array(group, &model, data1, 2);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_int8_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 2);

    gwy_serializable_group_append_int8_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_int8_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int16_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT16_ARRAY;
    model.name = "int16array";

    gint16 data1[2] = { 1, 5 };
    gwy_serializable_group_append_int16_array(group, &model, data1, 2);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_int16_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 2);

    gwy_serializable_group_append_int16_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_int16_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int32_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT32_ARRAY;
    model.name = "int32array";

    gint32 data1[2] = { 1, 5 };
    gwy_serializable_group_append_int32_array(group, &model, data1, 2);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_int32_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 2);

    gwy_serializable_group_append_int32_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_int32_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_int64_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT64_ARRAY;
    model.name = "int64array";

    gint64 data1[2] = { 1, 5 };
    gwy_serializable_group_append_int64_array(group, &model, data1, 2);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_int64_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 2);

    gwy_serializable_group_append_int64_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_int64_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_float_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_FLOAT_ARRAY;
    model.name = "floatarray";

    gfloat data1[3] = { 1.0, 5.0, 1e14 };
    gwy_serializable_group_append_float_array(group, &model, data1, 3);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_float_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 3);

    gwy_serializable_group_append_float_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_float_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_double_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY;
    model.name = "doublearray";

    gdouble data1[3] = { 1.0, 5.0, 1e14 };
    gwy_serializable_group_append_double_array(group, &model, data1, 3);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_double_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 3);

    gwy_serializable_group_append_double_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_double_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_append_string_array(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 3);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_STRING_ARRAY;
    model.name = "stringarray";

    gchar *data1[3] = { g_strdup("a"), g_strdup("A long string"), g_strdup("also here") };
    gwy_serializable_group_append_string_array(group, &model, data1, 3);
    g_assert_cmpuint(group_size(group), ==, 1);
    g_assert_true(group_item(group, 0)->value.v_string_array == data1);
    g_assert_cmpuint(group_item(group, 0)->array_size, ==, 3);

    gwy_serializable_group_append_string_array(group, &model, data1, 0);
    g_assert_cmpuint(group_size(group), ==, 1);

    gwy_serializable_group_append_string_array(group, &model, NULL, 2);
    g_assert_cmpuint(group_size(group), ==, 1);

    for (guint i = 0; i < 3; i++)
        g_free(data1[i]);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_group_discard_single(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 17);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT32;
    model.name = "int32";
    model.value.v_int32 = 7;

    for (gint i = 0; i < 17; i++)
        gwy_serializable_group_append_int32(group, &model, i);
    g_assert_cmpuint(group_size(group), ==, 17);

    gsize indices[] = { 1, 14, 3, 9, 5, 12, 16 };
    gwy_serializable_group_discard(group, indices, G_N_ELEMENTS(indices), TRUE);
    g_assert_cmpuint(group_size(group), ==, 10);
    g_assert_cmpint(group_item(group, 0)->value.v_int32, ==, 0);
    g_assert_cmpint(group_item(group, 1)->value.v_int32, ==, 2);
    g_assert_cmpint(group_item(group, 2)->value.v_int32, ==, 4);
    g_assert_cmpint(group_item(group, 3)->value.v_int32, ==, 6);
    g_assert_cmpint(group_item(group, 4)->value.v_int32, ==, 7);
    g_assert_cmpint(group_item(group, 5)->value.v_int32, ==, 8);
    g_assert_cmpint(group_item(group, 6)->value.v_int32, ==, 10);
    g_assert_cmpint(group_item(group, 7)->value.v_int32, ==, 11);
    g_assert_cmpint(group_item(group, 8)->value.v_int32, ==, 13);
    g_assert_cmpint(group_item(group, 9)->value.v_int32, ==, 15);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_group_discard_all(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 6);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT32;
    model.name = "int32";
    model.value.v_int32 = 3;

    for (gint i = 0; i < 6; i++)
        gwy_serializable_group_append_int32(group, &model, i);
    g_assert_cmpuint(group_size(group), ==, 6);

    gsize indices[] = { 5, 4, 1, 0, 2, 3 };
    gwy_serializable_group_discard(group, indices, G_N_ELEMENTS(indices), TRUE);
    g_assert_cmpuint(group_size(group), ==, 0);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_group_discard_repeated(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 5);
    GwySerializableItem model;
    gwy_clear1(model);
    model.ctype = GWY_SERIALIZABLE_INT32;
    model.name = "int32";
    model.value.v_int32 = 72;

    for (gint i = 0; i < 5; i++)
        gwy_serializable_group_append_int32(group, &model, i);
    g_assert_cmpuint(group_size(group), ==, 5);

    gsize indices[] = { 3, 0, 0, 3, 0, 4, 0, 4, 4, 0, 3 };
    gwy_serializable_group_discard(group, indices, G_N_ELEMENTS(indices), TRUE);
    g_assert_cmpuint(group_size(group), ==, 2);
    g_assert_cmpint(group_item(group, 0)->value.v_int32, ==, 1);
    g_assert_cmpint(group_item(group, 1)->value.v_int32, ==, 2);

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_fill_pspec(void)
{
    enum {
        ITEM_I1, ITEM_I2, ITEM_I3,
        ITEM_ENUM,
        ITEM_D1, ITEM_D2, ITEM_D3,
        ITEM_BOOL1, ITEM_BOOL2, ITEM_BOOL3,
        NUM_ITEMS
    };
    static GwySerializableItem group[NUM_ITEMS] = {
        { .name = NULL,    .ctype = GWY_SERIALIZABLE_INT32,    },
        { .name = "i2",    .ctype = GWY_SERIALIZABLE_INT32,    },
        { .name = "i3",    .ctype = GWY_SERIALIZABLE_INT32,    .value.v_int32 = 11, },
        { .name = "enum",  .ctype = GWY_SERIALIZABLE_INT32,    },
        { .name = NULL,    .ctype = GWY_SERIALIZABLE_DOUBLE,   },
        { .name = "d2",    .ctype = GWY_SERIALIZABLE_DOUBLE,   },
        { .name = "d3",    .ctype = GWY_SERIALIZABLE_DOUBLE,   .value.v_double = 3.75 },
        { .name = NULL,    .ctype = GWY_SERIALIZABLE_BOOLEAN,  },
        { .name = "bool2", .ctype = GWY_SERIALIZABLE_BOOLEAN,  },
        { .name = "bool3", .ctype = GWY_SERIALIZABLE_BOOLEAN,  .value.v_boolean = TRUE, },
    };
    group[ITEM_I1].aux.pspec = g_param_spec_int("i1", NULL, NULL, -5, 5, 2, G_PARAM_STATIC_STRINGS);
    group[ITEM_I2].aux.pspec = g_param_spec_int("i2", NULL, NULL, 100, 200, 140, G_PARAM_STATIC_STRINGS);
    group[ITEM_ENUM].aux.pspec = g_param_spec_enum("enum", NULL, NULL, GWY_TYPE_BYTE_ORDER, GWY_BYTE_ORDER_BIG_ENDIAN,
                                                   G_PARAM_STATIC_STRINGS);
    group[ITEM_D1].aux.pspec = g_param_spec_double("d1", NULL, NULL, 1.0, G_MAXDOUBLE, 2.0, G_PARAM_STATIC_STRINGS);
    group[ITEM_D2].aux.pspec = g_param_spec_double("d2", NULL, NULL, -5.0, 5.0, -4.0, G_PARAM_STATIC_STRINGS);
    group[ITEM_BOOL1].aux.pspec = g_param_spec_boolean("bool1", NULL, NULL, TRUE, G_PARAM_STATIC_STRINGS);
    group[ITEM_BOOL2].aux.pspec = g_param_spec_boolean("bool2", NULL, NULL, FALSE, G_PARAM_STATIC_STRINGS);
    gwy_fill_serializable_defaults_pspec(group, NUM_ITEMS, FALSE);

    g_assert_cmpstr(group[ITEM_I1].name, ==, "i1");
    g_assert_cmpstr(group[ITEM_I2].name, ==, "i2");
    g_assert_cmpstr(group[ITEM_I3].name, ==, "i3");
    g_assert_cmpstr(group[ITEM_ENUM].name, ==, "enum");
    g_assert_cmpstr(group[ITEM_D1].name, ==, "d1");
    g_assert_cmpstr(group[ITEM_D2].name, ==, "d2");
    g_assert_cmpstr(group[ITEM_D3].name, ==, "d3");
    g_assert_cmpstr(group[ITEM_BOOL1].name, ==, "bool1");
    g_assert_cmpstr(group[ITEM_BOOL2].name, ==, "bool2");
    g_assert_cmpstr(group[ITEM_BOOL3].name, ==, "bool3");

    g_assert_cmpint(group[ITEM_I1].value.v_int32, ==, 2);
    g_assert_cmpint(group[ITEM_I2].value.v_int32, ==, 140);
    g_assert_cmpint(group[ITEM_I3].value.v_int32, ==, 11);
    g_assert_cmpint(group[ITEM_ENUM].value.v_int32, ==, GWY_BYTE_ORDER_BIG_ENDIAN);
    g_assert_cmpfloat(group[ITEM_D1].value.v_double, ==, 2.0);
    g_assert_cmpfloat(group[ITEM_D2].value.v_double, ==, -4.0);
    g_assert_cmpfloat(group[ITEM_D3].value.v_double, ==, 3.75);
    g_assert_true(group[ITEM_BOOL1].value.v_boolean);
    g_assert_false(group[ITEM_BOOL2].value.v_boolean);
    g_assert_true(group[ITEM_BOOL3].value.v_boolean);
}

void
test_serialization_filter_items_normal(void)
{
    enum { ITEM_FLAG, ITEM_VALUE, ITEM_SPAM, NUM_ITEMS };
    GwySerializableItem tmplt[NUM_ITEMS], items[NUM_ITEMS];

    gwy_clear(tmplt, NUM_ITEMS);
    tmplt[ITEM_FLAG].name = "flag";
    tmplt[ITEM_FLAG].ctype = GWY_SERIALIZABLE_BOOLEAN;
    tmplt[ITEM_VALUE].name = "value";
    tmplt[ITEM_VALUE].ctype = GWY_SERIALIZABLE_INT32;
    tmplt[ITEM_SPAM].name = "spam";
    tmplt[ITEM_SPAM].ctype = GWY_SERIALIZABLE_DOUBLE;

    gwy_assign(items, tmplt, NUM_ITEMS);
    items[ITEM_FLAG].value.v_boolean = TRUE;
    items[ITEM_VALUE].value.v_uint32 = 0xdeadbeef;
    items[ITEM_SPAM].value.v_double = G_MINDOUBLE;

    GwySerializableGroup *group = gwy_serializable_group_new("Group", NUM_ITEMS);
    gwy_serializable_group_append(group, items, NUM_ITEMS);

    for (guint i = 0; i < NUM_ITEMS; i++) {
        GwyErrorList *error_list = NULL;
        GwySerializableItem to_be_filled[1];
        to_be_filled[0] = tmplt[i];
        /* Note that we recycle group[] so in each iteration one item is replaced with GWY_SERIALIZABLE_CONSUMED. */
        gwy_deserialize_filter_items(to_be_filled, 1, group, "Normal", &error_list);
        /* No errors are expected. Extra group are OK until we are done. Filtering does not report extra group. */
        assert_error_list(error_list, NULL, FALSE);
        gwy_error_list_clear(&error_list);

        if (i == ITEM_FLAG)
            g_assert_cmpuint(to_be_filled[0].value.v_boolean, ==, TRUE);
        else if (i == ITEM_VALUE)
            g_assert_cmpuint(to_be_filled[0].value.v_uint32, ==, 0xdeadbeef);
        else if (i == ITEM_SPAM)
            g_assert_cmpfloat(to_be_filled[0].value.v_double, ==, G_MINDOUBLE);
    }

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_filter_items_repeating(void)
{
    enum { ITEM_FLOAT, ITEM_VALUE, NUM_ITEMS };
    GwySerializableItem tmplt[NUM_ITEMS];

    gwy_clear(tmplt, NUM_ITEMS);
    tmplt[ITEM_FLOAT].name = "float";
    tmplt[ITEM_FLOAT].ctype = GWY_SERIALIZABLE_DOUBLE;
    tmplt[ITEM_VALUE].name = "value";
    tmplt[ITEM_VALUE].ctype = GWY_SERIALIZABLE_INT32;

    for (guint i = 0; i < 8; i++) {
        /* Try all 8 combinations how we can combine the two items into a three-item array. */
        GwySerializableItem items[3];
        guint which[G_N_ELEMENTS(items)];
        for (guint j = 0; j < G_N_ELEMENTS(items); j++)
            which[j] = !!(i & (1 << j));

        gboolean counts[NUM_ITEMS] = { 0, 0 };
        gdouble first_float = 0.0;
        gint first_int = 0;
        GwyErrorList *expected_errors = NULL;
        for (guint j = 0; j < G_N_ELEMENTS(items); j++) {
            guint idx = which[j];
            items[j] = tmplt[idx];
            if (idx == ITEM_FLOAT) {
                items[j].value.v_double = i + 0.25;
                if (!counts[idx])
                    first_float = items[j].value.v_double;
            }
            else if (idx == ITEM_VALUE) {
                items[j].value.v_int32 = 42 - i;
                if (!counts[idx])
                    first_int = items[j].value.v_int32;
            }
            else {
                g_assert_not_reached();
            }
            counts[idx]++;
            /* The first repetition we create for each item should be caught by gwy_deserialize_filter_items(). */
            if (counts[idx] == 2) {
                gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                                   "Item ‘%s’ of type 0x%02x is present multiple times in the representation "
                                   "of object ‘%s’. Values other than the first were ignored.",
                                   items[j].name, items[j].ctype, "Repeating");
            }
        }

        GwySerializableGroup *group = gwy_serializable_group_new("Group", G_N_ELEMENTS(items));
        gwy_serializable_group_append(group, items, G_N_ELEMENTS(items));

        GwySerializableItem to_be_filled[NUM_ITEMS];
        gwy_assign(to_be_filled, tmplt, NUM_ITEMS);

        GwyErrorList *error_list = NULL;
        gwy_deserialize_filter_items(to_be_filled, NUM_ITEMS, group, "Repeating", &error_list);
        assert_error_list(error_list, expected_errors, FALSE);
        gwy_error_list_clear(&error_list);
        gwy_error_list_clear(&expected_errors);

        /* The obtained value should be the first given. */
        if (counts[ITEM_FLOAT])
            g_assert_cmpfloat(to_be_filled[ITEM_FLOAT].value.v_double, ==, first_float);
        if (counts[ITEM_VALUE])
            g_assert_cmpint(to_be_filled[ITEM_VALUE].value.v_int32, ==, first_int);

        gwy_serializable_group_free(group, FALSE, NULL);
    }
}

void
test_serialization_filter_items_bad_type(void)
{
    enum { N = 1 };
    GwySerializableItem tmplt[N], items[N], to_be_filled[N];

    gwy_clear(tmplt, N);
    tmplt[0].name = "value";
    tmplt[0].ctype = GWY_SERIALIZABLE_INT32;

    gwy_assign(items, tmplt, N);
    items[0].value.v_uint32 = 0xdeadbeef;
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 1);
    gwy_serializable_group_append(group, items, 1);

    gwy_assign(to_be_filled, tmplt, N);
    to_be_filled[0].ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY;

    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    gwy_deserialize_filter_items(to_be_filled, 1, group, "BadType", &error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                       "Item ‘%s’ in the representation of object ‘%s’ has type 0x%02x instead of expected 0x%02x. "
                       "It was ignored.",
                       tmplt[0].name, "BadType", tmplt[0].ctype, to_be_filled[0].ctype);
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);
    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_filter_items_pspec_good(void)
{
    enum { ITEM_BOOL, ITEM_INT32, ITEM_INT64, ITEM_DOUBLE, ITEM_ENUM, NUM_ITEMS };
    GwySerializableItem tmplt[NUM_ITEMS], items[NUM_ITEMS];

    gwy_clear(tmplt, NUM_ITEMS);
    tmplt[ITEM_BOOL].ctype = GWY_SERIALIZABLE_BOOLEAN;
    tmplt[ITEM_BOOL].aux.pspec = g_param_spec_boolean("bool", NULL, NULL, TRUE, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_INT32].ctype = GWY_SERIALIZABLE_INT32;
    tmplt[ITEM_INT32].aux.pspec = g_param_spec_int("int32", NULL, NULL, -10, 10, 1, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_INT64].ctype = GWY_SERIALIZABLE_INT64;
    tmplt[ITEM_INT64].aux.pspec = g_param_spec_int64("int64", NULL, NULL, 100, 1000, 200, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_DOUBLE].ctype = GWY_SERIALIZABLE_DOUBLE;
    tmplt[ITEM_DOUBLE].aux.pspec = g_param_spec_double("double", NULL, NULL, 0.0, 6.0, 2.0, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_ENUM].ctype = GWY_SERIALIZABLE_INT32;
    tmplt[ITEM_ENUM].aux.pspec = g_param_spec_enum("enum", NULL, NULL, GWY_TYPE_BYTE_ORDER, GWY_BYTE_ORDER_BIG_ENDIAN,
                                                   G_PARAM_STATIC_STRINGS);
    gwy_fill_serializable_defaults_pspec(tmplt, NUM_ITEMS, FALSE);

    gwy_assign(items, tmplt, NUM_ITEMS);
    items[ITEM_BOOL].value.v_boolean = FALSE;
    items[ITEM_INT32].value.v_int32 = -3;
    items[ITEM_INT64].value.v_int64 = 999;
    items[ITEM_DOUBLE].value.v_double = 0.25;
    items[ITEM_ENUM].value.v_int32 = GWY_BYTE_ORDER_NATIVE;
    GwySerializableGroup *group = gwy_serializable_group_new("Group", NUM_ITEMS);
    gwy_serializable_group_append(group, items, NUM_ITEMS);

    for (guint i = 0; i < NUM_ITEMS; i++) {
        GwyErrorList *error_list = NULL;
        GwySerializableItem to_be_filled[1];
        to_be_filled[0] = tmplt[i];
        /* Note that we recycle group so in each iteration one item is replaced with GWY_SERIALIZABLE_CONSUMED. */
        gwy_deserialize_filter_items(to_be_filled, 1, group, "Normal", &error_list);
        /* No errors are expected. Extra group are OK until we are done. Filtering does not report extra group. */
        assert_error_list(error_list, NULL, FALSE);
        gwy_error_list_clear(&error_list);

        if (i == ITEM_BOOL)
            g_assert_cmpuint(to_be_filled[0].value.v_boolean, ==, FALSE);
        else if (i == ITEM_INT32)
            g_assert_cmpuint(to_be_filled[0].value.v_int32, ==, -3);
        else if (i == ITEM_INT64)
            g_assert_cmpuint(to_be_filled[0].value.v_int64, ==, 999);
        else if (i == ITEM_DOUBLE)
            g_assert_cmpfloat(to_be_filled[0].value.v_double, ==, 0.25);
        else if (i == ITEM_ENUM)
            g_assert_cmpuint(to_be_filled[0].value.v_int32, ==, GWY_BYTE_ORDER_NATIVE);
    }

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_filter_items_pspec_bad(void)
{
    enum { ITEM_INT32, ITEM_INT64, ITEM_DOUBLE, ITEM_ENUM, NUM_ITEMS };
    GwySerializableItem tmplt[NUM_ITEMS], items[NUM_ITEMS];

    gwy_clear(tmplt, NUM_ITEMS);
    tmplt[ITEM_INT32].ctype = GWY_SERIALIZABLE_INT32;
    tmplt[ITEM_INT32].aux.pspec = g_param_spec_int("int32", NULL, NULL, -10, 10, 1, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_INT64].ctype = GWY_SERIALIZABLE_INT64;
    tmplt[ITEM_INT64].aux.pspec = g_param_spec_int64("int64", NULL, NULL, 100, 1000, 200, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_DOUBLE].ctype = GWY_SERIALIZABLE_DOUBLE;
    tmplt[ITEM_DOUBLE].aux.pspec = g_param_spec_double("double", NULL, NULL, 0.0, 6.0, 2.0, G_PARAM_STATIC_STRINGS);
    tmplt[ITEM_ENUM].ctype = GWY_SERIALIZABLE_INT32;
    tmplt[ITEM_ENUM].aux.pspec = g_param_spec_enum("enum", NULL, NULL, GWY_TYPE_BYTE_ORDER, GWY_BYTE_ORDER_BIG_ENDIAN,
                                                   G_PARAM_STATIC_STRINGS);
    gwy_fill_serializable_defaults_pspec(tmplt, NUM_ITEMS, FALSE);

    gwy_assign(items, tmplt, NUM_ITEMS);
    items[ITEM_INT32].value.v_int32 = -11;
    items[ITEM_INT64].value.v_int64 = 1001;
    items[ITEM_DOUBLE].value.v_double = G_MAXDOUBLE;
    items[ITEM_ENUM].value.v_int32 = -13;
    GwySerializableGroup *group = gwy_serializable_group_new("Group", NUM_ITEMS);
    gwy_serializable_group_append(group, items, NUM_ITEMS);

    for (guint i = 0; i < NUM_ITEMS; i++) {
        GwyErrorList *error_list = NULL;
        GwySerializableItem to_be_filled[1];
        to_be_filled[0] = tmplt[i];
        /* Note that we recycle group[] so in each iteration one item is replaced with GWY_SERIALIZABLE_CONSUMED. */
        gwy_deserialize_filter_items(to_be_filled, 1, group, "Normal", &error_list);
        /* Each error is different, depending on the type. Just check that we are getting REPLACED errors and the
         * invalid values are ignored. */
        g_assert_nonnull(error_list);
        GError *error = error_list->data;
        g_assert_cmpint(error->domain, ==, GWY_DESERIALIZE_ERROR);
        g_assert_cmpint(error->code, ==, GWY_DESERIALIZE_ERROR_REPLACED);
        gwy_error_list_clear(&error_list);

        if (i == ITEM_INT32)
            g_assert_cmpuint(to_be_filled[0].value.v_int32, ==, 1);
        else if (i == ITEM_INT64)
            g_assert_cmpuint(to_be_filled[0].value.v_int64, ==, 200);
        else if (i == ITEM_DOUBLE)
            g_assert_cmpuint(to_be_filled[0].value.v_double, ==, 2.0);
        else if (i == ITEM_ENUM)
            g_assert_cmpuint(to_be_filled[0].value.v_int32, ==, GWY_BYTE_ORDER_BIG_ENDIAN);
    }

    gwy_serializable_group_free(group, FALSE, NULL);
}

/* The silly case. */
void
test_serialization_check_dimensions_0d(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    g_assert_true(gwy_check_data_dimension(&error_list, "Scalar", 0, 0, NULL));
    assert_error_list(error_list, expected_errors, FALSE);

    g_assert_true(gwy_check_data_dimension(&error_list, "Scalar", 0, 1, NULL));
    assert_error_list(error_list, expected_errors, FALSE);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                       "Dimension %u of ‘%s’ do not match expected total size %" G_GSIZE_FORMAT ".",
                       1, "NonScalar", (gsize)2);
    g_assert_false(gwy_check_data_dimension(&error_list, "NonScalar", 0, 2, NULL));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_check_dimensions_1d(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u of ‘%s’ is invalid.", 0, "Empty1D");
    gsize logical_dim0[1] = { 0 };
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty1D", 1, 0, logical_dim0));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim1[1] = { 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Small1D", 1, 0, logical_dim1));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dimbig[1] = { G_MAXUINT };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big1D", 1, 0, logical_dimbig));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim42[1] = { 42 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Normal1D", 1, 0, logical_dim42));
    assert_error_list(error_list, expected_errors, FALSE);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                       "Dimension %u of ‘%s’ do not match expected total size %" G_GSIZE_FORMAT ".",
                       42, "Different1D", (gsize)43);
    g_assert_false(gwy_check_data_dimension(&error_list, "Different1D", 1, 43, logical_dim42));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_check_dimensions_2d(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u of ‘%s’ is invalid.", 0, 0, "Empty2D");
    gsize logical_dim00[2] = { 0, 0 };
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty2D", 2, 0, logical_dim00));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u of ‘%s’ is invalid.", 0, 1, "Empty2D");
    gsize logical_dim01[2] = { 0, 1 };
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty2D", 2, 0, logical_dim01));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u of ‘%s’ is invalid.", 1, 0, "Empty2D");
    gsize logical_dim10[2] = { 1, 0 };
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty2D", 2, 0, logical_dim10));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim11[2] = { 1, 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Small2D", 2, 0, logical_dim11));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim1big[2] = { 1, G_MAXUINT };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big2D", 2, 0, logical_dim1big));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dimbig1[2] = { G_MAXUINT, 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big2D", 2, 0, logical_dimbig1));
    assert_error_list(error_list, expected_errors, FALSE);

    /* XXX: This is not expected to not fail because the upper total size limit is G_MAXSIZE so we are OK in this
     * regard. However, is this what the function should actually do? Someone is surely trying something… */
    gsize logical_dimbigbig[2] = { G_MAXUINT, G_MAXUINT };
    g_assert_true(gwy_check_data_dimension(&error_list, "VeryBig2D", 2, 0, logical_dimbigbig));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim23[2] = { 2, 3 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Normal2D", 2, 6, logical_dim23));
    assert_error_list(error_list, expected_errors, FALSE);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                       "Dimension %u×%u of ‘%s’ do not match expected total size %" G_GSIZE_FORMAT ".",
                       2, 3, "Different2D", (gsize)7);
    g_assert_false(gwy_check_data_dimension(&error_list, "Different2D", 2, 7, logical_dim23));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_check_dimensions_3d(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", 0, 0, 0, "Empty3D");
    gsize logical_dim000[3] = { 0, 0, 0 };
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty3D", 3, 0, logical_dim000));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim110[3] = { 1, 1, 0 };
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", 1, 1, 0, "Empty3D");
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty3D", 3, 0, logical_dim110));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim101[3] = { 1, 0, 1 };
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", 1, 0, 1, "Empty3D");
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty3D", 3, 0, logical_dim101));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim011[3] = { 0, 1, 1 };
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", 0, 1, 1, "Empty3D");
    g_assert_false(gwy_check_data_dimension(&error_list, "Empty3D", 3, 0, logical_dim011));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gsize logical_dim111[3] = { 1, 1, 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Small3D", 3, 0, logical_dim111));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim11big[3] = { 1, 1, G_MAXUINT };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big3D", 3, 0, logical_dim11big));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim1big1[3] = { 1, G_MAXUINT, 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big3D", 3, 0, logical_dim1big1));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dimbig11[3] = { G_MAXUINT, 1, 1 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Big3D", 3, 0, logical_dimbig11));
    assert_error_list(error_list, expected_errors, FALSE);

    gsize logical_dim235[3] = { 2, 3, 5 };
    g_assert_true(gwy_check_data_dimension(&error_list, "Normal3D", 3, 30, logical_dim235));
    assert_error_list(error_list, expected_errors, FALSE);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", G_MAXUINT, G_MAXUINT, G_MAXUINT, "TooBig3D");
    gsize logical_dimbigbigbig[3] = { G_MAXUINT, G_MAXUINT, G_MAXUINT };
    g_assert_false(gwy_check_data_dimension(&error_list, "TooBig3D", 3, 0, logical_dimbigbigbig));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Dimension %u×%u×%u of ‘%s’ is invalid.", G_MAXUINT, G_MAXUINT, 2, "TooBig3D");
    gsize logical_dimbigbig2[3] = { G_MAXUINT, G_MAXUINT, 2 };
    g_assert_false(gwy_check_data_dimension(&error_list, "TooBig3D", 3, 0, logical_dimbigbig2));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                       "Dimension %u×%u×%u of ‘%s’ do not match expected total size %" G_GSIZE_FORMAT ".",
                       2, 3, 5, "Different3D", (gsize)31);
    g_assert_false(gwy_check_data_dimension(&error_list, "Different3D", 3, 31, logical_dim235));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_double_component_normal(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    GwySerializableItem item;
    gwy_clear1(item);

    item.name = "dbl";
    item.ctype = GWY_SERIALIZABLE_DOUBLE;
    item.value.v_double = 1.0;
    g_assert_true(gwy_check_double_component(&item, "Test", -1.0, 1.0, &error_list));

    item.value.v_double = 0.0;
    g_assert_true(gwy_check_double_component(&item, "Test", -1.0, 1.0, &error_list));

    item.value.v_double = 1.0;
    g_assert_true(gwy_check_double_component(&item, "Test", 0.0, G_MAXDOUBLE, &error_list));

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       "Component ‘%s’ of object %s has invalid value %g.",
                       "dbl", "Test", 0.0);
    item.value.v_double = 0.0;
    g_assert_false(gwy_check_double_component(&item, "Test", G_MINDOUBLE, G_MAXDOUBLE, &error_list));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       "Component ‘%s’ of object %s has invalid value %g.",
                       "dbl", "Test", -G_MINDOUBLE);
    item.value.v_double = -G_MINDOUBLE;
    g_assert_false(gwy_check_double_component(&item, "Test", G_MINDOUBLE, G_MAXDOUBLE, &error_list));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_double_component_infnan(void)
{
    GwyErrorList *error_list = NULL;

    GwySerializableItem item;
    gwy_clear1(item);
    item.name = "dbl";
    item.ctype = GWY_SERIALIZABLE_DOUBLE;

    GDoubleIEEE754 dbl;

    /* XXX: Checking the error message would mean trusting we always get exactly the same formatting of the bad
     * values. */
    dbl.v_double = 1.0;
    dbl.mpn.biased_exponent = 0x7ff;
    item.value.v_double = dbl.v_double;
    g_assert_false(gwy_check_double_component(&item, "Test", -G_MAXDOUBLE, G_MAXDOUBLE, &error_list));
    gwy_error_list_clear(&error_list);

    dbl.v_double = 0.0;
    dbl.mpn.biased_exponent = 0x7ff;
    item.value.v_double = dbl.v_double;
    g_assert_false(gwy_check_double_component(&item, "Test", -G_MAXDOUBLE, G_MAXDOUBLE, &error_list));
    gwy_error_list_clear(&error_list);

    gwy_clear1(dbl);
    dbl.mpn.biased_exponent = 0x7ff;
    item.value.v_double = dbl.v_double;
    g_assert_false(gwy_check_double_component(&item, "Test", -G_MAXDOUBLE, G_MAXDOUBLE, &error_list));
    gwy_error_list_clear(&error_list);
}

void
test_serialization_int32_component(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    GwySerializableItem item;
    gwy_clear1(item);

    item.name = "int";
    item.ctype = GWY_SERIALIZABLE_INT32;
    item.value.v_int32 = 1;
    g_assert_true(gwy_check_int32_component(&item, "Test", 0, 1, &error_list));

    item.value.v_int32 = 0;
    g_assert_true(gwy_check_int32_component(&item, "Test", G_MININT32, 0, &error_list));

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       "Component ‘%s’ of object %s has invalid value %d.",
                       "int", "Test", 0);
    item.value.v_int32 = 0;
    g_assert_false(gwy_check_int32_component(&item, "Test", 1, G_MAXINT32, &error_list));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       "Component ‘%s’ of object %s has invalid value %d.",
                       "int", "Test", G_MININT32);
    item.value.v_int32 = G_MININT32;
    g_assert_false(gwy_check_int32_component(&item, "Test", G_MININT32+1, G_MAXINT32, &error_list));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_enum_component(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    GwySerializableItem item;
    gwy_clear1(item);

    item.name = "enum";
    item.ctype = GWY_SERIALIZABLE_INT32;
    item.value.v_int32 = GWY_BYTE_ORDER_IMPLICIT;
    g_assert_true(gwy_check_enum_component(&item, "Test", GWY_TYPE_BYTE_ORDER, &error_list));

    item.value.v_int32 = GWY_BYTE_ORDER_NATIVE;
    g_assert_true(gwy_check_enum_component(&item, "Test", GWY_TYPE_BYTE_ORDER, &error_list));

    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       "Component ‘%s’ of object %s has invalid value %d.",
                       "enum", "Test", 1);
    item.value.v_int32 = 1;
    g_assert_false(gwy_check_enum_component(&item, "Test", GWY_TYPE_BYTE_ORDER, &error_list));
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&expected_errors);
    gwy_error_list_clear(&error_list);
}

void
test_serialization_object_component_null(void)
{
    GwyErrorList *error_list = NULL;

    GwySerializableItem item;
    gwy_clear1(item);
    item.name = "null_object";
    item.ctype = GWY_SERIALIZABLE_OBJECT;

    /* The functions are expected to return FALSE but set no errors. */
    g_assert_false(gwy_check_object_component(&item, "Test", GWY_TYPE_SER_TEST, &error_list));
    assert_error_list(error_list, NULL, FALSE);

    item.name = "null_object_array";
    item.ctype = GWY_SERIALIZABLE_OBJECT_ARRAY;
    g_assert_false(gwy_check_object_component(&item, "Test", GWY_TYPE_SER_TEST, &error_list));
    assert_error_list(error_list, NULL, FALSE);
}

void
test_serialization_object_component_single(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    GwySerializableItem item;
    gwy_clear1(item);
    item.name = "object";
    item.ctype = GWY_SERIALIZABLE_OBJECT;
    item.value.v_object = g_object_new(GWY_TYPE_SER_TEST, NULL);

    g_assert_true(gwy_check_object_component(&item, "Test", GWY_TYPE_SER_TEST, &error_list));
    assert_error_list(error_list, NULL, FALSE);

    g_assert_false(gwy_check_object_component(&item, "Test", GWY_TYPE_STRING_LIST, &error_list));
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Component ‘%s’ of object %s is of type %s instead of %s.",
                       "object", "Test", "GwySerTest", "GwyStringList");
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    g_object_unref(item.value.v_object);
}

void
test_serialization_object_component_array(void)
{
    GwyErrorList *error_list = NULL, *expected_errors = NULL;

    GObject *objects[2];

    GwySerializableItem item;
    gwy_clear1(item);
    item.name = "objects";
    item.ctype = GWY_SERIALIZABLE_OBJECT_ARRAY;
    item.value.v_object_array = objects;
    item.array_size = 2;

    objects[0] = g_object_new(GWY_TYPE_SER_TEST, NULL);
    objects[1] = g_object_new(GWY_TYPE_SER_TEST, NULL);

    g_assert_true(gwy_check_object_component(&item, "Test", GWY_TYPE_SER_TEST, &error_list));
    assert_error_list(error_list, NULL, FALSE);

    g_assert_false(gwy_check_object_component(&item, "Test", GWY_TYPE_STRING_LIST, &error_list));
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Object %lu in component ‘%s’ of object %s is of type %s instead of %s.",
                       (gulong)0, "objects", "Test", "GwySerTest", "GwyStringList");
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    g_object_unref(objects[0]);
    objects[0] = G_OBJECT(gwy_string_list_new());

    g_assert_false(gwy_check_object_component(&item, "Test", GWY_TYPE_STRING_LIST, &error_list));
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       "Object %lu in component ‘%s’ of object %s is of type %s instead of %s.",
                       (gulong)1, "objects", "Test", "GwySerTest", "GwyStringList");
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    g_object_unref(objects[0]);
    g_object_unref(objects[1]);
}

void
test_serialization_group_grow_one(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 0);
    gchar name[2];
    GwySerializableItem item;

    gwy_clear1(item);
    item.name = name;
    item.ctype = GWY_SERIALIZABLE_INT32;
    name[1] = '\0';

    gsize n = 0;
    for (gchar c = 'A'; c <= 'z'; c++) {
        if (!g_ascii_isalpha(c))
            continue;

        gwy_serializable_group_append_int32(group, &item, c);
        n++;
        gsize group_n;
        const GwySerializableItem *group_items = gwy_serializable_group_items(group, &group_n);
        g_assert_nonnull(group_items);
        g_assert_cmpuint(group_n, ==, n);
    }

    gwy_serializable_group_free(group, FALSE, NULL);
}

void
test_serialization_group_grow_many(void)
{
    GwySerializableGroup *group = gwy_serializable_group_new("Group", 0);
    enum { NUM_ITEMS = 12 };
    GwySerializableItem items[NUM_ITEMS];

    gwy_clear(items, NUM_ITEMS);
    for (guint i = 0; i < NUM_ITEMS; i++) {
        items[i].name = g_strdup("...");
        items[i].ctype = GWY_SERIALIZABLE_INT32;
    }

    gsize n = 0;
    for (gsize i = 0; i < 30; i++) {
        for (guint j = 0; j < NUM_ITEMS; j++) {
            gchar *name = (gchar*)items[j].name;
            name[0] = 'a' + (n*NUM_ITEMS + 4*i + j) % 26;
            name[1] = 'a' + (n*NUM_ITEMS + 4*i + j+1) % 26;
            name[2] = 'a' + (n*NUM_ITEMS + 4*i + j+2) % 26;
        }
        gsize n_items = i % (NUM_ITEMS + 1);

        gwy_serializable_group_append(group, items, n_items);
        n += n_items;
        gsize group_n;
        const GwySerializableItem *group_items = gwy_serializable_group_items(group, &group_n);
        if (n)
            g_assert_nonnull(group_items);
        else
            g_assert_null(group_items);
        g_assert_cmpuint(group_n, ==, n);
    }

    gwy_serializable_group_free(group, FALSE, NULL);

    for (guint i = 0; i < NUM_ITEMS; i++)
        g_free((gpointer)items[i].name);
}

/* 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 : */
