. "$BST_LIB" # evemu-tools 2.7.0+git (meta-oe recipes-support/evemu-tools, SRCREV 86a5627). # freedesktop's kernel-device emulation tools: five programs over libevemu, # which is itself a thin layer over libevdev. # # evemu-describe dump a device's capabilities as an evemu description file # evemu-record the same, then append the live event stream # evemu-device create a synthetic device from a description file # evemu-play replay an event stream into a device # evemu-event inject one event into a device # # THIS IMAGE EXPOSES NO EVDEV INPUT DEVICE. wasm32_defconfig sets CONFIG_INPUT=y, # CONFIG_INPUT_EVDEV=y and CONFIG_VIRTIO_INPUT=y, so /dev/input/eventN exists # whenever the host attaches a virtio-input device -- and neither the host-side # runner nor the browser attaches one. CONFIG_INPUT_UINPUT is not set at all, so # no guest can create a synthetic device either (brintos/linux#82, the same gap # 79-libevdev's U group reports next door). # # --------------------------------------------------------------------------- # WHAT MAKES THE PACKAGE ASSERTABLE ANYWAY -- three facts about the source. # # 1. THE MODE IS CHOSEN BY argv[0]. evemu-describe and evemu-record are one # binary, as are evemu-device and evemu-play; each picks its half from # program_invocation_short_name. So the usage and error text a tool prints # is itself evidence the right half was entered, and that is checked below # for both pairs. # 2. THE DESCRIPTION-FILE READER TAKES A FILE, NOT A DEVICE. evemu_read() # parses N:/I:/P:/B:/A:/L:/S: lines and the `# EVEMU major.minor` header, # and diagnoses each malformed line in its own words before anything opens # /dev/uinput. Every one of those diagnostics is reachable from an ordinary # file, and group B drives all ten of them. # 3. THE EVENT WRITER TAKES ANY WRITABLE fd. evemu-event does a bare # open(path, O_WRONLY) with no S_ISCHR test, so a REGULAR FILE is a valid # target and the encoded struct input_event lands in it byte for byte. # That is what group C measures -- including that the libevdev name tables # resolve EV_KEY/KEY_A to the same bytes as 1/30, which is the strongest # single assertion this suite can make without a device. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86-64 BUILD OF THIS EXACT SOURCE. # # Every string and status pinned below was measured by building evemu 2.7.0+git # at SRCREV 86a5627 against libevdev 1.13.1 on x86-64 and running the case. They # are properties of the program's control flow, not of the architecture, so the # wasm32 build must agree; a divergence is a real defect. # # Two of them are pinned DELIBERATELY LOOSELY, because the native run and the # target differ for a reason that is not evemu's: # # * `error: could not create device:` is asserted WITHOUT its strerror tail. # The native run is a non-root user against a root-owned /dev/uinput and # says "Permission denied"; this image has no /dev/uinput at all and will # say "No such file or directory". The prefix is what the case is about. # * The encoded event's SIZE is asserted as a relation, never as 24. struct # input_event is two longs plus 8 bytes, so it is 24 on x86-64 and may be 16 # on this ILP32 arm. `--sync writes exactly twice one event` holds on both. # # NO TOOL HAS A --version OPTION -- not one of the five; getopt_long rejects it # like any other unknown option, and the package's only version surface is the # `# EVEMU 1.3` file-format header, which B12 and B13 assert in both directions. # # Every invocation is bounded. Nothing here blocks: evemu-record refuses at # EVIOCGRAB before reaching its read loop, evemu-play reads its stream to EOF # from a file, and the one path that would read the console (evemu-record with # no device argument, which scanf()s a menu selection) is given /dev/null. require evemu-record evemu/present bst_ok evemu/present # --- P: the package is whole -------------------------------------------------- # `[ -x ]` IS NOT AN ORACLE ON THE VIRTIOFS ROOT: the rootfs files are # owned 1000:1001 while the guest runs as root, so busybox test falls through to # the supplementary-group check and getgroups(2) answers ENOSYS -- a false result # with `sh: getgroups: Function not implemented` beside it (brintos/linux#19, nr # 115 in its table). The mode string is read off `ls -l` instead, which is also # the stronger assertion: do_install must leave all five executable. for b in evemu-record evemu-describe evemu-play evemu-device evemu-event; do check "evemu/bin-$b-present" test -f "/usr/bin/$b" check_eq "evemu/bin-$b-executable" "-rwxr-xr-x" "$(ls -l "/usr/bin/$b" 2>&1 | cut -c1-10)" done # --disable-shared is image-wide. libevemu ships as an archive in # evemu-tools-staticdev; a .so here would mean that stopped being true -- and # would also be the only thing that could make the upstream python bindings # work, which is why they are not on this image (see the image recipe's note). if [ -e /usr/lib/libevemu.so ] || [ -e /usr/lib/libevemu.so.3 ]; then bst_notok evemu/no-shared "a shared libevemu is installed on a static-only distro" else bst_ok evemu/no-shared fi check evemu/staticlib test -s /usr/lib/libevemu.a check evemu/header test -s /usr/include/evemu.h check evemu/pkgconfig test -s /usr/lib/pkgconfig/evemu.pc check_out evemu/pkgconfig-version "2.7.0" cat /usr/lib/pkgconfig/evemu.pc check_out evemu/pkgconfig-libs "-levemu" cat /usr/lib/pkgconfig/evemu.pc # --- fixtures ----------------------------------------------------------------- # Under $TMPDIR, never on the virtiofs root: a stdin redirect the top-level shell # performs on the rootfs makes dup2(2) return 513 and ash EXITS, which would end # this suite silently mid-run (brintos/linux#73). Group B and group C both # redirect, and every source they name is one of these. T=${TMPDIR:-/tmp}/evemu-$$ mkdir -p "$T" || { bst_notok evemu/fixtures "could not create $T"; exit 0; } : > "$T/empty.prop" printf 'X: junk\n' > "$T/badname.prop" printf '# EVEMU 1.3\nN: Test device\nJUNK\n' > "$T/badid.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\n' > "$T/ok.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nP: 00 01 02\n' \ > "$T/badprop.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nB: 00 0b\n' \ > "$T/badbit.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nB: 40 0b 00 00 00 00 00 00 00\n' \ > "$T/badidx.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nA: 00 0 1 2 3\n' \ > "$T/badabs13.prop" printf 'N: Test device\nI: 0003 046d c52b 0111\nA: 00 0 1 2 3\n' > "$T/okabs10.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nL: 00\n' > "$T/badled.prop" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nS: 00\n' > "$T/badsw.prop" printf '# EVEMU 9.9\nN: Test device\nI: 0003 046d c52b 0111\n' > "$T/newver.prop" # Four events at ONE timestamp. evemu_read_event_realtime() sleeps for the delta # between consecutive events, so a recording with advancing times would make the # replay case take as long as the recording did; identical times make every # delta zero and the whole replay immediate. printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nE: 100.000000 0001 001e 0001\nE: 100.000000 0000 0000 0000\nE: 100.000000 0001 001e 0000\nE: 100.000000 0000 0000 0000\n' \ > "$T/rec.event" printf '# EVEMU 1.3\nN: Test device\nI: 0003 046d c52b 0111\nE: bogus line here x\n' \ > "$T/badevent.event" check evemu/fixtures test -s "$T/rec.event" # --- A: usage, mode selection and argument validation ------------------------- # All device-free. The two exit statuses are not a typo and are asserted as # measured: evemu-record/evemu-describe `return 1` from main, while # evemu-play/evemu-device/evemu-event return -1, which the shell sees as 255. # The mode pair. Each usage line names the tool that printed it through # program_invocation_short_name, so these two cases are what proves the # argv[0] switch works on this arm rather than both halves collapsing to one. check_out evemu/record-usage-names-record "Usage: evemu-record [" sh -c 'evemu-record --badopt 2>&1; exit 0' check_out evemu/describe-usage-names-describe "Usage: evemu-describe [" sh -c 'evemu-describe --badopt 2>&1; exit 0' check_out evemu/record-usage-body "This option is only valid for evemu-record." sh -c 'evemu-record --badopt 2>&1; exit 0' check_rc evemu/record-badopt-rc 1 evemu-record --badopt # --autorestart is the only option any of the five takes, and safe_atoi() plus an # explicit `<= 0` guard is the whole of its validation. check_out evemu/record-autorestart-zero "Usage: evemu-record [" sh -c 'evemu-record --autorestart=0 /dev/null 2>&1; exit 0' check_out evemu/record-autorestart-nan "Usage: evemu-record [" sh -c 'evemu-record --autorestart=abc /dev/null 2>&1; exit 0' check_out evemu/record-autorestart-needs-file "Option --autorestart requires an output file" \ sh -c 'evemu-record --autorestart=5 /dev/null 2>&1; exit 0' check_rc evemu/record-autorestart-zero-rc 1 evemu-record --autorestart=0 /dev/null # open(2) refusals, and the ioctl refusal behind them. /dev/null OPENS, so these # two reach further than an ENOENT does: evemu-record gets past open and fails at # EVIOCGRAB, evemu-describe gets past open and fails inside evemu_extract() -- # which is exactly the pair of device-layer entry points this image cannot # satisfy, each refusing in its own voice. check_out evemu/record-open-enoent "error: could not open device (" sh -c 'evemu-record /nonexistent/dev 2>&1; exit 0' check_out evemu/describe-open-enoent "error: could not open device (" sh -c 'evemu-describe /nonexistent/dev 2>&1; exit 0' check_out evemu/record-not-evdev "error: this device is grabbed and I cannot record events" \ sh -c 'evemu-record /dev/null 2>&1; exit 0' check_out evemu/record-not-evdev-manref "see the evemu-record man page for more information" \ sh -c 'evemu-record /dev/null 2>&1; exit 0' check_out evemu/describe-not-evdev "error: could not describe device" sh -c 'evemu-describe /dev/null 2>&1; exit 0' check_rc evemu/describe-not-evdev-rc 1 evemu-describe /dev/null # No device argument at all. find_event_devices() scandir()s /dev/input and, on # an image that has none, returns NULL without reading anything -- but where a # device IS attached it prints a menu and scanf()s a selection, so stdin is # /dev/null to keep the case bounded either way. Both roads end in usage. check_out evemu/record-no-device "Usage: evemu-record [" sh -c 'evemu-record &1; exit 0' # The play/device pair. Their usage text is printed with argv[0] rather than the # short name, so the assertion is on the body, which differs between the halves # and is therefore still a mode check. check_out evemu/play-usage "|" sh -c 'evemu-play 2>&1; exit 0' check_out evemu/play-usage-body "event data is read from standard input." sh -c 'evemu-play 2>&1; exit 0' check_out evemu/device-usage "" sh -c 'evemu-device 2>&1; exit 0' check_rc evemu/play-usage-rc 255 evemu-play check_rc evemu/device-usage-rc 255 evemu-device check_out evemu/play-open-enoent "error: could not open file or device (" sh -c 'evemu-play /nonexistent 2>&1; exit 0' check_out evemu/device-open-enoent "error: could not open file (" sh -c 'evemu-device /nonexistent 2>&1; exit 0' # evemu-event's own validation, which is the only one of the five that parses # more than a filename: a type, a code and a value, each accepted as a libevdev # symbolic name or as a number in any base strtol() takes. check_out evemu/event-usage "Usage: evemu-event [--sync] --type --code --value " \ sh -c 'evemu-event 2>&1; exit 0' check_rc evemu/event-usage-rc 255 evemu-event check_out evemu/event-bad-type "error: invalid type argument 'NOPE'" \ sh -c 'evemu-event --type NOPE --code KEY_A --value 1 --device /dev/null 2>&1; exit 0' check_out evemu/event-bad-code "error: invalid code argument 'NOPE'" \ sh -c 'evemu-event --type EV_KEY --code NOPE --value 1 --device /dev/null 2>&1; exit 0' check_out evemu/event-bad-value "error: invalid value argument 'abc'" \ sh -c 'evemu-event --type EV_KEY --code KEY_A --value abc --device /dev/null 2>&1; exit 0' check_out evemu/event-missing-device "error: missing device path" \ sh -c 'evemu-event --type EV_KEY --code KEY_A --value 1 2>&1; exit 0' # A second positional beside --device is rejected by argc-optind, not by getopt. check_out evemu/event-extra-arg "Usage: evemu-event [--sync]" \ sh -c 'evemu-event --type EV_KEY --code KEY_A --value 1 --device /dev/null extra 2>&1; exit 0' check_rc evemu/event-bad-type-rc 255 evemu-event --type NOPE --code KEY_A --value 1 --device /dev/null # --- B: the description-file reader ------------------------------------------- # evemu_read() over an ordinary file. Every case here is a REAL parse: the file # is opened, read and diagnosed line by line, and only afterwards does the tool # try to create a device and fail for want of /dev/uinput. The diagnostic is # therefore the whole of what is being asserted, and it is asserted from # evemu-device, whose sole argument is a description file. # # THE STATUS IS NOT PART OF THESE CASES, and must not be: every one of them ends # at the device-creation refusal this image cannot avoid, so all of them exit # 255 whether the parse was diagnosed correctly or not. `out_has` therefore # scores the diagnostic alone -- check_out would fail every case here on the # status while the text it was asked about matched perfectly. out_has() { _id=$1; _needle=$2; shift 2 _o=$("$@" 2>&1) case "$_o" in *"$_needle"*) bst_ok "$_id" ;; *) if bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "want:'$_needle' got:'$(_bst_1line "$_o")'"; fi ;; esac } pr() { evemu-device "$1" 2>&1; } out_has evemu/parse-empty "WARNING: This appears to be an empty file" pr "$T/empty.prop" out_has evemu/parse-bad-name "FATAL: Expected device name, but got: X: junk" pr "$T/badname.prop" out_has evemu/parse-bad-id "FATAL: Expected bus/vendor/product/version, got: JUNK" pr "$T/badid.prop" out_has evemu/parse-bad-prop "WARNING: Invalid INPUT_PROP line. Parsed 3 numbers, expected 8:" pr "$T/badprop.prop" out_has evemu/parse-bad-bit "FATAL: Invalid EV_BIT line. Parsed 2 numbers, expected 9:" pr "$T/badbit.prop" out_has evemu/parse-bad-ev-index "FATAL: Invalid EV_* index 0x40 in line:" pr "$T/badidx.prop" out_has evemu/parse-bad-led "FATAL: Invalid EV_LED line. Parsed 1 numbers, expected 2:" pr "$T/badled.prop" out_has evemu/parse-bad-sw "FATAL: Invalid EV_SW line. Parsed 1 numbers, expected 2:" pr "$T/badsw.prop" # The file-format header is load-bearing, not decoration: an A: line carries a # resolution field only from format 1.2 on, so the SAME five-number line is a # fatal parse error under `# EVEMU 1.3` and a clean one under the 1.0 default # that applies when no header is present. This pair is the whole of the version # surface this package has, and it is asserted in both directions. out_has evemu/parse-abs-needs-resolution-at-13 \ "FATAL: Invalid EV_ABS line. Parsed 5 numbers, expected 6:" pr "$T/badabs13.prop" out=$(pr "$T/okabs10.prop") case "$out" in *FATAL*|*"Invalid EV_ABS"*) bst_notok evemu/parse-abs-5-fields-at-10 "a 5-number A: line was refused under the 1.0 default: $(_bst_1line "$out")" ;; *"error: could not create device:"*) bst_ok evemu/parse-abs-5-fields-at-10 ;; *) bst_notok evemu/parse-abs-5-fields-at-10 "unexpected: $(_bst_1line "$out")" ;; esac out_has evemu/parse-newer-format \ "Warning: file format 9.9 is newer than supported version 1.3." pr "$T/newver.prop" # A WELL-FORMED file is the discriminator for all of the above: it must produce # NO parse diagnostic and get all the way to the device-creation refusal. Without # this case every red one above would pass just as well against a reader that # rejected everything. # # The strerror tail is deliberately not asserted -- see the header: this image # has no /dev/uinput and says ENOENT where the native oracle, an unprivileged # user against a root-owned node, says EACCES. out=$(pr "$T/ok.prop") case "$out" in *FATAL*|*WARNING*|*Warning*) bst_notok evemu/parse-clean "a well-formed description file drew a parse diagnostic: $(_bst_1line "$out")" ;; *"error: could not create device:"*) bst_ok evemu/parse-clean ;; *) bst_notok evemu/parse-clean "want the device-creation refusal, got: $(_bst_1line "$out")" ;; esac check_rc evemu/parse-clean-rc 255 evemu-device "$T/ok.prop" # The same reader is reached from evemu-play's other entry point, where the # argument is a recording rather than a description. One case, to show the two # halves share it rather than each carrying a parser of its own. out_has evemu/play-parses-description "FATAL: Expected device name, but got: X: junk" \ sh -c 'evemu-play "$1" 2>&1; exit 0' sh "$T/badname.prop" # --- C: the event encoder and the replay path --------------------------------- # evemu-event opens its --device with a bare O_WRONLY and no S_ISCHR test, so a # regular file receives the encoded struct input_event verbatim. There is no # O_CREAT and no O_TRUNC, so each target is created empty first. # # The size is asserted only as a RELATION. struct input_event is two longs plus # a u16/u16/s32 tail: 24 bytes where the native oracle ran, and possibly 16 on # this ILP32 arm. "--sync writes exactly twice one event" is true of both. : > "$T/one.bin"; evemu-event --type EV_KEY --code KEY_A --value 1 --device "$T/one.bin" 2>/dev/null : > "$T/two.bin"; evemu-event --type EV_KEY --code KEY_A --value 1 --sync --device "$T/two.bin" 2>/dev/null : > "$T/num.bin"; evemu-event --type 1 --code 30 --value 1 --device "$T/num.bin" 2>/dev/null : > "$T/hex.bin"; evemu-event --type EV_KEY --code 0x1e --value 1 --device "$T/hex.bin" 2>/dev/null : > "$T/neg.bin"; evemu-event --type EV_REL --code REL_X --value -1 --device "$T/neg.bin" 2>/dev/null s1=$(wc -c < "$T/one.bin" 2>/dev/null | tr -d ' ') s2=$(wc -c < "$T/two.bin" 2>/dev/null | tr -d ' ') sn=$(wc -c < "$T/neg.bin" 2>/dev/null | tr -d ' ') check_rc evemu/event-to-file-rc 0 evemu-event --type EV_KEY --code KEY_A --value 1 --device "$T/one.bin" if [ -n "$s1" ] && [ "$s1" -gt 0 ] 2>/dev/null; then bst_ok evemu/event-writes-one-event else bst_notok evemu/event-writes-one-event "wrote $s1 bytes to a regular file" fi check_eq evemu/event-sync-writes-two "$(( ${s1:-0} * 2 ))" "${s2:-0}" check_eq evemu/event-negative-value-same-width "${s1:-0}" "${sn:-0}" # THE STRONGEST DEVICE-FREE CASE IN THIS SUITE. evemu-event resolves --type and # --code through libevdev's generated name tables before encoding, so the bytes # it writes for EV_KEY/KEY_A must be identical to those for 1/30 and for # EV_KEY/0x1e. That asserts the encoding, the name table and the numeric parser # in one comparison -- and it is an assertion a device would add nothing to. check evemu/event-name-equals-number cmp -s "$T/one.bin" "$T/num.bin" check evemu/event-hex-code-equals-name cmp -s "$T/one.bin" "$T/hex.bin" # Replay. evemu-play takes the S_ISCHR road when its argument is a character # device, reading the event stream from stdin and writing each decoded event to # the fd -- so /dev/null is a complete, if silent, sink and the whole reader runs. out=$(evemu-play /dev/null < "$T/rec.event" 2>&1); rc=$? if [ "$rc" -eq 0 ] && [ -z "$out" ]; then bst_ok evemu/replay-from-stdin else bst_notok evemu/replay-from-stdin "rc=$rc out:'$(_bst_1line "$out")'" fi out=$(evemu-play /dev/null < /dev/null 2>&1); rc=$? if [ "$rc" -eq 0 ] && [ -z "$out" ]; then bst_ok evemu/replay-empty-stream else bst_notok evemu/replay-empty-stream "rc=$rc out:'$(_bst_1line "$out")'" fi # A malformed E: line is diagnosed by evemu_read_event() and stops the stream. # The status is NOT asserted: evemu_play() returns 0 regardless, which is # upstream's behaviour and not this port's to change. out=$(evemu-play /dev/null < "$T/badevent.event" 2>&1) case "$out" in *"FATAL: Invalid event format:"*) bst_ok evemu/replay-bad-event-line ;; *) bst_notok evemu/replay-bad-event-line "want the invalid-event diagnostic, got:'$(_bst_1line "$out")'" ;; esac # --- D: the halves that need a device, probed ----------------------------------- # The ticket asks for these to be PROBED and skipped only on a probe that # confirms the absence -- so a device that appears makes them run, and their # absence is a skip rather than a red. That is a deliberate difference from # 79-libevdev's U group, whose ticket asked for the uinput surface itself and so # scores it as an attributed red against brintos/linux#82; here the device layer # is not the subject, it is the part the ticket set aside. # EVERY ONE OF THESE HOLDS THE DEVICE OPEN AND NEVER RETURNS, by design, so all # three are bounded by timeout(1) and scored on what they PRODUCED rather than on # a status. evemu-record's read loop only ends when the device does; # evemu-device's open_and_hold_device() read()s the node until it errors; and # evemu-play against a recording is worse than blocking -- its `while (1)` calls # fgets() and then evemu_play(), which returns 0 unconditionally, so a stdin at # EOF spins it forever. That last one is why there is no replay-into-a-created- # device case here: on a guest that HAS uinput it would wedge the run, and a # suite that wedges the guest costs every member of the image its round. if [ -e /dev/input/event0 ]; then out_has evemu/describe-real-device "N: " sh -c 'evemu-describe /dev/input/event0 2>&1' rm -f "$T/rec.out" bst_bound 5 evemu-record /dev/input/event0 "$T/rec.out" >/dev/null 2>&1 if [ -s "$T/rec.out" ] && grep -q '^N: ' "$T/rec.out" 2>/dev/null; then bst_ok evemu/record-real-device else bst_notok evemu/record-real-device "no description written to the recording in 5s" fi else bst_skip evemu/describe-real-device "no /dev/input/event0: CONFIG_VIRTIO_INPUT=y but this guest boots with no input device attached" bst_skip evemu/record-real-device "no /dev/input/event0: CONFIG_VIRTIO_INPUT=y but this guest boots with no input device attached" fi if [ -e /dev/uinput ]; then # Creating the device prints ": " before it settles into its # hold loop, so that line is the evidence and timeout(1) ends the hold. out_has evemu/device-creates "Test device: /dev/input/event" \ sh -c '. "$BST_LIB"; bst_bound 5 evemu-device "$1" 2>&1' sh "$T/ok.prop" else bst_skip evemu/device-creates "no /dev/uinput: wasm32_defconfig sets CONFIG_INPUT=y and CONFIG_INPUT_EVDEV=y but not CONFIG_INPUT_UINPUT -- brintos/linux#82 (https://brintos.io/brintos/linux/issues/82)" fi printf '#diag: evemu event0=%s uinput=%s event-width=%s\n' \ "$( [ -e /dev/input/event0 ] && echo present || echo absent )" \ "$( [ -e /dev/uinput ] && echo present || echo absent )" \ "${s1:-none}" rm -rf "$T"