Edit

kc3-lang/libxkbcommon/scripts/update-merge-modes-tests.py

Branch :

  • Show log

    Commit

  • Author : Pierre Le Marre
    Date : 2025-03-24 09:21:40
    Hash : 21a341f2
    Message : test: Enable 3rd party compilers

  • scripts/update-merge-modes-tests.py
  • #!/usr/bin/env python3
    
    # Copyright © 2025 Pierre Le Marre <dev@wismill.eu>
    # SPDX-License-Identifier: MIT
    
    """
    This script generate tests for merge modes.
    """
    
    from __future__ import annotations
    
    from abc import ABCMeta, abstractmethod
    import argparse
    from collections import defaultdict
    from collections.abc import Callable, Generator, Iterable, Iterator, Sequence
    import dataclasses
    import itertools
    import re
    import textwrap
    from dataclasses import dataclass
    from enum import Flag, IntFlag, StrEnum, auto, unique
    from pathlib import Path
    from string import Template
    from typing import Any, ClassVar, NewType, Self
    
    import jinja2
    
    SCRIPT = Path(__file__)
    # Root of the project
    ROOT = SCRIPT.parent.parent
    
    ################################################################################
    #
    # XKB compiler
    #
    ################################################################################
    
    
    @dataclass
    class XkbCompiler:
        name: str
        extended_syntax: bool
    
        default: ClassVar[str] = "xkbcommon"
    
        @property
        def is_default(self) -> bool:
            return self.name == self.default
    
        @property
        def suffix(self) -> str:
            return "" if self.is_default else "-" + self.name
    
        @classmethod
        def parse(cls, name: str) -> Self:
            match name:
                case cls.default:
                    return cls(name=name, extended_syntax=True)
                case "xkbcomp":
                    return cls(name=name, extended_syntax=False)
                case "kbvm":
                    return cls(name=name, extended_syntax=True)
                case _:
                    raise ValueError(f"Invalid XKB compiler: {name}")
    
    
    ################################################################################
    #
    # Merge modes
    #
    ################################################################################
    
    
    @unique
    class MergeMode(StrEnum):
        Default = auto()
        Augment = auto()
        Override = auto()
        Replace = auto()
    
        @property
        def include(self) -> str:
            if self is self.__class__.Default:
                return "include"
            else:
                return self
    
        @property
        def char(self) -> str:
            match self:
                case self.__class__.Default:
                    return "+"
                case self.__class__.Augment:
                    return "|"
                case self.__class__.Override:
                    return "+"
                case self.__class__.Replace:
                    return "^"
                case _:
                    raise ValueError(self)
    
    
    INDENT = "\t"
    
    
    ################################################################################
    #
    # Components
    #
    ################################################################################
    
    
    @unique
    class Component(StrEnum):
        keycodes = auto()
        types = auto()
        compat = auto()
        symbols = auto()
    
        def render_keymap(self, content) -> Generator[str]:
            yield "xkb_keymap {"
            for component in self.__class__:
                yield from (
                    textwrap.indent(t, INDENT)
                    for t in self.render(component, content if component is self else "")
                )
            yield "};"
    
        @classmethod
        def render(cls, component: Self, content: str, name: str = "") -> Generator[str]:
            if not content:
                match component:
                    case cls.keycodes:
                        pass
                    case cls.types:
                        pass
                    case cls.compat:
                        content = "interpret.repeat= True;"
                    case cls.symbols:
                        pass
            if name:
                name = f' "{name}"'
            if content:
                yield f'xkb_{component}{name} "" {{'
                yield textwrap.indent(content, INDENT)
                yield "};"
            else:
                yield f'xkb_{component}{name} "" {{}};'
    
    
    class ComponentTemplate(Template):
        def __init__(self, template) -> Self:
            super().__init__(textwrap.dedent(template).strip())
    
        def __bool__(self) -> bool:
            return bool(self.template)
    
        def render(self, merge: MergeMode) -> str:
            mode = "" if merge is MergeMode.Default else f"{merge} "
            return self.substitute(mode=mode)
    
        def __add__(self, other: Any) -> Self:
            if isinstance(other, str):
                return self.__class__(self.template + "\n" + other)
            elif isinstance(other, Template):
                return self.__class__(self.template + "\n" + other.template)
            else:
                return NotImplemented
    
        def __radd__(self, other: Any) -> Self:
            if isinstance(other, str):
                return self.__class__(other + "\n" + self.template)
            elif isinstance(other, Template):
                return self.__class__(other.template + "\n" + self.template)
            else:
                return NotImplemented
    
    
    ################################################################################
    #
    # Test templates
    #
    ################################################################################
    
    
    @dataclass
    class Test:
        title: str
        file: Path
        content: str
        expected: str
    
    
    @dataclass
    class TestGroup:
        title: str
        tests: Sequence[Test]
    
        c_template: ClassVar[Path] = Path("test/merge_modes.c.jinja")
    
        SLUG_PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"[^-\w()]+")
    
        @classmethod
        def file(cls, title: str) -> Path:
            return Path(cls.SLUG_PATTERN.sub("-", title.lower().replace(" ", "_")))
    
        @classmethod
        def _write(
            cls,
            root: Path,
            jinja_env: jinja2.Environment,
            path: Path,
            **kwargs,
        ) -> None:
            template_path = root / cls.c_template
            template = jinja_env.get_template(template_path.relative_to(root).as_posix())
    
            with path.open("wt", encoding="utf-8") as fd:
                fd.writelines(template.generate(script=SCRIPT.relative_to(ROOT), **kwargs))
    
        @classmethod
        def write_c(
            cls,
            root: Path,
            jinja_env: jinja2.Environment,
            path: Path,
            compiler: XkbCompiler,
            tests: Sequence[TestGroup],
        ) -> None:
            return cls._write(
                root=root, jinja_env=jinja_env, path=path, compiler=compiler, tests=tests
            )
    
        @classmethod
        def write_data(
            cls,
            root: Path,
            compiler: XkbCompiler,
            tests: Sequence[TestGroup],
        ) -> None:
            tests_: dict[Path, list[Test]] = defaultdict(list)
            suffix = compiler.suffix
            for group in tests:
                for test in group.tests:
                    file = test.file.parent
                    file = file.with_name(file.name + suffix)
                    tests_[file].append(test)
            for file, group_ in tests_.items():
                path = root / "test" / "data" / file
                with path.open("wt", encoding="utf-8") as fd:
                    fd.write(
                        "// WARNING: This file was auto-generated by: "
                        f"{SCRIPT.relative_to(ROOT)}\n"
                    )
                    for test in group_:
                        fd.write(test.content)
    
    
    @dataclass
    class TestTemplate(metaclass=ABCMeta):
        title: str
    
        _plain: ClassVar[str] = "plain"
        _include: ClassVar[str] = "include"
        _base: ClassVar[str] = "base"
        _update: ClassVar[str] = "update"
        _file: ClassVar[str] = "merge_modes"
    
        def make_title(self, name: str) -> str:
            return f"{self.title}: {name}"
    
        @classmethod
        def make_file(cls, name: str) -> Path:
            return TestGroup.file(name)
    
        @abstractmethod
        def generate_tests(self) -> TestGroup: ...
    
        @abstractmethod
        def generate_data(self) -> TestGroup: ...
    
        @classmethod
        @abstractmethod
        def write(
            cls,
            root: Path,
            jinja_env: jinja2.Environment,
            c_file: Path | None,
            xkb: bool,
            compiler: XkbCompiler,
            tests: Sequence[Self],
            debug: bool,
        ): ...
    
    
    @dataclass
    class ComponentTestTemplate(TestTemplate):
        component: Component
        # Templates to test
        base_template: ComponentTemplate
        update_template: ComponentTemplate
        # Expected result
        augment: str
        override: str
        replace: str = ""
    
        def __post_init__(self):
            self.augment = textwrap.dedent(self.augment).strip()
            self.override = textwrap.dedent(self.override).strip()
            if not self.replace:
                self.replace = self.override
            else:
                self.replace = textwrap.dedent(self.replace).strip()
    
        def expected(self, mode: MergeMode) -> str:
            match mode:
                case MergeMode.Default:
                    return self.override
                case MergeMode.Augment:
                    return self.augment
                case MergeMode.Override:
                    return self.override
                case MergeMode.Replace:
                    return self.replace
                case _:
                    raise ValueError(mode)
    
        def _make_section_name(self, type: str, mode: MergeMode) -> str:
            file = self.make_file(f"{self.title}-{mode}" if self.title else mode)
            return f"{type}-{file}"
    
        def _make_file(self, type: str, mode: MergeMode) -> Path:
            section_name = self._make_section_name(type, mode)
            return Path(self.component) / self._file / section_name
    
        def render_section(self, content: str, name: str = "") -> str:
            return "\n".join(self.component.render(self.component, content, name))
    
        def render_keymap(self, content: str) -> str:
            return "\n".join(self.component.render_keymap(content))
    
        def _generate_tests(
            self, render: Callable[[str], str], compiler: XkbCompiler
        ) -> Generator[Test]:
            include_suffix = compiler.suffix
            for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
                # Plain
                content = (
                    self.base_template.render(base_mode)
                    + ("\n" if self.base_template else "")
                    + self.update_template.render(update_mode)
                )
                yield Test(
                    title=self.make_title(
                        f"{self._plain} ({base_mode}) - {self._plain} ({update_mode})"
                    ),
                    file=self.make_file(self.title) / self.make_file(update_mode),
                    content=render(content),
                    expected=render(self.expected(update_mode)),
                )
    
            for base_mode, update_mode in itertools.product(
                (MergeMode.Default,), MergeMode
            ):
                # Plain + Include
                file = self._make_file(self._update, update_mode)
                section = file.name
                file = file.parent.name
                for mode in MergeMode:
                    yield Test(
                        title=self.make_title(
                            f"{self._plain} ({base_mode}) - {mode.include} ({update_mode})"
                        ),
                        file=self.make_file(self.title) / self.make_file(mode),
                        content=render(
                            self.base_template.render(base_mode)
                            + (
                                f'\n{mode.include} "{file}{include_suffix}({section})"'
                                if self.base_template
                                else ""
                            )
                        ),
                        # Include does not leak local merge modes
                        expected=render(self.expected(mode)),
                    )
    
            # for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
            #     # Include + Plain
            #     file = self._make_file(self.base, base_mode)
            #     section = file.name
            #     file = file.parent.name
            #     for mode in (MergeMode.default,):
            #         yield Test(
            #             title=self.make_title(
            #                 f"{mode.include} ({base_mode}) - {self.plain} ({update_mode})"
            #             ),
            #             file=self.make_file(self.title)
            #             / self.make_file(
            #                 f"{mode.include}({base_mode})-{self.plain}({update_mode})"
            #             ),
            #             content=render(
            #                 f'{mode.include} "{file}{include_suffix}({section})"\n'
            #                 + self.update_template.render(update_mode)
            #             ),
            #             expected=render(self.expected(update_mode)),
            #         )
    
            # for base_mode, update_mode in itertools.product((MergeMode.Default,), MergeMode):
            #     # Include + Include
            #     mode = MergeMode.Default
            #     base_file = self._make_file(self.base, mode)
            #     base_section = base_file.name
            #     base_file = base_file.parent.name
            #     update_file = self._make_file(self.update, mode)
            #     update_section = update_file.name
            #     update_file = update_file.parent.name
            #     yield Test(
            #         title=self.make_title(
            #             f"{base_mode.include} ({mode}) - {update_mode.include} ({mode})"
            #         ),
            #         file=self.make_file(self.title)
            #         / self.make_file(
            #             f"{base_mode.include}({mode})-{update_mode.include}({mode})"
            #         ),
            #         content=render(
            #             f'{base_mode.include} "{base_file}{include_suffix}({base_section})"\n'
            #             + f'{update_mode.include} "{update_file}{include_suffix}({update_section})"'
            #         ),
            #         expected=render(self.expected(update_mode)),
            #     )
    
            for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
                # Multi-include
                base_file = self._make_file(self._base, base_mode)
                base_section = base_file.name
                base_file = base_file.parent.name
                update_file = self._make_file(self._update, update_mode)
                update_section = update_file.name
                update_file = update_file.parent.name
                for mode in (MergeMode.Override, MergeMode.Augment, MergeMode.Replace):
                    yield Test(
                        title=self.make_title(
                            f"{self._include} ({base_mode} {mode.char} {update_mode})"
                        ),
                        file=self.make_file(self.title) / self.make_file(mode),
                        content=render(
                            f'{base_mode.include} "{base_file}{include_suffix}({base_section})'
                            + mode.char
                            + f'{update_file}{include_suffix}({update_section})"'
                        ),
                        expected=render(self.expected(mode)),
                    )
    
        def _generate_data(self) -> Generator[Test]:
            for type, template in (
                (self._base, self.base_template),
                (self._update, self.update_template),
            ):
                for mode in MergeMode:
                    section_name = self._make_section_name(type, mode)
                    file = self._make_file(type, mode)
                    content = template.render(mode)
                    content = self.render_section(content=content, name=section_name) + "\n"
                    yield Test(title="", file=file, content=content, expected="")
    
        def generate_tests(
            self, compiler: XkbCompiler, as_keymap: bool = False
        ) -> TestGroup:
            if as_keymap:
    
                def render(content: str) -> str:
                    return self.render_keymap(content)
            else:
    
                def render(content: str) -> str:
                    return self.render_section(content)
    
            return TestGroup(self.title, tuple(self._generate_tests(render, compiler)))
    
        def generate_data(self) -> TestGroup:
            return TestGroup(self.title, tuple(self._generate_data()))
    
        @classmethod
        def write(
            cls,
            root: Path,
            jinja_env: jinja2.Environment,
            c_file: Path | None,
            xkb: bool,
            compiler: XkbCompiler,
            tests: Sequence[Self],
            debug: bool = False,
        ):
            if c_file is not None and c_file.name:
                c_data = tuple(
                    t.generate_tests(as_keymap=True, compiler=compiler) for t in tests
                )
                TestGroup.write_c(
                    root=root,
                    jinja_env=jinja_env,
                    path=c_file,
                    compiler=compiler,
                    tests=c_data,
                )
            if xkb:
                xkb_data = tuple(t.generate_data() for t in tests)
                TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data)
    
    
    KeymapTemplate = Template(
        """\
    xkb_keymap {
    ${keycodes}
    ${types}
    ${compat}
    ${symbols}
    };"""
    )
    
    
    @dataclass
    class KeymapTestTemplate(TestTemplate):
        keycodes: ComponentTestTemplate | None = None
        types: ComponentTestTemplate | None = None
        compat: ComponentTestTemplate | None = None
        symbols: ComponentTestTemplate | None = None
    
        def __post_init__(self):
            if self.keycodes:
                self.keycodes = dataclasses.replace(self.keycodes, title="")
            if self.types:
                self.types = dataclasses.replace(self.types, title="")
            if self.compat:
                self.compat = dataclasses.replace(self.compat, title="")
            if self.symbols:
                self.symbols = dataclasses.replace(self.symbols, title="")
    
        def __iter__(self) -> Generator[ComponentTestTemplate]:
            def make_empty_template(component: Component) -> ComponentTestTemplate:
                return ComponentTestTemplate(
                    title="",
                    component=component,
                    base_template=ComponentTemplate(""),
                    update_template=ComponentTemplate(""),
                    augment="",
                    override="",
                )
    
            yield self.keycodes or make_empty_template(Component.keycodes)
            yield self.types or make_empty_template(Component.types)
            yield self.compat or make_empty_template(Component.compat)
            yield self.symbols or make_empty_template(Component.symbols)
    
        def _generate_tests(self, compiler: XkbCompiler) -> Generator[Test]:
            groups = tuple(
                c.generate_tests(as_keymap=False, compiler=compiler) for c in self
            )
            keycodes: Test
            types: Test
            compat: Test
            symbols: Test
            for keycodes, types, compat, symbols in zip(*(g.tests for g in groups)):
                title = self.title + keycodes.title
                file = keycodes.file
                content = KeymapTemplate.substitute(
                    keycodes=textwrap.indent(keycodes.content, INDENT),
                    types=textwrap.indent(types.content, INDENT),
                    compat=textwrap.indent(compat.content, INDENT),
                    symbols=textwrap.indent(symbols.content, INDENT),
                )
                expected = KeymapTemplate.substitute(
                    keycodes=textwrap.indent(keycodes.expected, INDENT),
                    types=textwrap.indent(types.expected, INDENT),
                    compat=textwrap.indent(compat.expected, INDENT),
                    symbols=textwrap.indent(symbols.expected, INDENT),
                )
                yield Test(title=title, file=file, content=content, expected=expected)
    
        def generate_tests(self, compiler: XkbCompiler) -> TestGroup:
            return TestGroup(self.title, tuple(self._generate_tests(compiler)))
    
        def _generate_data(self) -> Generator[Test]:
            for component in self:
                yield from component.generate_data().tests
    
        def generate_data(self) -> TestGroup:
            return TestGroup(self.title, tuple(self._generate_data()))
    
        @classmethod
        def write(
            cls,
            root: Path,
            jinja_env: jinja2.Environment,
            c_file: Path | None,
            xkb: bool,
            compiler: XkbCompiler,
            tests: Sequence[Self],
            debug: bool = False,
        ):
            if c_file is not None and c_file.name:
                c_data = tuple(t.generate_tests(compiler) for t in tests)
                TestGroup.write_c(
                    root=root,
                    jinja_env=jinja_env,
                    path=c_file,
                    compiler=compiler,
                    tests=c_data,
                )
            if xkb:
                xkb_data = tuple(t.generate_data() for t in tests)
                TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data)
    
    
    ################################################################################
    #
    # Key types tests
    #
    ################################################################################
    
    TYPES_TESTS = ComponentTestTemplate(
        "Types",
        component=Component.types,
        base_template=ComponentTemplate(
            """
            ${mode}virtual_modifiers NumLock, LevelThree=none, LevelFive=Mod3;
            ${mode}virtual_modifiers U1, U2, U3;
            // ${mode}virtual_modifiers U4, U5, U6;
            ${mode}virtual_modifiers Z1=none, Z2=none, Z3=none;
            // ${mode}virtual_modifiers Z4=none, Z5=none, Z6=none;
            ${mode}virtual_modifiers M1=0x1000, M2=0x2000, M3=0x3000;
            // ${mode}virtual_modifiers M4=0x4000, M5=0x5000, M6=0x6000;
    
            ${mode}type "ONE_LEVEL" {
            	modifiers = None;
            	map[None] = Level1;
            	level_name[Level1]= "Any";
            };
    
            ${mode}type "TWO_LEVEL" {
            	modifiers = Shift;
            	map[Shift] = Level2;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            };
    
            ${mode}type "ALPHABETIC" {
            	modifiers = Shift+Lock;
            	map[Shift] = Level2;
            	map[Lock] = Level2;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Caps";
            };
    
            ${mode}type "KEYPAD" {
            	modifiers = Shift+NumLock;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[NumLock] = Level2;
            	map[Shift+NumLock] = Level1;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Number";
            };
    
            ${mode}type "FOUR_LEVEL" {
            	modifiers = Shift+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "FOUR_LEVEL_ALPHABETIC" {
            	modifiers = Shift+Lock+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[Lock]  = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	map[Lock+LevelThree] =  Level4;
            	map[Lock+Shift+LevelThree] =  Level3;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" {
            	modifiers = Shift+Lock+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[Lock]  = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	map[Lock+LevelThree] =  Level3;
            	map[Lock+Shift+LevelThree] = Level4;
            	preserve[Lock+LevelThree] = Lock;
            	preserve[Lock+Shift+LevelThree] = Lock;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "XXX" {
            	modifiers = Shift+Lock+LevelThree;
                map[None]       = 1;
                map[Shift]      = 2;
                map[Lock]       = 2;
                map[LevelThree] = 3;
                level_name[1] = "1";
                level_name[2] = "2";
                level_name[3] = "3";
            };
            """
        ),
        update_template=ComponentTemplate(
            """\
            ${mode}virtual_modifiers NumLock=Mod2;    // Changed: now mapped
            ${mode}virtual_modifiers LevelThree=Mod5; // Changed: altered mapping (from 0)
            ${mode}virtual_modifiers LevelFive=Mod3;  // Unhanged: same mapping
    
            ${mode}virtual_modifiers U7, Z7=0, M7=0x700000; // Changed: new
    
            ${mode}virtual_modifiers U1;            // Unchanged (unmapped)
            ${mode}virtual_modifiers U2 = none;     // Changed: now mapped
            ${mode}virtual_modifiers U3 = 0x300000; // Changed: now mapped
            // ${mode}virtual_modifiers U4;            // Unchanged (unmapped)
            // ${mode}virtual_modifiers U5 = none;     // Changed: now mapped
            // ${mode}virtual_modifiers U6 = 0x600000; // Changed: now mapped
            ${mode}virtual_modifiers Z1;            // Unchanged (= 0)
            ${mode}virtual_modifiers Z2 = none;     // Unchanged (same mapping)
            ${mode}virtual_modifiers Z3 = 0x310000; // Changed: altered mapping (from 0)
            // ${mode}virtual_modifiers Z4;            // Unchanged (= 0)
            // ${mode}virtual_modifiers Z5 = none;     // Unchanged (same mapping)
            // ${mode}virtual_modifiers Z6 = 0x610000; // Changed: altered mapping (from 0)
            ${mode}virtual_modifiers M1;            // Unchanged (≠ 0)
            ${mode}virtual_modifiers M2 = none;     // Changed: reset
            ${mode}virtual_modifiers M3 = 0x320000; // Changed: altered mapping (from ≠ 0)
            // ${mode}virtual_modifiers M4;            // Unchanged (≠ 0)
            // ${mode}virtual_modifiers M5 = none;     // Changed: reset
            // ${mode}virtual_modifiers M6 = 0x620000; // Changed: altered mapping (from ≠ 0)
    
            ${mode}type "ONE_LEVEL" {
            	modifiers = None;
            	map[None] = Level1;
            	level_name[Level1]= "New"; // Change name
            };
    
            ${mode}type "TWO_LEVEL" {
            	modifiers = Shift+M1; // Changed
            	map[Shift] = Level2;
            	map[M1] = Level2; // Changed
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            };
    
            ${mode}type "ALPHABETIC" {
            	modifiers = Lock; // Changed
            	map[None] = Level2; // Changed
            	map[Lock] = Level1; // Changed
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Caps";
            };
    
            ${mode}type "KEYPAD" {
            	modifiers = Shift+NumLock;
            	map[None] = Level1;
            	map[Shift] = Level2; // Changed
            	map[NumLock] = Level2;
            	map[Shift+NumLock] = Level1;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Number";
            };
    
            // Unchanged
            ${mode}type "FOUR_LEVEL" {
            	modifiers = Shift+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "FOUR_LEVEL_ALPHABETIC" {
            	modifiers = Shift+Lock+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[Lock]  = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	map[Lock+LevelThree] =  Level4;
            	map[Lock+Shift+LevelThree] =  Level3;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" {
            	modifiers = Shift+Lock+LevelThree;
            	map[None] = Level1;
            	map[Shift] = Level2;
            	map[Lock]  = Level2;
            	map[LevelThree] = Level3;
            	map[Shift+LevelThree] = Level4;
            	map[Lock+LevelThree] =  Level3;
            	map[Lock+Shift+LevelThree] = Level4;
            	preserve[Lock+LevelThree] = Lock;
            	preserve[Lock+Shift+LevelThree] = Lock;
            	level_name[Level1] = "Base";
            	level_name[Level2] = "Shift";
            	level_name[Level3] = "Alt Base";
            	level_name[Level4] = "Shift Alt";
            };
    
            ${mode}type "XXX" {
            	modifiers = Shift+Lock+LevelThree;
                map[None]       = 1;
                map[Shift]      = 2;
                // map[Lock]       = 2;    // Changed
                // map[LevelThree] = 3;
                map[LevelThree] = 1;       // Changed
                map[Shift+LevelThree] = 4; // Changed
                level_name[1] = "A"; // Changed
                level_name[2] = "2";
                level_name[3] = "3";
                level_name[4] = "4"; // Changed
            };
    
            // New
            ${mode}type "YYY" {
            	modifiers = None;
            	map[None] = Level1;
            	level_name[Level1]= "New";
            };
            """
        ),
        augment="""
            virtual_modifiers NumLock=Mod2,LevelThree=none,LevelFive=Mod3;
    
            virtual_modifiers U1,U2=0,U3=0x300000;
            // virtual_modifiers U4,U5=0,U6=0x600000;
            virtual_modifiers Z1=0,Z2=0,Z3=0;
            // virtual_modifiers Z4=0,Z5=0,Z6=0;
            virtual_modifiers M1=0x1000,M2=0x2000,M3=0x3000;
            // virtual_modifiers M4=0x4000,M5=0x5000,M6=0x6000;
            virtual_modifiers U7,Z7=0,M7=0x700000;
    
            type "ONE_LEVEL" {
            	modifiers= none;
                map[none]= 1;
            	level_name[1]= "Any";
            };
            type "TWO_LEVEL" {
            	modifiers= Shift;
            	map[Shift]= 2;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            };
            type "ALPHABETIC" {
            	modifiers= Shift+Lock;
            	map[Shift]= 2;
            	map[Lock]= 2;
            	level_name[1]= "Base";
            	level_name[2]= "Caps";
            };
            type "KEYPAD" {
            	modifiers= Shift+NumLock;
            	map[Shift]= 2;
            	map[NumLock]= 2;
            	level_name[1]= "Base";
            	level_name[2]= "Number";
            };
            type "FOUR_LEVEL" {
            	modifiers= Shift+LevelThree;
                map[none]= 1;
            	map[Shift]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree]= 4;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "FOUR_LEVEL_ALPHABETIC" {
            	modifiers= Shift+Lock+LevelThree;
                map[none]= 1;
            	map[Shift]= 2;
            	map[Lock]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree]= 4;
            	map[Lock+LevelThree]= 4;
            	map[Shift+Lock+LevelThree]= 3;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "FOUR_LEVEL_SEMIALPHABETIC" {
            	modifiers= Shift+Lock+LevelThree;
            	map[None]= 1;
            	map[Shift]= 2;
            	map[Lock]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree]= 4;
            	map[Lock+LevelThree]= 3;
            	preserve[Lock+LevelThree]= Lock;
            	map[Shift+Lock+LevelThree]= 4;
            	preserve[Shift+Lock+LevelThree]= Lock;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "XXX" {
            	modifiers = Shift+Lock+LevelThree;
                map[None]       = 1;
                map[Shift]      = 2;
                map[Lock]       = 2;
                map[LevelThree] = 3;
                level_name[1] = "1";
                level_name[2] = "2";
                level_name[3] = "3";
            };
            type "YYY" {
            	modifiers = None;
            	map[None] = Level1;
            	level_name[Level1]= "New";
            };
            """,
        override="""
            virtual_modifiers NumLock=Mod2,LevelThree=Mod5,LevelFive=Mod3;
    
            virtual_modifiers U1,U2=0,U3=0x300000;
            // virtual_modifiers U4,U5=0,U6=0x600000;
            virtual_modifiers Z1=0,Z2=0,Z3=0x310000;
            // virtual_modifiers Z4=0,Z5=0,Z6=0x610000;
            virtual_modifiers M1=0x1000,M2=0,M3=0x320000;
            // virtual_modifiers M4=0x4000,M5=0,M6=0x620000;
            virtual_modifiers U7,Z7=0,M7=0x700000;
    
            type "ONE_LEVEL" {
            	modifiers= none;
            	map[None] = Level1;
            	level_name[1]= "New";
            };
            type "TWO_LEVEL" {
            	modifiers= Shift+M1;
            	map[Shift]= 2;
            	map[M1]= 2;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            };
            type "ALPHABETIC" {
            	modifiers= Lock;
            	map[none]= 2;
            	map[Lock]= 1;
            	level_name[1]= "Base";
            	level_name[2]= "Caps";
            };
            type "KEYPAD" {
            	modifiers= Shift+NumLock;
            	map[Shift] = Level2;
            	map[NumLock]= 2;
            	level_name[1]= "Base";
            	level_name[2]= "Number";
            };
            type "FOUR_LEVEL" {
            	modifiers= Shift+LevelThree;
            	map[None]= 1;
            	map[Shift]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree] = Level4;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "FOUR_LEVEL_ALPHABETIC" {
            	modifiers= Shift+Lock+LevelThree;
            	map[Shift]= 2;
            	map[Lock]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree]= 4;
            	map[Lock+LevelThree]= 4;
            	map[Shift+Lock+LevelThree]= 3;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "FOUR_LEVEL_SEMIALPHABETIC" {
            	modifiers= Shift+Lock+LevelThree;
            	map[None]= 1;
            	map[Shift]= 2;
            	map[Lock]= 2;
            	map[LevelThree]= 3;
            	map[Shift+LevelThree]= 4;
            	map[Lock+LevelThree]= 3;
            	preserve[Lock+LevelThree]= Lock;
            	map[Shift+Lock+LevelThree]= 4;
            	preserve[Shift+Lock+LevelThree]= Lock;
            	level_name[1]= "Base";
            	level_name[2]= "Shift";
            	level_name[3]= "Alt Base";
            	level_name[4]= "Shift Alt";
            };
            type "XXX" {
            	modifiers = Shift+Lock+LevelThree;
                map[None]       = 1;
                map[Shift]      = 2;
                // map[LevelThree] = 3;
                map[LevelThree] = 1;
                map[Shift+LevelThree] = 4;
                level_name[1] = "A";
                level_name[2] = "2";
                level_name[3] = "3";
                level_name[4] = "4";
            };
            type "YYY" {
            	modifiers = None;
            	map[None] = Level1;
            	level_name[Level1]= "New";
            };
            """,
    )
    
    
    ################################################################################
    #
    # Compat tests
    #
    ################################################################################
    
    COMPAT_TESTS = ComponentTestTemplate(
        "Compat",
        component=Component.compat,
        base_template=ComponentTemplate(
            """
            ${mode}virtual_modifiers NumLock;
    
            interpret.repeat= False;
            setMods.clearLocks= True;
            latchMods.clearLocks= True;
            latchMods.latchToLock= True;
    
            ${mode}interpret Any + Any {
            	action= SetMods(modifiers=modMapMods);
            };
    
            ${mode}interpret Caps_Lock {
            	action = LockMods(modifiers = Lock);
            };
    
            ${mode}indicator "Caps Lock" {
            	!allowExplicit;
            	whichModState= Locked;
            	modifiers= Lock;
            };
    
            ${mode}indicator "Num Lock" {
            	!allowExplicit;
            	whichModState= Locked;
            	modifiers= NumLock;
            };
            """
        ),
        update_template=ComponentTemplate(
            """
            ${mode}virtual_modifiers NumLock;
    
            ${mode}interpret.repeat= False;
            ${mode}setMods.clearLocks= False; // Changed
    
            // Unchanged
            ${mode}interpret Any + Any {
            	action= SetMods(modifiers=modMapMods);
            };
    
            // Changed
            ${mode}interpret Caps_Lock {
            	action = LockMods(modifiers = NumLock);
            };
    
            // Unchanged
            ${mode}indicator "Caps Lock" {
            	!allowExplicit;
            	whichModState= Locked;
            	modifiers= Lock;
            };
    
            // Changed
            ${mode}indicator "Num Lock" {
            	!allowExplicit;
            	whichModState= Base;
            	modifiers= Lock;
            };
    
            // New
            ${mode}indicator "Kana" {
            	!allowExplicit;
            	whichModState= Locked;
            	modifiers= Control;
            };
            """
        ),
        augment="""
            virtual_modifiers NumLock;
    
            interpret.useModMapMods= AnyLevel;
            interpret.repeat= False;
            interpret Caps_Lock+AnyOfOrNone(all) {
            	action= LockMods(modifiers=Lock);
            };
            interpret Any+AnyOf(all) {
            	action= SetMods(modifiers=modMapMods,clearLocks);
            };
            indicator "Caps Lock" {
            	whichModState= locked;
            	modifiers= Lock;
            };
            indicator "Num Lock" {
            	whichModState= locked;
            	modifiers= NumLock;
            };
            indicator "Kana" {
            	whichModState= locked;
            	modifiers= Control;
    	    };
            """,
        override="""
            virtual_modifiers NumLock;
    
            interpret.repeat= False;
            setMods.clearLocks= False;
            interpret Caps_Lock+AnyOfOrNone(all) {
            	action= LockMods(modifiers=NumLock);
            };
            interpret Any+AnyOf(all) {
            	action= SetMods(modifiers=modMapMods);
            };
            indicator "Caps Lock" {
            	whichModState= locked;
            	modifiers= Lock;
            };
            indicator "Kana" {
            	whichModState= locked;
            	modifiers= Control;
            };
            indicator "Num Lock" {
            	whichModState= base;
            	modifiers= Lock;
            };
            """,
    )
    
    
    ################################################################################
    #
    # Symbols tests
    #
    ################################################################################
    
    
    LINUX_EVENT_CODES = (
        None,
        "ESC",
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "0",
        "MINUS",
        "EQUAL",
        "BACKSPACE",
        "TAB",
        "Q",
        "W",
        "E",
        "R",
        "T",
        "Y",
        "U",
        "I",
        "O",
        "P",
        "LEFTBRACE",
        "RIGHTBRACE",
        "ENTER",
        "LEFTCTRL",
        "A",
        "S",
        "D",
        "F",
        "G",
        "H",
        "J",
        "K",
        "L",
        "SEMICOLON",
        "APOSTROPHE",
        "GRAVE",
        "LEFTSHIFT",
        "BACKSLASH",
        "Z",
        "X",
        "C",
        "V",
        "B",
        "N",
        "M",
        "COMMA",
        "DOT",
        "SLASH",
        "RIGHTSHIFT",
        "KPASTERISK",
        "LEFTALT",
        "SPACE",
        "CAPSLOCK",
        "F1",
        "F2",
        "F3",
        "F4",
        "F5",
        "F6",
        "F7",
        "F8",
        "F9",
        "F10",
        "NUMLOCK",
        "SCROLLLOCK",
        "KP7",
        "KP8",
        "KP9",
        "KPMINUS",
        "KP4",
        "KP5",
        "KP6",
        "KPPLUS",
        "KP1",
        "KP2",
        "KP3",
        "KP0",
        "KPDOT",
        None,
        "ZENKAKUHANKAKU",
        "102ND",
        "F11",
        "F12",
        "RO",
        "KATAKANA",
        "HIRAGANA",
        "HENKAN",
        "KATAKANAHIRAGANA",
        "MUHENKAN",
        "KPJPCOMMA",
        "KPENTER",
        "RIGHTCTRL",
        "KPSLASH",
        "SYSRQ",
        "RIGHTALT",
        "LINEFEED",
        "HOME",
        "UP",
        "PAGEUP",
        "LEFT",
        "RIGHT",
        "END",
        "DOWN",
        "PAGEDOWN",
        "INSERT",
        "DELETE",
        "MACRO",
        "MUTE",
        "VOLUMEDOWN",
        "VOLUMEUP",
        "POWER",
        "KPEQUAL",
        "KPPLUSMINUS",
        "PAUSE",
        "SCALE",
        "KPCOMMA",
        "HANGEUL",
        "HANJA",
        "YEN",
        "LEFTMETA",
        "RIGHTMETA",
        "COMPOSE",
        "STOP",
        "AGAIN",
        "PROPS",
        "UNDO",
        "FRONT",
        "COPY",
        "OPEN",
        "PASTE",
        "FIND",
        "CUT",
        "HELP",
        "MENU",
        "CALC",
        "SETUP",
        "SLEEP",
        "WAKEUP",
        "FILE",
        "SENDFILE",
        "DELETEFILE",
        "XFER",
        "PROG1",
        "PROG2",
        "WWW",
        "MSDOS",
        "COFFEE",
        "DIRECTION",
        "CYCLEWINDOWS",
        "MAIL",
        "BOOKMARKS",
        "COMPUTER",
        "BACK",
        "FORWARD",
        "CLOSECD",
        "EJECTCD",
        "EJECTCLOSECD",
        "NEXTSONG",
        "PLAYPAUSE",
        "PREVIOUSSONG",
        "STOPCD",
        "RECORD",
        "REWIND",
        "PHONE",
        "ISO",
        "CONFIG",
        "HOMEPAGE",
        "REFRESH",
        "EXIT",
        "MOVE",
        "EDIT",
        "SCROLLUP",
        "SCROLLDOWN",
        "KPLEFTPAREN",
        "KPRIGHTPAREN",
        "NEW",
        "REDO",
        "F13",
        "F14",
        "F15",
        "F16",
        "F17",
        "F18",
        "F19",
        "F20",
        "F21",
        "F22",
        "F23",
        "F24",
        None,
        None,
        None,
        None,
        None,
        "PLAYCD",
        "PAUSECD",
        "PROG3",
        "PROG4",
        "DASHBOARD",
        "SUSPEND",
        "CLOSE",
        "PLAY",
        "FASTFORWARD",
        "BASSBOOST",
        "PRINT",
        "HP",
        "CAMERA",
        "SOUND",
        "QUESTION",
    )
    
    Comment = NewType("Comment", str)
    
    
    @dataclass(frozen=True)
    class KeyCode:
        _evdev: str
        _xkb: str
    
        @property
        def c(self) -> str:
            return f"KEY_{self._evdev}"
    
        @property
        def xkb(self) -> str:
            return f"<{self._xkb}>"
    
    
    class Keysym(str):
        @property
        def c(self) -> str:
            return f"XKB_KEY_{self}"
    
        @classmethod
        def parse(cls, raw: str | None) -> Self:
            if not raw:
                return cls("NoSymbol")
            else:
                return cls(raw)
    
    
    NoSymbol = Keysym("NoSymbol")
    
    
    class Modifier(IntFlag):
        NoModifier = 0
        Shift = 1 << 0
        Lock = 1 << 1
        Control = 1 << 2
        Mod1 = 1 << 3
        Mod2 = 1 << 4
        Mod3 = 1 << 5
        Mod4 = 1 << 6
        Mod5 = 1 << 7
        LevelThree = Mod5
    
        def __iter__(self) -> Iterator[Self]:
            for m in self.__class__:
                if m & self:
                    yield m
    
        def __str__(self) -> str:
            return "+".join(m.name for m in self)
    
    
    class Action(metaclass=ABCMeta):
        @abstractmethod
        def __bool__(self) -> bool: ...
    
        @classmethod
        def parse(cls, raw: Any) -> Self:
            if raw is None:
                return GroupAction.parse(None)
            elif isinstance(raw, Modifier):
                return ModAction.parse(raw)
            elif isinstance(raw, int):
                return GroupAction.parse(raw)
            else:
                raise ValueError(raw)
    
        @abstractmethod
        def action_to_keysym(self, index: int, level: int) -> Keysym: ...
    
    
    @dataclass
    class GroupAction(Action):
        """
        SetGroup or NoAction
        """
    
        group: int
        keysyms: ClassVar[dict[tuple[int, int, int], Keysym]] = {
            (2, 0, 0): Keysym("a"),
            (2, 0, 1): Keysym("A"),
            (2, 1, 0): Keysym("b"),
            (2, 1, 1): Keysym("B"),
            (3, 0, 0): Keysym("Greek_alpha"),
            (3, 0, 1): Keysym("Greek_ALPHA"),
            (3, 1, 0): Keysym("Greek_beta"),
            (3, 1, 1): Keysym("Greek_BETA"),
        }
    
        def __str__(self) -> str:
            if self.group > 0:
                return f"SetGroup(group={self.group})"
            else:
                return "NoAction()"
    
        def __bool__(self) -> bool:
            return bool(self.group)
    
        @classmethod
        def parse(cls, raw: int | None) -> Self:
            if not raw:
                return cls(0)
            else:
                return cls(raw)
    
        def action_to_keysym(self, index: int, level: int) -> Keysym:
            if not self.group:
                return NoSymbol
            else:
                if (
                    keysym := self.keysyms.get((self.group % 4, index % 2, level % 2))
                ) is None:
                    raise ValueError((self, index, level))
                return keysym
    
    
    @dataclass
    class ModAction(Action):
        """
        SetMod or NoAction
        """
    
        mods: Modifier
        keysyms: ClassVar[dict[tuple[Modifier, int, int], Keysym]] = {
            (Modifier.Control, 0, 0): Keysym("x"),
            (Modifier.Control, 0, 1): Keysym("X"),
            (Modifier.Control, 1, 0): Keysym("y"),
            (Modifier.Control, 1, 1): Keysym("Y"),
            (Modifier.Mod5, 0, 0): Keysym("Greek_xi"),
            (Modifier.Mod5, 0, 1): Keysym("Greek_XI"),
            (Modifier.Mod5, 1, 0): Keysym("Greek_upsilon"),
            (Modifier.Mod5, 1, 1): Keysym("Greek_UPSILON"),
        }
    
        def __str__(self) -> str:
            if self.mods is Modifier.NoModifier:
                return "NoAction()"
            else:
                return f"SetMods(mods={self.mods})"
    
        def __bool__(self) -> bool:
            return self.mods is Modifier.NoModifier
    
        @classmethod
        def parse(cls, raw: Modifier | None) -> Self:
            if not raw:
                return cls(Modifier.NoModifier)
            else:
                return cls(raw)
    
        def action_to_keysym(self, index: int, level: int) -> Keysym:
            if self.mods is Modifier.NoModifier:
                return NoSymbol
            else:
                if (keysym := self.keysyms.get((self.mods, index % 2, level % 2))) is None:
                    raise ValueError((self, index, level))
                return keysym
    
    
    @dataclass
    class Level:
        keysyms: tuple[Keysym, ...]
        actions: tuple[Action, ...]
    
        @staticmethod
        def _c(default: str, xs: Iterable[Any]) -> str:
            match len(xs):
                case 0:
                    return default
                case 1:
                    return xs[0].c
                case _:
                    return ", ".join(map(lambda x: x.c, xs))
    
        @staticmethod
        def _xkb(default: str, xs: Iterable[Any]) -> str:
            match len(xs):
                case 0:
                    return default
                case 1:
                    return str(xs[0])
                case _:
                    return "{" + ", ".join(map(str, xs)) + "}"
    
        @classmethod
        def has_empty_symbols(cls, keysyms: tuple[Keysym, ...]) -> bool:
            return all(ks == NoSymbol for ks in keysyms)
    
        @property
        def empty_symbols(self) -> bool:
            return self.has_empty_symbols(self.keysyms)
    
        @property
        def keysyms_c(self) -> str:
            if not self.keysyms and self.actions:
                return self._c(
                    NoSymbol.c, tuple(itertools.repeat(NoSymbol, len(self.actions)))
                )
            return self._c(NoSymbol.c, self.keysyms)
    
        @property
        def keysyms_xkb(self) -> str:
            return self._xkb(NoSymbol, self.keysyms)
    
        @classmethod
        def has_empty_actions(cls, actions: tuple[Action, ...]) -> bool:
            return not any(actions)
    
        @property
        def empty_actions(self) -> bool:
            return self.has_empty_actions(self.actions)
    
        @property
        def actions_xkb(self) -> str:
            return self._xkb("NoAction()", self.actions)
    
        @classmethod
        def Keysyms(cls, *keysyms: str | None) -> Self:
            return cls.Mix(keysyms, ())
    
        @classmethod
        def Actions(cls, *actions: int | Modifier | None) -> Self:
            return cls.Mix((), actions)
    
        @classmethod
        def Mix(
            cls, keysyms: tuple[str | None, ...], actions: tuple[int | Modifier | None,]
        ) -> Self:
            return cls(tuple(map(Keysym.parse, keysyms)), tuple(map(Action.parse, actions)))
    
        def add_keysyms(self, keep_actions: bool, level: int) -> Self:
            return self.__class__(
                keysyms=tuple(
                    a.action_to_keysym(index=k, level=level)
                    for k, a in enumerate(self.actions)
                ),
                actions=self.actions if keep_actions else (),
            )
    
        @property
        def target_group(self) -> int:
            for a in self.actions:
                if isinstance(a, GroupAction) and a.group > 1:
                    return a.group
            else:
                return 0
    
        @property
        def target_level(self) -> int:
            for a in self.actions:
                if isinstance(a, ModAction) and a.mods:
                    match a.mods:
                        case Modifier.LevelThree:
                            return 2
                        case _:
                            return 0
            else:
                return 0
    
    
    @dataclass
    class KeyEntry:
        levels: tuple[Level, ...]
    
        def __init__(self, *levels: Level):
            self.levels = levels
    
        @property
        def xkb(self) -> Iterator[str]:
            if not self.levels:
                yield ""
                return
            keysyms = tuple(l.keysyms for l in self.levels)
            has_keysyms = any(not Level.has_empty_symbols(s) for s in keysyms)
            no_keysyms = all(not s for s in keysyms)
            actions = tuple(l.actions for l in self.levels)
            has_actions = any(not Level.has_empty_actions(a) for a in actions)
            if has_keysyms or (not no_keysyms and not has_actions):
                yield "["
                yield ", ".join(l.keysyms_xkb for l in self.levels)
                yield "]"
            if has_actions or no_keysyms:
                if has_keysyms:
                    yield ", "
                yield "["
                yield ", ".join(l.actions_xkb for l in self.levels)
                yield "]"
    
        def add_keysyms(self, keep_actions: bool) -> Self:
            return self.__class__(
                *(
                    l.add_keysyms(keep_actions=keep_actions, level=k)
                    for k, l in enumerate(self.levels)
                )
            )
    
    
    class TestType(IntFlag):
        KeysymsOnly = auto()
        ActionsOnly = auto()
        KeysymsAndActions = auto()
        All = ActionsOnly | KeysymsOnly | KeysymsAndActions
    
    
    @dataclass
    class TestId:
        type: TestType
        base: int = 0
        _last_base: ClassVar[int] = 0
        _max_type: ClassVar[int] = TestType.KeysymsAndActions >> 1
        _forbidden_keys: tuple[str, ...] = ("LEFTSHIFT", "RIGHTALT")
    
        def __post_init__(self):
            if self.base == -1:
                self.base = self.__class__._last_base
            elif self.base < 0:
                raise ValueError(self.base)
            elif self.base == 0:
                self.__class__._last_base += 1
                self.base = self.__class__._last_base
                while not self.check_linux_keys(self._base_id):
                    self.__class__._last_base += 1
                    self.base = self.__class__._last_base
            else:
                self.__class__._last_base = max(self.__class__._last_base, self.base)
            assert self.linux_key, self
            # print(self.base, self.id, self.xkb_key)
    
        def __hash__(self) -> int:
            return self.id
    
        @property
        def _base_id(self) -> int:
            return (self.base - 1) * (self._max_type + 1) + 1
    
        @property
        def id(self) -> int:
            # return (self.base << 2) | (self.type >> 1)
            return self._base_id + (self.type >> 1)
    
        def with_type(self, type: TestType) -> Self:
            return dataclasses.replace(self, type=type)
    
        @property
        def xkb_key(self) -> str:
            return f"T{self.id:0>3}"
    
        @classmethod
        def check_linux_key(cls, idx: int) -> bool:
            if idx >= len(LINUX_EVENT_CODES):
                raise ValueError("Not enough Linux keys available!")
            key: str | None = LINUX_EVENT_CODES[idx]
            return bool(key) and key not in cls._forbidden_keys
    
        @classmethod
        def check_linux_keys(cls, base_id) -> bool:
            return all(
                cls.check_linux_key(base_id + k) for k in range(0, cls._max_type + 1)
            )
    
        @property
        def linux_key(self) -> str:
            if self.id >= len(LINUX_EVENT_CODES):
                raise ValueError("Not enough Linux keys available!")
            return LINUX_EVENT_CODES[self.id]
    
        @property
        def key(self) -> KeyCode:
            return KeyCode(self.linux_key, self.xkb_key)
    
    
    @unique
    class Implementation(Flag):
        x11 = auto()
        xkbcommon = auto()
        all = x11 | xkbcommon
    
    
    @dataclass
    class TestEntry:
        id: TestId
        key: KeyCode = dataclasses.field(init=False)
        base: KeyEntry
        update: KeyEntry
        augment: KeyEntry
        override: KeyEntry
        replace: KeyEntry
        types: TestType
        implementations: Implementation
    
        group_keysyms: ClassVar[tuple[tuple[str, str], ...]] = (
            ("Ukrainian_i", "Ukrainian_I", "Ukrainian_yi", "Ukrainian_YI"),
            ("ch", "Ch", "c_h", "C_h"),
        )
    
        def __init__(
            self,
            id: TestId | None,
            base: KeyEntry,
            update: KeyEntry,
            augment: KeyEntry,
            override: KeyEntry,
            types: TestType = TestType.All,
            replace: KeyEntry | None = None,
            implementations: Implementation = Implementation.all,
        ):
            self.id = TestId(0, 0) if id is None else id
            self.key = id.key
            self.base = base
            self.update = update
            self.augment = augment
            self.override = override
            self.types = types
            self.replace = self.update if replace is None else replace
            self.implementations = implementations
    
        @property
        def default(self) -> KeyEntry:
            return self.override
    
        def add_keysyms(self, keep_actions: bool) -> Self:
            types = TestType.KeysymsAndActions if keep_actions else TestType.KeysymsOnly
            return dataclasses.replace(
                self,
                id=self.id.with_type(types),
                base=self.base.add_keysyms(keep_actions=keep_actions),
                update=self.update.add_keysyms(keep_actions=keep_actions),
                augment=self.augment.add_keysyms(keep_actions=keep_actions),
                override=self.override.add_keysyms(keep_actions=keep_actions),
                replace=self.replace.add_keysyms(keep_actions=keep_actions),
                types=types,
            )
    
        @classmethod
        def alt_keysym(cls, group: int, level: int) -> Keysym:
            return Keysym(cls.group_keysyms[group % 2][level % 4])
    
        @classmethod
        def alt_keysyms(cls, group: int) -> Iterator[Keysym]:
            for keysym in cls.group_keysyms[group % 2]:
                yield Keysym(keysym)
    
    
    @dataclass
    class SymbolsTestGroup:
        name: str
        tests: tuple[TestEntry | Comment, ...]
    
        def _with_implementation(
            self, implementation: Implementation
        ) -> Iterable[TestEntry | Comment]:
            pending_comment: Comment | None = None
            for t in self.tests:
                if not isinstance(t, TestEntry):
                    pending_comment = t
                elif t.implementations & implementation:
                    if pending_comment is not None:
                        yield pending_comment
                        pending_comment = None
                    yield t
    
        def with_implementation(self, implementation: Implementation) -> Self:
            return dataclasses.replace(
                self, tests=tuple(self._with_implementation(implementation))
            )
    
        @staticmethod
        def _add_keysyms(entry: TestEntry | Comment) -> Iterable[TestEntry | Comment]:
            if isinstance(entry, TestEntry) and entry.id.type is TestType.ActionsOnly:
                if entry.types & TestType.KeysymsOnly:
                    yield entry.add_keysyms(keep_actions=False)
                yield entry
                if entry.types & TestType.KeysymsAndActions:
                    yield entry.add_keysyms(keep_actions=True)
            else:
                yield entry
    
        def add_keysyms(self, name: str = "") -> Self:
            return dataclasses.replace(
                self,
                name=name or self.name,
                tests=tuple(t for ts in self.tests for t in self._add_keysyms(ts)),
            )
    
        def __add__(self, other: Any) -> Self:
            if isinstance(other, tuple):
                return dataclasses.replace(self, tests=self.tests + other)
            elif isinstance(other, self.__class__):
                return dataclasses.replace(self, tests=self.tests + other.tests)
            else:
                return NotImplemented
    
        def get_keycodes(self, base: ComponentTestTemplate) -> ComponentTestTemplate:
            ids: set[TestId] = set(t.id for t in self.tests if isinstance(t, TestEntry))
    
            base_template = Template(
                "\n".join(f"${{mode}}<{id.xkb_key}> = {id.id + 8};" for id in ids) + "\n"
            )
            keycodes = base_template.substitute(mode="")
    
            return dataclasses.replace(
                base,
                base_template=base_template + base.base_template,
                update_template=base.update_template,
                augment=keycodes + base.augment,
                override=keycodes + base.override,
                replace=keycodes + base.replace,
            )
    
        def _get_symbols(self, type: str, debug: bool) -> Generator[str]:
            for entry in self.tests:
                if isinstance(entry, str):
                    if type == "update":
                        yield f"\n////// {entry} //////"
                else:
                    data: KeyEntry = getattr(entry, type)
                    if debug:
                        yield f"// base:     {''.join(entry.base.xkb) or '(empty)'}"
                        yield "// key to merge / replace result"
                    yield f"${{mode}}key {entry.key.xkb} {{ {''.join(data.xkb)} }};"
                    if debug:
                        yield f"// override: {''.join(entry.override.xkb) or '(empty)'}"
                        yield f"// augment:  {''.join(entry.augment.xkb) or '(empty)'}"
    
        def get_symbols(self, debug: bool = False) -> ComponentTestTemplate:
            base_template = ComponentTemplate("\n".join(self._get_symbols("base", False)))
            update_template = ComponentTemplate(
                "\n".join(self._get_symbols("update", debug))
            )
            augment = ComponentTemplate(
                "\n".join(self._get_symbols("augment", False))
            ).substitute(mode="")
            override = ComponentTemplate(
                "\n".join(self._get_symbols("override", False))
            ).substitute(mode="")
            replace = ComponentTemplate(
                "\n".join(self._get_symbols("replace", False))
            ).substitute(mode="")
            return ComponentTestTemplate(
                title=self.name,
                component=Component.symbols,
                base_template=base_template,
                update_template=update_template,
                augment=augment,
                override=override,
                replace=replace,
            )
    
    
    SYMBOLS_TESTS_BOTH = SymbolsTestGroup(
        "",
        (
            Comment("Trivial cases"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(),
                update=KeyEntry(),
                augment=KeyEntry(),
                override=KeyEntry(),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(),
                update=KeyEntry(Level.Actions(3)),
                augment=KeyEntry(Level.Actions(3)),
                override=KeyEntry(Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2)),
                update=KeyEntry(),
                augment=KeyEntry(Level.Actions(2)),
                override=KeyEntry(Level.Actions(2)),
            ),
            Comment("Same key"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2)),
                update=KeyEntry(Level.Actions(2)),
                augment=KeyEntry(Level.Actions(2)),
                override=KeyEntry(Level.Actions(2)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                override=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Mismatch levels count"),
            (
                TEST_BOTH_Q := TestEntry(
                    TestId(TestType.ActionsOnly),
                    KeyEntry(Level.Actions(None), Level.Actions(2)),
                    update=KeyEntry(
                        Level.Actions(3), Level.Actions(None), Level.Actions(None)
                    ),
                    augment=KeyEntry(
                        Level.Actions(3), Level.Actions(2), Level.Actions(None)
                    ),
                    override=KeyEntry(
                        Level.Actions(3), Level.Actions(2), Level.Actions(None)
                    ),
                    # X11 and xkbcommon handle keysyms-only case differently
                    types=TestType.ActionsOnly | TestType.KeysymsAndActions,
                )
            ),
            # Trailing NoSymbols are discarded in xkbcomp
            dataclasses.replace(
                TEST_BOTH_Q,
                augment=KeyEntry(Level.Actions(3), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(2)),
                replace=KeyEntry(Level.Actions(3)),
                implementations=Implementation.x11,
            ).add_keysyms(keep_actions=False),
            dataclasses.replace(
                TEST_BOTH_Q, implementations=Implementation.xkbcommon
            ).add_keysyms(keep_actions=False),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)),
                override=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)),
                # X11 and xkbcommon handle keysyms-only case differently
                types=TestType.ActionsOnly | TestType.KeysymsAndActions,
            ),
            (
                TEST_BOTH_W := TestEntry(
                    TestId(TestType.ActionsOnly),
                    KeyEntry(Level.Actions(None), Level.Actions(2), Level.Actions(None)),
                    update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                    augment=KeyEntry(
                        Level.Actions(3), Level.Actions(2), Level.Actions(None)
                    ),
                    override=KeyEntry(
                        Level.Actions(3), Level.Actions(2), Level.Actions(None)
                    ),
                    # X11 and xkbcommon handle keysyms-only case differently
                    types=TestType.ActionsOnly | TestType.KeysymsAndActions,
                )
            ),
            dataclasses.replace(
                TEST_BOTH_W,
                augment=KeyEntry(Level.Actions(3), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(2)),
                replace=KeyEntry(Level.Actions(3)),
                implementations=Implementation.x11,
            ).add_keysyms(keep_actions=False),
            dataclasses.replace(
                TEST_BOTH_W, implementations=Implementation.xkbcommon
            ).add_keysyms(keep_actions=False),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(3)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(2)),
            ),
            Comment("Single keysyms -> single keysyms"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(None), Level.Actions(None)),
                override=KeyEntry(Level.Actions(None), Level.Actions(None)),
            ),
            (
                TEST_BOTH_Y := TestEntry(
                    TestId(TestType.ActionsOnly),
                    KeyEntry(Level.Actions(None), Level.Actions(None)),
                    update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                    augment=KeyEntry(Level.Actions(3), Level.Actions(None)),
                    override=KeyEntry(Level.Actions(3), Level.Actions(None)),
                    # X11 and xkbcommon handle keysyms-only case differently
                    types=TestType.ActionsOnly | TestType.KeysymsAndActions,
                )
            ),
            dataclasses.replace(
                TEST_BOTH_Y,
                augment=KeyEntry(Level.Actions(3)),
                override=KeyEntry(Level.Actions(3)),
                replace=KeyEntry(Level.Actions(3)),
                implementations=Implementation.x11,
            ).add_keysyms(keep_actions=False),
            dataclasses.replace(
                TEST_BOTH_Y, implementations=Implementation.xkbcommon
            ).add_keysyms(keep_actions=False),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(None), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(None), Level.Actions(3)),
                override=KeyEntry(Level.Actions(None), Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(3), Level.Actions(3)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(2), Level.Actions(2)),
            ),
            (
                TEST_BOTH_P := TestEntry(
                    TestId(TestType.ActionsOnly),
                    KeyEntry(Level.Actions(2), Level.Actions(2)),
                    update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                    augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                    override=KeyEntry(Level.Actions(3), Level.Actions(2)),
                    # X11 and xkbcommon handle keysyms-only case differently
                    types=TestType.ActionsOnly | TestType.KeysymsAndActions,
                )
            ),
            dataclasses.replace(
                TEST_BOTH_P,
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(2)),
                replace=KeyEntry(Level.Actions(3)),
                implementations=Implementation.x11,
            ).add_keysyms(keep_actions=False),
            dataclasses.replace(
                TEST_BOTH_P, implementations=Implementation.xkbcommon
            ).add_keysyms(keep_actions=False),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(None), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(2), Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3)),
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a"), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Keysyms("X")),
                augment=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))),
                override=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))),
            ),
            Comment("Single keysyms -> multiple keysyms"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)),
                augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                override=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                override=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                augment=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                override=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(2)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(2)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(2), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(2), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple keysyms -> multiple keysyms"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(None, None)),
                update=KeyEntry(
                    Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
                ),
                augment=KeyEntry(
                    Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
                ),
                override=KeyEntry(
                    Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(None, None)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                augment=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                augment=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(2, Modifier.Control),
                ),
                override=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                augment=KeyEntry(
                    Level.Actions(2, None),
                    Level.Actions(None, 2),
                ),
                override=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(
                    Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control)
                ),
                update=KeyEntry(
                    Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
                ),
                augment=KeyEntry(
                    Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control)
                ),
                override=KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(
                    Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2)
                ),
                update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                augment=KeyEntry(
                    Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2)
                ),
                override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)),
                update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)),
                augment=KeyEntry(
                    Level.Actions(None, None), Level.Actions(None, None, None)
                ),
                override=KeyEntry(
                    Level.Actions(None, None), Level.Actions(None, None, None)
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)),
                update=KeyEntry(
                    Level.Actions(3, None, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                augment=KeyEntry(
                    Level.Actions(3, None, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                override=KeyEntry(
                    Level.Actions(3, None, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(Modifier.Control, None, 2),
                ),
                update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)),
                augment=KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(Modifier.Control, None, 2),
                ),
                override=KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(Modifier.Control, None, 2),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(Modifier.Control, None, 2),
                ),
                update=KeyEntry(
                    Level.Actions(3, None, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                augment=KeyEntry(
                    Level.Actions(2, Modifier.Control),
                    Level.Actions(Modifier.Control, None, 2),
                ),
                override=KeyEntry(
                    Level.Actions(3, None, Modifier.LevelThree),
                    Level.Actions(Modifier.LevelThree, 3),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple keysyms -> single keysyms"),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(None), Level.Actions(None)),
                augment=KeyEntry(
                    Level.Actions(None, None), Level.Actions(2, Modifier.Control)
                ),
                override=KeyEntry(
                    Level.Actions(None, None), Level.Actions(2, Modifier.Control)
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(3), Level.Actions(2, Modifier.Control)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                update=KeyEntry(Level.Actions(None), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                override=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(3)),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Mix"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Actions(2)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                augment=KeyEntry(Level.Actions(2), Level.Actions(3, Modifier.LevelThree)),
                override=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2, Modifier.Control), Level.Actions(3)),
                override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
                override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Mix"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a"), Level.Actions(2)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y")
                ),
                augment=KeyEntry(
                    Level.Mix(("a",), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2,)),
                ),
                override=KeyEntry(
                    Level.Mix(("a",), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2,)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple keysyms/actions –> single"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(Level.Actions(3), Level.Keysyms("X")),
                augment=KeyEntry(
                    Level.Mix(("a", "b"), (3,)),
                    Level.Mix(("X",), (2, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix(("a", "b"), (3,)),
                    Level.Mix(("X",), (2, Modifier.Control)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple keysyms/actions –> multiple (xor)"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y")
                ),
                augment=KeyEntry(
                    Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2, Modifier.Control)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple keysyms/actions –> multiple (mix)"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a", None), Level.Actions(2, None)),
                update=KeyEntry(
                    Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
                ),
                augment=KeyEntry(
                    Level.Mix(("a", None), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2, None)),
                ),
                override=KeyEntry(
                    Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
                update=KeyEntry(
                    Level.Mix(("x", None), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (3, None)),
                ),
                augment=KeyEntry(
                    Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (2, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix(("x", None), (3, Modifier.LevelThree)),
                    Level.Mix(("X", "Y"), (3, None)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple (mix) –> multiple keysyms/actions"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Mix(("a", "b"), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (2, Modifier.Control)),
                ),
                update=KeyEntry(Level.Keysyms("x", None), Level.Actions(3, None)),
                augment=KeyEntry(
                    Level.Mix(("a", "b"), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (2, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix(("x", None), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (3, None)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Mix(("a", None), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (2, None)),
                ),
                update=KeyEntry(
                    Level.Keysyms("x", "y"), Level.Actions(3, Modifier.LevelThree)
                ),
                augment=KeyEntry(
                    Level.Mix(("a", None), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (2, None)),
                ),
                override=KeyEntry(
                    Level.Mix(("x", "y"), (2, Modifier.Control)),
                    Level.Mix(("A", "B"), (3, Modifier.LevelThree)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Multiple (mix) –> multiple (mix)"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Mix(("a", "b"), (2, Modifier.Control)),
                    Level.Mix((None, "B"), (2, None)),
                ),
                update=KeyEntry(
                    Level.Mix((None, "y"), (3, None)),
                    Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
                ),
                augment=KeyEntry(
                    Level.Mix(("a", "b"), (2, Modifier.Control)),
                    Level.Mix((None, "B"), (2, None)),
                ),
                override=KeyEntry(
                    Level.Mix((None, "y"), (3, None)),
                    Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Mix(("a", None), (2, None)),
                    Level.Mix((None, "B"), (None, Modifier.Control)),
                ),
                update=KeyEntry(
                    Level.Mix((None, "y"), (None, Modifier.LevelThree)),
                    Level.Mix(("X", None), (3, None)),
                ),
                augment=KeyEntry(
                    Level.Mix(("a", None), (2, None)),
                    Level.Mix((None, "B"), (None, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix((None, "y"), (None, Modifier.LevelThree)),
                    Level.Mix(("X", None), (3, None)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Mismatch count with mix"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Keysyms("a"),
                    Level.Keysyms("A", "B"),
                ),
                update=KeyEntry(
                    Level.Actions(3, Modifier.LevelThree),
                    Level.Actions(3),
                ),
                augment=KeyEntry(
                    Level.Mix(("a"), (3, Modifier.LevelThree)),
                    Level.Mix(("A", "B"), (3,)),
                ),
                override=KeyEntry(
                    Level.Mix(("a"), (3, Modifier.LevelThree)),
                    Level.Mix(("A", "B"), (3,)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Actions(3),
                    Level.Actions(3, Modifier.LevelThree),
                ),
                update=KeyEntry(
                    Level.Keysyms("A", "B"),
                    Level.Keysyms("a"),
                ),
                augment=KeyEntry(
                    Level.Mix(("A", "B"), (3,)),
                    Level.Mix(("a"), (3, Modifier.LevelThree)),
                ),
                override=KeyEntry(
                    Level.Mix(("A", "B"), (3,)),
                    Level.Mix(("a"), (3, Modifier.LevelThree)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(
                    Level.Mix(("a",), (2,)),
                    Level.Mix(("A", "B"), (2, Modifier.Control)),
                ),
                update=KeyEntry(
                    Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                    Level.Mix(("X",), (3,)),
                ),
                augment=KeyEntry(
                    Level.Mix(("a",), (2,)),
                    Level.Mix(("A", "B"), (2, Modifier.Control)),
                ),
                override=KeyEntry(
                    Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                    Level.Mix(("X",), (3,)),
                ),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Issue #564"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Keysyms("A")),
                update=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))),
                augment=KeyEntry(Level.Mix(("A",), (3, Modifier.LevelThree))),
                override=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Drop NoSymbol/NoAction"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Mix(("A",), (2,))),
                update=KeyEntry(Level.Mix((None, "Y", None), (None, 3, None))),
                augment=KeyEntry(Level.Mix(("A",), (2,))),
                override=KeyEntry(Level.Mix(("Y",), (3,))),
                implementations=Implementation.xkbcommon,
            ),
            Comment("Drop NoSymbol/NoAction and invalid keysyms"),
            TestEntry(
                TestId(TestType.KeysymsAndActions),
                KeyEntry(Level.Mix(("notAKeysym", None, "thisEither"), (None, None))),
                update=KeyEntry(Level.Mix((None, None), (None, None))),
                augment=KeyEntry(Level.Keysyms(None)),
                override=KeyEntry(Level.Keysyms(None)),
                replace=KeyEntry(Level.Keysyms(None)),
                implementations=Implementation.xkbcommon,
            ),
        ),
    ).add_keysyms()
    
    
    ################################################################################
    #
    # Keycodes tests
    #
    ################################################################################
    
    KEYCODES_TESTS_BASE = ComponentTestTemplate(
        "Keycodes",
        component=Component.keycodes,
        base_template=ComponentTemplate(
            """
            ${mode}<1> = 241;
            ${mode}<2> = 242;
    
            ${mode}alias <A> = <1>;
            ${mode}alias <B> = <2>;
    
            ${mode}indicator 1 = "Caps Lock";
            ${mode}indicator 2 = "Num Lock";
            ${mode}indicator 3 = "Scroll Lock";
            ${mode}indicator 4 = "Compose";
            ${mode}indicator 5 = "Kana";
            """
        ),
        update_template=ComponentTemplate(
            """
            ${mode}<1> = 241;            // Unchanged
            ${mode}<2> = 244;            // Changed
            ${mode}<3> = 243;            // New
    
            ${mode}alias <A> = <1>;    // Unchanged
            ${mode}alias <B> = <3>;    // Changed
            ${mode}alias <C> = <3>;    // New
    
            ${mode}indicator 1 = "Caps Lock";   // Unchanged
            ${mode}indicator 6 = "Num Lock";    // Changed index (free)
            ${mode}indicator 5 = "Scroll Lock"; // Changed index (not free)
            ${mode}indicator 4 = "XXXX";        // Changed name
            ${mode}indicator 7 = "Suspend";     // New
            """
        ),
        augment="""
            <1> = 241;
            <2> = 242;
            <3> = 243;
            alias <A> = <1>;
            alias <B> = <2>;
            alias <C> = <3>;
            indicator 1 = "Caps Lock";
            indicator 2 = "Num Lock";
            indicator 3 = "Scroll Lock";
            indicator 4 = "Compose";
            indicator 5 = "Kana";
            indicator 7 = "Suspend";
            """,
        override="""
            <1> = 241;
            <2> = 244;
            <3> = 243;
            alias <A> = <1>;
            alias <B> = <3>;
            alias <C> = <3>;
            indicator 1 = "Caps Lock";
            indicator 4 = "XXXX";
            indicator 5 = "Scroll Lock";
            indicator 6 = "Num Lock";
            indicator 7 = "Suspend";
            """,
    )
    
    
    ################################################################################
    #
    # Main
    #
    ################################################################################
    
    if __name__ == "__main__":
        # Parse commands
        parser = argparse.ArgumentParser(description="Generate merge mode tests")
        parser.add_argument(
            "compiler",
            type=XkbCompiler.parse,
            nargs="?",
            default="xkbcommon",
            help="XKB compiler to use",
        )
        parser.add_argument(
            "--root",
            type=Path,
            default=ROOT,
            help="Path to the root of the project (default: %(default)s)",
        )
        parser.add_argument(
            "--c-out",
            type=Path,
            # default=ROOT / TestGroup.c_template.stem,
            help="Path to the output C test file (default: %(default)s)",
        )
        parser.add_argument("--no-xkb", action="store_true", help="Do not update XKB data")
        parser.add_argument("--debug", action="store_true", help="Activate debug mode")
        args = parser.parse_args()
    
        # Prepare data
        template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8")
        jinja_env = jinja2.Environment(
            loader=template_loader,
            keep_trailing_newline=True,
            trim_blocks=True,
            lstrip_blocks=True,
        )
    
        def escape_quotes(s: str) -> str:
            return s.replace('"', '\\"')
    
        jinja_env.filters["escape_quotes"] = escape_quotes
        compiler: XkbCompiler = args.compiler
    
        SYMBOLS_TESTS = SYMBOLS_TESTS_BOTH.with_implementation(
            Implementation.xkbcommon if compiler.extended_syntax else Implementation.x11
        )
    
        KEYCODES_TESTS = SYMBOLS_TESTS.get_keycodes(KEYCODES_TESTS_BASE)
    
        TESTS = (
            KeymapTestTemplate(
                title="",
                keycodes=KEYCODES_TESTS,
                types=TYPES_TESTS,
                compat=COMPAT_TESTS,
                symbols=SYMBOLS_TESTS.get_symbols(debug=args.debug),
            ),
        )
    
        # Write tests
        TESTS[0].write(
            root=args.root,
            jinja_env=jinja_env,
            c_file=args.c_out,
            xkb=not args.no_xkb,
            compiler=compiler,
            tests=TESTS,
        )