. "$BST_LIB" require setserial setserial/present # setserial 2.17 (poky meta/recipes-bsp/setserial) -- Ted Ts'o's tool for # getting and setting the configuration of a Linux serial port. It is one # 781-line C file and two ioctls: TIOCGSERIAL/TIOCSSERIAL on a file descriptor # opened O_RDWR|O_NONBLOCK, with argv parsed into the struct serial_struct they # carry. # # THE TICKET'S PREMISE IS PARTLY FALSE, AND ESTABLISHING THAT IS HALF THIS FILE. # BRINTOS-1543 asked for "the full option parser including every documented UART # type name" to be ASSERTED FOR REAL, with only the read/write of the port # configuration probed and skipped. The parser cannot be asserted that way, # because it does not sit where the ticket assumed. set_serial() runs in this # order: # # open(device, O_RDWR|O_NONBLOCK) -- fail => exit(201) # ioctl(fd, TIOCGSERIAL, &old) -- fail => exit(1) # while (*arg) { ...parse flags, uart types, numbers... } # # So EVERY parser diagnostic -- "Illegal UART type", "Invalid flag", "This flag # can not be inverted", "Missing argument for" -- is behind BOTH the open and a # successful TIOCGSERIAL, and is unreachable on a machine with no serial driver. # That is not a wasm32 finding: it was measured on x86-64, against a build of # this same patched 2.17, where a pty and a regular file BOTH answer the ioctl # with ENOTTY ("Cannot get serial info: Inappropriate ioctl for device") and the # parse loop is never entered. Opening something is easy here; opening something # that implements TIOCGSERIAL is the whole difficulty. # # So the suite splits three ways: # # ASSERTED FOR REAL, needing no serial device -- the version and usage # banners including the documented UART-type list byte for byte, the # getopt-level argument handling, the ESP build fact, and the exit-status # contract. That last is the ticket's own named criterion and it is asserted # in every arm it has, including the two that are easy to lose. # # ASSERTED AGAINST THE SHIPPED BINARY -- every name in serial_type_tbl[], # including the four the usage banner does not document. This is a statement # about the table the parser reads, not about the parser executing, and it is # labelled that way case by case. It is here because the ticket asks after # the UART type names and this is the strongest true thing that can be said # about them on a machine where uart_type() cannot be called. # # PROBED, then skipped -- reading and writing the port configuration, and the # parser cases above. The probe is setserial's OWN get operation, run over # every tty node the machine has, cross-read against /proc/tty/drivers. A # probe that fails IS the port failing rather than a proxy for it, and the # skip reason carries every reading it took. # # Every expectation below was executed against setserial 2.17 -- the poky # tarball at the recipe's own sha256, with poky's three patches applied -- built # on an x86-64 host before it was written down, rc and output compared. The # host used carried a real /dev/ttyS0 and working ptys, so the negative results # are negative for the tool's reasons and not for want of a device to try. BIN=/usr/bin/setserial # _case : ok iff exits AND its # combined output contains . Nearly every assertion here needs both # halves: setserial answers most things with a status that carries no message # and a message that carries no status. _case() { _cid=$1; _cwant=$2; _cn=$3; shift 3 _co=$("$@" 2>&1); _cr=$? case "$_co" in *"$_cn"*) _ch=1 ;; *) _ch=0 ;; esac if [ "$_cr" -eq "$_cwant" ] && [ "$_ch" -eq 1 ]; then bst_ok "$_cid" elif bst_execfail "$_co"; then bst_unrun "$_cid" "$_co" else bst_notok "$_cid" "rc=$_cr want=$_cwant needle:'$_cn' got:'$(_bst_1line "$_co")'"; fi } # _rc_only : the status alone, for the cases whose whole # content is the status. _rc_only() { _rid=$1; _rwant=$2; shift 2 _ro=$("$@" 2>&1); _rr=$? if [ "$_rr" -eq "$_rwant" ]; then bst_ok "$_rid" elif bst_execfail "$_ro"; then bst_unrun "$_rid" "$_ro" else bst_notok "$_rid" "rc=$_rr want=$_rwant out:'$(_bst_1line "$_ro")'"; fi } # --- the package ------------------------------------------------------------- check setserial/path test -x $BIN check_out setserial/on-path "$BIN" command -v setserial # THE MAN PAGE IS NOT ON THIS IMAGE, and asserting its presence would be a red # on every boot. The recipe's do_install writes setserial.8 into ${mandir}/man8, # which Yocto's default packaging splits into setserial-doc -- and this image # names only `setserial` on IMAGE_INSTALL, as it ships no -doc split for any # port. Verified against the built packages: setserial_2.17-r0_wasm32.deb # carries exactly one file, /usr/bin/setserial, and the man page is the whole of # setserial-doc. So the split is asserted from BOTH sides -- the binary is here # and the man page is not -- which turns a would-be red into a statement about # how this image is composed. A man page appearing means setserial-doc reached # IMAGE_INSTALL, which is a change worth seeing. check setserial/doc-split-no-man test ! -e /usr/share/man/man8/setserial.8 # --- the version banner ------------------------------------------------------ # -V is the ONLY flag that exits 0 and prints nothing else, and it writes to # STDERR. All three properties are pinned separately: a port that sent the # version to stdout, or exited 1 the way usage() does, would still satisfy a # single "does it print its version" case. _case setserial/version-rc 0 "setserial version 2.17, 27-Jan-2000" setserial -V _vout=$(setserial -V 2>/dev/null); _verr=$(setserial -V 2>&1 >/dev/null) if [ -z "$_vout" ]; then bst_ok setserial/version-stdout-empty else bst_notok setserial/version-stdout-empty "-V wrote to stdout: '$(_bst_1line "$_vout")'"; fi case "$_verr" in *"setserial version 2.17, 27-Jan-2000"*) bst_ok setserial/version-on-stderr ;; *) bst_notok setserial/version-on-stderr "version not on stderr: '$(_bst_1line "$_verr")'" ;; esac # --- the usage banner -------------------------------------------------------- # There is no --help and no -h: usage() is the whole of it, it is reached by # running setserial with NO arguments (main's `argc == 1`) or by any # unrecognised option, it goes to stderr, and it exits 1 -- not 0, which is what # separates it from -V. _case setserial/usage-rc 1 "usage:" setserial _case setserial/usage-synopsis 1 "serial-device -abqvVWz" setserial _case setserial/usage-get-form 1 "-g [-abGv] device1 ..." setserial _case setserial/usage-version-header 1 "setserial version 2.17" setserial _case setserial/usage-caution 1 "CAUTION: Using an invalid port can lock up your machine!" setserial _uout=$(setserial 2>/dev/null) if [ -z "$_uout" ]; then bst_ok setserial/usage-stdout-empty else bst_notok setserial/usage-stdout-empty "usage() wrote to stdout: '$(_bst_1line "$_uout")'"; fi # An unrecognised option goes through getopt's own diagnostic and then usage(). # Both halves are asserted: the getopt message proves the option was rejected by # getopt rather than silently ignored, and rc=1 proves it fell into usage(). _case setserial/bad-option-rc 1 "invalid option" setserial -x _case setserial/bad-option-usage 1 "usage:" setserial -x # The banner is 49 lines. This is a deliberate tripwire on the WHOLE shape # rather than on the lines the cases below happen to name -- a usage() that grew # or lost a line is a change worth seeing even where every named line survived. _ulines=$(setserial 2>&1 | wc -l | tr -d ' ') check_eq setserial/usage-line-count 49 "$_ulines" # --- the documented UART type names ------------------------------------------ # The ticket asks for "every documented UART type name". DOCUMENTED is the usage # banner's two-line list, and it is asserted whole, both lines, so a list that # lost or reordered a name reds even though every individual name still appears # somewhere in the binary. _case setserial/uart-doc-line1 1 "set UART type (none, 8250, 16450, 16550, 16550A," setserial _case setserial/uart-doc-line2 1 "16650, 16650V2, 16750, 16850, 16950, 16954)" setserial # The OTHER commands the banner documents, one case each. These are the tokens # the parser's flag_type_tbl carries, asserted where they are reachable -- in # the text -- because the table itself is not. `port`, `irq` and `base` are # deliberately asserted HERE and not against the binary's string data: they are # suffixes of `set_multiport`, `auto_irq` and `baud_base`, so a compiler that # tail-merges string literals stores no separate copy of them, and their absence # from the data section says nothing about the parser. for _d in \ '* port set the I/O port' \ '* irq set the interrupt' \ '* baud_base set base baud rate (CLOCK_FREQ / 16)' \ '* divisor set the custom divisor (see spd_custom)' \ '^ fourport configure the port as an AST Fourport' \ ' autoconfig automatically configure the serial port' \ '^ auto_irq try to determine irq during autoconfiguration' \ '^ skip_test skip UART test during autoconfiguration' \ '^ sak set the break key as the Secure Attention Key' \ '^ low_latency Minimize receive latency at the cost of greater' \ ' get_multiport Display the multiport configuration' \ ' set_multiport Set the multiport configuration' \ ' spd_hi use 56kb instead of 38.4kb' \ ' spd_vhi use 115kb instead of 38.4kb' \ ' spd_shi use 230kb instead of 38.4kb' \ ' spd_warp use 460kb instead of 38.4kb' \ ' spd_cust use the custom divisor to set the speed at 38.4kb' \ ' spd_normal use 38.4kb when a buad rate of 38.4kb is selected' ; do _id=$(printf '%s' "$_d" | sed 's/^[*^ ]*//; s/[ ].*//') _case "setserial/usage-cmd-$_id" 1 "$_d" setserial done # --- the ESP block is a per-arch BUILD fact, and it is OFF -------------------- # Six commands (rx_trigger, tx_trigger, flow_off, flow_on, rx_timeout, # dma_channel) and their six usage lines are #ifdef'd on TIOCGHAYESESP, which # arrives only with linux/hayesesp.h. That header was removed from the kernel # long ago and no current uapi set carries it, so this arm compiles them out # entirely -- the commands vanish from BOTH the banner and flag_type_tbl with no # error anywhere, and the banner is the one place it is observable. # # Asserted as ABSENT rather than assumed. A header set that brought # TIOCGHAYESESP back would grow six lines here, and that is a finding about the # arm rather than a regression of this port: the expectation would then need # re-deriving, not relaxing. Confirmed identical on the x86-64 oracle. _esp=$(setserial 2>&1 | grep -c "ESP-only") check_eq setserial/esp-block-absent 0 "$_esp" # --- the exit-status contract, which is the ticket's named criterion --------- # THE DOCUMENTED STATUS FOR A PORT THAT CANNOT BE OPENED IS 201, and it is a # property of the SET path only. This block asserts all of it without a device. # # Two device names are used and neither exists. /dev/bst-nosuch0 gives ENOENT; # /tmp exists and is a directory, so open(O_RDWR) gives EISDIR. The pair is what # separates the two arms of set_serial's one `if`: # # if (verbosity==0 && errno==ENOENT) exit(201); /* silent */ # perror(device); exit(201); /* loud */ # # both of which exit 201. A suite that tried only a missing path could not tell # those apart, and a change that dropped the errno test would go unseen. NODEV=/dev/bst-nosuch0 _case setserial/set-enoent-201 201 "No such file or directory" setserial $NODEV uart none _case setserial/set-enoent-201-name 201 "$NODEV" setserial $NODEV uart none _case setserial/set-eisdir-201 201 "Is a directory" setserial /tmp uart none # The silent arm: -b sets verbosity 0, and with ENOENT that suppresses the # perror while keeping the status. Asserted as an EMPTY stderr plus rc=201 -- # both halves, because either alone is satisfied by the loud arm or by a crash. _bo=$(setserial -b $NODEV uart none 2>&1); _br=$? if [ "$_br" -eq 201 ] && [ -z "$_bo" ]; then bst_ok setserial/set-b-enoent-silent-201 elif bst_execfail "$_bo"; then bst_unrun setserial/set-b-enoent-silent-201 "$_bo" else bst_notok setserial/set-b-enoent-silent-201 "rc=$_br want=201 with empty output, got:'$(_bst_1line "$_bo")'"; fi # The other side of the same `if`: -b with a NON-ENOENT errno must still print. # This is the case that fails if someone widens the silent arm to "-b means # quiet", which would still leave the case above passing. _case setserial/set-b-eisdir-loud-201 201 "Is a directory" setserial -b /tmp uart none # 201 is the status of the SET path, not of a flag. Three flags that change # verbosity, quietness and the zero-flags behaviour must not move it. _rc_only setserial/set-201-under-q 201 setserial -q $NODEV uart none _rc_only setserial/set-201-under-z 201 setserial -z $NODEV uart none _rc_only setserial/set-201-under-v 201 setserial -v $NODEV uart none # get_multiport takes the SET path too -- main() routes on argc, not on the # keyword -- so it is 201 rather than 0 for the same missing device. _rc_only setserial/set-multiport-arg-201 201 setserial $NODEV get_multiport # --- one missing device, three different statuses ---------------------------- # This is the property most easily lost in a port and it needs no device at all. # For the SAME nonexistent path, the status is decided purely by argv shape: # # setserial DEV cmd... -> set_serial() -> exit(201) # setserial DEV -> get_serial() -> RETURNS; main exits 0 # setserial -W DEV -> do_wild_intr() -> exit(1) # # get_serial() is the odd one: it perror()s and RETURNS rather than exiting, so # the open failure is reported on stderr and the process still succeeds. A shell # script testing `setserial $dev` for success gets 0 for a device that was never # opened. Pinned rather than treated as a bug to assert away. _case setserial/get-enoent-rc0 0 "No such file or directory" setserial $NODEV _case setserial/get-g-enoent-rc0 0 "No such file or directory" setserial -g $NODEV _rc_only setserial/wild-intr-enoent-rc1 1 setserial -W $NODEV # -b, -a and -G change get_serial's verbosity but not its status: the open fails # before any of them is consulted, so all three still exit 0. _rc_only setserial/get-b-enoent-rc0 0 setserial -b $NODEV _rc_only setserial/get-a-enoent-rc0 0 setserial -a $NODEV _rc_only setserial/get-G-enoent-rc0 0 setserial -G $NODEV # -g walks argv to the end: get_serial() returning rather than exiting is what # lets a second device be tried after the first one failed. Both names must # appear, which a version that exited on the first failure could not produce. _go=$(setserial -g $NODEV /dev/bst-nosuch1 2>&1); _gr=$? case "$_go" in *"$NODEV"*) case "$_go" in *"/dev/bst-nosuch1"*) if [ "$_gr" -eq 0 ]; then bst_ok setserial/get-g-continues-after-failure else bst_notok setserial/get-g-continues-after-failure "both devices reported but rc=$_gr want=0"; fi ;; *) bst_notok setserial/get-g-continues-after-failure "stopped after the first device: '$(_bst_1line "$_go")'" ;; esac ;; *) bst_notok setserial/get-g-continues-after-failure "first device not reported: '$(_bst_1line "$_go")'" ;; esac # -g with no device at all is a clean no-op: the while loop body never runs. _no=$(setserial -g 2>&1); _nr=$? if [ "$_nr" -eq 0 ] && [ -z "$_no" ]; then bst_ok setserial/get-g-no-device-noop else bst_notok setserial/get-g-no-device-noop "rc=$_nr want=0 with empty output, got:'$(_bst_1line "$_no")'"; fi # --- serial_type_tbl[], read out of the shipped binary ----------------------- # WHAT THIS BLOCK IS, EXACTLY. uart_type() cannot be called on this machine, so # these cases assert that the NAME TABLE THE PARSER READS is present and # complete in the module that was built -- not that the parser routes it. That # is weaker than the ticket asked for and it is the strongest true statement # available here; the parser cases themselves are skipped below, on the probe. # # The table carries FOUR names the usage banner does not document -- unknown, # Cirrus, 16654 and the combined 16950/954 -- so this is not a restatement of # the banner cases above. All fifteen entries were confirmed present as distinct # NUL-delimited literals in the x86-64 oracle. # # The match is anchored at END of line only, never at both ends: a compiler that # tail-merges string literals stores a name that is a suffix of another as a # pointer into it, and either layout is correct. Absence entirely is the finding. _tblnames=$(cat $BIN | tr -c '[:print:]' '\n') for _n in unknown 8250 16450 16550 16550A Cirrus 16650 16650V2 16750 '16950/954' 16950 16954 16654 16850 none; do _sid=$(printf '%s' "$_n" | tr '/' '-') if printf '%s\n' "$_tblnames" | grep -q -- "${_n}\$"; then bst_ok "setserial/uart-name-$_sid" else bst_notok "setserial/uart-name-$_sid" "'$_n' is in serial_type_tbl[] but no such string is in $BIN"; fi done # The ESP command names must NOT be in the binary, for the same #ifdef reason # the banner case above asserts -- checked from the data side so the two # statements are independent. for _n in rx_trigger tx_trigger dma_channel; do if printf '%s\n' "$_tblnames" | grep -q -- "${_n}\$"; then bst_notok "setserial/esp-name-absent-$_n" "'$_n' is in $BIN, so TIOCGHAYESESP was defined -- the banner and flag table now carry the ESP block and every expectation here needs re-deriving" else bst_ok "setserial/esp-name-absent-$_n"; fi done # --- probe: is there a device on this machine that implements TIOCGSERIAL? ---- # The probe is `setserial DEV` itself -- the TIOCGSERIAL read, byte for byte # what the port's own "show the settings" mode issues -- run over every # serial-family device node the machine has. Its STATUS cannot be used: get_serial() exits 0 whether it # read anything or not (asserted above), so the decision is on the OUTPUT. A # successful read prints ", UART: , Port: ..."; every failure prints # either the device name and an errno, or "Cannot get serial info". # # The cross-read is WHICH NODES CAN BE OPENED, and it is neither # /proc/tty/drivers nor the mere existence of a node. usb-serial sets its tty # driver's type to TTY_DRIVER_TYPE_SERIAL and CONFIG_USB_SERIAL is built into # this kernel, so /proc/tty/drivers lists a `serial` driver on this image # whether or not any port is bound. Node existence is no better: wasm-serial # registers /dev/ttySER0 -- which matches the ttyS* family -- at driver init and # refuses open(2) with ENODEV until a host WebSerial port attaches, so the node # is present on every unattended boot with nothing behind it. Keying the # contradiction on either would red every boot, which is the opposite of what a # probe is for. # # open(2) is the honest cross-read, and get_serial() reports it distinguishably: # a refused open perrors the DEVICE NAME ("/dev/ttySER0: No such device"), a # refused ioctl perrors "Cannot get serial info". The first says no port is # behind the node and is the same world as no node at all; only the second is # the contradiction this probe exists to catch. /proc/tty/drivers is still read, # and reported in the skip reason as information rather than used as the # decision. _serdev= _seen= _serstuck= _nseen=0 # ONLY the serial-family names are opened, and that is a deliberate narrowing # rather than an oversight. /dev/console, /dev/tty0 and /dev/hvc0 exist on this # image and CANNOT be serial ports -- the VT driver registers # TTY_DRIVER_TYPE_CONSOLE and hvc registers TTY_DRIVER_TYPE_SYSTEM, and neither # implements TIOCGSERIAL -- so opening them could only ever add a reading that # says "no", at the cost of this suite opening the live console of the guest it # is running on. A suite that wedges the machine takes every OTHER port's # results down with it, so the reading is not worth the risk; the driver-type # argument above is the stronger evidence anyway, and /proc/tty/drivers is read # below for the live listing without opening anything. setserial does the # opening, always O_RDWR|O_NONBLOCK, so no arm of this loop can block on carrier # detect. for _d in /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* /dev/ttyAMA* /dev/ttyUL*; do [ -e "$_d" ] || continue _po=$(setserial "$_d" 2>&1) case "$_po" in *"UART:"*) _hit=yes ;; "$_d:"*) _hit=no-open ;; *) _hit=no-ioctl ;; esac # The reason string is bounded: a host with 32 ttyS nodes would otherwise # push the whole of a skip past anything a reader will look at. _nseen=$((_nseen + 1)) if [ "$_nseen" -le 12 ]; then _seen="$_seen $_d($_hit)" elif [ "$_nseen" -eq 13 ]; then _seen="$_seen ..."; fi [ "$_hit" = yes ] && [ -z "$_serdev" ] && _serdev=$_d [ "$_hit" = no-ioctl ] && _serstuck="$_serstuck $_d" done [ -n "$_seen" ] || _seen=" (no serial-family device node exists to open)" _serdrv= if [ -r /proc/tty/drivers ]; then _serdrv=$(awk '$NF == "serial" { printf " %s", $1 }' /proc/tty/drivers 2>/dev/null) fi # The probe passes in every self-consistent world and reds only on a # contradiction. A kernel that grows a UART driver is not a regression of this # port, so "a device answered" is a pass exactly as "nothing opened" is; the # genuine finding is a node setserial can OPEN and still not read, because then # something other than the absence of a port is refusing and the skips below # would blame the wrong cause. if [ -n "$_serdev" ]; then bst_ok setserial/probe-serial-device elif [ -z "$_serstuck" ]; then bst_ok setserial/probe-serial-device else bst_notok setserial/probe-serial-device \ "serial-family node(s)$_serstuck opened but refused TIOCGSERIAL --$_seen" fi # --- the device half, skipped only on the probe above ------------------------ # Everything from here needs a device that implements TIOCGSERIAL: the two # operations the ticket names (reading and writing the port configuration), and # the whole parser, which sits behind that same ioctl as the header explains. if [ -z "$_serdev" ]; then _why="no device on this image implements TIOCGSERIAL. wasm32_defconfig carries \`# CONFIG_SERIAL_8250 is not set\` and no other serial_core driver, so no 8250 UART is registered; CONFIG_TTY, CONFIG_VT and CONFIG_VIRTIO_CONSOLE do provide tty0/ttyN, /dev/console and hvc0, and NONE of those implements TIOCGSERIAL -- they answer ENOTTY, exactly as a pty and a regular file do on x86-64. The one serial-family node that DOES exist is wasm-serial's /dev/ttySER0, the WebSerial bridge: it is registered at driver init and refuses open(2) with ENODEV until a host port attaches, which an unattended verify never does. CONFIG_USB_SERIAL and CONFIG_USB_SERIAL_FTDI_SIO ARE built in, so an FTDI adapter attached over WebUSB would appear as /dev/ttyUSB0 and would answer, but a headless verify attaches none either. Nodes probed with setserial's own TIOCGSERIAL read:$_seen (no-open = open refused, so nothing is bound behind the node; no-ioctl = opened and refused TIOCGSERIAL). /proc/tty/drivers serial-type entries:${_serdrv:- none}" # The two operations the ticket asks to be probed and skipped. bst_skip setserial/port-read "$_why" bst_skip setserial/port-set "$_why" # The parser. Unreachable for the SAME reason and not for a different one: # set_serial() issues TIOCGSERIAL before it looks at a single argument. for _skid in uart-type-accepts-all uart-type-illegal invalid-flag \ flag-not-invertible missing-argument numeric-hex numeric-octal \ closing-wait-infinite closing-wait-none flag-invert-caret \ autoconfig zero-flag-clears set-then-read-back; do bst_skip "setserial/$_skid" "$_why" done else # Unreached on this image; kept correct for the day a UART appears -- an # FTDI adapter over WebUSB is the realistic one, and it is why these are # written out rather than left as a comment. _case setserial/port-read 0 "UART:" setserial "$_serdev" _case setserial/uart-type-illegal 1 "Illegal UART type: bstbogus" setserial "$_serdev" uart bstbogus _case setserial/invalid-flag 1 "Invalid flag: bstbogus" setserial "$_serdev" bstbogus _case setserial/flag-not-invertible 1 "This flag can not be inverted: spd_hi" setserial "$_serdev" ^spd_hi _case setserial/missing-argument 1 "Missing argument for uart" setserial "$_serdev" uart # Every documented UART name through the real parser. A name the table # lost reaches "Illegal UART type" and reds here. _bad= for _n in none 8250 16450 16550 16550A 16650 16650V2 16750 16850 16950 16954; do setserial "$_serdev" uart "$_n" >/dev/null 2>&1 || _bad="$_bad $_n" done if [ -z "$_bad" ]; then bst_ok setserial/uart-type-accepts-all else bst_notok setserial/uart-type-accepts-all "documented UART name(s) refused:$_bad"; fi for _skid in numeric-hex numeric-octal closing-wait-infinite closing-wait-none \ flag-invert-caret autoconfig zero-flag-clears set-then-read-back \ port-set; do bst_skip "setserial/$_skid" "a serial device ($_serdev) answers on this image; these arms write to it and are not exercised by an unattended verify" done fi