. "$BST_LIB" # agent-proxy 1.97 (meta-oe recipes-kernel, git.kernel.org kgdb utils) -- the # proxy that lets a console session and a kgdb/gdbserver session share one # serial line or one terminal-server port. # # WHAT CAN BE ASSERTED HERE, AND WHAT CANNOT. # agent-proxy's whole job needs an endpoint on the far side: a serial device, or # a terminal server reachable over TCP. This image has neither it can OPEN -- # the wasm-serial bridge registers /dev/ttySER0 and refuses open(2) with ENODEV # until a browser-granted WebSerial port is attached, and an unattended verify # grants none -- see the endpoint probe at the bottom, which is what turns that # sentence into a measurement rather than a claim. So the PROXIED SESSION is probed and skipped, # and what is asserted for real is the part that runs entirely on this side: the # usage text, the option scanner, and the local-port parser -- including the # port-PAIR syntax (`^` and `+`) that is the reason this tool exists rather than # a plain socat. # # EVERY INVOCATION HERE IS ONE THAT CANNOT REACH select(2), BY CONSTRUCTION. # This is the single most important property of this file. agent-proxy has no # timeout of its own: once the local port is bound AND the remote is open it # enters an unbounded select(2) loop and never returns. On this target that is # not a slow test, it is a wedged suite -- sleep is a no-op (brintos/linux#16 # and the note in data/wget-httpd.tcl) and, before the brintos/linux#77 fix, # timeout(1) never fired, so nothing could reap it, and brintos_local_verify # would quarantine the whole suite and cost every other member of the run its # image. # # Two disjoint classes, and nothing else is used: # (A) runs that fail during the ARGUMENT SCAN, before any socket is opened -- # no args, an unknown option, a missing option argument, too many # arguments. These call usage()/exit() from inside the parse loop. # (B) runs that reach port setup but name a REMOTE THAT CANNOT BE OPENED, # /nonexistent-agent-proxy-tty. setup_remote_port() does open(port, O_RDWR) # on a leading-'/' remote and returns 1 when it fails, so main() exits 1. # open(2) on a path that does not exist fails on any POSIX stack, which # makes termination structural rather than timed -- and it holds whatever # the LOCAL port did, so even a case whose bind unexpectedly SUCCEEDS ends # as a red rather than a hang. # Do not add a case that names an openable remote (/dev/null included: it would # rely on tcgetattr failing, which is a property of the stack rather than of the # filesystem), and do not name a real serial device, whose open(2) has no # O_NONBLOCK here and may block on carrier. # # WHY A tcl PAYLOAD FOR THE IN-USE CASES. Asserting "this port is already bound" # needs something to be holding it. A shell cannot: backgrounding a listener and # then running agent-proxy is a race, and this target has no timer to close it # with. data/agent-proxy-busyport.tcl binds the ports and exec's agent-proxy from # the same interpreter, so the ordering is structural. Same shape, same reason, # as data/wget-httpd.tcl. # # ALL DIAGNOSTICS BELOW ARE UPSTREAM'S, QUOTED VERBATIM -- including # "Too many aguments" (sic) and "Open of local port failed" printed for a # REMOTE-side failure. Both are upstream bugs of a cosmetic kind; they are the # contract this binary actually has, so they are asserted as-is rather than # corrected. If a future version fixes either spelling these cases go red, which # is the correct signal: the observable contract changed. require agent-proxy agent-proxy/present DEV=/nonexistent-agent-proxy-tty,115200 # has : ok iff contains the literal . # Local to this file (bst.sh has check_out, which runs a command; several # assertions here score a string already captured). has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) if bst_execfail "$3"; then bst_unrun "$1" "$3" else bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'"; fi ;; esac } # hasnt : the negative form, for discriminators that # assert a message is ABSENT (which is how a remote-side failure is told from a # local-side one -- both exit 1 and both end "Open of local port failed"). hasnt() { case "$3" in *"$2"*) bst_notok "$1" "unwanted:'$2' in:'$(_bst_1line "$3")'" ;; *) bst_ok "$1" ;; esac } # --- (A) the usage text ------------------------------------------------------ # With no arguments at all agent-proxy prints usage() and exits 1. usage() uses # printf(3), so this lands on STDOUT -- asserted separately below, because a # tool that moved it to stderr would still pass every content case here. usage_out=$(agent-proxy 2>/dev/null); usage_rc=$? check_eq agent-proxy/usage-rc 1 "$usage_rc" # The version line is the first thing usage() prints, and it is printed as # "%1.2f" of the AGENT_VER the Makefile passes with -D. Asserting the rendered # string covers the recipe's PV, the -D reaching the compiler, and the wasm32 # libc formatting a double, in one case. has agent-proxy/usage-version "agent-proxy version 1.97" "$usage_out" # The synopsis, which is the documented grammar of the argument this suite # spends most of its cases on. has agent-proxy/usage-synopsis \ " agent-proxy <[udp:][virt IP:]local port | stdin> <[udp:]remote port>" \ "$usage_out" # The debug-splitter options. These are the three the tool's own README leads # with, and they are the ones the option scanner is asserted on below. has agent-proxy/usage-opt-B "-B to turn off break on connect" "$usage_out" has agent-proxy/usage-opt-G "-G to turn off gdb protocol split" "$usage_out" has agent-proxy/usage-opt-s "-s ### to set alternate break char" "$usage_out" # The two port-PAIR examples. They are the documentation of the '+' and '^' # syntax whose PARSING is asserted for real further down, so the pair of cases # together says "documented, and it does what the document says". has agent-proxy/usage-pair-plus "agent-proxy 4440+4441 10.0.0.10 2011" "$usage_out" has agent-proxy/usage-pair-caret "agent-proxy 4440^4441 10.0.0.10 2011" "$usage_out" has agent-proxy/usage-pair-tty "agent-proxy 4440^4441 0 /dev/ttyS0,115200" "$usage_out" # usage() goes to stdout, and stderr stays empty. A tool that swapped the two # would break every caller that pipes it; nothing else here would notice. check_eq agent-proxy/usage-stream-stderr "" "$(agent-proxy 2>&1 >/dev/null)" check agent-proxy/usage-stream-stdout test -n "$usage_out" # --- (A) the option scanner -------------------------------------------------- # An unrecognized option: the diagnostic names the option and goes to STDERR, # then usage() runs and the status is 1. opt_err=$(agent-proxy -Z 2>&1 >/dev/null) agent-proxy -Z >/dev/null 2>&1; opt_rc=$? check_eq agent-proxy/opt-unknown-rc 1 "$opt_rc" has agent-proxy/opt-unknown-msg "option -Z not recognized" "$opt_err" # An option that takes an argument, with the argument missing at end-of-argv. # -f (pidfile) is the one such option that is not compiled out on this build: # -b and -l live under USE_LATENCY and fall through to the "not recognized" arm. f_err=$(agent-proxy -f 2>&1 >/dev/null) agent-proxy -f >/dev/null 2>&1; f_rc=$? check_eq agent-proxy/opt-missing-arg-rc 1 "$f_rc" has agent-proxy/opt-missing-arg-msg "no argument specified for option -f" "$f_err" # -b IS compiled out here, and the case that says so is worth having: it fixes # which build this is. USE_LATENCY is not set by the recipe, so the usage text # omits the "Optional Delay Args" line and -b falls through to the default arm. # The ARGUMENT is required: -b/-l/-p/-s share an arm that consumes the next argv # BEFORE the inner switch decides, so a bare `-b` fails at "no argument # specified for option -b" and never reaches the arm this case is about. has agent-proxy/no-latency-opt "option -b not recognized" \ "$(agent-proxy -b 9600 2>&1 >/dev/null)" hasnt agent-proxy/no-latency-usage "Optional Delay Args" "$usage_out" # A FOURTH positional argument. agent-proxy takes exactly three; the fourth is # refused during the scan, before any port is touched, and the status is the # odd one out -- exit(-1), which the shell reports as 255. many_err=$(agent-proxy a b c d 2>&1 >/dev/null) agent-proxy a b c d >/dev/null 2>&1; many_rc=$? check_eq agent-proxy/too-many-args-rc 255 "$many_rc" # "aguments" is upstream's typo -- see the header. Asserted verbatim. has agent-proxy/too-many-args-msg "Too many aguments at: d" "$many_err" # --- (B) the local-port parser, without a helper ----------------------------- # These name a bind ADDRESS that is not assigned to this machine (192.0.2.1 is # TEST-NET-1, RFC 5737) and a remote that cannot be opened. The bind must fail; # the remote guarantees termination if it somehow does not. # # The 'virt IP:' form is the documented way to bind one interface, and the split # at ':' is the same code path the port-pair cases below depend on. virt_out=$(agent-proxy 192.0.2.1:45570 0 "$DEV" 2>&1) agent-proxy 192.0.2.1:45570 0 "$DEV" >/dev/null 2>&1; virt_rc=$? check_eq agent-proxy/virtip-rc 1 "$virt_rc" has agent-proxy/virtip-bind-fails "Error: on socket bind, address in use" "$virt_out" # The udp: prefix selects SOCK_DGRAM before the address is parsed, so this # asserts the prefix was consumed AND the address behind it was still read. udp_out=$(agent-proxy udp:192.0.2.1:45571 0 "$DEV" 2>&1) has agent-proxy/udp-prefix-bind-fails "Error: on socket bind, address in use" "$udp_out" # The `stdin` local-port form takes a branch that opens NO socket at all: the # local side becomes fd 0 and the run proceeds straight to the remote. So the # bind diagnostic must be ABSENT and the remote's must be present -- which is # exactly the discriminator that tells the two halves of this tool apart. stdin_out=$(agent-proxy stdin 0 "$DEV" 2>&1 &1 &1 &1 &1 >/dev/null /dev/null 2>&1; then bst_skip agent-proxy/busyport "no tclsh on this image to hold a port with" elif [ ! -f "$PAYLOAD" ]; then bst_notok agent-proxy/busyport "payload $PAYLOAD not installed" else ap_out=$(tclsh "$PAYLOAD" 2>&1); ap_rc=$? # : the value of the single AP- line, or "" if the payload never # got that far. Every case below reads through this, so a payload that died # early yields empty strings and honest reds rather than stale ones. ap() { printf '%s\n' "$ap_out" | awk -v k="AP-$1" '$1==k{sub(/^[^ ]+ /,"");print;exit}'; } # all AP--out lines of one run, joined -- the `has` cases score these. apout() { printf '%s\n' "$ap_out" | awk -v k="AP-$1-out" '$1==k{sub(/^[^ ]+ /,"");print}'; } # Can tcl exec here at all? Read FIRST, because it decides whether anything # below is about agent-proxy. See the note at the gate further down. ap_exec=$(ap exec) case "$ap_exec" in supported) ap_can_exec=1 ;; *) ap_can_exec=0 ;; esac # Whole-payload gate: everything after it reads values the payload only prints # if it ran to the end. When tcl cannot exec, the payload deliberately stops # early -- so that is a SKIP and not a red: the payload did what it should, # and the thing it could not do is the platform's, not this package's. case "$ap_out" in *ALL-AGENT-PROXY-OK*) ap_all=1 ;; *) ap_all=0 ;; esac if [ "$ap_rc" -eq 0 ] && [ "$ap_all" -eq 1 ]; then bst_ok agent-proxy/busyport-payload elif [ "$ap_can_exec" -ne 1 ]; then bst_skip agent-proxy/busyport-payload \ "the payload stopped at its own exec probe, by design -- see the skips below" else bst_notok agent-proxy/busyport-payload "rc=$ap_rc $(_bst_1line "$ap_out")"; fi # The payload could bind all three ports. Reported on its own so that "the # harness could not set up" is never read as "agent-proxy ignored an in-use # port" -- the two would otherwise be indistinguishable. check_eq agent-proxy/busyport-setup "OK" "$(ap setup)" # CAN tcl exec HERE? The payload asks before judging anything, and this is # where that answer is acted on. popen(3) and system(3) are ENOSYS on this arm # (brintos/linux#14) and tcl's [exec] is a different mechanism again, so # "could not start agent-proxy" is a real possibility -- and it is a PLATFORM # gap, not agent-proxy refusing a port. Reporting it as a red would put the # blame on this package for something it did not do, so the in-use cases are # skipped with the reason instead. A genuine agent-proxy failure is not this: # the probe is agent-proxy's own no-argument path, whose exit status is known, # so only a failure to STAGE the process at all lands here. if [ "$ap_can_exec" -ne 1 ]; then for _c in inuse-rc inuse-msg inuse-final pair-caret-rc pair-caret-splits \ pair-plus-rc pair-plus-splits freepair-rc freepair-no-bind-error \ freepair-reaches-remote hexport-rc hexport-parsed; do bst_skip "agent-proxy/$_c" \ "tcl cannot exec on this arm, so no process could be started against a held port (brintos/linux#14 is the neighbouring popen/system gap); this is a platform limit, not agent-proxy: $(_bst_1line "$ap_exec")" done else # 1. THE ALREADY-BOUND PORT. The ticket's own criterion: a local port another # process holds is refused, with the documented status. check_eq agent-proxy/inuse-rc 1 "$(ap busy-rc)" has agent-proxy/inuse-msg "Error: on socket bind, address in use" "$(apout busy)" has agent-proxy/inuse-final "Open of local port failed" "$(apout busy)" # 2./3. THE PORT PAIR. `45552^45551` with only 45551 held: the FIRST half is # free and binds, and the run then fails on the SECOND, naming it alone. # That message is the evidence the string was split -- an unsplit # "45552^45551" reaching atoi() is 45552, which is free, so the run would # have bound it and reached select(2) instead. '+' and '^' differ only in # whether a break is sent on connect, which needs a session to observe; the # SPLIT is what is observable here, and both separators are asserted. check_eq agent-proxy/pair-caret-rc 1 "$(ap pair-caret-rc)" has agent-proxy/pair-caret-splits "Error: connecting to 45551" "$(apout pair-caret)" check_eq agent-proxy/pair-plus-rc 1 "$(ap pair-plus-rc)" has agent-proxy/pair-plus-splits "Error: connecting to 45551" "$(apout pair-plus)" # 4. THE CONTROL, and it is the case that makes 1-3 falsifiable. Both halves # of this pair are free, so the run must get PAST the local ports and fail # on the remote instead: status still 1, but with no bind complaint at all. # A build whose bind always failed -- a plausible way for a wasm32 socket # layer to be broken -- passes 1-3 for the wrong reason and fails here. check_eq agent-proxy/freepair-rc 1 "$(ap free-pair-rc)" hasnt agent-proxy/freepair-no-bind-error "Error: on socket bind" "$(apout free-pair)" has agent-proxy/freepair-reaches-remote "ERROR: opening /nonexistent-agent-proxy-tty" \ "$(apout free-pair)" # 5. THE HEX PORT SPELLING the usage text documents (udp:0x1001, 0x4321). # 0xB1AE is held by the payload, so a strtol(...,16) collides and the bind # fails, while an atoi() stopping at 'x' would yield port 0, bind an # ephemeral port successfully and show no bind error. The two readings are # told apart by outcome rather than by reading the source. check_eq agent-proxy/hexport-rc 1 "$(ap hexport-rc)" has agent-proxy/hexport-parsed "Error: on socket bind, address in use" "$(apout hexport)" fi fi # --- the proxied session: probed, and skipped on what the probe finds -------- # This is the one thing agent-proxy exists to do, and it is the one thing this # image cannot exercise: it needs an endpoint on the FAR side. The probe below # looks for each of the three kinds upstream documents, and the session case is # skipped only when all three are confirmed absent -- never skipped on # assumption, which is why the probe's own result is a case of its own. # # serial -- a /dev/ttyS* or /dev/ttyUSB* character device to open # kgdb -- kgdboc/kgdbts registered, i.e. something to debug # off-link -- an interface other than lo, i.e. a terminal server to reach # # A NODE THAT EXISTS IS NOT AN ENDPOINT, which is what the first version of this # probe got wrong. The runtime always registers the wasm-serial WebSerial bridge # as /dev/ttySER0 -- a name the ttyS* glob matches -- and that node refuses # open(2) with ENODEV until a browser-granted port is attached # (arch/wasm32/kernel/serial_wasm.c, wasm_serial_port_activate: `if # (!wasm_serial_active()) return -ENODEV`). An unattended verify grants none; # crossing that boundary is linux-in-the-browser#60 M2, the same seam # 79-linux-serial-test and 99-serialcheck skip on. So what is asserted is that # no serial endpoint can be OPENED, not that no serial node exists. # # Only ttySER* is probed by opening it, and that is safe by the same # construction the header rests on: the refusal is immediate, and the port # publishes no ->carrier_raised, so tty_port_carrier_raised() is true and # tty_port_block_til_ready() cannot wait for DCD. Any OTHER serial node counts # as an endpoint WITHOUT being opened -- a real UART's open(2) has no O_NONBLOCK # here and may block on carrier, and nothing on this target can reap it. # # The image itself is built without CONFIG_KGDB and registers no virtio-net # device; lo with 127.0.0.1/8 is the whole of its networking (the same fact # 60-net-tools.test and 60-wget.test rest on). serial_nodes=$(find /dev -maxdepth 1 \( -name 'ttyS*' -o -name 'ttyUSB*' \) 2>/dev/null) kgdb=$(find /sys/module -maxdepth 1 \( -name 'kgdboc' -o -name 'kgdbts' \) 2>/dev/null) offlink=$(find /sys/class/net -mindepth 1 -maxdepth 1 ! -name 'lo' 2>/dev/null) serial= serial_detached= for _n in $serial_nodes; do case "$_n" in /dev/ttySER*) if (exec 3<"$_n") 2>/dev/null; then serial="$serial $_n" else serial_detached="$serial_detached $_n"; fi ;; *) serial="$serial $_n" ;; esac done serial=${serial# } serial_detached=${serial_detached# } # The probe result itself, as cases. They are ASSERTIONS ABOUT THIS IMAGE, not a # defect check: each is the recorded state the skip below rests on, and a red # here means the premise changed and the session case can now be written. check_eq agent-proxy/probe-no-attached-serial "" "$serial" check_eq agent-proxy/probe-no-kgdb "" "$kgdb" check_eq agent-proxy/probe-no-offlink "" "$offlink" # Present-but-detached is the expected state of the bridge, and it is worth a # case of its own: it is the difference between "this image has no serial # hardware" and "this image has a serial line nobody has granted", and only the # second can change under an operator's hands without the image changing. if [ -n "$serial_nodes" ]; then check_eq agent-proxy/probe-serial-nodes-detached \ "$(_bst_1line "$serial_nodes")" "$(_bst_1line "$serial_detached")" fi if [ -z "$serial" ] && [ -z "$kgdb" ] && [ -z "$offlink" ]; then bst_skip agent-proxy/proxied-session \ "$(_bst_1line "no endpoint to proxy: no openable ttyS*/ttyUSB* device (present-but-detached: '$serial_detached'), no kgdboc/kgdbts module, no interface but lo")" else # Deliberately NOT attempted here even though an endpoint now exists. A live # session run reaches select(2), and nothing on this target can reap it -- see # the header. The red is the request to write that case by hand, with whatever # bounded harness the new endpoint makes possible. # _bst_1line is applied HERE because bst_notok does not collapse its own # detail -- only check/check_eq do, before they call it. $serial is routinely # multi-line (a host with ttyS0..ttyS31), and an embedded newline lands as one # counted result plus N uncounted stray lines: the verdict survives and the # evidence is what goes missing. bst_notok agent-proxy/proxied-session \ "$(_bst_1line "an endpoint appeared (serial:'$serial' kgdb:'$kgdb' offlink:'$offlink'); the session case can now be written -- it is NOT run here because it would block for ever")" fi