diff --git a/meson.build b/meson.build index 6d470242..56ba31d0 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project('libwacom', 'c', version: '2.11.0', license: 'HPND', default_options: [ 'c_std=gnu99', 'warning_level=2' ], - meson_version: '>= 0.51.0') + meson_version: '>= 0.53.0') dir_bin = get_option('prefix') / get_option('bindir') dir_data = get_option('prefix') / get_option('datadir') / 'libwacom' @@ -17,6 +17,7 @@ if dir_udev == '' dir_udev = dir_sys_udev endif +fs = import('fs') pymod = import('python') python = pymod.find_installation('python3', required: true) @@ -295,12 +296,15 @@ if get_option('tests').enabled() message('valgrind not found, disabling valgrind test suite') endif + env = environment() + env.set('MESON_SOURCE_ROOT', meson.current_source_dir()) + env.set('LD_LIBRARY_PATH', fs.parent(lib_libwacom.full_path())) pymod.find_installation(modules: ['libevdev', 'pyudev', 'pytest']) pytest = find_program('pytest-3', 'pytest') test('pytest', pytest, args: [meson.current_source_dir()], - env: ['MESON_SOURCE_ROOT=@0@'.format(meson.current_source_dir())], + env: env, suite: ['all']) endif diff --git a/test/test_libwacom.py b/test/test_libwacom.py new file mode 100644 index 00000000..0a0a679e --- /dev/null +++ b/test/test_libwacom.py @@ -0,0 +1,934 @@ +#!/usr/bin/env python3 +# +# This file is formatted with ruff format +# +# Run with pytest: +# $ export LD_LIBRARY_PATH=$PWD/builddir/ +# $ export MESON_SOURCE_ROOT=$PWD # optional, defaults to $PWD. +# $ pytest -v --log-level=DEBUG +# +# Introduction +# ============ +# +# This is a Python-based test suite making use of ctypes to test the +# libwacom.so C library. +# +# The main components are: +# - LibWacom: the Python class wrapping libwacom.so via ctypes. +# This is a manually maintained mapping, any API additions/changes must +# updated here. +# - WacomDevice, WacomDatabase, ...: pythonic wrappers around the +# underlying C object. + +from ctypes import c_char_p, c_char, c_int, c_uint32, c_void_p +from typing import Optional, Tuple, Type, List +from dataclasses import dataclass +from pathlib import Path + +import ctypes +import enum +import itertools +import logging +import os +import pytest + +# If asan/ubsan is set, ctypes.CDLL blows up on us, so skip +# this module if either is set +if any(os.environ.get(v) for v in ["ASAN_OPTIONS", "UBSAN_OPTIONS"]): + pytest.skip("ASAN/UBSAN not supported in this module", allow_module_level=True) + +logger = logging.getLogger(__name__) + + +PREFIX = "libwacom_" + + +@dataclass +class _Api: + name: str + args: Tuple[Type[ctypes._SimpleCData], ...] + return_type: Optional[Type[ctypes._SimpleCData]] + + @property + def basename(self) -> str: + return self.name[len(PREFIX) :] + + +@dataclass +class _Enum: + name: str + value: int + + @property + def basename(self) -> str: + return self.name.removeprefix("WACOM_").removeprefix("W") + + +class LibWacom: + """ + libwacom.so wrapper. This is a singleton ctypes wrapper into libwacom.so with + minimal processing. Example: + + >>> lib = LibWacom.instance() + >>> ctx = lib.database_new(None) + >>> lib.wacom_unref(ctx) + + In most cases you probably want to use the ``WacomDevice`` class instead. + """ + + _lib = None + + @staticmethod + def _cdll(): + return ctypes.CDLL("libwacom.so.9", use_errno=True) + + @classmethod + def _load(cls): + cls._lib = cls._cdll() + for api in cls._api_prototypes: + func = getattr(cls._lib, api.name) + func.argtypes = api.args + func.restype = api.return_type + setattr(cls, api.basename, func) + + for e in cls._enums: + setattr(cls, e.basename, e.value) + + @classmethod + def instance(cls): + if cls._lib is None: + cls._load() + return cls + + _api_prototypes: List[_Api] = [ + _Api(name="libwacom_error_new", args=(c_void_p,), return_type=c_void_p), + _Api(name="libwacom_error_free", args=(c_void_p,), return_type=None), + _Api(name="libwacom_error_get_code", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_error_get_message", args=(c_void_p,), return_type=c_char_p), + _Api(name="libwacom_database_new", args=(), return_type=c_void_p), + _Api( + name="libwacom_database_new_for_path", + args=(c_char_p,), + return_type=c_void_p, + ), + _Api(name="libwacom_database_destroy", args=(c_void_p,), return_type=None), + _Api( + name="libwacom_new_from_builder", + args=(c_void_p, c_void_p, c_int, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_new_from_path", + args=(c_void_p, c_char_p, c_int, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_new_from_usbid", + args=(c_void_p, c_int, c_int, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_new_from_name", + args=(c_void_p, c_char_p, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_list_devices_from_database", + args=(c_void_p, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_print_device_description", + args=(c_int, c_void_p), + return_type=None, + ), + _Api(name="libwacom_destroy", args=(c_void_p,), return_type=None), + _Api( + name="libwacom_compare", args=(c_void_p, c_void_p, c_int), return_type=c_int + ), + _Api(name="libwacom_get_class", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_name", args=(c_void_p,), return_type=c_char_p), + _Api(name="libwacom_get_model_name", args=(c_void_p,), return_type=c_char_p), + _Api( + name="libwacom_get_layout_filename", args=(c_void_p,), return_type=c_char_p + ), + _Api(name="libwacom_get_vendor_id", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_match", args=(c_void_p,), return_type=c_char_p), + _Api( + name="libwacom_get_matches", + args=(c_void_p,), + return_type=ctypes.POINTER(c_void_p), + ), + _Api(name="libwacom_get_paired_device", args=(c_void_p,), return_type=c_void_p), + _Api(name="libwacom_get_product_id", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_width", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_height", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_has_stylus", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_has_touch", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_num_buttons", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_num_keys", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_get_supported_styli", + args=(c_void_p, c_void_p), + return_type=c_void_p, + ), + _Api(name="libwacom_has_ring", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_has_ring2", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_num_rings", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_has_touchswitch", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_ring_num_modes", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_ring2_num_modes", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_num_strips", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_strips_num_modes", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_num_dials", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_dial_num_modes", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_get_dial2_num_modes", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_get_status_leds", + args=(c_void_p, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_get_button_led_group", + args=(c_void_p, c_char), + return_type=c_int, + ), + _Api(name="libwacom_is_builtin", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_is_reversible", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_get_integration_flags", args=(c_void_p,), return_type=c_int + ), + _Api(name="libwacom_get_bustype", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_get_button_flag", args=(c_void_p, c_char), return_type=c_int + ), + _Api( + name="libwacom_get_button_evdev_code", + args=(c_void_p, c_char), + return_type=c_int, + ), + _Api( + name="libwacom_stylus_get_for_id", + args=(c_void_p, c_int), + return_type=c_void_p, + ), + _Api(name="libwacom_stylus_get_id", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_get_name", args=(c_void_p,), return_type=c_char_p), + _Api( + name="libwacom_stylus_get_paired_ids", + args=(c_void_p, c_void_p), + return_type=c_void_p, + ), + _Api( + name="libwacom_stylus_get_num_buttons", args=(c_void_p,), return_type=c_int + ), + _Api(name="libwacom_stylus_has_eraser", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_is_eraser", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_has_lens", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_has_wheel", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_get_axes", args=(c_void_p,), return_type=c_int), + _Api(name="libwacom_stylus_get_type", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_stylus_get_eraser_type", args=(c_void_p,), return_type=c_int + ), + _Api( + name="libwacom_print_stylus_description", + args=(c_int, c_void_p), + return_type=None, + ), + _Api(name="libwacom_builder_new", args=(), return_type=c_void_p), + _Api(name="libwacom_builder_destroy", args=(c_void_p,), return_type=None), + _Api( + name="libwacom_builder_set_device_name", + args=(c_void_p, c_char_p), + return_type=None, + ), + _Api( + name="libwacom_builder_set_match_name", + args=(c_void_p, c_char_p), + return_type=None, + ), + _Api( + name="libwacom_builder_set_uniq", + args=(c_void_p, c_char_p), + return_type=None, + ), + _Api( + name="libwacom_builder_set_bustype", + args=(c_void_p, c_int), + return_type=None, + ), + _Api( + name="libwacom_builder_set_usbid", + args=(c_void_p, c_int, c_int), + return_type=None, + ), + _Api(name="libwacom_match_get_name", args=(c_void_p,), return_type=c_char_p), + _Api(name="libwacom_match_get_uniq", args=(c_void_p,), return_type=c_char_p), + _Api(name="libwacom_match_get_bustype", args=(c_void_p,), return_type=c_int), + _Api( + name="libwacom_match_get_product_id", args=(c_void_p,), return_type=c_uint32 + ), + _Api( + name="libwacom_match_get_vendor_id", args=(c_void_p,), return_type=c_uint32 + ), + _Api( + name="libwacom_match_get_match_string", + args=(c_void_p,), + return_type=c_char_p, + ), + ] + + _enums: List[_Enum] = [ + _Enum(name="WERROR_NONE", value=0), + _Enum(name="WERROR_BAD_ALLOC", value=1), + _Enum(name="WERROR_INVALID_PATH", value=2), + _Enum(name="WERROR_INVALID_DB", value=3), + _Enum(name="WERROR_BAD_ACCESS", value=4), + _Enum(name="WERROR_UNKNOWN_MODEL", value=5), + _Enum(name="WERROR_BUG_CALLER", value=6), + _Enum(name="WBUSTYPE_UNKNOWN", value=0), + _Enum(name="WBUSTYPE_USB", value=1), + _Enum(name="WBUSTYPE_SERIAL", value=2), + _Enum(name="WBUSTYPE_BLUETOOTH", value=3), + _Enum(name="WBUSTYPE_I2C", value=4), + _Enum(name="WACOM_DEVICE_INTEGRATED_NONE", value=0), + _Enum(name="WACOM_DEVICE_INTEGRATED_DISPLAY", value=1), + _Enum(name="WACOM_DEVICE_INTEGRATED_SYSTEM", value=2), + _Enum(name="WCLASS_UNKNOWN", value=0), + _Enum(name="WCLASS_INTUOS3", value=1), + _Enum(name="WCLASS_INTUOS4", value=2), + _Enum(name="WCLASS_INTUOS5", value=3), + _Enum(name="WCLASS_CINTIQ", value=4), + _Enum(name="WCLASS_BAMBOO", value=5), + _Enum(name="WCLASS_GRAPHIRE", value=6), + _Enum(name="WCLASS_ISDV4", value=7), + _Enum(name="WCLASS_INTUOS", value=8), + _Enum(name="WCLASS_INTUOS2", value=9), + _Enum(name="WCLASS_PEN_DISPLAYS", value=10), + _Enum(name="WCLASS_REMOTE", value=2), + _Enum(name="WSTYLUS_UNKNOWN", value=0), + _Enum(name="WSTYLUS_GENERAL", value=1), + _Enum(name="WSTYLUS_INKING", value=2), + _Enum(name="WSTYLUS_AIRBRUSH", value=3), + _Enum(name="WSTYLUS_CLASSIC", value=4), + _Enum(name="WSTYLUS_MARKER", value=5), + _Enum(name="WSTYLUS_STROKE", value=6), + _Enum(name="WSTYLUS_PUCK", value=7), + _Enum(name="WSTYLUS_3D", value=8), + _Enum(name="WSTYLUS_MOBILE", value=9), + _Enum(name="WACOM_ERASER_UNKNOWN", value=0), + _Enum(name="WACOM_ERASER_NONE", value=1), + _Enum(name="WACOM_ERASER_INVERT", value=2), + _Enum(name="WACOM_ERASER_BUTTON", value=3), + _Enum(name="WACOM_BUTTON_NONE", value=0), + _Enum(name="WACOM_BUTTON_POSITION_LEFT", value=1 << 1), + _Enum(name="WACOM_BUTTON_POSITION_RIGHT", value=1 << 2), + _Enum(name="WACOM_BUTTON_POSITION_TOP", value=1 << 3), + _Enum(name="WACOM_BUTTON_POSITION_BOTTOM", value=1 << 4), + _Enum(name="WACOM_BUTTON_RING_MODESWITCH", value=1 << 5), + _Enum(name="WACOM_BUTTON_RING2_MODESWITCH", value=1 << 6), + _Enum(name="WACOM_BUTTON_TOUCHSTRIP_MODESWITCH", value=1 << 7), + _Enum(name="WACOM_BUTTON_TOUCHSTRIP2_MODESWITCH", value=1 << 8), + _Enum(name="WACOM_BUTTON_OLED", value=1 << 9), + _Enum(name="WACOM_BUTTON_DIAL_MODESWITCH", value=1 << 10), + _Enum(name="WACOM_BUTTON_DIAL2_MODESWITCH", value=1 << 11), + _Enum( + name="WACOM_BUTTON_MODESWITCH", + value=1 << 5 | 1 << 6 | 1 << 7 | 1 << 8 | 1 << 10 | 1 << 11, + ), + _Enum(name="WACOM_BUTTON_DIRECTION", value=1 << 1 | 1 << 2 | 1 << 3 | 1 << 4), + _Enum(name="WACOM_BUTTON_RINGS_MODESWITCH", value=1 << 5 | 1 << 6), + _Enum(name="WACOM_BUTTON_TOUCHSTRIPS_MODESWITCH", value=1 << 7 | 1 << 8), + _Enum(name="WACOM_BUTTON_DIALS_MODESWITCH", value=1 << 10 | 1 << 11), + _Enum(name="WACOM_AXIS_TYPE_NONE", value=0), + _Enum(name="WACOM_AXIS_TYPE_TILT", value=1 << 1), + _Enum(name="WACOM_AXIS_TYPE_ROTATION_Z", value=1 << 2), + _Enum(name="WACOM_AXIS_TYPE_DISTANCE", value=1 << 3), + _Enum(name="WACOM_AXIS_TYPE_PRESSURE", value=1 << 4), + _Enum(name="WACOM_AXIS_TYPE_SLIDER", value=1 << 5), + _Enum(name="WFALLBACK_NONE", value=0), + _Enum(name="WFALLBACK_GENERIC", value=1), + _Enum(name="WCOMPARE_NORMAL", value=0), + _Enum(name="WCOMPARE_MATCHES", value=1), + _Enum(name="WACOM_STATUS_LED_UNAVAILABLE", value=0), + _Enum(name="WACOM_STATUS_LED_RING", value=1), + _Enum(name="WACOM_STATUS_LED_RING2", value=2), + _Enum(name="WACOM_STATUS_LED_TOUCHSTRIP", value=3), + _Enum(name="WACOM_STATUS_LED_TOUCHSTRIP2", value=4), + ] + + +class WacomBustype(enum.IntEnum): + UNKNOWN = 0x0 + USB = 0x1 + I2C = 0x2 + BLUETOOTH = 0x3 + + +class WacomMatch: + def __init__(self, match): + self.match = match + lib = LibWacom.instance() + + def wrapper(func): + return lambda *args, **kwargs: func(self.match, *args, **kwargs) + + # Map all device-specifice accessors into respective functions + for api in lib._api_prototypes: + allowlist = ["match"] + if any(api.basename.startswith(n) for n in allowlist): + func = getattr(lib, api.basename) + setattr(self, api.basename.removeprefix("match_"), wrapper(func)) + + @property + def name(self) -> Optional[str]: + name = self.get_name() + return name.decode("utf-8") if name else None + + @property + def uniq(self) -> Optional[str]: + uniq = self.get_uniq() + return uniq.decode("utf-8") if uniq else None + + @property + def bustype(self) -> WacomBustype: + return WacomBustype(self.get_bustype()) + + @property + def vendor_id(self) -> int: + return self.get_vendor_id() + + @property + def product_id(self) -> int: + return self.get_product_id() + + +class WacomBuilder: + def __init__(self, builder): + self.builder = builder + lib = LibWacom.instance() + self._device_name = None + self._match_name = None + self._uniq = None + self._usbid = None + self._bustype = None + + def wrapper(func): + return lambda *args, **kwargs: func(self.builder, *args, **kwargs) + + # Map all device-specifice accessors into respective functions + for api in lib._api_prototypes: + allowlist = ["builder"] + if any(api.basename.startswith(n) for n in allowlist): + func = getattr(lib, api.basename) + setattr(self, api.basename.removeprefix("builder_"), wrapper(func)) + + @property + def device_name(self) -> Optional[str]: + return self._device_name + + @property + def match_name(self) -> Optional[str]: + return self._match_name + + @property + def uniq(self) -> Optional[str]: + return self._uniq + + @property + def bustype(self) -> Optional[WacomBustype]: + return self._bustype + + @property + def usbid(self) -> Optional[Tuple[int, int]]: + return self._usbid + + @bustype.setter + def bustype(self, bus: WacomBustype): + self._bustype = bus + self.set_bustype(bus.value) + + @usbid.setter + def usbid(self, usbid: Tuple[int, int]): + self._usbid = usbid + self.set_usbid(usbid[0], usbid[1]) + + @device_name.setter + def device_name(self, name: str): + self._device_name = name + self.set_device_name(name.encode("utf-8")) + + @match_name.setter + def match_name(self, name: str): + self._match_name = name + self.set_match_name(name.encode("utf-8")) + + @uniq.setter + def uniq(self, uniq: str): + self._uniq = uniq + self.set_uniq(uniq.encode("utf-8")) + + @classmethod + def create( + cls, + device_name: Optional[str] = None, + match_name: Optional[str] = None, + uniq: Optional[str] = None, + usbid: Optional[Tuple[int, int]] = None, + bus: Optional[WacomBustype] = None, + ) -> "WacomBuilder": + lib = LibWacom.instance() + builder = WacomBuilder(lib.builder_new()) + if device_name is not None: + builder.device_name = device_name + if match_name is not None: + builder.match_name = match_name + if uniq is not None: + builder.uniq = uniq + if bus is not None: + builder.bustype = bus + if usbid is not None: + builder.usbid = usbid + return builder + + def __del__(self): + lib = LibWacom.instance() + lib.builder_destroy(self.builder) + + +class WacomDevice: + """ + Convenience wrapper to make using libwacom a bit more pythonic. + """ + + class IntegrationFlags(enum.IntEnum): + DISPLAY = 1 << 0 + SYSTEM = 1 << 1 + + class ButtonFlags(enum.IntEnum): + NONE = 0 + POSITION_LEFT = 1 << 1 + POSITION_RIGHT = 1 << 2 + POSITION_TOP = 1 << 3 + POSITION_BOTTOM = 1 << 4 + RING_MODESWITCH = 1 << 5 + RING2_MODESWITCH = 1 << 6 + TOUCHSTRIP_MODESWITCH = 1 << 7 + TOUCHSTRIP2_MODESWITCH = 1 << 8 + OLED = 1 << 9 + DIAL_MODESWITCH = 1 << 10 + DIAL2_MODESWITCH = 1 << 11 + + def __init__(self, device): + self.device = device + + lib = LibWacom.instance() + + def wrapper(func): + return lambda *args, **kwargs: func(self.device, *args, **kwargs) + + # Map all device-specifice accessors into respective functions + for api in lib._api_prototypes: + allowlist = ["get_", "is_", "has_"] + if any(api.basename.startswith(n) for n in allowlist): + denylist = ["get_paired_device", "get_matches"] + if all(not api.basename.startswith(n) for n in denylist): + func = getattr(lib, api.basename) + setattr(self, api.basename, wrapper(func)) + + # This mashes all enums into the same namespace but oh well + for e in lib._enums: + val = getattr(lib, e.basename) + setattr(self, e.basename, val) + + def get_paired_device(self) -> Optional[WacomMatch]: + lib = LibWacom.instance() + match = lib.get_paired_device(self.device) + return WacomMatch(match) if match else None + + def get_matches(self) -> List[WacomMatch]: + lib = LibWacom.instance() + matches = lib.get_matches(self.device) + + print(matches) + print(matches[2]) + + return [ + WacomMatch(m) + for m in itertools.takewhile(lambda ptr: ptr is not None, matches) + ] + + def __del__(self): + lib = LibWacom.instance() + lib.destroy(self.device) + + @property + def paired_device(self) -> Optional[WacomMatch]: + return self.get_paired_device() + + @property + def name(self): + return self.get_name().decode("utf-8") + + @property + def model_name(self): + model = self.get_model_name() + return model.decode("utf-8") if model else None + + @property + def bustype(self): + return self.get_bustype() + + @property + def vendor_id(self): + return self.get_vendor_id() + + @property + def product_id(self): + return self.get_product_id() + + @property + def width(self): + return self.get_width() + + @property + def height(self): + return self.get_height() + + @property + def num_buttons(self): + return self.get_num_buttons() + + @property + def num_keys(self): + return self.get_num_keys() + + @property + def num_rings(self): + return self.get_num_rings() + + @property + def num_strips(self): + return self.get_num_strips() + + @property + def num_dials(self): + return self.get_num_dials() + + @property + def ring_num_modes(self): + return self.get_ring_num_modes() + + @property + def ring2_num_modes(self): + return self.get_ring2_num_modes() + + @property + def strip_num_modes(self): + return self.get_strip_num_modes() + + @property + def dial_num_modes(self): + return self.get_dial_num_modes() + + @property + def match(self): + return self.get_match() + + @property + def matches(self): + return self.get_matches() + + @property + def integration_flags(self) -> List[IntegrationFlags]: + flags = self.get_integration_flags() + return [f for f in WacomDevice.IntegrationFlags if f & flags != 0] + + def button_flags(self, button: str) -> List[ButtonFlags]: + flags = self.get_button_flag(button.encode("utf-8")) + return [f for f in WacomDevice.ButtonFlags if f & flags != 0] + + def button_evdev_code(self, button: str) -> int: + return self.get_button_evdev_code(button.encode("utf-8")) + + +class WacomDatabase: + """ + Convenience wrapper to make using libwacom a bit more pythonic. + """ + + class Fallback(enum.IntEnum): + NONE = 0x0 + GENERIC = 0x1 + + def __init__(self, path: Optional[Path] = None): + lib = LibWacom.instance() + if path is None: + self.db = lib.database_new() # type: ignore + else: + self.db = lib.database_new_for_path(str(path).encode("utf-8")) # type: ignore + + def wrapper(func): + return lambda *args, **kwargs: func(self.db, *args, **kwargs) + + for api in lib._api_prototypes: + if api.basename.startswith("new_from_"): + func = getattr(lib, api.basename) + setattr(self, api.name, wrapper(func)) + + def __del__(self): + db = getattr(self, "db", None) + if db is not None: + lib = LibWacom.instance() + lib.database_destroy(db) + + def new_from_name(self, name: str) -> Optional[WacomDevice]: + device = self.libwacom_new_from_name(name.encode("utf-8"), 0) + return WacomDevice(device) if device else None + + def new_from_usbid(self, vid: int, pid: int) -> Optional[WacomDevice]: + device = self.libwacom_new_from_usbid(vid, pid, 0) + return WacomDevice(device) if device else None + + def new_from_builder( + self, builder: WacomBuilder, fallback: Fallback = Fallback.NONE + ) -> Optional[WacomDevice]: + device = self.libwacom_new_from_builder(builder.builder, fallback.value, 0) + return WacomDevice(device) if device else None + + +@pytest.fixture() +def libwacom(): + try: + return LibWacom.instance() + except AttributeError as e: + pytest.exit( + f"Failed to initialize and wrap libwacom.so: {e}. You may need to set LD_LIBRARY_PATH and optionally MESON_SOURCE_ROOT" + ) + + +@pytest.fixture() +def db(libwacom): + try: + dbpath = os.environ.get("MESON_SOURCE_ROOT") + if dbpath is None and (Path.cwd() / "meson.build").exists(): + dbpath = Path.cwd() + logger.info(f"Defaulting to MESON_SOURCE_ROOT={dbpath}") + return WacomDatabase(path=Path(dbpath) / "data" if dbpath else None) + except AttributeError as e: + pytest.fail( + f"Failed to initialize and wrap libwacom.so: {e}. You may need to set LD_LIBRARY_PATH and optionally MESON_SOURCE_ROOT" + ) + + +def test_database_init(db): + """Just a test to make sure it doesn't crash""" + assert db is not None + + +def test_invalid_device(db): + device = db.new_from_usbid(0x0, 0x0) + assert device is None + + +def test_intuos4(db): + device = db.new_from_usbid(0x056A, 0x00BC) + assert device is not None + + assert device.name == "Wacom Intuos4 WL" + assert device.get_class() == device.CLASS_INTUOS4 + assert device.vendor_id == 0x56A + assert device.product_id == 0xBC + assert device.bustype == WacomBustype.USB + assert device.num_buttons == 9 + assert device.has_stylus() + assert device.is_reversible() + assert not device.has_touch() + assert device.has_ring() + assert not device.has_ring2() + assert device.num_rings == 1 + assert not device.has_touchswitch() + assert device.num_strips == 0 + assert device.num_dials == 0 + assert device.integration_flags == [] + assert device.width == 8 + assert device.height == 5 + + matches = device.matches + assert len(matches) == 2 + assert any(match.bustype == device.bustype for match in matches) + assert any(match.vendor_id == device.vendor_id for match in matches) + assert any(match.product_id == device.product_id for match in matches) + + +def test_intuos4_wl(db): + device = db.new_from_usbid(0x056A, 0x00B9) + assert device is not None + + assert WacomDevice.ButtonFlags.RING_MODESWITCH in device.button_flags("A") + assert WacomDevice.ButtonFlags.OLED in device.button_flags("I") + assert device.ring_num_modes == 4 + + +def test_cintiq24hd(db): + device = db.new_from_usbid(0x056A, 0x00F4) + assert device is not None + + assert device.ring_num_modes == 3 + assert device.ring2_num_modes == 3 + + +def test_cintiq21ux(db): + device = db.new_from_usbid(0x056A, 0x00CC) + assert device is not None + + assert device.num_strips == 2 + assert device.num_dials == 0 + + +def test_wacf004(db): + device = db.new_from_name("Wacom Serial Tablet WACf004") + assert device is not None + assert device.model_name is None + assert device.integration_flags == [ + WacomDevice.IntegrationFlags.DISPLAY, + WacomDevice.IntegrationFlags.SYSTEM, + ] + + +def test_cintiq24hdt(db): + device = db.new_from_usbid(0x056A, 0x00F8) + assert device is not None + + match = device.paired_device + assert match is not None + assert match.vendor_id == 0x56A + assert match.product_id == 0xF6 + assert match.bustype == device.BUSTYPE_USB + + +def test_cintiq13hd(db): + libevdev = pytest.importorskip("libevdev") + device = db.new_from_name("Wacom Cintiq 13HD") + assert device is not None + + assert device.button_evdev_code("A") == libevdev.EV_KEY.BTN_0.value + assert device.button_evdev_code("B") == libevdev.EV_KEY.BTN_1.value + assert device.button_evdev_code("C") == libevdev.EV_KEY.BTN_2.value + assert device.button_evdev_code("D") == libevdev.EV_KEY.BTN_3.value + assert device.button_evdev_code("E") == libevdev.EV_KEY.BTN_4.value + assert device.button_evdev_code("F") == libevdev.EV_KEY.BTN_5.value + assert device.button_evdev_code("G") == libevdev.EV_KEY.BTN_6.value + assert device.button_evdev_code("H") == libevdev.EV_KEY.BTN_7.value + assert device.button_evdev_code("I") == libevdev.EV_KEY.BTN_8.value + assert device.model_name == "DTK-1300" + + +def test_cintiqpro13(db): + device = db.new_from_name("Wacom Cintiq Pro 13") + assert device is not None + assert device.num_keys == 5 + + +def test_dell_canvas(db): + device = db.new_from_name("Dell Canvas 27") + assert device is not None + assert device.integration_flags == [WacomDevice.IntegrationFlags.DISPLAY] + + +def test_bamboo_pen(db): + libevdev = pytest.importorskip("libevdev") + + device = db.new_from_name("Wacom Bamboo Pen") + assert device is not None + assert device.button_evdev_code("A") == libevdev.EV_KEY.BTN_BACK.value + assert device.button_evdev_code("B") == libevdev.EV_KEY.BTN_FORWARD.value + assert device.button_evdev_code("C") == libevdev.EV_KEY.BTN_LEFT.value + assert device.button_evdev_code("D") == libevdev.EV_KEY.BTN_RIGHT.value + assert device.model_name == "MTE-450" + + +def test_isdv4_4800(db): + device = db.new_from_usbid(0x56A, 0x4800) + assert device is not None + + assert device.integration_flags == [ + WacomDevice.IntegrationFlags.DISPLAY, + WacomDevice.IntegrationFlags.SYSTEM, + ] + assert device.model_name is None + + assert device.vendor_id == 0x56A + assert device.product_id == 0x4800 + assert device.num_buttons == 0 + + +@pytest.mark.parametrize( + "bus,vid,pid", + [ + (WacomBustype.USB, 0x56A, 0xBC), + (WacomBustype.BLUETOOTH, 0x56A, 0xBD), + (WacomBustype.UNKNOWN, 0x56A, 0xBD), + ], +) +def test_new_from_builder_ids(db, bus, vid, pid): + match = WacomBuilder.create(bus=bus, usbid=(vid, pid)) + device = db.new_from_builder(match) + + assert device is not None + assert device.vendor_id == vid + assert device.product_id == pid + if bus != WacomBustype.UNKNOWN: + assert device.bustype == bus + else: + # unkonwn bustype means "search for it" and + # for this test that's bluetooth: + # 0x56a/0bd is a bluetooth tablet + assert device.bustype == WacomBustype.BLUETOOTH + + +def test_new_from_builder_empty(db): + builder = WacomBuilder.create() + device = db.new_from_builder(builder) + assert device is None, f"Unexpected device: {device.name}" + + +def test_new_from_builder_device_name(db): + builder = WacomBuilder.create(device_name="Wacom Bamboo Pen") + device = db.new_from_builder(builder) + assert device is not None + + # Fallback device with name override + builder.device_name = "does not exist" + device = db.new_from_builder(builder, fallback=WacomDatabase.Fallback.GENERIC) + assert device is not None + assert device.name == "does not exist" + + +def test_new_from_builder_uniq(db): + builder = WacomBuilder.create(uniq="OEM02_T18e") + device = db.new_from_builder(builder) + assert device is not None + assert device.name == "GAOMON S620" + + # uniq + match name triggers normal builder + # but since vid/pid isn't set this does not find a match + builder = WacomBuilder.create(uniq="OEM02_T18e") + builder.match_name = "GAOMON Gaomon Tablet Pen" + device = db.new_from_builder(builder) + assert device is None + + # Once we set the vid/pid we get a match + builder.usbid = (0x256C, 0x6D) + device = db.new_from_builder(builder) + assert device is not None + assert device.name == "GAOMON S620" + + # uniq + name triggers normal builder but we don't have a code path + # for that, it's a normal nameless match and + # since vid/pid isn't set this does not find a match + builder = WacomBuilder.create(uniq="OEM02_T18e") + builder.device_name = "GAOMON S620" + device = db.new_from_builder(builder) + assert device is None