. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole cost: the bare # # `import evdev' at the shell -- the platform gate, which # # cannot be folded into the probe because the point of it is # # what a CALLER on this image gets -- and the probe, which # # takes one start for 120 measurements. # # # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps the probe's transcript. The heaviest # # work inside the probe is an 832-entry table compared three # # ways against the module's own dicts: microseconds beside # # an interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 1200 is 600s per start, matching 99-python3-crc32c and # # 99-python3-spidev, and ten times the ~60s a python3 start # # has measured at here. Nothing in this suite sleeps or # # retries. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed belongs in # # the test plan, not in this comment -- editing the shipped # # suite to record it would cost an image build. # # python3-evdev 1.6.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-evdev_1.6.1.bb, `inherit pypi setuptools3') # on the wasm32-hwjs glibc image, plus this layer's python3-evdev_%.bbappend. # # ============================================================================ # WHAT THE PACKAGE IS. # ============================================================================ # # The Python binding for the Linux input subsystem. THREE C extensions -- # _input (the evdev ioctls), _uinput (the writer side) and _ecodes (the # EV_/KEY_/ABS_/REL_ tables) -- under eleven .py modules. # # THERE IS NO PURE-PYTHON HALF TO FALL BACK ON, even though most of the # distribution is .py. `import evdev' runs evdev/__init__.py, which imports # evdev.device at line 5; device.py line 8 is `from evdev import _input, ecodes, # util'; and ecodes.py line 44 is `from evdev import _ecodes'. All three are # module scope. So the package imports completely or not at all, and a red on # `import' is not a partial-functionality finding. # # ============================================================================ # THE TWO THINGS THIS SUITE IS FOR. # ============================================================================ # # 1. THE EMPTY ENUMERATION. There is no /dev/input/event* and no /dev/uinput # behind this machine, so the assertion is that the binding says so promptly # and in the documented way -- an empty list from list_devices(), a # FileNotFoundError with errno 2 from InputDevice(), a UInputError naming the # path from UInput() -- rather than blocking, or aborting the interpreter. # # 2. THE ecodes TABLES, WHICH ARE A CROSS-BUILD CHECK IN DISGUISE. The recipe # generates evdev/ecodes.c at build time from the kernel headers the image is # built against: # # SETUPTOOLS_BUILD_ARGS = "build_ecodes --evdev-headers \ # ${STAGING_DIR_TARGET}/usr/include/linux/input.h:...-event-codes.h" # # so asserting the tables is asserting that the cross-build read the TARGET # headers and not the build host's -- which costs nothing and catches a # silently wrong cross-build. data/python3-evdev-ecodes.json holds the # expected 832 name/value pairs, generated at port time from this image's own # linux-brintos-headers by COMPILING each macro rather than parsing it, using # evdev/genecodes.py's own macro_regex to pick the names. Both directions are # checked over the whole table, not a sample. # # ============================================================================ # THE PLATFORM GATE: WHAT WAS PREDICTED, AND WHAT WAS MEASURED. # ============================================================================ # # THIS BLOCK RECORDS A MEASUREMENT. The recipe phase could not boot the image, # so it predicted this gate from the source; the batch-2397 verify of # 2026-09-09 measured it, and the prediction was WRONG about the cause. Both # are kept here because the suite's shape was chosen for the prediction and is # vindicated by the measurement -- but a later reader must not mistake the one # for the other. # # WHAT WAS PREDICTED: `import evdev' fails on the side-module park violation -- # `hwjs-user-rt VIOLATION[lane 0]: set-over-set', rc=132, i.e. # https://gitlab.com/brintos/linux-in-the-browser/-/work_items/184. That did # NOT happen. No park violation, no rc=132: the import raised an ordinary # Python exception and the interpreter survived it. # # WHAT WAS MEASURED: `import evdev' fails with # # ModuleNotFoundError: No module named '_ctypes' # # evdev/__init__.py imports evdev.uinput and evdev.ff at module scope, and both # carry a module-scope `import ctypes' -- uinput.py needs ctypes.CDLL over # _uinput.so, ff.py builds the whole force-feedback ABI out of ctypes.Structure # subclasses at import time. This interpreter is configured without _ctypes, # because wasmld refuses libffi's ffi_closure_alloc: brintos/hwjs-cc#15, the # gap documented in docs/design/BRINTOS-1476-port-python3.md and already cited # by several landed suites. `import' and `import-ok' are the two reds that # carry it; they turn green by themselves the day #15 closes, with no change # to this suite. # # TWO THINGS MEASURED AT PORT TIME THAT STILL STAND, and that rule out the two # cheaper explanations: # # * THE THREE EXTENSIONS BIND. tools/brintos_dlopen_census.py, run at port # time against this batch's own staged rootfs with the evdev .so files # dropped in: _ecodes 30 env imports, _input 47, _uinput 46, ALL BOUND, # `0 module(s) would fail to dlopen'. The main exports 1715 symbols. # # * SO BRINTOS-2046 IS NOT THE ISSUE HERE, and a suite written against it # would be wrong. That ticket -- python3 exporting no Py* symbols, so no # extension could load -- was FIXED and landed on 2026-09-02 (meta-brintos # 4994fba2, d97461ce), well before the commit this image is built from. # # A KILL WAS ALLOWED FOR AND DID NOT ARRIVE. rc=132 would have taken the # process down with whatever was still in its stdio buffer, so the probe prints # and FLUSHES every fact that does not need evdev before it attempts the # import. In the event the failure was a catchable exception, so the probe # recorded it and ran on -- but the ordering costs nothing and is what keeps a # killed run informative, which is why it stays. # # THIS SUITE IS INFORMATIVE EITHER WAY, and was. Measured: 147 cases, 47 green, # 2 red, 98 skipped with the load error attached. The packaging, artifact-shape # and dpkg cases ran with no interpreter at all; the behavioural cases are # gated on one import case and SKIPPED rather than reded, so one platform fact # cost two reds that name it instead of ninety-eight that would bury it. # # ============================================================================ # WHERE THE EXPECTATIONS COME FROM. # ============================================================================ # # EVERY BEHAVIOURAL EXPECTATION BELOW IS THE OUTPUT OF AN x86-64 CONTROL built # from the same sdist the SRC_URI sha256 pins, not a reading of the source. That # caught four things the source does not read like: # # * evdev 1.6.1 HAS NO `__version__' ATTRIBUTE. The distribution metadata is # the only version the package states. `has-version-attr' pins the absence. # * UInputError IS NOT a subclass of EvdevError -- both derive straight from # Exception. An `issubclass' expectation would have been red on a working # port. # * ecodes.keys DOES NOT CONTAIN KEY_MAX: ecodes.py deletes KEY_MAX and # KEY_CNT from it so the dict is safe as a default uinput capability list. # * A REVERSE MAP HOLDS A LIST where several names share a value # (KEY_WIMAX and KEY_WWAN are both 246), so the round trip tests MEMBERSHIP. # An equality test would go red on every alias and say nothing. # # Two cases the control could not measure, and how they are covered instead: on # a control host /dev/input/event0 EXISTS and answers EACCES, and /dev/uinput # exists so UInput() succeeds. Both are measured there against a path that # cannot exist on any machine -- event999, and an explicit absent devnode -- so # the documented ENOENT and UInputError shapes are pinned somewhere rather than # left to the target run alone. P=python3-evdev # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/evdev DI=$SITE/evdev-1.6.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-evdev.list PROBE=$BST_DATA/python3-evdev-probe.py TABLE=$BST_DATA/python3-evdev-ecodes.json tmp=${TMPDIR:-/tmp} W=$tmp/bst-evdev.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # These run before any import, so a red here says the .deb is wrong rather than # that the module misbehaves -- which matters more than usual for this port, # since a missing .so and an unloadable one both end at the same ImportError. check $P/site-packages-dir test -d "$SITE" check $P/pkg-dir test -d "$PKG" check $P/distinfo-dir test -d "$DI" # THE THREE EXTENSIONS. Named one case each: they fail independently, and # _ecodes is the one that carries the cross-build evidence. check $P/ext-input-present test -s "$PKG/_input.cpython-312.so" check $P/ext-uinput-present test -s "$PKG/_uinput.cpython-312.so" check $P/ext-ecodes-present test -s "$PKG/_ecodes.cpython-312.so" # THE PURE-PYTHON HALF. Present, and it is not the port: every one of these # imports an extension at module scope. Listed so a packaging split that dropped # one is a named red rather than an import error attributed to the platform. for _m in __init__ device events uinput util ecodes ff eventio eventio_async \ evtest genecodes; do check $P/py-$_m test -s "$PKG/$_m.py" done # The suite's own fixture. A missing table would otherwise surface as 832 # mismatches attributed to the cross-build. check $P/ecodes-table-present test -s "$TABLE" # ASSERTED, not assumed. Every ENOENT the probe measures is attributed to these # absences; a machine that had grown an event node would make those expectations # wrong while every one of them still "passed" for the wrong reason. check $P/no-dev-input test ! -e /dev/input check $P/no-event0 test ! -e /dev/input/event0 check $P/no-uinput-node test ! -e /dev/uinput # ------------------------------------------------------------- dpkg shape --- # The package's own manifest, so a rename or a split shows up here rather than # as a missing file three cases up. check $P/dpkg-list-present test -s "$PKGLIST" check_out $P/dpkg-lists-input "_input.cpython-312.so" cat "$PKGLIST" check_out $P/dpkg-lists-uinput "_uinput.cpython-312.so" cat "$PKGLIST" check_out $P/dpkg-lists-ecodes "_ecodes.cpython-312.so" cat "$PKGLIST" # The stdlib splits the recipe declares. This image installs them for other # reasons, so these change no rootfs -- they stop a later image that drops one # from breaking evdev in silence. for _d in python3-core python3-ctypes python3-fcntl python3-io python3-shell \ python3-stringold; do check $P/rdepends-$_d test -s "/var/lib/dpkg/info/$_d.list" done # ------------------------------------------------- the import, from a shell -- # THE PLATFORM GATE, and the first of two python3 starts. This is what a caller # on this image actually gets. See the header: it FAILS, measured, with # ModuleNotFoundError: No module named '_ctypes' (brintos/hwjs-cc#15, reached # through evdev/uinput.py's and evdev/ff.py's module-scope `import ctypes'), # and the day it stops failing is the day this whole suite turns green with no # change to it. check $P/import python3 -c 'import evdev' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so a hung probe is reported as a probe overrun with the # last line it printed, rather than as a suite timeout that says nothing about # where it stopped. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "evdev works" -- `import-ok' below is what carries that. A # park violation kills it with rc=132 instead, which is why every pre-import # case is read from the flushed prefix rather than gated on this. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first flush; every CASE below is unscorable" fi # ------------------------------------------------- pre-import measurements --- # EMITTED AND FLUSHED BEFORE THE IMPORT, so these are scorable even when the # runtime kills the process at first load. They are the probe agreeing with the # packaging cases above from inside the interpreter. check_eq $P/probe-started "yes" "$(pv probe_start)" check_eq $P/probe-site-dir "$SITE" "$(pv site_dir)" check_eq $P/probe-pkg-dir "yes" "$(pv pkg_dir_found)" check_eq $P/probe-so-input "yes" "$(pv so_input_found)" check_eq $P/probe-so-uinput "yes" "$(pv so_uinput_found)" check_eq $P/probe-so-ecodes "yes" "$(pv so_ecodes_found)" check_eq $P/probe-distinfo "yes" "$(pv distinfo_found)" check_eq $P/probe-devinput-absent "yes" "$(pv devinput_absent)" check_eq $P/probe-event0-absent "yes" "$(pv event0_absent)" check_eq $P/probe-uinput-absent "yes" "$(pv uinput_node_absent)" check_eq $P/probe-table-loaded "yes" "$(pv table_loaded)" check_eq $P/probe-table-count "832" "$(pv table_count)" # ------------------------------------------------------- the gate's verdict --- _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # evdev did not import. SKIP -- not red -- every behavioural case, each with # the reason attached: they are all one fact, and ninety reds would bury it # rather than report it. `import' and `import-ok' above are the two reds that # carry the verdict, and the design doc attributes them. _why="import evdev failed: $(pv import_error)" [ -n "$(pv import_error)" ] || _why="import evdev did not complete: the probe stopped at the import with no exception recorded, which is the rc=132 kill shape (a side-module park violation, linux-in-the-browser#184) and NOT the measured ModuleNotFoundError path" for _c in probe-complete probe-count \ module-answered ext-input-answered ext-uinput-answered \ ext-ecodes-answered \ has-version-attr version-meta dist-version version-agrees \ has-InputDevice has-DeviceInfo has-AbsInfo has-EvdevError \ has-InputEvent has-KeyEvent has-RelEvent has-SynEvent \ has-AbsEvent has-UInput has-UInputError has-list_devices \ has-categorize has-resolve_ecodes has-ecodes has-ff has-util \ has-device has-events has-uinput \ list-devices-type list-devices-len list-devices-empty \ list-devices-tmp-len \ inputdevice-enoent inputdevice-enoent-absent survived-inputdevice \ ecodes-checked ecodes-expected-count ecodes-missing \ ecodes-mismatch ecodes-dict-missing ecodes-dict-mismatch \ roundtrip-absent roundtrip-bad \ ec-EV_SYN ec-EV_KEY ec-EV_REL ec-EV_ABS ec-EV_MAX ec-KEY_A \ ec-KEY_Z ec-KEY_ENTER ec-KEY_MAX ec-BTN_LEFT ec-ABS_X \ ec-ABS_MAX ec-REL_X ec-REL_MAX ec-SYN_REPORT ec-LED_NUML \ ec-SND_BELL ec-SW_LID \ rt-ecodes-KEY_A rt-KEY-30 rt-EV-1 bytype-EV_KEY keys-no-KEY_MAX \ ie-build ie-repr ie-sec ie-usec ie-type ie-code ie-value \ ie-timestamp \ ai-build ai-value ai-min ai-max ai-fuzz ai-flat ai-resolution \ cat-key cat-rel cat-abs cat-syn cat-unknown \ keyevent-keycode keyevent-keystate \ uinput-error-type uinput-names-path uinput-absent-type \ uinput-absent-names-path uinputerror-not-evdeverror \ async-raises has-async-read-one has-async-read-loop; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "120" "$(pv emitted_count)" # ----------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this every measurement below would be a statement # about whatever was first on sys.path. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/ext-input-answered "$PKG/_input.cpython-312.so" "$(pv input_ext_file)" check_eq $P/ext-uinput-answered "$PKG/_uinput.cpython-312.so" "$(pv uinput_ext_file)" check_eq $P/ext-ecodes-answered "$PKG/_ecodes.cpython-312.so" "$(pv ecodes_ext_file)" # MEASURED ON THE CONTROL: evdev 1.6.1 exposes no `__version__'. The absence is # the assertion; the metadata is where the version actually lives. check_eq $P/has-version-attr "no" "$(pv has_version_attr)" check_eq $P/version-meta "1.6.1" "$(pv version_meta)" check_eq $P/dist-version "1.6.1" "$(pv dist_version)" check_eq $P/version-agrees "yes" "$(pv version_agrees)" # The names __init__.py gathers. A release that moved one would break callers # without breaking any behavioural case below. for _n in InputDevice DeviceInfo AbsInfo EvdevError InputEvent KeyEvent \ RelEvent SynEvent AbsEvent UInput UInputError list_devices \ categorize resolve_ecodes ecodes ff util device events uinput; do check_eq $P/has-$_n "yes" "$(pv has_$_n)" done # ------------------------------------------------------------ list_devices --- # THE EMPTY ENUMERATION IS THE ASSERTION. Three cases rather than one: the type, # the length, and the literal repr, because "returned something falsy" and # "returned []" are not the same statement about an API. check_eq $P/list-devices-type "list" "$(pv list_devices_type)" check_eq $P/list-devices-len "0" "$(pv list_devices_len)" check_eq $P/list-devices-empty "[]" "$(pv list_devices_result)" # Against a directory that DOES exist and holds no event node, which separates # "empty because /dev/input is absent" from "empty because nothing matched". check_eq $P/list-devices-tmp-len "0" "$(pv list_devices_tmp_len)" # ---------------------------------------------------- InputDevice on ENOENT --- # The documented FileNotFoundError, carrying errno and the filename -- and, the # ticket's point, NOT an interpreter abort. `survived-inputdevice' is the second # half of that: the probe reached the line after it. check_eq $P/inputdevice-enoent \ "FileNotFoundError|2|/dev/input/event0" "$(pv inputdevice_enoent)" check_eq $P/inputdevice-enoent-absent \ "FileNotFoundError|2|/dev/input/event999" "$(pv inputdevice_enoent_absent)" check_eq $P/survived-inputdevice "yes" "$(pv survived_inputdevice)" # ---------------------------------------------------------------- ecodes --- # THE CROSS-BUILD CHECK. Counts, not samples: all 832 names from this image's # own headers, compared against _ecodes, against the re-exported name->value # dict, and back again through the per-prefix reverse maps. check_eq $P/ecodes-checked "yes" "$(pv ecodes_checked)" check_eq $P/ecodes-expected-count "832" "$(pv ecodes_expected_count)" check_eq $P/ecodes-missing "0" "$(pv ecodes_missing_count)" check_eq $P/ecodes-mismatch "0" "$(pv ecodes_mismatch_count)" check_eq $P/ecodes-dict-missing "0" "$(pv ecodes_dict_missing_count)" check_eq $P/ecodes-dict-mismatch "0" "$(pv ecodes_dict_mismatch_count)" check_eq $P/roundtrip-absent "0" "$(pv roundtrip_absent_count)" check_eq $P/roundtrip-bad "0" "$(pv roundtrip_bad_count)" # Named spot values, so a total failure of the table check still names something # a reader recognises rather than only a count. check_eq $P/ec-EV_SYN "0" "$(pv ec_EV_SYN)" check_eq $P/ec-EV_KEY "1" "$(pv ec_EV_KEY)" check_eq $P/ec-EV_REL "2" "$(pv ec_EV_REL)" check_eq $P/ec-EV_ABS "3" "$(pv ec_EV_ABS)" check_eq $P/ec-EV_MAX "31" "$(pv ec_EV_MAX)" check_eq $P/ec-KEY_A "30" "$(pv ec_KEY_A)" check_eq $P/ec-KEY_Z "44" "$(pv ec_KEY_Z)" check_eq $P/ec-KEY_ENTER "28" "$(pv ec_KEY_ENTER)" check_eq $P/ec-KEY_MAX "767" "$(pv ec_KEY_MAX)" check_eq $P/ec-BTN_LEFT "272" "$(pv ec_BTN_LEFT)" check_eq $P/ec-ABS_X "0" "$(pv ec_ABS_X)" check_eq $P/ec-ABS_MAX "63" "$(pv ec_ABS_MAX)" check_eq $P/ec-REL_X "0" "$(pv ec_REL_X)" check_eq $P/ec-REL_MAX "15" "$(pv ec_REL_MAX)" check_eq $P/ec-SYN_REPORT "0" "$(pv ec_SYN_REPORT)" check_eq $P/ec-LED_NUML "0" "$(pv ec_LED_NUML)" check_eq $P/ec-SND_BELL "1" "$(pv ec_SND_BELL)" check_eq $P/ec-SW_LID "0" "$(pv ec_SW_LID)" # BOTH DIRECTIONS, named. The counts above cover the whole table; these are the # two the module's own docstring uses as its examples. check_eq $P/rt-ecodes-KEY_A "30" "$(pv rt_ecodes_KEY_A)" check_eq $P/rt-KEY-30 "KEY_A" "$(pv rt_KEY_30)" check_eq $P/rt-EV-1 "EV_KEY" "$(pv rt_EV_1)" check_eq $P/bytype-EV_KEY "yes" "$(pv bytype_EV_KEY_is_keys)" # MEASURED ON THE CONTROL: ecodes.py deletes KEY_MAX and KEY_CNT from `keys' so # the dict is safe as a default uinput capability list. check_eq $P/keys-no-KEY_MAX "no" "$(pv keys_has_KEY_MAX)" # ------------------------------------------------------- InputEvent/AbsInfo --- check_eq $P/ie-build "ok" "$(pv ie_build)" check_eq $P/ie-repr "InputEvent(1, 2, 1, 30, 1)" "$(pv ie_repr)" check_eq $P/ie-sec "1" "$(pv ie_sec)" check_eq $P/ie-usec "2" "$(pv ie_usec)" check_eq $P/ie-type "1" "$(pv ie_type)" check_eq $P/ie-code "30" "$(pv ie_code)" check_eq $P/ie-value "1" "$(pv ie_value)" check_eq $P/ie-timestamp "1.000002" "$(pv ie_timestamp)" check_eq $P/ai-build "ok" "$(pv ai_build)" check_eq $P/ai-value "1" "$(pv ai_value)" check_eq $P/ai-min "-32" "$(pv ai_min)" check_eq $P/ai-max "32" "$(pv ai_max)" check_eq $P/ai-fuzz "0" "$(pv ai_fuzz)" check_eq $P/ai-flat "2" "$(pv ai_flat)" check_eq $P/ai-resolution "11" "$(pv ai_resolution)" # ------------------------------------------------------------- categorize --- # Synthetic events -- there is no device to read them from, which is the point: # classification is a pure function of the event's type. check_eq $P/cat-key "KeyEvent" "$(pv cat_key)" check_eq $P/cat-rel "RelEvent" "$(pv cat_rel)" check_eq $P/cat-abs "AbsEvent" "$(pv cat_abs)" check_eq $P/cat-syn "SynEvent" "$(pv cat_syn)" # An unclassified type falls through to the InputEvent itself. check_eq $P/cat-unknown "InputEvent" "$(pv cat_unknown)" check_eq $P/keyevent-keycode "KEY_A" "$(pv keyevent_keycode)" check_eq $P/keyevent-keystate "1" "$(pv keyevent_keystate)" # ---------------------------------------------------------------- UInput --- # The documented UInputError NAMING THE PATH, and promptly. Split into the type # and the naming so a message reword upstream is one red rather than two. check_eq $P/uinput-error-type \ 'UInputError|"/dev/uinput" does not exist or is not a character device file - verify that the uinput module is loaded' \ "$(pv uinput_noden)" check_eq $P/uinput-names-path "yes" "$(pv uinput_names_path)" # The same shape against an explicit absent devnode -- the form the control # could measure, since a control host HAS a /dev/uinput. check_eq $P/uinput-absent-type \ 'UInputError|"/dev/uinput-does-not-exist" does not exist or is not a character device file - verify that the uinput module is loaded' \ "$(pv uinput_absent_devnode)" check_eq $P/uinput-absent-names-path "yes" "$(pv uinput_absent_names_path)" # MEASURED ON THE CONTROL: UInputError is NOT an EvdevError -- both derive # straight from Exception. Asserted so a later release that unifies them is a # named red rather than a silent change to what callers must catch. check_eq $P/uinputerror-not-evdeverror "no" "$(pv uinputerror_is_evdeverror)" # ------------------------------------------------------ async read helper --- # ASSERTED TO RAISE RATHER THAN TO BLOCK. The helper is reached through an # InputDevice, so on this machine the constructor is what refuses -- and that it # refuses WITHOUT an event loop running is the property worth pinning. check_eq $P/async-raises "ctor-FileNotFoundError" "$(pv async_read)" check_eq $P/has-async-read-one "yes" "$(pv has_async_read_one)" check_eq $P/has-async-read-loop "yes" "$(pv has_async_read_loop)" rm -rf "$W" exit 0