Edit

kc3-lang/libxkbcommon/test/registry.c

Branch :

  • Show log

    Commit

  • Author : Pierre Le Marre
    Date : 2025-02-13 16:57:46
    Hash : e1892266
    Message : clang-tidy: Miscellaneous fixes

  • test/registry.c
  • /*
     * Copyright © 2020 Red Hat, Inc.
     * SPDX-License-Identifier: MIT
     */
    
    #include "config.h"
    
    #include <assert.h>
    #if HAVE_UNISTD_H
    #include <unistd.h>
    #endif
    #include <stdarg.h>
    #include <stdio.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <libxml/parser.h>
    #include <libxml/tree.h>
    
    #include "xkbcommon/xkbregistry.h"
    
    #include "utils.h"
    #include "test.h"
    
    #define NO_VARIANT NULL
    
    enum {
        MODEL = 78,
        LAYOUT,
        VARIANT,
        OPTION,
    };
    
    enum popularity {
        POPULARITY_UNDEFINED = 0, /* Not member of enum rxkb_popularity */
        POPULARITY_STANDARD = RXKB_POPULARITY_STANDARD,
        POPULARITY_EXOTIC = RXKB_POPULARITY_EXOTIC,
    };
    
    struct test_model {
        const char *name; /* required */
        const char *vendor;
        const char *description;
    };
    
    struct test_layout {
        const char *name; /* required */
        const char *variant;
        const char *brief;
        const char *description;
        const char *iso639[3];  /* language list (iso639 three letter codes), 3 is enough for our test  */
        const char *iso3166[3]; /* country list (iso3166 two letter codes), 3 is enough for our tests */
        enum popularity popularity;
    };
    
    struct test_option {
        const char *name;
        const char *description;
    };
    
    struct test_option_group {
        const char *name;
        const char *description;
        bool allow_multiple_selection;
    
        struct test_option options[10];
    };
    
    static const char *
    popularity_attr(enum popularity popularity)
    {
        switch (popularity) {
            case POPULARITY_UNDEFINED:
                return "";
            case RXKB_POPULARITY_STANDARD:
                return " popularity=\"standard\"";
            case RXKB_POPULARITY_EXOTIC:
                return " popularity=\"exotic\"";
            default:
                fprintf(stderr, "ERROR: unsupported popularity: %d\n", popularity);
                assert(false);
        }
    }
    
    static void
    fprint_config_item(FILE *fp,
                       const char *name,
                       const char *vendor,
                       const char *brief,
                       const char *description,
                       const char * const iso639[3],
                       const char * const iso3166[3],
                       enum popularity popularity)
    {
        fprintf(fp, "  <configItem%s>\n"
                    "    <name>%s</name>\n", popularity_attr(popularity), name);
        if (brief)
            fprintf(fp, "    <shortDescription>%s</shortDescription>\n", brief);
        if (description)
            fprintf(fp, "    <description>%s</description>\n", description);
        if (vendor)
            fprintf(fp, "    <vendor>%s</vendor>\n", vendor);
        if (iso3166 && iso3166[0]) {
            fprintf(fp, "    <countryList>\n");
            for (int i = 0; i < 3; i++) {
                const char *iso = iso3166[i];
                if (!iso)
                    break;
                fprintf(fp, "        <iso3166Id>%s</iso3166Id>\n", iso);
            }
            fprintf(fp, "    </countryList>\n");
        }
        if (iso639 && iso639[0]) {
            fprintf(fp, "    <languageList>\n");
            for (int i = 0; i < 3; i++) {
                const char *iso = iso639[i];
                if (!iso)
                    break;
                fprintf(fp, "        <iso639Id>%s</iso639Id>\n", iso);
            }
            fprintf(fp, "    </languageList>\n");
        }
    
        fprintf(fp, "  </configItem>\n");
    }
    
    /**
     * Create a directory populated with a rules/<ruleset>.xml that contains the
     * given items.
     *
     * @return the XKB base directory
     */
    static char *
    test_create_rules(const char *ruleset,
                      const struct test_model *test_models,
                      const struct test_layout *test_layouts,
                      const struct test_option_group *test_groups)
    {
        static int iteration;
        char *tmpdir;
        char buf[PATH_MAX];
        int rc;
        FILE *fp;
    
        char *template = asprintf_safe("%s.%d.XXXXXX", ruleset, iteration++);
        assert(template != NULL);
        tmpdir = test_maketempdir(template);
        free(template);
    
        free(test_makedir(tmpdir, "rules"));
    
        rc = snprintf_safe(buf, sizeof(buf), "%s/rules/%s.xml", tmpdir, ruleset);
        assert(rc);
    
        fp = fopen(buf, "w");
        assert(fp);
    
        fprintf(fp,
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                "<!DOCTYPE xkbConfigRegistry SYSTEM \"xkb.dtd\">\n"
                "<xkbConfigRegistry version=\"1.1\">\n");
    
        if (test_models) {
            fprintf(fp, "<modelList>\n");
    
            for (const struct test_model *m = test_models; m->name; m++) {
                fprintf(fp, "<model>\n");
                fprint_config_item(fp, m->name, m->vendor, NULL, m->description,
                                   NULL, NULL, POPULARITY_UNDEFINED);
                fprintf(fp, "</model>\n");
            }
            fprintf(fp, "</modelList>\n");
        }
    
        if (test_layouts) {
            const struct test_layout *l, *next;
    
            fprintf(fp, "<layoutList>\n");
    
            l = test_layouts;
            next = l + 1;
    
            assert(l->variant == NULL);
    
            while (l->name) {
                fprintf(fp, "<layout>\n");
                fprint_config_item(fp, l->name, NULL, l->brief, l->description, l->iso639, l->iso3166, l->popularity);
    
                if (next->name && streq(next->name, l->name)) {
                    fprintf(fp, "<variantList>\n");
                    do {
                        fprintf(fp, "<variant>\n");
                        fprint_config_item(fp, next->variant, NULL, next->brief, next->description, next->iso639, next->iso3166, next->popularity);
                        fprintf(fp, "</variant>\n");
                        l = next;
                        next++;
                    } while (next->name && streq(next->name, l->name));
                    fprintf(fp, "</variantList>\n");
                }
                fprintf(fp, "</layout>\n");
                l++;
                next = l + 1;
            }
            fprintf(fp, "</layoutList>\n");
        }
    
        if (test_groups) {
            fprintf(fp, "<optionList>\n");
    
            for (const struct test_option_group *g = test_groups; g->name; g++) {
                fprintf(fp, "<group allowMultipleSelection=\"%s\">\n",
                        g->allow_multiple_selection ? "true" : "false");
                fprint_config_item(fp, g->name, NULL, NULL, g->description,
                                   NULL, NULL, POPULARITY_UNDEFINED);
                for (const struct test_option *o = g->options; o->name; o++) {
                    fprintf(fp, "  <option>\n");
                    fprint_config_item(fp, o->name, NULL, NULL, o->description,
                                       NULL, NULL, POPULARITY_UNDEFINED);
                    fprintf(fp, "</option>\n");
                }
                fprintf(fp, "</group>\n");
            }
            fprintf(fp, "</optionList>\n");
        }
    
        fprintf(fp, "</xkbConfigRegistry>\n");
        fclose(fp);
    
        return tmpdir;
    }
    
    static void
    test_remove_rules(char *basedir, const char *ruleset)
    {
        char path[PATH_MAX];
        int rc;
    
        rc = snprintf_safe(path, sizeof(path), "%s/rules/%s.xml", basedir,
                           ruleset);
        assert(rc);
        unlink(path);
        rc = snprintf_safe(path, sizeof(path), "%s/xkb/rules", basedir);
        assert(rc);
        rmdir(path);
        rmdir(basedir);
        free(basedir);
    }
    
    static struct rxkb_context *
    test_setup_context_for(const char *ruleset,
                           struct test_model *system_models,
                           struct test_model *user_models,
                           struct test_layout *system_layouts,
                           struct test_layout *user_layouts,
                           struct test_option_group *system_groups,
                           struct test_option_group *user_groups)
    {
        char *sysdir = NULL, *userdir = NULL;
        struct rxkb_context *ctx;
    
        sysdir = test_create_rules(ruleset, system_models, system_layouts,
                                   system_groups);
        if (user_models || user_layouts || user_groups)
            userdir = test_create_rules(ruleset, user_models, user_layouts,
                                        user_groups);
    
        ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
        assert(ctx);
        if (userdir)
            assert(rxkb_context_include_path_append(ctx, userdir));
        assert(rxkb_context_include_path_append(ctx, sysdir));
        assert(rxkb_context_parse(ctx, ruleset));
    
        test_remove_rules(sysdir, ruleset);
        if (userdir)
            test_remove_rules(userdir, ruleset);
    
        return ctx;
    }
    
    static struct rxkb_context *
    test_setup_context(struct test_model *system_models,
                       struct test_model *user_models,
                       struct test_layout *system_layouts,
                       struct test_layout *user_layouts,
                       struct test_option_group *system_groups,
                       struct test_option_group *user_groups)
    {
        const char *ruleset = "xkbtests";
        return test_setup_context_for(ruleset, system_models,
                                      user_models, system_layouts,
                                      user_layouts, system_groups,
                                      user_groups);
    }
    
    static struct rxkb_model *
    fetch_model(struct rxkb_context *ctx, const char *model)
    {
        struct rxkb_model *m = rxkb_model_first(ctx);
        while (m) {
            if (streq(rxkb_model_get_name(m), model))
                return rxkb_model_ref(m);
            m = rxkb_model_next(m);
        }
        return NULL;
    }
    
    static bool
    find_model(struct rxkb_context *ctx, const char *model)
    {
        struct rxkb_model *m = fetch_model(ctx, model);
        rxkb_model_unref(m);
        return m != NULL;
    }
    
    static bool
    find_models(struct rxkb_context *ctx, ...)
    {
        va_list args;
        const char *name;
        int idx = 0;
        bool rc = false;
    
        va_start(args, ctx);
        name = va_arg(args, const char *);
        while(name) {
            ++idx;
            assert(idx < 20); /* safety guard */
            if (!find_model(ctx, name))
                goto out;
            name = va_arg(args, const char *);
        };
    
        rc = true;
    out:
        va_end(args);
        return rc;
    }
    
    static struct rxkb_layout *
    fetch_layout(struct rxkb_context *ctx, const char *layout, const char *variant)
    {
        struct rxkb_layout *l = rxkb_layout_first(ctx);
        while (l) {
            const char *v = rxkb_layout_get_variant(l);
    
            if (streq(rxkb_layout_get_name(l), layout) &&
                ((v == NULL && variant == NULL) ||
                 (v != NULL && variant != NULL && streq(v, variant))))
                return rxkb_layout_ref(l);
            l = rxkb_layout_next(l);
        }
        return NULL;
    }
    
    static bool
    find_layout(struct rxkb_context *ctx, const char *layout, const char *variant)
    {
        struct rxkb_layout *l = fetch_layout(ctx, layout, variant);
        rxkb_layout_unref(l);
        return l != NULL;
    }
    
    static bool
    find_layouts(struct rxkb_context *ctx, ...)
    {
        va_list args;
        const char *name, *variant;
        int idx = 0;
        bool rc = false;
    
        va_start(args, ctx);
        name = va_arg(args, const char *);
        variant = va_arg(args, const char *);
        while(name) {
            ++idx;
            assert(idx < 20); /* safety guard */
            if (!find_layout(ctx, name, variant))
                goto out;
            name = va_arg(args, const char *);
            if (name)
                variant = va_arg(args, const char *);
        };
    
        rc = true;
    out:
        va_end(args);
        return rc;
    }
    
    static bool
    check_layouts_order(struct rxkb_context *ctx, ...)
    {
        va_list args;
        const char *layout, *variant;
        int idx = 0;
        bool rc = false;
        struct rxkb_layout *l = rxkb_layout_first(ctx);
    
        va_start(args, ctx);
        layout = va_arg(args, const char *);
        variant = va_arg(args, const char *);
        while(layout) {
            ++idx;
            assert(idx < 20); /* safety guard */
            if (!l) {
                fprintf(stderr, "ERROR: expected layout #%d \"%s(%s)\", got none\n",
                        idx, layout, variant ? variant : "-");
                goto out;
            }
            const char *v = rxkb_layout_get_variant(l);
            if (!streq(rxkb_layout_get_name(l), layout) || !streq_null(v, variant)) {
                fprintf(stderr, "ERROR: expected layout #%d \"%s(%s)\", got \"%s(%s)\"\n",
                        idx, layout, variant ? variant : "-",
                        rxkb_layout_get_name(l), v ? v : "-");
                goto out;
            }
            layout = va_arg(args, const char *);
            if (layout)
                variant = va_arg(args, const char *);
            l = rxkb_layout_next(l);
        };
        if (l) {
            const char *v = rxkb_layout_get_variant(l);
            fprintf(stderr, "ERROR: unexpected layout at #%d: \"%s(%s)\"\n",
                    idx + 1, rxkb_layout_get_name(l), v ? v : "-");
        }
        rc = l == NULL;
    out:
        va_end(args);
        return rc;
    }
    
    static struct rxkb_option_group *
    fetch_option_group(struct rxkb_context *ctx, const char *grp)
    {
        struct rxkb_option_group *g = rxkb_option_group_first(ctx);
        while (g) {
            if (streq(grp, rxkb_option_group_get_name(g)))
                return rxkb_option_group_ref(g);
            g = rxkb_option_group_next(g);
        }
        return NULL;
    }
    
    static struct rxkb_option *
    fetch_option(struct rxkb_context *ctx, const char *grp, const char *opt)
    {
        struct rxkb_option_group *g = rxkb_option_group_first(ctx);
        while (g) {
            if (streq(grp, rxkb_option_group_get_name(g))) {
                struct rxkb_option *o = rxkb_option_first(g);
    
                while (o) {
                    if (streq(opt, rxkb_option_get_name(o)))
                        return rxkb_option_ref(o);
                    o = rxkb_option_next(o);
                }
            }
            g = rxkb_option_group_next(g);
        }
        return NULL;
    }
    
    static bool
    find_option(struct rxkb_context *ctx, const char *grp, const char *opt)
    {
        struct rxkb_option *o = fetch_option(ctx, grp, opt);
        rxkb_option_unref(o);
        return o != NULL;
    }
    
    static bool
    find_options(struct rxkb_context *ctx, ...)
    {
        va_list args;
        const char *grp, *opt;
        int idx = 0;
        bool rc = false;
    
        va_start(args, ctx);
        grp = va_arg(args, const char *);
        opt = va_arg(args, const char *);
        while(grp) {
            ++idx;
            assert(idx < 20); /* safety guard */
            if (!find_option(ctx, grp, opt))
                goto out;
            grp = va_arg(args, const char *);
            if (grp)
                opt = va_arg(args, const char *);
        };
    
        rc = true;
    out:
        va_end(args);
        return rc;
    }
    
    static bool
    cmp_models(struct test_model *tm, struct rxkb_model *m)
    {
        if (!tm || !m)
            return false;
    
        if (!streq(tm->name, rxkb_model_get_name(m)))
            return false;
    
        if (!streq_null(tm->vendor, rxkb_model_get_vendor(m)))
            return false;
    
        if (!streq_null(tm->description, rxkb_model_get_description(m)))
            return false;
    
        return true;
    }
    
    static bool
    cmp_layouts(struct test_layout *tl, struct rxkb_layout *l)
    {
        struct rxkb_iso3166_code *iso3166 = NULL;
        struct rxkb_iso639_code *iso639 = NULL;
    
        if (!tl || !l)
            return false;
    
        if (!streq(tl->name, rxkb_layout_get_name(l)))
            return false;
    
        if (!streq_null(tl->variant, rxkb_layout_get_variant(l)))
            return false;
    
        if (!streq_null(tl->brief, rxkb_layout_get_brief(l)))
            return false;
    
        if (!streq_null(tl->description, rxkb_layout_get_description(l)))
            return false;
    
        iso3166 = rxkb_layout_get_iso3166_first(l);
        for (size_t i = 0; i < ARRAY_SIZE(tl->iso3166); i++) {
            const char *iso = tl->iso3166[i];
            if (iso == NULL && iso3166 == NULL)
                break;
    
            if (!streq_null(iso, rxkb_iso3166_code_get_code(iso3166)))
                return false;
    
            iso3166 = rxkb_iso3166_code_next(iso3166);
        }
    
        if (iso3166 != NULL)
            return false;
    
        iso639 = rxkb_layout_get_iso639_first(l);
        for (size_t i = 0; i < ARRAY_SIZE(tl->iso639); i++) {
            const char *iso = tl->iso639[i];
            if (iso == NULL && iso639 == NULL)
                break;
    
            if (!streq_null(iso, rxkb_iso639_code_get_code(iso639)))
                return false;
    
            iso639 = rxkb_iso639_code_next(iso639);
        }
    
        if (iso639 != NULL)
            return false;
    
        return true;
    }
    
    static bool
    cmp_options(struct test_option *to, struct rxkb_option *o)
    {
        if (!to || !o)
            return false;
    
        if (!streq(to->name, rxkb_option_get_name(o)))
            return false;
    
        if (!streq_null(to->description, rxkb_option_get_description(o)))
            return false;
    
        return true;
    }
    
    enum cmp_type {
        CMP_EXACT,
        CMP_MATCHING_ONLY,
    };
    
    static bool
    cmp_option_groups(struct test_option_group *tg, struct rxkb_option_group *g,
                      enum cmp_type cmp)
    {
        struct rxkb_option *o;
        struct test_option *to;
    
        if (!tg || !g)
            return false;
    
        if (!streq(tg->name, rxkb_option_group_get_name(g)))
            return false;
    
        if (!streq_null(tg->description, rxkb_option_group_get_description(g)))
            return false;
    
        if (tg->allow_multiple_selection != rxkb_option_group_allows_multiple(g))
            return false;
    
        to = tg->options;
        o = rxkb_option_first(g);
    
        while (o && to->name) {
            if (!cmp_options(to, o))
                return false;
            to++;
            o = rxkb_option_next(o);
        }
    
        if (cmp == CMP_EXACT && (o || to->name))
            return false;
    
        return true;
    }
    
    static void
    test_load_basic(void)
    {
        struct test_model system_models[] =  {
            {"m1"},
            {"m2"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1"},
            {"l1", "v1"},
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", NULL, true,
              { {"grp1:1"}, {"grp1:2"} } },
            {"grp2", NULL, false,
              { {"grp2:1"}, {"grp2:2"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
    
        ctx = test_setup_context(system_models, NULL,
                                 system_layouts, NULL,
                                 system_groups, NULL);
    
        assert(find_models(ctx, "m1", "m2", NULL));
        assert(find_layouts(ctx, "l1", NO_VARIANT,
                                 "l1", "v1", NULL));
        assert(find_options(ctx, "grp1", "grp1:1",
                                 "grp1", "grp1:2",
                                 "grp2", "grp2:1",
                                 "grp2", "grp2:2", NULL));
        rxkb_context_unref(ctx);
    }
    
    static void
    test_load_full(void)
    {
        struct test_model system_models[] =  {
            {"m1", "vendor1", "desc1"},
            {"m2", "vendor2", "desc2"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
            {"l1", "v1", "vbrief1", "vdesc1"},
            {"l1", "v2", NULL, "vdesc2"},
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", "gdesc1", true,
              { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
            {"grp2", "gdesc2", false,
              { {"grp2:1", "odesc21"}, {"grp2:2", "odesc22"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
        struct rxkb_model *m;
        struct rxkb_layout *l;
        struct rxkb_option_group *g;
    
        ctx = test_setup_context(system_models, NULL,
                                 system_layouts, NULL,
                                 system_groups, NULL);
    
        m = fetch_model(ctx, "m1");
        assert(cmp_models(&system_models[0], m));
        rxkb_model_unref(m);
    
        m = fetch_model(ctx, "m2");
        assert(cmp_models(&system_models[1], m));
        rxkb_model_unref(m);
    
        l = fetch_layout(ctx, "l1", NO_VARIANT);
        assert(cmp_layouts(&system_layouts[0], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l1", "v1");
        assert(cmp_layouts(&system_layouts[1], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l1", "v2");
        struct test_layout expected = {"l1", "v2", "lbrief1", "vdesc2"};
        assert(cmp_layouts(&expected, l));
        rxkb_layout_unref(l);
    
        g = fetch_option_group(ctx, "grp1");
        assert(cmp_option_groups(&system_groups[0], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        g = fetch_option_group(ctx, "grp2");
        assert(cmp_option_groups(&system_groups[1], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        rxkb_context_unref(ctx);
    }
    
    static void
    test_load_languages(void)
    {
        struct test_model system_models[] =  {
            {"m1", "vendor1", "desc1"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1", NO_VARIANT, "lbrief1", "ldesc1",
                .iso639 = { "abc", "def" },
                .iso3166 = { "uv", "wx" }},
            {"l1", "v1", "vbrief1", "vdesc1",
                .iso639 = {"efg"},
                .iso3166 = {"yz"}},
            {"l2", NO_VARIANT, "lbrief1", "ldesc1",
                .iso639 = { "hij", "klm" },
                .iso3166 = { "op", "qr" }},
            {"l2", "v2", "lbrief1", "ldesc1",
                .iso639 = { NULL }, /* inherit from parent */
                .iso3166 = { NULL }},  /* inherit from parent */
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", "gdesc1", true,
              { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
        struct rxkb_layout *l;
        struct rxkb_iso3166_code *iso3166;
        struct rxkb_iso639_code *iso639;
    
        ctx = test_setup_context(system_models, NULL,
                                 system_layouts, NULL,
                                 system_groups, NULL);
    
        l = fetch_layout(ctx, "l1", NO_VARIANT);
        assert(cmp_layouts(&system_layouts[0], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l1", "v1");
        assert(cmp_layouts(&system_layouts[1], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l2", "v2");
        iso3166 = rxkb_layout_get_iso3166_first(l);
        assert(streq(rxkb_iso3166_code_get_code(iso3166), "op"));
        iso3166 = rxkb_iso3166_code_next(iso3166);
        assert(streq(rxkb_iso3166_code_get_code(iso3166), "qr"));
    
        iso639 = rxkb_layout_get_iso639_first(l);
        assert(streq(rxkb_iso639_code_get_code(iso639), "hij"));
        iso639 = rxkb_iso639_code_next(iso639);
        assert(streq(rxkb_iso639_code_get_code(iso639), "klm"));
    
        rxkb_layout_unref(l);
        rxkb_context_unref(ctx);
    }
    
    static void
    test_load_invalid_languages(void)
    {
        struct test_model system_models[] =  {
            {"m1", "vendor1", "desc1"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1", NO_VARIANT, "lbrief1", "ldesc1",
                .iso639 = { "ab", "def" },
                .iso3166 = { "uvw", "xz" }},
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", "gdesc1", true,
              { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
        struct rxkb_layout *l;
        struct rxkb_iso3166_code *iso3166;
        struct rxkb_iso639_code *iso639;
    
        ctx = test_setup_context(system_models, NULL,
                                 system_layouts, NULL,
                                 system_groups, NULL);
    
        l = fetch_layout(ctx, "l1", NO_VARIANT);
        /* uvw is invalid, we expect 2 letters, verify it was ignored */
        iso3166 = rxkb_layout_get_iso3166_first(l);
        assert(streq(rxkb_iso3166_code_get_code(iso3166), "xz"));
        assert(rxkb_iso3166_code_next(iso3166) == NULL);
    
        /* ab is invalid, we expect 3 letters, verify it was ignored */
        iso639 = rxkb_layout_get_iso639_first(l);
        assert(streq(rxkb_iso639_code_get_code(iso639), "def"));
        assert(rxkb_iso639_code_next(iso639) == NULL);
        rxkb_layout_unref(l);
    
        rxkb_context_unref(ctx);
    }
    
    static void
    test_popularity(void)
    {
        assert(POPULARITY_UNDEFINED != POPULARITY_STANDARD);
        assert(POPULARITY_UNDEFINED != POPULARITY_EXOTIC);
    
        struct test_layout system_layouts[] = {
            {.name = "l1", .variant = NO_VARIANT }, /* Default popularity */
            {.name = "l1", .variant = "v1" },       /* Default popularity */
            {.name = "l2", .popularity = POPULARITY_STANDARD },
            {.name = "l3", .popularity = POPULARITY_EXOTIC },
            {NULL},
        };
        struct rxkb_context *ctx;
        struct rxkb_layout *l;
        struct ruleset_conf {
            const char *ruleset;
            enum rxkb_context_flags flags;
            enum rxkb_popularity popularity; /* Default popularity */
        };
        struct ruleset_conf rulesets[] = {
            {   /* Rules with “standard” popularity */
                .ruleset="xkbtests",
                .flags=RXKB_CONTEXT_NO_DEFAULT_INCLUDES,
                .popularity=RXKB_POPULARITY_STANDARD
            },
            {   /* Rules with “exotic” popularity (hack, see below) */
                .ruleset="xkbtests.extras",
                .flags=RXKB_CONTEXT_NO_DEFAULT_INCLUDES |
                       RXKB_CONTEXT_LOAD_EXOTIC_RULES,
                .popularity=RXKB_POPULARITY_EXOTIC
            }
        };
        for (size_t k = 0; k < ARRAY_SIZE(rulesets); k++) {
            struct ruleset_conf *conf = &rulesets[k];
            char *dir = NULL;
    
            dir = test_create_rules(conf->ruleset, NULL, system_layouts, NULL);
            ctx = rxkb_context_new(conf->flags);
            assert(ctx);
            assert(rxkb_context_include_path_append(ctx, dir));
            /* Hack: ruleset "xkbtests.extras" above generates xkbtests.extras.xml,
             * loading "xkbtests" means the extras file counts as exotic */
            assert(rxkb_context_parse(ctx, "xkbtests"));
    
            /* Test implicit popularity */
            l = fetch_layout(ctx, "l1", NO_VARIANT);
            assert(rxkb_layout_get_popularity(l) == conf->popularity);
            rxkb_layout_unref(l);
    
            l = fetch_layout(ctx, "l1", "v1");
            assert(rxkb_layout_get_popularity(l) == conf->popularity);
            rxkb_layout_unref(l);
    
            /* Test explicit popularity */
            l = fetch_layout(ctx, "l2", NO_VARIANT);
            assert(rxkb_layout_get_popularity(l) == RXKB_POPULARITY_STANDARD);
            rxkb_layout_unref(l);
    
            l = fetch_layout(ctx, "l3", NO_VARIANT);
            assert(rxkb_layout_get_popularity(l) == RXKB_POPULARITY_EXOTIC);
            rxkb_layout_unref(l);
    
            test_remove_rules(dir, conf->ruleset);
            rxkb_context_unref(ctx);
        }
    }
    
    
    static void
    test_load_merge(void)
    {
        struct test_model system_models[] =  {
            {"m1", "vendor1", "desc1"},
            {"m2", "vendor2", "desc2"},
            {NULL},
        };
        struct test_model user_models[] =  {
            {"m3", "vendor3", "desc3"},
            {"m4", "vendor4", "desc4"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
            {"l1", "v1", "vbrief1", "vdesc1"},
            {NULL},
        };
        struct test_layout user_layouts[] =  {
            {"l2", NO_VARIANT, "lbrief2", "ldesc2"},
            {"l2", "v2", "vbrief2", "vdesc2"},
            // Duplicate with system
            {"l1", NO_VARIANT, "lbrief1bis", "ldesc1bis"},
            {"l1", "v1", "vbrief1bis", "vdesc1bis"},
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", NULL, true,
              { {"grp1:1"}, {"grp1:2"} } },
            {"grp2", NULL, false,
              { {"grp2:1"}, {"grp2:2"} } },
            { NULL },
        };
        struct test_option_group user_groups[] = {
            {"grp3", NULL, true,
              { {"grp3:1"}, {"grp3:2"} } },
            {"grp4", NULL, false,
              { {"grp4:1"}, {"grp4:2"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
        struct rxkb_model *m;
        struct rxkb_layout *l;
        struct rxkb_option_group *g;
    
        ctx = test_setup_context(system_models, user_models,
                                 system_layouts, user_layouts,
                                 system_groups, user_groups);
    
        assert(find_models(ctx, "m1", "m2", "m3", "m4", NULL));
        assert(find_layouts(ctx, "l1", NO_VARIANT,
                                 "l1", "v1",
                                 "l2", NO_VARIANT,
                                 "l2", "v2", NULL));
    
        m = fetch_model(ctx, "m1");
        assert(cmp_models(&system_models[0], m));
        rxkb_model_unref(m);
    
        m = fetch_model(ctx, "m2");
        assert(cmp_models(&system_models[1], m));
        rxkb_model_unref(m);
    
        m = fetch_model(ctx, "m3");
        assert(cmp_models(&user_models[0], m));
        rxkb_model_unref(m);
    
        m = fetch_model(ctx, "m4");
        assert(cmp_models(&user_models[1], m));
        rxkb_model_unref(m);
    
        l = fetch_layout(ctx, "l1", NO_VARIANT);
        assert(cmp_layouts(&system_layouts[0], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l1", "v1");
        assert(cmp_layouts(&system_layouts[1], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l2", NO_VARIANT);
        assert(cmp_layouts(&user_layouts[0], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l2", "v2");
        assert(cmp_layouts(&user_layouts[1], l));
        rxkb_layout_unref(l);
    
        /* Check that the duplicate layouts are skipped */
        assert(check_layouts_order(ctx,
                                   "l1", NO_VARIANT, "l1", "v1",
                                   "l2", NO_VARIANT, "l2", "v2",
                                   NULL));
    
        g = fetch_option_group(ctx, "grp1");
        assert(cmp_option_groups(&system_groups[0], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        g = fetch_option_group(ctx, "grp2");
        assert(cmp_option_groups(&system_groups[1], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        g = fetch_option_group(ctx, "grp3");
        assert(cmp_option_groups(&user_groups[0], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        g = fetch_option_group(ctx, "grp4");
        assert(cmp_option_groups(&user_groups[1], g, CMP_EXACT));
        rxkb_option_group_unref(g);
    
        rxkb_context_unref(ctx);
    }
    
    static void
    test_load_merge_no_overwrite(void)
    {
        struct test_model system_models[] =  {
            {"m1", "vendor1", "desc1"},
            {"m2", "vendor2", "desc2"},
            {NULL},
        };
        struct test_model user_models[] =  {
            {"m1", "vendor3", "desc3"}, /* must not overwrite */
            {"m4", "vendor4", "desc4"},
            {NULL},
        };
        struct test_layout system_layouts[] =  {
            {"l1", NO_VARIANT, "lbrief1", "ldesc1"},
            {"l1", "v1", "vbrief1", "vdesc1"},
            {NULL},
        };
        struct test_layout user_layouts[] =  {
            {"l2", NO_VARIANT, "lbrief2", "ldesc2"},
            {"l2", "v2", "vbrief2", "vdesc2"},
            {"l1", NO_VARIANT, "lbrief3", "ldesc3"}, /* must not overwrite */
            {"l1", "v2", "vbrief3", "vdesc3"}, /* must not overwrite */
            {NULL},
        };
        struct test_option_group system_groups[] = {
            {"grp1", "gdesc1", true,
              { {"grp1:1", "odesc11"}, {"grp1:2", "odesc12"} } },
            {"grp2", "gdesc2", false,
              { {"grp2:1", "odesc21"}, {"grp2:2", "odesc22"} } },
            { NULL },
        };
        struct test_option_group user_groups[] = {
            {"grp1", "XXXXX", false, /* must not overwrite */
              { {"grp1:1", "YYYYYYY"}, /* must not overwrite */
                {"grp1:3", "ZZZZZZ"} } }, /* append */
            {"grp4", "gdesc4", false,
              { {"grp4:1", "odesc41"}, {"grp4:2", "odesc42"} } },
            { NULL },
        };
        struct rxkb_context *ctx;
        struct rxkb_model *m;
        struct rxkb_layout *l;
        struct rxkb_option_group *g;
    
        ctx = test_setup_context(system_models, user_models,
                                 system_layouts, user_layouts,
                                 system_groups, user_groups);
    
        m = fetch_model(ctx, "m1");
        assert(cmp_models(&system_models[0], m));
        rxkb_model_unref(m);
    
        l = fetch_layout(ctx, "l1", NO_VARIANT);
        assert(cmp_layouts(&system_layouts[0], l));
        rxkb_layout_unref(l);
    
        l = fetch_layout(ctx, "l1", "v1");
        assert(cmp_layouts(&system_layouts[1], l));
        rxkb_layout_unref(l);
    
        assert(find_option(ctx, "grp1", "grp1:3"));
        g = fetch_option_group(ctx, "grp1");
        assert(cmp_option_groups(&system_groups[0], g, CMP_MATCHING_ONLY));
        rxkb_option_group_unref(g);
    
        rxkb_context_unref(ctx);
    }
    
    static void
    test_no_include_paths(void)
    {
        struct rxkb_context *ctx;
    
        ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
        assert(ctx);
        assert(!rxkb_context_parse_default_ruleset(ctx));
    
        rxkb_context_unref(ctx);
    }
    
    static void
    test_invalid_include(void)
    {
        struct rxkb_context *ctx;
    
        ctx = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES);
        assert(ctx);
        assert(!rxkb_context_include_path_append(ctx, "/foo/bar/baz/bat"));
        assert(!rxkb_context_parse_default_ruleset(ctx));
    
        rxkb_context_unref(ctx);
    }
    
    /* Check that libxml2 error handler is reset after parsing */
    static void
    test_xml_error_handler(void)
    {
        struct test_model system_models[] =  { {NULL} };
        struct test_layout system_layouts[] =  { {NULL} };
        struct test_option_group system_groups[] = { { NULL } };
        struct rxkb_context *ctx;
    
        ctx = test_setup_context(system_models, NULL,
                                 system_layouts, NULL,
                                 system_groups, NULL);
        assert(ctx);
        rxkb_context_unref(ctx);
    
        const char invalid_xml[] = "<test";
        /* This should trigger a segfault if error handler is not reset, because
         * else it would use our error handler with an invalid (freed) context. */
        xmlDocPtr doc = xmlParseMemory (invalid_xml, strlen(invalid_xml));
        assert(!doc);
        xmlFreeDoc (doc);
        xmlCleanupParser();
    }
    
    int
    main(void)
    {
        test_init();
    
        test_xml_error_handler();
        test_no_include_paths();
        test_invalid_include();
        test_load_basic();
        test_load_full();
        test_load_merge();
        test_load_merge_no_overwrite();
        test_load_languages();
        test_load_invalid_languages();
        test_popularity();
    
        return 0;
    }