. "$BST_LIB" # BST-TIMEOUT: 600 # 34 cases over 13 fresh wasm instantiations of a 33 MB # # module, one of which -- the resident daemon -- is EXPECTED # # to overrun its 20 s bound. Worst case is every one of the # # 13 overrunning, which is 260 s. Predicted ~150 s; # # NOT MEASURED ON TARGET YET -- this port's first on-target # # run is the batch verify, and the measured elapsed goes in # # the test plan rather than in this comment, which would # # cost an image build to keep the shipped suite identical. # # ser2net 4.6.1 (meta-openembedded meta-oe/recipes-connectivity/ser2net_4.6.1.bb, # `inherit autotools pkgconfig systemd', DEPENDS = gensio libyaml, plus a # meta-brintos bbappend that seeds LIBS for the static gensio link and no patch) # on the wasm32-hwjs glibc image -- the serial-to-network proxy. One program # ships, /usr/sbin/ser2net, beside two man pages in -doc. -dev is EMPTY: upstream # installs no header and no .pc file, so nothing but the program and the manuals # reaches the image. # # It sits beside 93-gensio, its main DEPENDS: every accepter and connector string # in the fixtures below is parsed by the same libgensio.a that suite covers. # # ============================================================================ # THERE IS NO SERIAL PORT ON THIS MACHINE, AND REPORTING THAT IS THE SUBJECT. # ============================================================================ # # arch/wasm32/configs/wasm32_defconfig sets CONFIG_DEVTMPFS=y and # CONFIG_DEVTMPFS_MOUNT=y, so /dev IS populated at run time even though the # shipped rootfs /dev is empty. What it leaves out is every serial driver that # could put a node there: `# CONFIG_SERIAL_8250 is not set', so there is no # /dev/ttyS*; CONFIG_USB_SERIAL=y and CONFIG_USB_SERIAL_FTDI_SIO=y are built in # but bind a real FTDI device, and none is attached here, so there is no # /dev/ttyUSB* either. # # That is a much more specific absence than "no /dev", and the `machine-*' cases # assert it as three separate facts rather than one: /dev is populated, a tty # node IS present (CONFIG_TTY=y and the VT layer), and the serial nodes # specifically are not. Without the first two a red would say only "nothing in # /dev", which is a different machine and a different bug. # # ============================================================================ # ser2net DOES NOT EXIT WHEN IT CANNOT OPEN THE DEVICE. IT NEVER OPENS IT. # ============================================================================ # # This is where the ticket's own wording had to be narrowed against the source, # and it is the most load-bearing thing in this file. The ticket asks that # ser2net "report the device it cannot open with its documented message and # EXIT rather than stay resident". Upstream 4.6.1 does neither of those things # for an ordinary connection, by design: # # * main() runs apply_new_ports(), then do_detach(), make_pidfile(), # start_threads() and op_loop(). op_loop does not return until a signal # asks it to, so a config that PARSES leaves ser2net RESIDENT -- always. # There is no path on which a device makes it exit. # * startup_port() (port.c:296) opens the accepter and then opens the # connector ONLY `if (port->connbacks)'. Without a connback the serialdev is # opened lazily, when a TCP client actually connects -- so on an idle # machine ser2net never touches /dev/ttyS0 at all and prints nothing about # it. # * Even WITH a connback the device failure is not fatal: port.c:327 prints # `Unable to enable port device %s: %s', calls shutdown_port() and then # sets `err = 0' with the comment "Don't report an error here, let the # shutdown run". # # So the assertion is rebuilt to say what is true, in three parts, and the # residency half is asserted POSITIVELY rather than inferred from a timeout: # # daemon/reaches-op-loop -P writes the pid file between start_threads() and # op_loop(), so the file APPEARING is proof ser2net # got past configuration and into the event loop. # daemon/stays-resident the bounded run overran instead of exiting. # device/open-reported a connback IS configured, which is the one # documented way to make ser2net open the device at # start-up, and that is where its documented message # about the device it cannot open comes from. It is # read out of SYSLOG, not stderr -- see the next # block, which is what the first on-target run of # this suite corrected. # # `daemon/accepter-started' is the control that keeps the last of those # attributable. port.c returns EARLY when gensio_acc_startup() fails, so the # connback branch is never reached and the device would go unmentioned for a # reason that has nothing to do with the device. If the accepter is what broke, # that case says so and `device/open-reported' is explained rather than # mysterious. It reads the same collector, for the same reason. # # ============================================================================ # A PORT DIAGNOSTIC GOES TO SYSLOG. STDERR NEVER SEES ONE, AND -d DOES NOT HELP. # ============================================================================ # # MEASURED on the batch-2048 verify of 2026-09-03: `device/open-reported' ran # as a plain stderr assertion and reported `want:'Unable to enable port device' # got:''' -- with the accepter control, the pid file and the residency case all # green beside it. The message is emitted. stderr is simply not where it goes. # # ser2net.c defines TWO `seout' absouts and picks between them by PLATFORM, not # by -d. The non-WIN32 one (ser2net.c:312) is `syslog_eout': it calls syslog(3) # and nothing else. Only `stderr_absout' -- which is what yaml_readconfig() is # handed -- writes to stderr, which is exactly why every yaml/* case above can # be asserted there and no port case can. # # -d does not rescue it. `-d' reaches syslog as # openlog(LOG_PID|LOG_CONS|LOG_PERROR) inside do_detach(), and main() calls # apply_new_ports() BEFORE do_detach() -- so every port start-up message is # already emitted by the time LOG_PERROR exists. With no collector listening on # /dev/log, glibc's syslog(3) has nowhere to put them and they are dropped # outright, which is the `got:'''` above. # # The message itself is reached synchronously, which is what makes it assertable # at all: sterm_sub_open() (gensio lib/sergensio_serialdev.c) calls open_dev() # on the missing /dev/ttyS0 and returns the error; fd_open() propagates it; # basen_open() returns anything that is neither 0 nor GE_INPROGRESS to its # caller -- so gensio_open() fails in-line, port_dev_enable() returns the error # and port.c:327 runs. The ASYNCHRONOUS failure path, port_dev_open_done(), is a # different report entirely (`Device open failure', written only into already # connected network clients) and is not what this case asserts. # # So the report is collected where it is sent. sysklogd's syslogd is this # image's collector and BRINTOS-195 measured this exact path green # (sysklogd 25 pass / 0 fail / 5 skip, "AF_UNIX works here"). `-C' points the # /dev/kmsg seqno cache at this run's own scratch dir for the reason # 86-sysklogd records, and the restart banner landing in OUR file is the control # that the collector is up and writing HERE rather than somewhere else. Where # there is no syslogd the case SKIPS: an absent collector is not evidence about # ser2net. # # ============================================================================ # `-h' IS NOT AN OPTION, AND THE SUITE ASSERTS THAT RATHER THAN WISHING IT WERE. # ============================================================================ # # The ticket asks for `ser2net -h'. There is no `case 'h'' in main()'s switch # and no -h line in help_string; -h takes the SAME default: branch as any # unknown letter. `cli/dash-h-not-an-option' asserts that identity deliberately. # The usage text is still reachable and still asserted -- it is what arg_error() # prints on the way out -- so the ticket's intent is covered; what is not # claimed is a flag upstream does not have. # # ser2netcfg is asked for "where the recipe ships it". It does not exist in # 4.6.1 at all: Makefile.am's entire program list is `sbin_PROGRAMS = ser2net'. # The requirement's own condition is unmet, so nothing here asserts it. # # ============================================================================ # EXIT STATUS 1 IS THE SUCCESS PATH FOR `-v', AND THAT IS NOT A TYPO. # ============================================================================ # # `case 'v': printf("%s version %s\n", argv[0], VERSION); return 1;'. ser2net # prints its version to STDOUT and returns 1 from main. Every other early exit # here is also 1, so the exit status alone never distinguishes them and each # case asserts the TEXT beside it. # # ============================================================================ # WHAT IS MEASURED AND WHAT IS PREDICTED. # ============================================================================ # # THERE IS NO NATIVE CONTROL FOR THIS PORT -- the build host has no gensio or # libyaml development files, so no x86-64 build of the same source could be # made. Everything below comes from the 4.6.1 source, EXCEPT the message # literals, which were read out of the SHIPPED wasm module with strings(1) at # recipe time and are measured facts: `Invalid option: '%s'', # `Invalid argument: '%s'', `No config file specified with -c', # `Invalid token at the main level', `Invalid token in the %s: %s', # `No accepter given in connection', `No connector given in connection', # `Unable to enable port device %s', `%s:%lu(column %lu): %s' and libyaml's # `found incompatible YAML document' are all present in the module that ships. # # The BRANCHES those messages sit on are predictions until the batch verify # runs -- specifically that the TCP accepter starts on this lo-only guest, that # the connback path reaches the serialdev open, and that ser2net stays resident # rather than dying earlier for some reason none of this anticipates. # # ============================================================================ # BOUNDING, AND WHY EVERY INVOCATION GOES THROUGH IT. # ============================================================================ # # timeout(1) can kill nothing this loop cannot (brintos/linux#21), and before # the brintos/linux#77 fix its timer never fired, which made it a second hang # rather than a bound. `s2_run' below is bst_probe's shape # (completion sentinel, /proc/uptime as the clock, `sleep 1' with builtins only # in the loop body) with the verdict left to the caller. # # EVERY ser2net invocation in this file goes through it, including the ones # that provably exit at once. A suite that wedges the guest produces NO case # lines at all and costs every member of a batched image its round, so the # cheap insurance is taken on all fourteen runs rather than on the four that # obviously need it. The kill is belt-and-braces for the same reason: `kill -9' # on the backgrounded subshell does not necessarily reach a resident ser2net # underneath it, so every run that can stay up is given `-P' and the pid file # is used to kill the daemon itself. PATH="$PATH:/usr/sbin:/sbin" export PATH require ser2net ser2net/present W=${TMPDIR:-/tmp}/bst-ser2net.$$ mkdir -p "$W" || { bst_notok ser2net/workdir "cannot create $W"; exit 0; } # A port nothing else on this image binds. Only ever 127.0.0.1. S2_PORT=29074 S2_CBPORT=29075 S2_BOUND=20 # every path here either answers on the first failing syscall # or is meant to stay up; this is a hang detector, not a budget. # s2_run -- run with stdout at $W/out, stderr at $W/err # and no result line of its own. Sets S2_RC to the exit status, or 124 when the # bound expired. Kills the daemon through its pid file as well as the subshell. s2_run() { _s2_bound=$1; shift rm -f "$W/out" "$W/err" "$W/rc" "$W/ser2net.pid" { "$@" >"$W/out" 2>"$W/err"; echo $? >"$W/rc"; } & _s2_pid=$! _s2_t0= if [ -r /proc/uptime ] && read -r _s2_u _ < /proc/uptime; then _s2_t0=${_s2_u%.*}; fi _s2_ticks=0 while [ ! -e "$W/rc" ]; do _s2_d=$_s2_ticks if [ -n "$_s2_t0" ] && read -r _s2_u _ < /proc/uptime; then _s2_d=$(( ${_s2_u%.*} - _s2_t0 )) fi if [ "$_s2_d" -ge "$_s2_bound" ]; then kill -9 "$_s2_pid" 2>/dev/null # The subshell is not the daemon; reach the daemon itself. if [ -s "$W/ser2net.pid" ]; then read -r _s2_dpid < "$W/ser2net.pid" 2>/dev/null case "$_s2_dpid" in [0-9]*) kill -9 "$_s2_dpid" 2>/dev/null ;; esac fi S2_RC=124 return 124 fi sleep 1 _s2_ticks=$((_s2_ticks + 1)) done S2_RC=0 # $W is under ${TMPDIR:-/tmp} -- tmpfs, where dup2 does not return 513. # BST-REDIRECT-OK: $W is the suite's tmpfs workdir, never the rootfs [ -s "$W/rc" ] && read -r S2_RC < "$W/rc" 2>/dev/null case "$S2_RC" in ''|*[!0-9]*) S2_RC=0 ;; esac return 0 } # s2_both -- everything the last s2_run printed, both streams. s2_both() { cat "$W/out" "$W/err" 2>/dev/null; } # s2_saw -- ok iff the last run printed on either stream. s2_saw() { _ss_id=$1; _ss_n=$2 _ss_o=$(s2_both) case "$_ss_o" in *"$_ss_n"*) bst_ok "$_ss_id" ;; *) if bst_execfail "$_ss_o"; then bst_unrun "$_ss_id" "$_ss_o" else bst_notok "$_ss_id" "want:'$_ss_n' got:'$(_bst_1line "$_ss_o")'"; fi ;; esac } # s2_notsaw -- the control form: ok iff is ABSENT. s2_notsaw() { _sn_id=$1; _sn_n=$2; _sn_why=$3 _sn_o=$(s2_both) case "$_sn_o" in *"$_sn_n"*) bst_notok "$_sn_id" "$_sn_why -- saw:'$(_bst_1line "$_sn_o")'" ;; *) bst_ok "$_sn_id" ;; esac } # s2_rc -- ok iff the last run's status was . s2_rc() { if [ "$S2_RC" -eq "$2" ]; then bst_ok "$1" else bst_notok "$1" "rc=$S2_RC want=$2"; fi } # s2_wait_for -- bounded poll for reaching , the # shape 86-sysklogd uses: a syslog record is written by another process, so the # only alternative to a poll is a sleep long enough to be a guess. 25 iterations # against this suite's 600 s bound. s2_wait_for() { _sw_f=$1; _sw_n=$2; _sw_i=0 while [ $_sw_i -lt 25 ]; do grep -q "$_sw_n" "$_sw_f" 2>/dev/null && return 0 _sw_i=$((_sw_i + 1)); sleep 1 done return 1 } # -------------------------------------------------------------------------- # WHAT SHIPS. # -------------------------------------------------------------------------- # The program is sbin_PROGRAMS upstream, so a caller resolving it through a PATH # without /usr/sbin finds nothing and reads the port as ABSENT rather than # broken. The install path is asserted directly, the shape 93-openlldp uses. if [ -x /usr/sbin/ser2net ]; then bst_ok ser2net/sbin-path else bst_notok ser2net/sbin-path "/usr/sbin/ser2net is not an executable regular file"; fi if [ -e /usr/bin/ser2net ]; then bst_notok ser2net/not-in-bin "/usr/bin/ser2net exists; upstream installs ser2net into sbin only" else bst_ok ser2net/not-in-bin; fi for m in /usr/share/man/man8/ser2net.8 /usr/share/man/man5/ser2net.yaml.5; do _id=ser2net/man-${m##*/} if [ -s "$m" ]; then bst_ok "$_id"; else bst_notok "$_id" "$m missing or empty"; fi done # -------------------------------------------------------------------------- # THE MACHINE: /dev IS REAL, AND THE SERIAL NODES SPECIFICALLY ARE NOT THERE. # -------------------------------------------------------------------------- if [ -n "$(ls -A /dev 2>/dev/null)" ]; then bst_ok machine/dev-populated else bst_notok machine/dev-populated "/dev is empty; devtmpfs did not mount, so the serial cases below would be vacuous"; fi if [ -e /dev/tty ] || [ -e /dev/console ]; then bst_ok machine/tty-node-present else bst_notok machine/tty-node-present "neither /dev/tty nor /dev/console; CONFIG_TTY=y says one should be here"; fi _ttys=$(ls -d /dev/ttyS[0-9]* 2>/dev/null | tr '\n' ' ') if [ -z "$_ttys" ]; then bst_ok machine/no-ttyS else bst_notok machine/no-ttyS "CONFIG_SERIAL_8250 is unset, yet: $_ttys"; fi _ttyusb=$(ls -d /dev/ttyUSB[0-9]* 2>/dev/null | tr '\n' ' ') if [ -z "$_ttyusb" ]; then bst_ok machine/no-ttyUSB else bst_notok machine/no-ttyUSB "no FTDI device is attached to this guest, yet: $_ttyusb"; fi # -------------------------------------------------------------------------- # THE COMMAND LINE. Every early exit is status 1, so the text carries the case. # -------------------------------------------------------------------------- s2_run "$S2_BOUND" ser2net -v s2_saw cli/version-text "version 4.6.1" s2_rc cli/version-rc 1 s2_run "$S2_BOUND" ser2net -Z s2_saw cli/invalid-option-text "Invalid option: '-Z'" s2_rc cli/invalid-option-rc 1 # arg_error() prints help_string on the way out, so the usage text is reachable # even though -h is not the way to ask for it. s2_saw cli/usage-lists-config-flag "-c " s2_run "$S2_BOUND" ser2net -h s2_saw cli/dash-h-not-an-option "Invalid option: '-h'" s2_run "$S2_BOUND" ser2net --help s2_saw cli/invalid-argument-text "Invalid argument: '--help'" s2_rc cli/invalid-argument-rc 1 s2_run "$S2_BOUND" ser2net -c s2_saw cli/missing-c-value "No config file specified with -c" # -------------------------------------------------------------------------- # THE CONFIG FILE ITSELF. # -------------------------------------------------------------------------- # No package on this image installs /etc/ser2net/ser2net.yaml -- upstream's # do_install ships the program and the manuals only, and CONFFILES naming the # yaml does not create it. So the default path is absent and a bare ser2net has # nothing to read. Asserted as a machine fact first, so the run below cannot be # the one thing that stays resident. if [ -e /etc/ser2net/ser2net.yaml ]; then bst_notok cfg/no-default-config "/etc/ser2net/ser2net.yaml exists; nothing in this image installs it" else bst_ok cfg/no-default-config; fi s2_run "$S2_BOUND" ser2net -d -c "$W/does-not-exist.yaml" s2_rc cli/missing-file-rc 1 # -------------------------------------------------------------------------- # THE YAML CONFIGURATION PARSER. Pure userland, and the half of this port that # is genuinely exercised on a machine with no serial hardware. # -------------------------------------------------------------------------- # Every fixture here is INVALID on purpose: a fixture that parsed would leave # ser2net resident, and these cases are about the message and the exit. # yaml_verrout_d_f() prefixes each one with `:(column ): '. cat > "$W/badkey.yaml" <<'EOF' %YAML 1.1 --- define: &confver 1.0 nosuchkey: whatever EOF s2_run "$S2_BOUND" ser2net -d -c "$W/badkey.yaml" s2_saw yaml/unknown-top-key "Invalid token at the main level: nosuchkey" s2_rc yaml/unknown-top-key-rc 1 s2_saw yaml/error-location "(column " cat > "$W/badconnkey.yaml" < "$W/noaccepter.yaml" <<'EOF' %YAML 1.1 --- connection: &con0 connector: serialdev,/dev/ttyS0,9600n81 EOF s2_run "$S2_BOUND" ser2net -d -c "$W/noaccepter.yaml" s2_saw yaml/no-accepter "No accepter given in connection" cat > "$W/noconnector.yaml" < "$W/malformed.yaml" <<'EOF' %YAML 1.1 --- connection: &con0 accepter: tcp,127.0.0.1,29074 connector: serialdev,/dev/ttyS0,9600n81 EOF s2_run "$S2_BOUND" ser2net -d -c "$W/malformed.yaml" s2_rc yaml/malformed-rc 1 s2_saw yaml/malformed-located "(column " # The %YAML version directive is libyaml's to enforce, not ser2net's: yamlconf.c # reads no version_directive field at all. 1.1 is what upstream's own shipped # ser2net.yaml declares; 2.0 is a document libyaml refuses outright. cat > "$W/ver20.yaml" <<'EOF' %YAML 2.0 --- define: &confver 1.0 EOF s2_run "$S2_BOUND" ser2net -d -c "$W/ver20.yaml" s2_saw yaml/version-2-0-rejected "found incompatible YAML document" s2_rc yaml/version-2-0-rc 1 # -------------------------------------------------------------------------- # THE DAEMON, AND THE DEVICE IT CANNOT OPEN. # -------------------------------------------------------------------------- # One bounded run of a VALID %YAML 1.1 fixture carrying a connback, which is the # one documented way to make ser2net open the connector at start-up rather than # lazily on a client connection. It is expected to overrun the bound: op_loop() # does not return. # The collector goes up FIRST: the message this section is about is emitted # during apply_new_ports(), before ser2net has printed anything else, so a # syslogd started afterwards would miss it. Foreground, so this shell owns it # and it dies with the suite; `-m 0' so no MARK lines pad the file; `-C' so the # kernel-ring seqno cache is this run's own rather than a stale /run one. S2_SLOG=$W/syslog.log S2_SLPID= S2_SYSLOG=no : > "$S2_SLOG" if command -v syslogd >/dev/null 2>&1; then printf '*.*\t-%s\n' "$S2_SLOG" > "$W/syslog.conf" syslogd -F -m 0 -P "$W/syslogd.pid" -C "$W/kmsg.seqno" -f "$W/syslog.conf" & S2_SLPID=$! # The banner in OUR file, not merely a live process: it proves the daemon # parsed this config and is writing here. /dev/log follows it (86-sysklogd # measured that ordering), so the socket check is second and not a race. if s2_wait_for "$S2_SLOG" "restart."; then _s2_i=0 while [ $_s2_i -lt 25 ] && [ ! -S /dev/log ]; do _s2_i=$((_s2_i + 1)); sleep 1; done [ -S /dev/log ] && S2_SYSLOG=yes fi fi cat > "$W/valid.yaml" </dev/null; then bst_notok daemon/accepter-started "the TCP accepter did not start, so device/open-reported below says nothing about the device -- saw:'$(_bst_1line "$(grep -m1 'Unable to startup network port' "$S2_SLOG")")'" else bst_ok daemon/accepter-started fi # make_pidfile() runs between start_threads() and op_loop(), so the file # existing is proof ser2net got into the event loop rather than exiting during # configuration. if [ -s "$W/ser2net.pid" ]; then bst_ok daemon/reaches-op-loop else bst_notok daemon/reaches-op-loop "no pid file: ser2net never reached op_loop()"; fi # The narrowing, asserted rather than assumed: a config naming an absent device # leaves ser2net RESIDENT. There is no path in 4.6.1 on which the device makes # it exit. s2_rc daemon/stays-resident 124 # And the report itself: port.c:327, the documented message naming the device # it could not open -- read where seout actually sends it. if [ "$S2_SYSLOG" = yes ]; then if s2_wait_for "$S2_SLOG" "Unable to enable port device"; then bst_ok device/open-reported else bst_notok device/open-reported "want:'Unable to enable port device' in the syslog capture; last: '$(_bst_1line "$(tail -n 3 "$S2_SLOG" 2>&1)")'" fi else bst_skip device/open-reported "no syslogd collecting /dev/log on this image, and ser2net's start-up diagnostics are syslog-only (ser2net.c:312), so there is nowhere to read the report from" fi # The collector belongs to this suite, not to the machine: take it back down, # which also unlinks /dev/log, so the next suite finds the image as it was. if [ -n "$S2_SLPID" ]; then kill "$S2_SLPID" 2>/dev/null if [ -s "$W/syslogd.pid" ]; then read -r _s2_slpid < "$W/syslogd.pid" 2>/dev/null case "$_s2_slpid" in [0-9]*) kill "$_s2_slpid" 2>/dev/null ;; esac fi fi rm -rf "$W"