diff options
author | Pierre Le Marre <dev@wismill.eu> | 2024-08-08 06:15:25 +0200 |
---|---|---|
committer | Pierre Le Marre <dev@wismill.eu> | 2024-08-19 18:46:24 +0200 |
commit | 127d08444152188cff5f43ed31786c2d4e6d8fc1 (patch) | |
tree | de0a9564d7bc7c6374d0e4c6092d861e8e78d6ea | |
parent | c4f76b584fe10bc2037966a70efad9bc5b682a4a (diff) |
rules: Add tests for layout compat rules
Part-of: <https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/merge_requests/728>
-rw-r--r-- | .gitlab-ci.yml | 3 | ||||
-rw-r--r-- | meson.build | 7 | ||||
-rw-r--r-- | tests/conftest.py | 14 | ||||
-rw-r--r-- | tests/test_compat_rules.py | 176 | ||||
-rw-r--r-- | tests/xkbcommon/__init__.py | 45 |
5 files changed, 245 insertions, 0 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 65d81e73..1c7da3d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -349,6 +349,9 @@ keymap_tests: - export PYTHONPATH="$PWD/tests:$PYTHONPATH" # Run the tests - pytest --junitxml=results.xml + tests + --layout-compat-config rules/compat/layoutsMapping.lst + --layout-compat-config rules/compat/variantsMapping.lst artifacts: reports: junit: results.xml diff --git a/meson.build b/meson.build index 776a6811..163f59c4 100644 --- a/meson.build +++ b/meson.build @@ -84,6 +84,13 @@ pytest = find_program('pytest-3', 'pytest', required: false) enable_pytest = python.found() and pytest.found() if enable_pytest pytest_args = ['--verbose', '--log-level=DEBUG'] + if get_option('compat-rules') + pytest_args += [ + 'tests', + '--layout-compat-config', layout_mappings, + '--layout-compat-config', variant_mappings, + ] + endif # use pytest xdist if available, it really speeds up the tests cases optional_python_modules = ['xdist'] if pymod.find_installation('python3', modules: optional_python_modules, required: false).found() diff --git a/tests/conftest.py b/tests/conftest.py index 9befeb7a..027955c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,11 @@ +# SPDX-License-Identifier: MIT + import os import sys from pathlib import Path +import pytest + tests_dir = Path(__file__).parent.resolve() sys.path.insert(0, str(tests_dir)) @@ -14,3 +18,13 @@ try: except ImportError: pass + + +def pytest_addoption(parser: pytest.Parser): + parser.addoption( + "--layout-compat-config", + action="append", + default=[], + type=Path, + help="List of layout compatibility files", + ) diff --git a/tests/test_compat_rules.py b/tests/test_compat_rules.py new file mode 100644 index 00000000..bff935c2 --- /dev/null +++ b/tests/test_compat_rules.py @@ -0,0 +1,176 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import functools +import itertools +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, Generator, Optional, Union, TYPE_CHECKING + +if TYPE_CHECKING: + import builtins + +import pytest +import xkbcommon + + +@dataclass +class RMLVO: + rules: str + model: Optional[str] + layout: Optional[str] + variant: Optional[str] + options: Optional[str] + + +@functools.total_ordering +@dataclass(frozen=True, order=False) +class Layout: + PATTERN: ClassVar[re.Pattern[str]] = re.compile( + r"(?P<layout>[^(]+)\((?P<variant>[^)]+)\)" + ) + + layout: str + variant: str + + @classmethod + def parse(cls, layout: str, variant: str = ""): + # parse a layout(variant) string + if match := cls.PATTERN.match(layout): + assert not variant + layout = match.group("layout") + variant = match.group("variant") + return cls(layout, variant) + + def __str__(self): + if self.variant: + return "{}({})".format(self.layout, self.variant) + else: + return "{}".format(self.layout) + + def __lt__(self, other): + """ + Custom compare function in order to deal with missing variant. + """ + if not isinstance(other, self.__class__): + return NotImplemented + elif self.layout == other.layout: + if self.variant == other.variant: + return False + # Handle missing variant + elif self.variant and not other.variant: + return True + # Handle missing variant + elif not self.variant and other.variant: + return False + else: + return self.variant < other.variant + else: + return self.layout < other.layout + + @classmethod + def read_file(cls, path: Path) -> Generator[tuple[Layout, Layout], None, None]: + """Returns a list of two-layout tuples [(layout1, layout2), ...]""" + + with path.open("rt", encoding="utf-8") as fd: + for line in fd: + # Remove optional comment + line = line.split("//")[0] + # Split on whitespaces + groups = tuple(line.split()) + length = len(groups) + if length == 2: + l1 = Layout.parse(groups[0]) + l2 = Layout.parse(groups[1]) + elif length == 4: + l1 = Layout.parse(groups[0], groups[1]) + l2 = Layout.parse(groups[2], groups[3]) + else: + raise ValueError(f"Invalid line: {line}") + yield (l1, l2) + + +def pytest_generate_tests(metafunc: pytest.Metafunc): + if "mapping" in metafunc.fixturenames: + if files := metafunc.config.getoption("layout_compat_config"): + # Read all files + mappings = tuple( + itertools.chain.from_iterable(map(Layout.read_file, files)), + ) + else: + mappings = () + metafunc.parametrize("mapping", mappings) + + +@pytest.fixture(scope="session") +def xkb_base(): + """Get the xkeyboard-config directory from the environment.""" + path = os.environ.get("XKB_CONFIG_ROOT") + if path: + return Path(path) + else: + raise ValueError("XKB_CONFIG_ROOT environment variable is not defined") + + +def compile_keymap( + xkb_base: Path, + rules: str, + ls: tuple[Union[Layout, "builtins.ellipsis"], ...], + layout: Layout, +) -> tuple[str, str, str]: + lsʹ: tuple[Layout, ...] = tuple(layout if x is Ellipsis else x for x in ls) + alias_layout = ",".join(l.layout for l in lsʹ) + alias_variant = ",".join(l.variant for l in lsʹ) + km = xkbcommon.ForeignKeymap( + xkb_base=xkb_base, rules=rules, layout=alias_layout, variant=alias_variant + ) + return alias_layout, alias_variant, km.as_string() + + +@pytest.mark.parametrize("rules", ("base", "evdev")) +def test_compat_layout(xkb_base: Path, rules: str, mapping: tuple[Layout, Layout]): + alias = mapping[0] + target = mapping[1] + us = Layout("us", "") + fr = Layout("fr", "") + es = Layout("es", "") + configs = ( + (...,), + (us, ...), + (..., us), + (us, fr, ...), + (us, fr, es, ...), + ) + for ls in configs: + # Compile alias + alias_layout, alias_variant, alias_string = compile_keymap( + xkb_base, rules, ls, alias + ) + assert alias_string != "", (rules, alias_layout, alias_variant) + + # Compile target + target_layout, target_variant, target_string = compile_keymap( + xkb_base, rules, ls, target + ) + assert target_string != "", (rules, target_layout, target_variant) + + # [HACK]: fix keycodes aliases + if alias.layout == "de" and target.layout != "de": + alias_string = alias_string.replace( + "<LatZ> = <AD06>", "<LatY> = <AD06>" + ) + alias_string = alias_string.replace( + "<LatY> = <AB01>", "<LatZ> = <AB01>" + ) + + # Keymap obtain using alias should be the same as if using its target directly + assert alias_string == target_string, ( + rules, + alias_layout, + alias_variant, + target_layout, + target_variant, + ) diff --git a/tests/xkbcommon/__init__.py b/tests/xkbcommon/__init__.py index 76775eea..4c9bc1b7 100644 --- a/tests/xkbcommon/__init__.py +++ b/tests/xkbcommon/__init__.py @@ -13,6 +13,7 @@ from ctypes import ( c_uint32, cdll, create_string_buffer, + string_at, ) from ctypes.util import find_library from enum import Enum, IntFlag @@ -25,6 +26,15 @@ from typing import TYPE_CHECKING, Any, NamedTuple, Optional, TypeAlias ############################################################################### +class allocated_c_char_p(c_char_p): + """ + This class is used in place of c_char_p when we need to free it manually. + Python would convert c_char_p to a bytes string and we would not be able to free it. + """ + + pass + + class xkb_context(Structure): pass @@ -144,6 +154,10 @@ xkbcommon.xkb_keymap_new_from_names.restype = POINTER(xkb_keymap) xkbcommon.xkb_keymap_key_by_name.argtypes = [POINTER(xkb_keymap), c_char_p] xkbcommon.xkb_keymap_key_by_name.restype = xkb_keycode_t +xkbcommon.xkb_keymap_get_as_string.argtypes = [POINTER(xkb_keymap), xkb_keymap_format] +# Note: should be c_char_p; see comment in the definiton of allocated_c_char_p. +xkbcommon.xkb_keymap_get_as_string.restype = allocated_c_char_p + xkbcommon.xkb_state_new.argtypes = [POINTER(xkb_keymap)] xkbcommon.xkb_state_new.restype = POINTER(xkb_state) @@ -190,6 +204,11 @@ xkbcommon.xkb_keymap_num_leds.restype = xkb_led_index_t xkbcommon.xkb_state_led_index_is_active.argtypes = [POINTER(xkb_state), xkb_led_index_t] xkbcommon.xkb_state_led_index_is_active.restype = c_int +if libc_path := find_library("c"): + libc = cdll.LoadLibrary(libc_path) +else: + raise ValueError("Cannot import libc") + def load_keymap( xkb_config_root: Path, @@ -230,6 +249,13 @@ def unref_keymap(keymap: xkb_keymap_p) -> None: xkbcommon.xkb_keymap_unref(keymap) +def keymap_as_string(keymap: xkb_keymap_p) -> str: + p = xkbcommon.xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1) + s = string_at(p).decode("utf-8") + libc.free(p) + return s + + def new_state(keymap: xkb_keymap_p) -> xkb_state_p: state = xkbcommon.xkb_state_new(keymap) if not state: @@ -423,6 +449,25 @@ class ForeignKeymap: def __exit__(self, exc_type, exc_val, exc_tb): unref_keymap(self._keymap) + def check(self) -> bool: + """ + Test if keymap compiles with corresponding RMLVO config. + """ + try: + with self: + return bool(self._keymap) + except ValueError: + return False + + def as_string(self) -> str: + try: + with self as keymap: + if not bool(keymap): + return "" + return keymap_as_string(keymap) + except ValueError: + return "" + class State: """ |