. "$BST_LIB" # BST-TIMEOUT-OK: GRANDFATHERED, NOT CLASSIFIED. This suite landed while the # timeout(1) gate was in flight on another PR, so neither review could see the # other, and its bounds have not been through the BRINTOS-1645 sweep. They # are inert, like every other timeout(1), on a kernel without the # brintos/linux#77 fix: there the cases pass because the wrapped commands exit # on their own, not because a bound holds. Some plausibly ARE load-bearing -- an # `iio_info -u ip:...' against a TEST-NET address, a bare `thd' that daemonises # -- and classifying those needs the suite's author, not a mechanical # conversion by someone who cannot test it. # This waiver unblocks the build and marks the debt; it does not claim the # bounds are fine. require thd triggerhappy/present # BST-TIMEOUT: 300 # ~40 cases, every one of which is documented to exit # # immediately; the only waits here are the `timeout` # # backstops that catch a regression to a blocking run. # # Not yet measured on target, so the bound is set well # # clear rather than tuned. # triggerhappy 0.5.0+git (SRCREV 44a1731, meta-oe recipes-extended) on a machine # with NO INPUT DEVICE. There is no /dev/input/event* behind this guest, so the # daemon has nothing to read -- and the port is proved by producing exactly the # documented behaviour for that, not by skipping it. # # The package is two programs and the suite splits along them: # # thd the daemon. Its whole reachable surface here is the STARTUP path -- # argument handling, the trigger-configuration parser, and the # decision about whether to enter its select(2) loop at all. THAT # DECISION IS THE ASSERTION THIS ENTRY EXISTS FOR: with no device # opened, `process_events' must return immediately rather than sit in # a 5-second select forever. Every case below that runs thd is # wrapped in `timeout', and a 124 is reported as its own named # failure, so a regression to a blocking run costs one case instead of # hanging the suite and wedging the machine for every other member. # # th-cmd the client. It speaks to a daemon over an AF_UNIX datagram socket; # with no daemon running, what is assertable is that it REFUSES -- # a named diagnostic and a non-zero exit, promptly. # # The configuration parser is the other half of the port and it needs no device # at all: it is driven here over files written to $TMPDIR, both the single-file # and the triggers.d directory form the package ships. # # EVERY LITERAL BELOW WAS MEASURED against a native x86-64 build of the same # SRCREV before it was written here, not read off the source. Where the measured # behaviour contradicts what BRINTOS-2419 predicted, the MEASUREMENT is what is # encoded and the divergence is called out in the comment above the case. CFG="${TMPDIR:-/tmp}/th-2419" rm -rf "$CFG"; mkdir -p "$CFG" || true check triggerhappy/tmpdir test -d "$CFG" # ------------------------------------------------------------- the versions -- # Both programs carry TH_VERSION, compiled in from version.inc, and they print # it in different voices -- thd on stdout under --help, th-cmd on STDERR as part # of its usage. Asserted separately: a mismatch is how a half-rebuilt package # shows up, and the recipe pins PV to 0.5.0+git off exactly this SRCREV. check_out triggerhappy/version-thd "Triggerhappy event daemon 0.5.0" thd --help check_out triggerhappy/version-thcmd "Triggerhappy command 0.5.0" \ sh -c 'th-cmd --help 2>&1; exit 0' # ---------------------------------------------------------------- the help --- # thd --help exits 0 on stdout. th-cmd's --help exits 1 on stderr -- upstream # routes its usage through the same show_help() the error paths use and returns # 1 unconditionally (th-cmd.c:80-85). That asymmetry is upstream's, measured, # and pinned here so it is not later mistaken for a wasm32 defect. check_rc triggerhappy/help-thd-rc 0 thd --help check_out triggerhappy/help-thd-usage " thd [switches] [devices]" thd --help check_out triggerhappy/help-thd-triggers \ " --triggers Load trigger definitions from " thd --help check_out triggerhappy/help-thd-dump " --dump Dump events to console" thd --help check_rc triggerhappy/help-thcmd-rc 1 th-cmd --help check_out triggerhappy/help-thcmd-usage \ " th-cmd --socket --clear" sh -c 'th-cmd --help 2>&1; exit 0' # ------------------------------------------- the daemon with nothing to read -- # THE CENTRAL CASE. `thd' with no device, no --deviceglob and no --socket never # reaches its event loop: start_readers() refuses at thd.c:293-297 with a named # diagnostic on stderr and returns 1. Asserted as a REFUSAL -- the diagnostic, # the stream and the status together -- inside a bound. nout=$(timeout 60 thd 2>&1); nrc=$? case "$nout" in *"No input device files or command pipe specified."*) nhit=1 ;; *) nhit=0 ;; esac if [ "$nrc" -eq 1 ] && [ "$nhit" -eq 1 ]; then bst_ok triggerhappy/no-device-refused elif [ "$nrc" -eq 124 ]; then bst_notok triggerhappy/no-device-refused \ "blocked past 60s -- thd entered its select loop with no device to read" else bst_notok triggerhappy/no-device-refused \ "rc=$nrc want=1 + the documented refusal; got: $(_bst_1line "$nout")" fi # The same refusal reached the other way: the diagnostic is on STDERR, not # stdout, so a caller redirecting only stdout still sees it. Asserted because # the init script the package ships runs thd with its output detached. eout=$(timeout 60 thd 2>&1 >/dev/null); erc=$? check_eq triggerhappy/no-device-stderr "No input device files or command pipe specified." "$eout" # ------------------------------------ a device node that does not exist ------- # BRINTOS-2419 predicted this path "reports the documented diagnostic and exits # with the documented status"; the MEASURED status is 0, not a failure, and the # reason is worth stating because it is the whole of what makes triggerhappy # safe to ship on a machine with no input device. # # add_device() cannot open the node, so it prints `Error opening ...' and # RETURNS WITHOUT ADDING IT (devices.c:87-89). start_readers() has already # passed its own argc check -- a device WAS named on the command line -- so it # proceeds, and process_events() is then entered with an EMPTY device list. Its # loop is `while (count_devices() > 0 || cmd_fd != -1)' (thd.c:159), which is # false at the first evaluation, so the body never runs and thd exits 0 without # ever calling select(). Measured at 2ms natively. # # THIS is the failure the entry exists to catch: a regression that entered the # loop anyway would block for as long as the guest survives, and the `timeout' # plus the explicit 124 arm are what turn that into one red instead of a hang. dout=$(timeout 60 thd --dump /dev/input/event-does-not-exist 2>&1); drc=$? case "$dout" in *"Error opening '/dev/input/event-does-not-exist': No such file or directory"*) dhit=1 ;; *) dhit=0 ;; esac if [ "$drc" -eq 0 ] && [ "$dhit" -eq 1 ]; then bst_ok triggerhappy/missing-node-bounded elif [ "$drc" -eq 124 ]; then bst_notok triggerhappy/missing-node-bounded \ "blocked past 60s -- thd entered its select loop with an EMPTY device list" else bst_notok triggerhappy/missing-node-bounded \ "rc=$drc want=0 + the documented open error; got: $(_bst_1line "$dout")" fi # --deviceglob matching nothing takes the same route through glob(3) and must # also come back rather than block: the glob contributes no device, so the # device list is empty for the same reason. gout=$(timeout 60 thd --dump --deviceglob '/dev/input/event-nothing-matches*' 2>&1); grc=$? if [ "$grc" -eq 0 ]; then bst_ok triggerhappy/empty-glob-bounded elif [ "$grc" -eq 124 ]; then bst_notok triggerhappy/empty-glob-bounded "blocked past 60s on an empty device glob" else bst_notok triggerhappy/empty-glob-bounded "rc=$grc want=0; got: $(_bst_1line "$gout")" fi # ------------------------------------------------------ the event vocabulary -- # --listevents prints the names the configuration parser resolves, and it is the # SAME table: both go through lookup_event_code() over the evtable_*.inc files. # Those files are generated at BUILD time from the target's own linux/input.h -- # `$(CC) -dM -E' over it, filtered by awk (Makefile:44-48) -- so this case is # also the only evidence that the cross build saw real kernel UAPI headers. # # The COUNT is deliberately not asserted: it is whatever the pinned kernel # headers define (657 natively) and would red on a headers bump that broke # nothing. Membership of individually stable names is asserted instead. # # AN ALIASED NAME IS NOT A STABLE NAME. EV_MAP expands to a DESIGNATED # initialiser -- `[ N ] = #N' (eventtable.h:9) -- so two names sharing one code # write the same slot and only one survives, and lookup_event_code() scans that # single-name table (eventnames.c:39-58): the loser is then unresolvable in a # trigger file, not merely unprinted. Which one loses is decided by the order # `$(CC) -dM -E' happens to dump its macros in, which is a compiler property # and not one this port controls. linux/input-event-codes.h aliases seven codes # in the KEY/BTN table -- 0x100 BTN_MISC/BTN_0, 0x110 BTN_MOUSE/BTN_LEFT, # 0x120 BTN_JOYSTICK/BTN_TRIGGER, 0x130 BTN_GAMEPAD/BTN_SOUTH, # 0x140 BTN_DIGI/BTN_TOOL_PEN, 0x150 BTN_GEAR_DOWN/BTN_WHEEL, # 0x2c0 BTN_TRIGGER_HAPPY/BTN_TRIGGER_HAPPY1 -- so none of the fourteen is # assertable by name. Every name in the loop below is unaliased; the alias case # is pinned separately, as the disjunction it actually is. check_rc triggerhappy/listevents-rc 0 thd --listevents for ev in KEY_A KEY_LEFTCTRL KEY_LEFTSHIFT KEY_VOLUMEUP KEY_POWER BTN_RIGHT REL_X SW_LID; do check_out triggerhappy/listevents-$ev "$ev" thd --listevents done # The 0x110 slot carries exactly one of its two spellings -- never both, and # never neither. This is the BTN_ table's real invariant, and it also proves # evtable_BTN.inc was generated and merged into KEY_NAME beside evtable_KEY.inc. lout=$(timeout 60 thd --listevents 2>/dev/null) lmouse=$(printf '%s\n' "$lout" | grep -cx 'BTN_MOUSE') lleft=$(printf '%s\n' "$lout" | grep -cx 'BTN_LEFT') if [ "$((lmouse + lleft))" -eq 1 ]; then bst_ok triggerhappy/listevents-btn-alias else bst_notok triggerhappy/listevents-btn-alias \ "code 0x110 wants exactly one name; BTN_MOUSE=$lmouse BTN_LEFT=$lleft" fi # ...and a name that is not in the table is not invented by it. if thd --listevents 2>/dev/null | grep -qx 'KEY_NOSUCHKEY'; then bst_notok triggerhappy/listevents-closed "KEY_NOSUCHKEY is in the event table" else bst_ok triggerhappy/listevents-closed fi # -------------------------------------------- the configuration parser: good -- # A committed hotkey configuration: a plain key, a key with a modifier, and a # repeat value. The assertion is SILENCE -- read_triggerfile() echoes every line # it cannot parse (trigger.c:78-80) and says nothing about one it can, so a # clean parse is the absence of that echo. Paired with the malformed cases # below, which prove the echo fires when it should, silence here is a real # result rather than an untested path. printf '# BRINTOS-2419 fixture\n' > "$CFG/good.conf" printf 'KEY_A\t1\t/bin/echo a-pressed\n' >> "$CFG/good.conf" printf 'KEY_B+KEY_LEFTCTRL\t1\t/bin/echo ctrl-b\n' >> "$CFG/good.conf" printf 'KEY_VOLUMEUP\t2\t/bin/echo volume-repeat\n' >> "$CFG/good.conf" check triggerhappy/cfg-fixture test -s "$CFG/good.conf" # --dump with a node that cannot open is what isolates the parser from the # no-device refusal above: it keeps the exit status at 0, so a non-zero here is # the parser's and nothing else's. gcout=$(timeout 60 thd --triggers "$CFG/good.conf" --dump /dev/input/nope 2>&1); gcrc=$? case "$gcout" in *"Unable to parse trigger line"*) gchit=1 ;; *) gchit=0 ;; esac if [ "$gcrc" -eq 0 ] && [ "$gchit" -eq 0 ]; then bst_ok triggerhappy/cfg-good-parses elif [ "$gcrc" -eq 124 ]; then bst_notok triggerhappy/cfg-good-parses "blocked past 60s" else bst_notok triggerhappy/cfg-good-parses \ "rc=$gcrc want=0 with no parse complaint; got: $(_bst_1line "$gcout")" fi # -------------------------------------------- the configuration parser: bad --- # BRINTOS-2419 predicted these are "rejected with the file and line named, and a # non-zero exit". MEASURED, upstream does neither: read_triggerfile() echoes the # offending LINE only -- the file it came from is not named -- and then CONTINUES # to the next line, returning 0. A malformed trigger file is a per-line WARNING, # not a fatal error, and only a file that cannot be stat'd is fatal (below). # # That is upstream's design rather than a defect of this arm, and it is pinned # here in the shape it actually has: for a daemon loading a triggers.d directory # a poisoned drop-in disables one binding rather than the whole daemon. Each # construct is driven on its own so the case names say which one failed. mkcase() { printf '%s\n' "$2" > "$CFG/$1.conf"; } mkcase unknownkey 'KEY_NOSUCHKEY 1 /bin/echo nope' mkcase malformed 'this-is-not-a-trigger-line' mkcase nocommand 'KEY_A 1' mkcase badmod 'KEY_A+KEY_BOGUSMOD 1 /bin/echo x' for c in unknownkey malformed nocommand badmod; do bout=$(timeout 60 thd --triggers "$CFG/$c.conf" --dump /dev/input/nope 2>&1); brc=$? case "$bout" in *"Unable to parse trigger line: "*) bhit=1 ;; *) bhit=0 ;; esac if [ "$brc" -eq 0 ] && [ "$bhit" -eq 1 ]; then bst_ok triggerhappy/cfg-reject-$c elif [ "$brc" -eq 124 ]; then bst_notok triggerhappy/cfg-reject-$c "blocked past 60s" else bst_notok triggerhappy/cfg-reject-$c \ "rc=$brc want=0 + the parse complaint; got: $(_bst_1line "$bout")" fi done # The echo carries the offending line VERBATIM, which is the whole of what makes # the warning actionable. Asserted on the unknown-key case against a literal. check_out triggerhappy/cfg-reject-echoes-line \ "Unable to parse trigger line: KEY_NOSUCHKEY 1 /bin/echo nope" \ sh -c 'timeout 60 thd --triggers "$1" --dump /dev/input/nope 2>&1; exit 0' _ "$CFG/unknownkey.conf" # THE "EMPTY LINE" BRANCH ALMOST NEVER FIRES, and the three cases below pin the # shape it actually has. read_triggerfile() suppresses the warning on # `strlen(copy) > 0' with the comment "perhaps it was just an empty line" # (trigger.c:78-80) -- but `copy' is the buffer parse_trigger() has already # MUTATED, and parse_trigger() truncates at the '#' rather than at the start of # the line (triggerparser.c:75-78). So what survives is whatever PRECEDED the # comment, and the suppression only lands when that is nothing at all: # # "# comment\n" -> '#' at index 0, copy becomes "" -> QUIET # "\n" -> copy is "\n", strlen 1 -> WARNS # " # comment\n" -> copy becomes " ", strlen 3 -> WARNS # # All three measured natively. The first is what matters in practice -- it is # what keeps a commented-out drop-in from warning on every boot -- and the other # two are upstream quirks pinned so that a later change to them is visible # rather than discovered. Asserted by warning COUNT, since the point of each is # how many lines complained. thwarn() { timeout 60 thd --triggers "$1" --dump /dev/input/nope 2>&1 | grep -c 'Unable to parse trigger line'; } printf '# only a comment\n' > "$CFG/comment.conf" check_eq triggerhappy/cfg-comment-quiet 0 "$(thwarn "$CFG/comment.conf")" printf '\n' > "$CFG/blank.conf" check_eq triggerhappy/cfg-blank-warns 1 "$(thwarn "$CFG/blank.conf")" printf ' # indented comment\n' > "$CFG/indented.conf" check_eq triggerhappy/cfg-indented-comment-warns 1 "$(thwarn "$CFG/indented.conf")" # The shipped /etc/triggerhappy/triggers.d is EMPTY, so none of that is reached # on a default boot -- there are no lines to complain about. Asserted below. # ------------------------------------------- a trigger file that is not there -- # THIS is the fatal one, and the only one: read_triggers() stat()s the path # first and a failure there perror()s and returns 1 (trigger.c:103-106), which # reload_triggerfile() turns into `Error loading triggerfile' and main() into a # non-zero exit BEFORE any device is opened. Both halves of the diagnostic are # asserted -- the perror and the named file -- because it is the pair that tells # an operator which path was wrong. mout=$(timeout 60 thd --triggers "$CFG/definitely-absent.conf" --dump /dev/input/nope 2>&1); mrc=$? case "$mout" in *"Error loading triggerfile '$CFG/definitely-absent.conf'"*) mhit=1 ;; *) mhit=0 ;; esac if [ "$mrc" -eq 1 ] && [ "$mhit" -eq 1 ]; then bst_ok triggerhappy/cfg-missing-fatal else bst_notok triggerhappy/cfg-missing-fatal \ "rc=$mrc want=1 + the named triggerfile; got: $(_bst_1line "$mout")" fi check_out triggerhappy/cfg-missing-perror "stat: No such file or directory" \ sh -c 'timeout 60 thd --triggers "$1" --dump /dev/input/nope 2>&1; exit 0' _ \ "$CFG/definitely-absent.conf" # ------------------------------------------------- the triggers.d directory --- # The package ships /etc/triggerhappy/triggers.d and the daemon is meant to be # pointed at it, so the DIRECTORY form of --triggers is exercised as its own # path: read_triggers() stats the path, finds a directory, and scandir()s it # through accept_triggerfile(), which admits a name only when it ENDS in # `.conf' (trigger.c:88-97). # # The suffix filter is asserted by discrimination rather than by inspection: the # .conf drop-in with a bad key must warn, and the .txt one -- carrying an # equally bad key -- must NOT, because it should never have been read at all. mkdir -p "$CFG/triggers.d" printf 'KEY_A\t1\t/bin/echo from-conf\n' > "$CFG/triggers.d/10-ok.conf" printf 'KEY_BOGUSINCONF\t1\t/bin/echo x\n' > "$CFG/triggers.d/20-bad.conf" printf 'KEY_BOGUSINTXT\t1\t/bin/echo y\n' > "$CFG/triggers.d/30-ignored.txt" tout=$(timeout 60 thd --triggers "$CFG/triggers.d" --dump /dev/input/nope 2>&1); trc=$? case "$tout" in *"KEY_BOGUSINCONF"*) tconf=1 ;; *) tconf=0 ;; esac case "$tout" in *"KEY_BOGUSINTXT"*) ttxt=1 ;; *) ttxt=0 ;; esac if [ "$trc" -eq 0 ] && [ "$tconf" -eq 1 ] && [ "$ttxt" -eq 0 ]; then bst_ok triggerhappy/triggersd-suffix-filter else bst_notok triggerhappy/triggersd-suffix-filter \ "rc=$trc conf-read=$tconf txt-read=$ttxt (want 0/1/0); got: $(_bst_1line "$tout")" fi # An empty triggers.d -- which is exactly what the package installs -- is not an # error. Asserted because it is the shipped state: a daemon that complained # about its own default configuration would fail on every boot. eout2=$(timeout 60 thd --triggers /etc/triggerhappy/triggers.d --dump /dev/input/nope 2>&1); erc2=$? if [ "$erc2" -eq 0 ]; then bst_ok triggerhappy/triggersd-shipped-empty else bst_notok triggerhappy/triggersd-shipped-empty \ "rc=$erc2 want=0 over the shipped triggers.d; got: $(_bst_1line "$eout2")" fi # ------------------------------------------------------------------ th-cmd --- # The client's two argument refusals, each a named diagnostic with a non-zero # exit. Both are on stderr, hence the sh -c wrapper. check_rc triggerhappy/thcmd-nocmd-rc 1 th-cmd check_out triggerhappy/thcmd-nocmd-msg "A single command must be specified!" \ sh -c 'th-cmd 2>&1; exit 0' check_rc triggerhappy/thcmd-nosocket-rc 1 th-cmd --clear check_out triggerhappy/thcmd-nosocket-msg "No socket specified!" \ sh -c 'th-cmd --clear 2>&1; exit 0' # A socket path with no daemon behind it. The ASSERTION IS THAT IT REFUSES # PROMPTLY rather than blocking, which is the ticket's requirement; the exact # diagnostic is pinned separately below so a platform divergence in the errno # text is visible without muddying this case. # # The path is kept short on purpose: connect_cmdsocket() does an unbounded # `strcpy(server.sun_path, name)' into a 108-byte field (cmdsocket.c:56), so a # long path here would smash the client's stack and the case would be measuring # that instead. SOCK="$CFG/absent.sock" sout=$(timeout 60 th-cmd --socket "$SOCK" --clear 2>&1); src=$? if [ "$src" -eq 1 ]; then bst_ok triggerhappy/thcmd-absent-socket-rc elif [ "$src" -eq 124 ]; then bst_notok triggerhappy/thcmd-absent-socket-rc "blocked past 60s against an absent socket" else bst_notok triggerhappy/thcmd-absent-socket-rc \ "rc=$src want=1; got: $(_bst_1line "$sout")" fi # WHERE the refusal comes from is upstream's oddity and is pinned so that a # change of shape is visible. connect_cmdsocket() DISCARDS connect(2)'s return # value and hands back the fd regardless (cmdsocket.c:52-61), so the # `perror("connect()")' arm in main() is unreachable for an absent path and the # failure surfaces one call later, at sendmsg(2), as ENOTCONN through # `perror("send_command")'. Measured natively as exactly this line. # # A different errno text here is a statement about the runtime's AF_UNIX # datagram support rather than about triggerhappy -- if socket(2) itself fails # this becomes EBADF -- so a red on this case is informative and should be read # beside the rc case above, which is the one carrying the ticket's requirement. check_out triggerhappy/thcmd-absent-socket-msg \ "send_command: Transport endpoint is not connected" \ sh -c 'timeout 60 th-cmd --socket "$1" --clear 2>&1; exit 0' _ "$SOCK" # --------------------------------------------------------------- packaging --- # What FILES:${PN} promises, asserted as files rather than as a package query. check triggerhappy/pkg-thd test -x /usr/sbin/thd check triggerhappy/pkg-thcmd test -x /usr/sbin/th-cmd check triggerhappy/pkg-initd test -x /etc/init.d/triggerhappy check triggerhappy/pkg-triggersd test -d /etc/triggerhappy/triggers.d check triggerhappy/pkg-udevrule test -f /lib/udev/rules.d/80-triggerhappy.rules # Both programs are on PATH at the names the init script and the udev rule # invoke -- /usr/sbin being on it is not a given for a non-root login. check triggerhappy/pkg-thd-onpath command -v thd check triggerhappy/pkg-thcmd-onpath command -v th-cmd # The udev rule is the package's only integration point with the rest of the # image, and it names th-cmd's socket. Asserted as content so a rule that # installed but was rewritten upstream is visible. check_out triggerhappy/udevrule-names-thcmd "th-cmd" \ sh -c 'cat /lib/udev/rules.d/80-triggerhappy.rules 2>&1; exit 0' # NO SYSTEMD UNIT IS SHIPPED, and that is decided rather than incidental: the # recipe's PACKAGECONFIG is `${@bb.utils.contains("DISTRO_FEATURES","systemd", # ...)}' and brintos.conf sets DISTRO_FEATURES = "", so the init-script path is # what ships and do_install:append skips the unit files entirely. This case goes # red if the distro ever gains the systemd feature, which is exactly when the # packaging above needs revisiting. if [ -e /lib/systemd/system/triggerhappy.service ] || \ [ -e /usr/lib/systemd/system/triggerhappy.service ]; then bst_notok triggerhappy/no-systemd-unit \ "a triggerhappy systemd unit is on the image -- revisit the packaging" else bst_ok triggerhappy/no-systemd-unit fi rm -rf "$CFG"