. "$BST_LIB" # BST-TIMEOUT: 240 # ~60 cases, ~55 execs, no loops and no corpus. Every # # serialcheck/serialstats call below either fails in argv # # parsing, is refused at open(2), or is stopped at the # # modem-line ioctl -- none reaches the transfer, whose # # poll(2) is what would take time. Every call is wrapped in # # `timeout 15` regardless, so the file's worst case is # # bounded by its own case count rather than by the tool. # serialcheck + serialstats (meta-oe recipes-devtools/serialcheck, SRCREV # 45eb2ffa) -- Sekhar Nori's UART tester. serialcheck writes a file to a serial # port and/or reads it back and compares; serialstats prints that port's # TIOCGICOUNT counters. # # --------------------------------------------------------------------------- # WHAT THIS MACHINE CAN AND CANNOT TEST, AND WHY IT IS NOT THE USUAL GAP. # # The neighbouring device-userland suites (99-spitools, 99-aoetools) each face a # kernel with the whole subsystem compiled out. This one does not. The tty layer # is BUILT and working -- CONFIG_TTY, CONFIG_VT, CONFIG_VT_CONSOLE, # CONFIG_FRAMEBUFFER_CONSOLE and CONFIG_VIRTIO_CONSOLE are all on # arch/wasm32/configs/wasm32_defconfig, devpts is mounted, and a pty pair can be # made on demand (45-expect, 36-libio-pty-perl and 59-picocom all depend on it). # And there IS a serial device: /dev/ttySER0, the wasm-serial WebSerial bridge, # char 254,0, in /proc/tty/drivers on every image. # # What is missing is not a driver but a LINE. The wasm-serial driver refuses # open(2) with -ENODEV until a browser-granted navigator.serial port attaches -- # the grant boundary, linux-in-the-browser#60 M2 -- and no unattended runner # crosses it. 59-picocom and 99-mbpoll both measured the same refusal. # # That costs exactly one thing. serialcheck calls set_modem() -- ioctl TIOCMGET # then TIOCMSET -- BEFORE it transfers a byte, and set_modem() dies on failure. # A pty has no modem lines and answers ENOTTY. So the transfer verification is # the one part of serialcheck this image cannot run. # # --------------------------------------------------------------------------- # AND THE DEEPEST REACHABLE POINT IS ASSERTED, WHICH IS FURTHER IN THAN THAT # SUGGESTS. # # A pty carries serialcheck through the WHOLE termios stack -- open(2) of the # node, tcgetattr, cfsetspeed, cfmakeraw, tcsetattr, tcflush, F_SETFL -- and # stops it at exactly one ioctl. serialcheck/pty-termios-stack below is that # case, and it is the assertion that separates "this build's termios handling # works on wasm32" from "this build refuses at open and we learned nothing". # /dev/null carries it as far as tcgetattr, and carries serialstats all the way # to a real TIOCGICOUNT on a real descriptor. # # Everything else -- every option either program parses, both usage texts, the # four argument-validation diagnostics, the three missing-argument diagnostics, # argp's EX_USAGE against the tools' own exit 1, the transfer file's own open(2) # and mmap(2) refusals -- is arithmetic on argv and libc calls with no serial # line in them. A wasm32 build that got those wrong is a wasm32 build that is # wrong, with no environmental excuse available. # # WHAT IS PROBED RATHER THAN ASSUMED. Every absence above is read at runtime # from ${libexecdir}/serialcheck-tests/serialcheck-probe and every branch below # is written against the live answer, so the day a WebSerial line attaches the # transfer skips turn back into assertions on their own evidence rather than on # this comment. The probe reports /dev/ttySER0's open errno AND, if it opens, # the TIOCMGET that decides whether the transfer can run at all. # # AND THE CONTROLS ARE THE POINT OF THE PROBE. "There is no serial line" and # "this process cannot open device nodes" produce the same refusal from both # tools, so two independent controls separate them: /dev/null and /dev/zero are # opened, and a pty pair is made and driven through tcgetattr/tcsetattr. If the # pty takes the termios stack while /dev/ttySER0 refuses open, the difference is # the line and nothing else. Every skip below is gated on those controls; if # they stop holding, this suite reds rather than quietly skipping. # # THERE IS NO --version, AND THAT IS UPSTREAM'S CHOICE. Neither program sets # argp_program_version, so glibc's argp adds no --version option and rejects it # as unrecognised with EX_USAGE. That refusal is a real, stable property of the # built binary and is asserted as one; a version banner that does not exist is # not asserted into being. bst_ok serialcheck/suite-canary # first line: absorbs brintos/linux#96, which drops it require serialcheck serialcheck/present work=${TMPDIR:-/tmp}/bst-serialcheck.$$ mkdir -p "$work" || { bst_notok serialcheck/workdir "cannot create $work"; exit 0; } NOSUCH=/dev/ttyBRINTOSNOSUCH # run_case -- ok iff the command exits exactly # AND its combined output contains . # # bst.sh's check_out requires rc=0, and almost every assertion here is about a # DIAGNOSTIC AND THE STATUS THAT CARRIES IT -- exit 64 out of argp against exit 1 # out of the tools' own dieh()/die(), which is the distinction the suite exists # to hold. The `timeout 15` is not decoration either: serialcheck's poll(2) waits # 100 seconds per iteration and its default loop count is UINT_MAX, so a case # that unexpectedly reached the transfer would otherwise sit past this file's # whole budget and take every sibling suite in the boot down with it. run_case() { _sc_id=$1; _sc_want=$2; _sc_needle=$3; shift 3 _sc_out=$(bst_bound 15 "$@" 2>&1); _sc_rc=$? if bst_execfail "$_sc_out"; then bst_unrun "$_sc_id" "$_sc_out"; return; fi case "$_sc_out" in *"$_sc_needle"*) _sc_hit=1 ;; *) _sc_hit=0 ;; esac if [ "$_sc_rc" -eq "$_sc_want" ] && [ "$_sc_hit" -eq 1 ]; then bst_ok "$_sc_id" else bst_notok "$_sc_id" "rc=$_sc_rc want=$_sc_want want:'$_sc_needle' got:'$(_bst_1line "$_sc_out")'" fi } # --- what upstream installs -------------------------------------------------- # # Both programs, by name and by the package each lands in. automake's # bin_PROGRAMS fails the build on a link error, so this is not a check against a # missing link -- it is here because the recipe splits serialstats into # ${PN}-stats with an explicit FILES, and a rename upstream would leave that # package empty and the serialstats half of this file skipping quietly. for t in serialcheck serialstats; do if command -v "$t" >/dev/null 2>&1; then bst_ok "serialcheck/present/$t" else bst_notok "serialcheck/present/$t" "not on PATH -- upstream's bin_PROGRAMS installs both serialcheck and serialstats to \${bindir}" fi done # --- the probe --------------------------------------------------------------- P=/usr/libexec/serialcheck-tests/serialcheck-probe H=/usr/libexec/serialcheck-tests/serialcheck-ptyhold if [ ! -x "$P" ]; then bst_notok serialcheck/probe/present "no probe at $P -- serialcheck-tests is not installed, so nothing below can be attributed" exit 0 fi bst_ok serialcheck/probe/present PO=$("$P" 2>&1); prc=$? if bst_execfail "$PO"; then bst_unrun serialcheck/probe/runs "$PO" exit 0 fi case "$PO" in *"probe end"*) bst_ok serialcheck/probe/runs ;; *) bst_notok serialcheck/probe/runs "rc=$prc, no 'probe end' -- the probe did not finish, so its values below are partial: $(_bst_1line "$PO")" exit 0 ;; esac # pfield -- the value of = on the first line that # begins with . `|` delimits the sed expression because every prefix # here carries a device path, and the prefix is replaced by TWO SPACES rather # than deleted: the field patterns below anchor on the space in front of # `=`, and a field that ended up first on the line would otherwise match # nothing and read as an empty value -- which every caller would then score as a # failure, silently and in the direction that reports a working image as broken. # # Reads from a variable, never a redirect: a top-level stdin redirect on the # virtiofs root makes dup2(2) return 513 and ash exits, ending the suite # silently (brintos/linux#73). pfield() { printf '%s\n' "$PO" | sed -n "s|^$1 | |p" | sed -n 1p \ | sed -n "s|.*[ ]$2=\([^ ]*\).*|\1|p" } # ptail -- everything after `=`, kept whole. # strerror texts contain spaces, so pfield cannot carry them. ptail() { printf '%s\n' "$PO" | sed -n "s|^$1 | |p" | sed -n 1p | sed -n "s|.*[ ]$2=||p" } # pword -- the n'th whitespace-separated word after the prefix. pword() { printf '%s\n' "$PO" | sed -n "s|^$1 ||p" | sed -n 1p | awk -v n="$2" '{print $n}' } # --- the controls ------------------------------------------------------------ # # If these do not hold, nothing below may be attributed to the absent serial # line: a process that cannot open a character device, or an image with no pty, # refuses serialcheck for reasons that have nothing to do with serialcheck. for c in /dev/null /dev/zero; do if [ "$(pfield "control $c" rc)" = "0" ]; then bst_ok "serialcheck/control/open$(printf '%s' "$c" | tr '/' '-')" else bst_notok "serialcheck/control/open$(printf '%s' "$c" | tr '/' '-')" \ "the probe could not open $c (err=$(pfield "control $c" err)) -- this process cannot open character devices, so no refusal below is about the serial line" fi done pty_master=$(pfield "pty master" rc) pty_slave=$(pword "pty slave" 1) pty_tcget=$(pfield "pty tcgetattr-slave" rc) pty_tcset=$(pfield "pty tcsetattr-slave" rc) pty_mget=$(pfield "pty tiocmget-slave" rc) pty_mget_err=$(pfield "pty tiocmget-slave" err) pty_mget_msg=$(ptail "pty tiocmget-slave" msg) pty_icount=$(pfield "pty tiocgicount-slave" rc) if [ "$pty_master" = "0" ]; then bst_ok serialcheck/control/pty-made else bst_notok serialcheck/control/pty-made \ "the probe could not make a pty (err=$(pfield "pty master" err)) -- this image's tty layer is not what 45-expect and 59-picocom measured, so the pty assertions below cannot separate a missing line from a missing tty" fi # The control that carries the whole gap: a pty takes termios and refuses the # modem-line ioctl. That pair is what makes serialcheck's stop attributable to # the line rather than to this build's termios handling. if [ "$pty_tcget" = "0" ] && [ "$pty_tcset" = "0" ]; then bst_ok serialcheck/control/pty-termios else bst_notok serialcheck/control/pty-termios \ "tcgetattr rc=$pty_tcget tcsetattr rc=$pty_tcset on a pty slave -- the termios stack does not work on this image, so a serialcheck stop cannot be attributed to the missing modem lines" fi # --- the serial node this image has, and the line it has not ------------------ ser=/dev/ttySER0 ser_node=$(pword "node $ser" 1) ser_open=$(pfield "open $ser" rc) ser_open_err=$(pfield "open $ser" err) ser_open_msg=$(ptail "open $ser" msg) ser_driver=$(pword "driver wasm-serial" 1) if [ "$ser_node" = "present" ]; then bst_ok serialcheck/ttyser/node if [ "$ser_driver" = "present" ]; then bst_ok serialcheck/ttyser/driver else bst_notok serialcheck/ttyser/driver "$ser exists but no wasm-serial row in /proc/tty/drivers" fi else bst_skip serialcheck/ttyser/node "$ser absent on this kernel" bst_skip serialcheck/ttyser/driver "$ser absent on this kernel" fi # --- usage and the options each program parses ------------------------------- # # Read off the built binary, not off a transcript: the option table IS the # program's interface, and argp renders it from the same struct the parser uses. run_case serialcheck/usage/help-rc 0 "user stress testing tool" serialcheck --help run_case serialcheck/usage/short 0 "Usage: serialcheck" serialcheck --usage run_case serialcheck/usage/question 0 "user stress testing tool" serialcheck -? for o in "-b, --baud=NUM" "-d, --device=FILE" "-f, --file=FILE" "-h, --hflow" \ "-k, --loopback" "-l, --loops=NUM" "-m, --mode=M" "-n, --no-termios"; do id=$(printf '%s' "$o" | sed -n 's|^-\(.\), .*|\1|p') run_case "serialcheck/usage/option-$id" 0 "$o" serialcheck --help done run_case serialstats/usage/help-rc 0 "uart stats tool" serialstats --help run_case serialstats/usage/short 0 "Usage: serialstats" serialstats --usage for o in "-d, --device=FILE" "-i, --interval=NUM" "-o, --once"; do id=$(printf '%s' "$o" | sed -n 's|^-\(.\), .*|\1|p') run_case "serialstats/usage/option-$id" 0 "$o" serialstats --help done # Upstream sets no argp_program_version, so argp adds no --version and refuses it # with EX_USAGE. Asserted as the refusal it is. run_case serialcheck/version/absent 64 "unrecognized option '--version'" serialcheck --version run_case serialstats/version/absent 64 "unrecognized option '--version'" serialstats --version # --- argument validation: the tool's own message, argp's status --------------- # # Each of these prints a diagnostic from parse_opt() and then returns # ARGP_ERR_UNKNOWN, so argp exits EX_USAGE (64). The two upstream misspellings # ("Unsuported") are asserted as spelled: this suite reports what the binary # says, and a silent correction upstream is a change to the interface that # should red here rather than pass unnoticed. run_case serialcheck/argv/baud-nonnumeric 64 "Unsupported baudrate: abc" serialcheck -b abc -f "$work/x" -d "$NOSUCH" -m t run_case serialcheck/argv/baud-zero 64 "Unsupported baudrate: 0" serialcheck -b 0 -f "$work/x" -d "$NOSUCH" -m t run_case serialcheck/argv/mode-bad 64 "Unsuported mode: x" serialcheck -m x -f "$work/x" -d "$NOSUCH" run_case serialcheck/argv/loops-bad 64 "Unsuported loop count: xyz" serialcheck -l xyz -f "$work/x" -d "$NOSUCH" -m t run_case serialcheck/argv/positional 64 "Too many arguments" serialcheck extra run_case serialstats/argv/interval-bad 64 "Unsupported interval: abc" serialstats -i abc -d "$NOSUCH" run_case serialstats/argv/positional 64 "Too many arguments" serialstats extra # The three main() requires, each from dieh() and each exit 1 -- a different # status from argp's, which is the point of asserting both. run_case serialcheck/argv/missing-file 1 "Error: Missing file for transfers. Use --help" serialcheck -d "$NOSUCH" -m t run_case serialcheck/argv/missing-device 1 "Error: Missing uart node. Use --help" serialcheck -f "$work/x" -m t run_case serialcheck/argv/missing-mode 1 "Error: Missing mode. Use --help" serialcheck -f "$work/x" -d "$NOSUCH" run_case serialcheck/argv/no-args 1 "Error: Missing file for transfers. Use --help" serialcheck run_case serialstats/argv/missing-device 1 "Error: Missing uart node. Use --help" serialstats # --- the transfer file, which is opened before the port ---------------------- run_case serialcheck/file/missing 1 "Failed to open $work/no-such-payload" \ serialcheck -f "$work/no-such-payload" -d "$NOSUCH" -m t # 256 bytes: large enough that mmap has something to map, small enough that a # transfer -- if this image ever reaches one -- completes in one write. payload=$work/payload.bin i=0; p= while [ $i -lt 16 ]; do p="${p}0123456789abcdef"; i=$((i + 1)); done printf '%s' "$p" > "$payload" empty=$work/empty.bin : > "$empty" # The zero-length refusal comes from mmap(2), and the probe has just made the # same call on this platform. Gated on that reading rather than on the belief # that mmap of length 0 is EINVAL everywhere -- and the message is matched on its # stem only, because serialcheck passes an off_t to a %d conversion and the tail # of that line is not a fixed string on a 32-bit target. if [ "$(pfield "prefile mmap-empty" rc)" = "-1" ]; then run_case serialcheck/file/empty-refused 1 "mmap() of $empty" \ serialcheck -f "$empty" -d "$NOSUCH" -m t else bst_skip serialcheck/file/empty-refused \ "this platform's mmap(2) accepts a zero length (probe: prefile mmap-empty rc=0), so serialcheck does not refuse an empty transfer file here" fi # The pre-open path serialcheck runs BEFORE it touches the port. Reported as # their own cases so that a wasm32 gap in any of them is named rather than # surfacing as an unexplained red against the port assertions below. for k in create getrlimit mmap; do if [ "$(pfield "prefile $k" rc)" = "0" ]; then bst_ok "serialcheck/prefile/$k" else bst_notok "serialcheck/prefile/$k" \ "the probe's $k failed (err=$(pfield "prefile $k" err)) -- serialcheck runs this before it opens the port, so every port case below would stop here instead" fi done # --- the port that cannot be opened ------------------------------------------ # # Asserted against the errno the probe read from open(2) on the same name, not # against a transcribed string, so the assertion follows the kernel if the # kernel moves. nosuch_open=$(pfield "open $NOSUCH" rc) nosuch_msg=$(ptail "open $NOSUCH" msg) if [ "$nosuch_open" = "-1" ] && [ -n "$nosuch_msg" ]; then run_case serialcheck/port/refused 1 "Failed to open $NOSUCH: $nosuch_msg" \ serialcheck -f "$payload" -d "$NOSUCH" -m t run_case serialstats/port/refused 1 "Failed to open $NOSUCH: $nosuch_msg" \ serialstats -d "$NOSUCH" -o else bst_notok serialcheck/port/refused \ "the probe OPENED $NOSUCH (rc=$nosuch_open) -- a name reserved as absent exists on this image, so this assertion has no control" bst_notok serialstats/port/refused \ "the probe OPENED $NOSUCH (rc=$nosuch_open) -- a name reserved as absent exists on this image, so this assertion has no control" fi # The real serial node, whose driver answers rather than the filesystem. This is # the deepest the port half reaches on an unattended image: serialcheck named a # device that exists, the wasm-serial driver was entered, and it refused. if [ "$ser_node" = "present" ] && [ "$ser_open" = "-1" ]; then run_case serialcheck/ttyser/refused 1 "Failed to open $ser: $ser_open_msg" \ serialcheck -f "$payload" -d "$ser" -m t run_case serialstats/ttyser/refused 1 "Failed to open $ser: $ser_open_msg" \ serialstats -d "$ser" -o elif [ "$ser_node" != "present" ]; then bst_skip serialcheck/ttyser/refused "$ser absent on this kernel" bst_skip serialstats/ttyser/refused "$ser absent on this kernel" else bst_skip serialcheck/ttyser/refused "$ser OPENS on this image -- a line is attached, so the refusal case does not apply; see the transfer case below" bst_skip serialstats/ttyser/refused "$ser OPENS on this image -- a line is attached, so the refusal case does not apply" fi # --- /dev/null: past the open, into termios and into a real ioctl ------------ # # open(2) on a character device that is not a tty SUCCEEDS, so /dev/null carries # serialcheck past open/getrlimit/mmap and into tcgetattr, and carries # serialstats all the way to a TIOCGICOUNT on a real descriptor. That is the # tools' own code computing, not merely being present. if [ "$(pfield "control /dev/null" rc)" = "0" ] && \ [ "$(pfield "devnull tcgetattr" rc)" = "-1" ]; then run_case serialcheck/devnull/tcgetattr-reached 1 "tcgetattr() failed" \ serialcheck -f "$payload" -d /dev/null -m t else bst_skip serialcheck/devnull/tcgetattr-reached \ "the probe's tcgetattr on /dev/null did not fail (rc=$(pfield "devnull tcgetattr" rc)) -- /dev/null is a tty on this image, so it no longer isolates the termios call" fi if [ "$(pfield "devnull tiocgicount" rc)" = "-1" ]; then run_case serialstats/devnull/ioctl-reached 1 "Failed to get counters for /dev/null" \ serialstats -d /dev/null -o else bst_skip serialstats/devnull/ioctl-reached \ "the probe's TIOCGICOUNT on /dev/null did not fail (rc=$(pfield "devnull tiocgicount" rc))" fi # --- the pty: the whole termios stack, and the one ioctl that stops it ------- # # This is the deepest reach in the file. serialcheck-ptyhold makes a pty, prints # its slave name, echoes what is written to it -- so a transfer would COMPLETE # rather than sit in poll(2) if this image ever gave ptys modem lines -- and ends # itself on SIGALRM whatever happens here. hold_out=$work/ptyhold.out SL= if [ ! -x "$H" ]; then bst_notok serialcheck/pty/hold "no helper at $H -- serialcheck-tests is installed but incomplete" else "$H" 30 > "$hold_out" 2>&1 & hold_pid=$! n=0 while [ $n -lt 50 ]; do SL=$(sed -n 's|^slave ||p' "$hold_out" 2>/dev/null) [ -n "$SL" ] && break n=$((n + 1)) sleep 1 done if [ -n "$SL" ] && [ -c "$SL" ]; then bst_ok serialcheck/pty/hold else bst_notok serialcheck/pty/hold "the helper named no usable slave in 50s: $(_bst_1line "$(cat "$hold_out" 2>/dev/null)")" SL= fi fi if [ -z "$SL" ]; then bst_skip serialcheck/pty/termios-stack "no pty slave to drive" bst_skip serialstats/pty/ioctl-reached "no pty slave to drive" elif [ "$pty_mget" = "-1" ]; then # The probe reads TIOCMGET as failing on a pty slave; serialcheck must # therefore reach set_modem() and die there, with the same strerror. Every # termios call before it must have SUCCEEDED for that to happen, which is # what this case actually asserts. run_case serialcheck/pty/termios-stack 1 "mcr get failed: $pty_mget_msg" \ serialcheck -f "$payload" -d "$SL" -m d -l 1 else # A pty with modem lines is not what this image has today; if it ever is, # the transfer runs for real against the helper's echo. run_case serialcheck/pty/termios-stack 0 "loops 1 / 1" \ serialcheck -f "$payload" -d "$SL" -m d -l 1 fi if [ -n "$SL" ]; then if [ "$pty_icount" = "-1" ]; then run_case serialstats/pty/ioctl-reached 1 "Failed to get counters for $SL" \ serialstats -d "$SL" -o else run_case serialstats/pty/ioctl-reached 0 "cts:" serialstats -d "$SL" -o fi fi # --- the transfer verification, which needs a line --------------------------- # # The one part of serialcheck this image cannot exercise, and the only thing the # absent WebSerial line costs. Gated on the probe's own reading of /dev/ttySER0 # rather than on a comment: if the node opens AND answers TIOCMGET, the loopback # transfer runs here for real. ser_mget=$(pfield "serial tiocmget" rc) if [ "$ser_node" = "present" ] && [ "$ser_open" = "0" ] && [ "$ser_mget" = "0" ]; then run_case serialcheck/transfer/loopback 0 "loops 1 / 1" \ serialcheck -f "$payload" -d "$ser" -m d -l 1 -k elif [ "$ser_node" = "present" ] && [ "$ser_open" = "0" ]; then bst_skip serialcheck/transfer/loopback \ "$ser opens but answers TIOCMGET with err=$(pfield "serial tiocmget" err) -- serialcheck's set_modem() dies before any transfer, so the loopback cannot be driven" elif [ "$ser_node" = "present" ]; then bst_skip serialcheck/transfer/loopback \ "$ser refuses open(2) with err=$ser_open_err -- no browser-granted navigator.serial port is attached; the grant boundary is linux-in-the-browser#60 M2, and no unattended runner crosses it" else bst_skip serialcheck/transfer/loopback "$ser absent on this kernel" fi [ -n "${hold_pid:-}" ] && kill "$hold_pid" 2>/dev/null rm -rf "$work" exit 0