#!/usr/bin/env python3 # # Copyright © 2020 Red Hat, Inc. # SPDX-License-Identifier: MIT import concurrent.futures import itertools import logging import os import re import resource import subprocess import sys import tempfile import unittest from abc import ABCMeta, abstractmethod from collections.abc import Callable, Generator from dataclasses import dataclass from functools import reduce from pathlib import Path from typing import ClassVar try: top_builddir = Path(os.environ["top_builddir"]) top_srcdir = Path(os.environ["top_srcdir"]) except KeyError: print( "Required environment variables not found: top_srcdir/top_builddir", file=sys.stderr, ) top_srcdir = Path(".") try: top_builddir = next(Path(".").glob("**/meson-logs/")).parent except StopIteration: sys.exit(1) print( 'Using srcdir "{}", builddir "{}"'.format(top_srcdir, top_builddir), file=sys.stderr, ) TIMEOUT = 10.0 # seconds INVALID_OPTION_ERROR_RE = re.compile( r"(unrecognized|unknown|illegal|invalid) option|usage|try --help", re.IGNORECASE, ) # Unset some environment variables, so that testing --enable-environment-names # does not actually depend on the current environment. for key in ( "XKB_DEFAULT_RULES", "XKB_DEFAULT_MODEL", "XKB_DEFAULT_LAYOUT", "XKB_DEFAULT_VARIANT", "XKB_DEFAULT_OPTIONS", ): if key in os.environ: del os.environ[key] # Ensure locale is C, so we can check error messages in English os.environ["LC_ALL"] = "C.UTF-8" logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger("test") logger.setLevel(logging.DEBUG) def powerset(iterable): "Subsequences of the iterable from shortest to longest." # powerset([1,2,3]) → () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3) s = tuple(iterable) return itertools.chain.from_iterable( itertools.combinations(s, r) for r in range(len(s) + 1) ) # Permutation of RMLVO that we use in multiple tests rmlvo_options = ( "--rules=evdev", "--model=pc104", "--layout=ch", "--options=eurosign:5", "--enable-environment-names", ) rmlvos = tuple( map( list, itertools.chain( itertools.permutations(rmlvo_options[:-1]), powerset(rmlvo_options), ), ) ) @dataclass class RMLVO: rules: str | None = None model: str | None = None layout: str | None = None variant: str | None = None options: str | None = None env: bool = False def __iter__(self) -> Generator[tuple[str, ...]]: if self.rules is not None: yield "--rules", self.rules if self.model is not None: yield "--model", self.model if self.layout is not None: yield "--layout", self.layout if self.variant is not None: yield "--variant", self.variant if self.options is not None: yield "--options", self.options if self.env: yield ("--enable-environment-names",) @property def args(self) -> list[str]: return list(itertools.chain.from_iterable(self)) class Target(metaclass=ABCMeta): @property @abstractmethod def args(self) -> list[str]: ... @property def stdin(self) -> str | None: return None class RmlvoTarget(Target): @property def args(self) -> list[str]: return ["--rmlvo"] class KccgstTarget(Target): @property def args(self) -> list[str]: return ["--kccgst"] class KccgstYamlTarget(Target): @property def args(self) -> list[str]: return ["--kccgst-yaml"] @dataclass class KeymapTarget(Target, RMLVO): arg: bool = False path: Path | None = None stdin: str | None = None def _args(self) -> Generator[str]: yield from itertools.chain.from_iterable(self) if self.arg: yield "--keymap" if self.path: yield str(self.path) @property def args(self) -> list[str]: return list(self._args()) @property def use_rmlvo(self) -> bool: return ( not self.arg and not self.path and ( bool(self.rules) or bool(self.model) or bool(self.layout) or bool(self.variant) or bool(self.options) or bool(self.env) or not self.stdin ) ) def _disable_coredump(): resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) def run_command(args, input: str | None = None) -> tuple[int, str, str]: logger.debug("run command: {}".format(" ".join(args))) try: p = subprocess.run( args, preexec_fn=_disable_coredump, capture_output=True, input=input, text=True, timeout=0.7, ) return p.returncode, p.stdout, p.stderr except subprocess.TimeoutExpired as e: return ( 0, e.stdout.decode("utf-8") if e.stdout else "", e.stderr.decode("utf-8") if e.stderr else "", ) @dataclass class XkbcliTool: xkbcli_tool: ClassVar[str] = "xkbcli" tool_path: ClassVar[Path] = top_builddir / "tools" subtool: str | None = None skipIf: tuple[tuple[bool, str], ...] = () skipError: tuple[tuple[Callable[[int, str, str], bool], str], ...] = () def run_command(self, args, input: str | None = None) -> tuple[int, str, str]: for condition, reason in self.skipIf: if condition: raise unittest.SkipTest(reason) if self.subtool is not None: tool = "{}-{}".format(self.xkbcli_tool, self.subtool) else: tool = self.xkbcli_tool args = [os.path.join(self.tool_path, tool)] + args return run_command(args, input=input) def run_command_success(self, args, input: str | None = None) -> tuple[str, str]: rc, stdout, stderr = self.run_command(args, input=input) if rc != 0: for testfunc, reason in self.skipError: if testfunc(rc, stdout, stderr): raise unittest.SkipTest(reason) assert rc == 0, (rc, stdout, stderr) return stdout, stderr def run_command_invalid( self, args, input: str | None = None ) -> tuple[int, str, str]: rc, stdout, stderr = self.run_command(args, input=input) assert rc == 2, (rc, stdout, stderr) return rc, stdout, stderr def run_command_unrecognized_option(self, args): rc, stdout, stderr = self.run_command(args) assert rc == 2, (rc, stdout, stderr) assert stdout.startswith("Usage") or stdout == "" # getopt/argument parsing diagnostics vary across libc/platforms # (for example GNU/Linux vs. Solaris), even when the option is rejected. assert INVALID_OPTION_ERROR_RE.search(f"{stdout}\n{stderr}"), ( rc, stdout, stderr, ) def run_command_missing_arg(self, args): rc, stdout, stderr = self.run_command(args) assert rc == 2, (rc, stdout, stderr) assert stdout.startswith("Usage") or stdout == "" assert "requires an argument" in stderr def __str__(self): return str(self.subtool) class TestXkbcli(unittest.TestCase): xkbcli: ClassVar[XkbcliTool] xkbcli_list: ClassVar[XkbcliTool] xkbcli_how_to_type: ClassVar[XkbcliTool] xkbcli_compile_keymap: ClassVar[XkbcliTool] xkbcli_compile_compose: ClassVar[XkbcliTool] xkbcli_interactive_evdev: ClassVar[XkbcliTool] xkbcli_interactive_x11: ClassVar[XkbcliTool] xkbcli_interactive_wayland: ClassVar[XkbcliTool] xkbcli_interactive: ClassVar[XkbcliTool] xkbcli_dump_keymap_x11: ClassVar[XkbcliTool] xkbcli_dump_keymap_wayland: ClassVar[XkbcliTool] xkbcli_dump_keymap: ClassVar[XkbcliTool] all_tools: ClassVar[list[XkbcliTool]] @classmethod def setUpClass(cls): cls.xkbcli = XkbcliTool() cls.xkbcli_list = XkbcliTool( "list", skipIf=( ( not int(os.getenv("HAVE_XKBCLI_LIST", "1")), "xkbregistory not enabled", ), ), ) cls.xkbcli_how_to_type = XkbcliTool("how-to-type") cls.xkbcli_compile_keymap = XkbcliTool("compile-keymap") cls.xkbcli_compile_compose = XkbcliTool("compile-compose") no_interactive_evdev = ( ( not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_EVDEV", "1")), "evdev not enabled", ), (not os.path.exists("/dev/input/event0"), "event node required"), ( not os.access("/dev/input/event0", os.R_OK), "insufficient permissions", ), ) cls.xkbcli_interactive_evdev = XkbcliTool( "interactive-evdev", skipIf=no_interactive_evdev, skipError=( ( lambda rc, stdout, stderr: "Couldn't find any keyboards" in stderr, "No keyboards available", ), ), ) no_interactive_x11 = ( ( not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_X11", "1")), "x11 not enabled", ), (not os.getenv("DISPLAY"), "DISPLAY not set"), ) cls.xkbcli_interactive_x11 = XkbcliTool( "interactive-x11", skipIf=no_interactive_x11, ) no_interactive_wayland = ( ( not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_WAYLAND", "1")), "wayland not enabled", ), (not os.getenv("WAYLAND_DISPLAY"), "WAYLAND_DISPLAY not set"), ) cls.xkbcli_interactive_wayland = XkbcliTool( "interactive-wayland", skipIf=no_interactive_wayland, ) # NOTE: `interactive` cannot be tested, because it hardcodes the paths # of the `xkbcli-interactive-{wayland,x11}` tools it calls to the # *install* directory, while we need to use those of the *build* # directory. cls.xkbcli_dump_keymap_x11 = XkbcliTool( "dump-keymap-x11", skipIf=no_interactive_x11, ) cls.xkbcli_dump_keymap_wayland = XkbcliTool( "dump-keymap-wayland", skipIf=no_interactive_wayland, ) # NOTE: `dump-keymap` cannot be tested, because it hardcodes the paths # of the `xkbcli-dump-keymap-{wayland,x11}` tools it calls to the # *install* directory, while we need to use those of the *build* # directory. cls.all_tools = [ cls.xkbcli, cls.xkbcli_list, cls.xkbcli_how_to_type, cls.xkbcli_compile_keymap, cls.xkbcli_compile_compose, cls.xkbcli_interactive_evdev, cls.xkbcli_interactive_x11, cls.xkbcli_interactive_wayland, cls.xkbcli_dump_keymap_x11, cls.xkbcli_dump_keymap_wayland, ] def test_help(self): # --help is supported by all tools for tool in self.all_tools: with self.subTest(tool=tool): stdout, stderr = tool.run_command_success(["--help"]) assert stdout.startswith("Usage:") assert stderr == "" def test_invalid_option(self): # --foobar generates "Usage:" for all tools for tool in self.all_tools: with self.subTest(tool=tool): tool.run_command_unrecognized_option(["--foobar"]) def test_xkbcli_version(self): # xkbcli --version stdout, stderr = self.xkbcli.run_command_success(["--version"]) assert stdout.startswith("1") assert stderr == "" def test_xkbcli_too_many_args(self): self.xkbcli.run_command_invalid(["a"] * 64) def test_compile_keymap(self): for args in ( ["--verbose", "-h"], ["--format=v2", "-h"], ["--input-format=xkb_v1", "-h"], ["--output-format=xkb_v2", "-h"], ["--input-strict", "-h"], ["--output-strict", "-h"], ["--strict", "-h"], ["--no-pretty", "-h"], ["--drop-unused", "-h"], ["--explicit-defaults", "-h"], ["--explicit-vmods", "-h"], ["--explicit-keys", "-h"], ["--explicit-values", "-h"], ["--layouts-mask", "0x1", "-h"], ): with self.subTest(args=args): self.xkbcli_compile_keymap.run_command_success(args) def test_compile_keymap_args(self): xkb_root = Path(os.environ["XKB_CONFIG_ROOT"]) keymap_path = xkb_root / "keymaps/masks.xkb" keymap_string = keymap_path.read_text(encoding="utf-8") test_option = "--test" target: Target for target in ( RmlvoTarget(), KccgstTarget(), KccgstYamlTarget(), # Keymap from RMLVO KeymapTarget(), # Keymap from RMLVO (stdin ignored) KeymapTarget(layout="us", stdin=keymap_string), KeymapTarget(env=True, stdin=keymap_string), # Keymap parsed from keymap file KeymapTarget(arg=False, path=keymap_path), KeymapTarget(arg=True, path=keymap_path), # Keymap parsed from stdin KeymapTarget(arg=False, stdin=keymap_string), KeymapTarget(arg=True, stdin=keymap_string), ): for options in powerset(("--verbose", test_option)): args = list(options) + target.args with self.subTest(args=args): stdout, stderr = self.xkbcli_compile_keymap.run_command_success( args, input=target.stdin ) if test_option in options: # Expect no output assert stdout == "", (target, stdout) if isinstance(target, KeymapTarget) and test_option not in options: # Check keymap output for excepted bits: # - is not defined in mask.xkb # - virtual_modifiers Test01 is only defined in masks.xkb assert ("" in stdout) ^ (not target.use_rmlvo), ( target, stdout, stderr, ) assert ("virtual_modifiers Test01" in stdout) ^ ( target.use_rmlvo ), (target, stdout, stderr) def test_compile_keymap_mutually_exclusive_args(self): xkb_root = Path(os.environ["XKB_CONFIG_ROOT"]) keymap_path = xkb_root / "keymaps/basic.xkb" keymap_string = keymap_path.read_text(encoding="utf-8") keymap_from_stdin = KeymapTarget(arg=True, stdin=keymap_string) keymap_from_path1 = KeymapTarget(arg=True, path=Path(keymap_path)) keymap_from_path2 = KeymapTarget(arg=False, path=Path(keymap_path)) rmlvo = RmlvoTarget() kccgst = KccgstTarget() kccgstYaml = KccgstYamlTarget() for entry in ( # --keymap does not use RMLVO options ("--rules", "some-rules", keymap_from_stdin), ("--model", "some-model", keymap_from_stdin), ("--layout", "some-layout", keymap_from_stdin), ("--variant", "some-variant", keymap_from_stdin), ("--options", "some-option", keymap_from_stdin), ("--rules", "some-rules", keymap_from_path1), ("--model", "some-model", keymap_from_path1), ("--layout", "some-layout", keymap_from_path1), ("--variant", "some-variant", keymap_from_path1), ("--options", "some-option", keymap_from_path1), # Trailing keymap file with RMLVO options ("--rules", "some-rules", keymap_from_path2), ("--model", "some-model", keymap_from_path2), ("--layout", "some-layout", keymap_from_path2), ("--variant", "some-variant", keymap_from_path2), ("--options", "some-option", keymap_from_path2), # Incompatible output types (rmlvo, keymap_from_stdin), (rmlvo, keymap_from_path1), (rmlvo, keymap_from_path2), (kccgst, kccgstYaml), (kccgst, keymap_from_stdin), (kccgst, keymap_from_path1), (kccgst, keymap_from_path2), (kccgst, rmlvo), (kccgst, rmlvo, keymap_from_stdin), (kccgst, rmlvo, keymap_from_path1), (kccgst, rmlvo, keymap_from_path2), (kccgstYaml, keymap_from_stdin), (kccgstYaml, keymap_from_path1), (kccgstYaml, keymap_from_path2), (kccgstYaml, rmlvo), (kccgstYaml, rmlvo, keymap_from_stdin), (kccgstYaml, rmlvo, keymap_from_path1), (kccgstYaml, rmlvo, keymap_from_path2), ): with self.subTest(args=entry): args: list[str] = list( itertools.chain.from_iterable( arg.args if isinstance(arg, Target) else arg for arg in entry ) ) input: str | None = reduce( lambda acc, arg: ( acc or (arg.stdin if isinstance(arg, Target) else None) ), args, None, ) self.xkbcli_compile_keymap.run_command_invalid(args, input=input) def test_compile_keymap_rmlvo(self): def run(target, rmlvo): return self.xkbcli_compile_keymap.run_command_success(target + rmlvo) with concurrent.futures.ThreadPoolExecutor() as executor: futures = { executor.submit(run, target.args, rmlvo): (target, rmlvo) for target in ( RmlvoTarget(), KccgstTarget(), KccgstYamlTarget(), KeymapTarget(), ) for rmlvo in rmlvos } for future in concurrent.futures.as_completed(futures, TIMEOUT): target, rmlvo = futures[future] with self.subTest(target=target, rmlvo=rmlvo): future.result() def test_compile_keymap_include(self): for args in ( ["--include", ".", "--include-defaults"], ["--include", "/tmp", "--include-defaults"], ): with self.subTest(args=args): # Succeeds thanks to include-defaults self.xkbcli_compile_keymap.run_command_success(args) def test_compile_keymap_include_invalid(self): # A non-directory is rejected by default args = ["--include", "/proc/version"] rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) assert rc == 1, (stdout, stderr) assert "There are no include paths to search" in stderr # A non-existing directory is rejected by default args = ["--include", "/tmp/does/not/exist"] rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) assert rc == 1, (stdout, stderr) assert "There are no include paths to search" in stderr # Valid dir, but missing files args = ["--include", "/tmp"] rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) assert rc == 1, (stdout, stderr) assert "Couldn't look up rules" in stderr def test_compile_compose(self): for args in (["--verbose"],): with self.subTest(args=args): self.xkbcli_compile_compose.run_command_success(args) def test_how_to_type(self): for args in (["--verbose", "1"],): with self.subTest(args=args): self.xkbcli_how_to_type.run_command_success(args) @dataclass class Entry: args: list[str] name: str value: int for entry in ( # Unicode codepoint conversions, we support whatever strtol does Entry(args=["123"], name="braceleft", value=0x007B), Entry(args=["0123"], name="braceleft", value=0x007B), Entry(args=["0a"], name="Linefeed", value=0xFF0A), Entry(args=["0x123"], name="gcedilla", value=0x03BB), Entry(args=["U+123"], name="gcedilla", value=0x03BB), # Characters Entry(args=["1"], name="1", value=0x0031), Entry(args=["a"], name="a", value=0x0061), Entry(args=["á"], name="aacute", value=0x00E1), # Keysyms names (fallback without --keysym option) Entry(args=["acute"], name="acute", value=0x00B4), Entry(args=["U123"], name="U0123", value=0x1000123), # Keysyms names (with --keysym) Entry(args=["--keysym", "1"], name="1", value=0x0031), Entry(args=["--keysym", "a"], name="a", value=0x0061), Entry(args=["--keysym", "acute"], name="acute", value=0x00B4), Entry(args=["--keysym", "U123"], name="U0123", value=0x1000123), # Keysym values Entry(args=["--keysym", "123"], name="braceleft", value=0x007B), Entry(args=["--keysym", "0x123"], name="0x00000123", value=0x0123), ): with self.subTest(args=args): stdout, _stderr = self.xkbcli_how_to_type.run_command_success( entry.args ) expected = f"keysym: {entry.name} (0x{entry.value:04x})" lines = stdout.splitlines() assert len(lines) >= 1 assert lines[0] == expected, ( entry, f'expected: "{expected}", but got: "{lines[0]}"', ) def test_how_to_type_rmlvo(self): def run(rmlvo): args = rmlvo + ["0x1234"] return self.xkbcli_how_to_type.run_command_success(args) with concurrent.futures.ThreadPoolExecutor() as executor: futures = {executor.submit(run, rmlvo): rmlvo for rmlvo in rmlvos} for future in concurrent.futures.as_completed(futures, TIMEOUT): rmlvo = futures[future] with self.subTest(rmlvo=rmlvo): future.result() def test_list_rmlvo(self): for args in ( ["--verbose"], ["-v"], ["--verbose", "--load-exotic"], ["--load-exotic"], ["--ruleset=evdev"], ["--ruleset=base"], ): with self.subTest(args=args): self.xkbcli_list.run_command_success(args) def test_list_rmlvo_includes(self): args = ["/tmp/"] self.xkbcli_list.run_command_success(args) def test_list_rmlvo_includes_invalid(self): args = ["/proc/version"] rc, stdout, stderr = self.xkbcli_list.run_command(args) assert rc == 1 assert "Failed to append include path" in stderr def test_list_rmlvo_includes_no_defaults(self): args = ["--skip-default-paths", "/tmp"] rc, stdout, stderr = self.xkbcli_list.run_command(args) assert rc == 1 assert "Failed to parse XKB description" in stderr def test_interactive_evdev_rmlvo(self): def run(rmlvo): return self.xkbcli_interactive_evdev.run_command_success(rmlvo) with concurrent.futures.ThreadPoolExecutor() as executor: futures = {executor.submit(run, rmlvo): rmlvo for rmlvo in rmlvos} for future in concurrent.futures.as_completed(futures, TIMEOUT): rmlvo = futures[future] with self.subTest(rmlvo=rmlvo): future.result() # Note: use -h in the interactive tools to speedup the tests def test_interactive_evdev(self): # Note: --enable-compose fails if $prefix doesn't have the compose tables # installed for args in ( ["--verbose", "-h"], ["--uniline", "-h"], ["--multiline", "-h"], ["--report-state-changes", "-h"], ["--no-state-report", "-h"], ["--consumed-mode=xkb", "-h"], ["--consumed-mode=gtk", "-h"], ["--without-x11-offset", "-h"], ["--format=xkb_v2", "-h"], ["--strict", "-h"], ["--enable-compose", "-h"], ["--legacy-state-api", "-h"], ["--legacy-state-api=false", "-h"], ["--legacy-state-api=true", "-h"], ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"], ["--modifiers-mapping=Control+Alt:Level3", "-h"], ["--shortcuts-mask=Control+Alt+Super", "-h"], ["--shortcuts-mapping=2:1", "-h"], ): with self.subTest(args=args): self.xkbcli_interactive_evdev.run_command_success(args) def test_interactive_x11(self): for args in ( ["--verbose", "-h"], ["--uniline", "-h"], ["--multiline", "-h"], ["--no-state-report", "-h"], ["--consumed-mode=xkb", "-h"], ["--consumed-mode=gtk", "-h"], ["--format=xkb_v2", "-h"], ["--strict", "-h"], ["--enable-compose", "-h"], ["--local-state", "-h"], ["--legacy-state-api", "-h"], ["--legacy-state-api=false", "-h"], ["--legacy-state-api=true", "-h"], ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"], ["--modifiers-mapping=Control+Alt:Level3", "-h"], ["--shortcuts-mask=Control+Alt+Super", "-h"], ["--shortcuts-mapping=2:1", "-h"], ): with self.subTest(args=args): self.xkbcli_interactive_x11.run_command_success(args) def test_interactive_wayland(self): for args in ( ["--verbose", "-h"], ["--uniline", "-h"], ["--multiline", "-h"], ["--no-state-report", "-h"], ["--consumed-mode=xkb", "-h"], ["--consumed-mode=gtk", "-h"], ["--format=xkb_v2", "-h"], ["--strict", "-h"], ["--enable-compose", "-h"], ["--local-state", "-h"], ["--legacy-state-api", "-h"], ["--legacy-state-api=false", "-h"], ["--legacy-state-api=true", "-h"], ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"], ["--modifiers-mapping=Control+Alt:Level3", "-h"], ["--shortcuts-mask=Control+Alt+Super", "-h"], ["--shortcuts-mapping=2:1", "-h"], ): with self.subTest(args=args): self.xkbcli_interactive_wayland.run_command_success(args) def test_dump_keymap_wayland(self): for args in ( ["--verbose", "-h"], ["--format=v2", "-h"], ["--strict", "-h"], ["--no-pretty", "-h"], ["--drop-unused", "-h"], ): with self.subTest(args=args): self.xkbcli_dump_keymap_wayland.run_command_success(args) def test_dump_keymap_x11(self): for args in ( ["--verbose", "-h"], ["--format=v2", "-h"], ["--strict", "-h"], ["--no-pretty", "-h"], ["--drop-unused", "-h"], ): with self.subTest(args=args): self.xkbcli_dump_keymap_x11.run_command_success(args) if __name__ == "__main__": with tempfile.TemporaryDirectory() as tmpdir: # Use our own test xkeyboard-config copy. os.environ["XKB_CONFIG_ROOT"] = str(top_srcdir / "test/data") # Use our own X11 locale copy. os.environ["XLOCALEDIR"] = str(top_srcdir / "test/data/locale") # Use our own locale. os.environ["LC_CTYPE"] = "en_US.UTF-8" # libxkbcommon has fallbacks when XDG_CONFIG_HOME isn't set so we need # to override it with a known (empty) directory. Otherwise our test # behavior depends on the system the test is run on. os.environ["XDG_CONFIG_HOME"] = tmpdir # Prevent the legacy $HOME/.xkb from kicking in. del os.environ["HOME"] # This needs to be separated if we do specific extra path testing os.environ["XKB_CONFIG_EXTRA_PATH"] = tmpdir os.environ["XKB_CONFIG_VERSIONED_EXTENSIONS_PATH"] = tmpdir os.environ["XKB_CONFIG_UNVERSIONED_EXTENSIONS_PATH"] = tmpdir unittest.main()