# BST-TIMEOUT: 180 . "$BST_LIB" # asio 1.30.2 (upstream chriskohlhoff/asio, meta-oe recipes-support/asio) -- a # cross-platform C++ library for network and low-level I/O programming. # Header-only: the port ships /usr/include/asio.hpp, the /usr/include/asio tree # and one pkg-config file. No archive, no shared object, no CLI. Upstream ships # no cmake package config, which is a difference from span-lite and libeigen # and is recorded rather than repaired. # # WHY THIS SUITE IS ABOUT BEHAVIOUR AND NOT ABOUT FILES. asio is not a library # that is either present or absent -- it is a set of ASYNCHRONOUS I/O BACKENDS # SELECTED BY THE PREPROCESSOR from the platform it believes it is on. It # believes this is Linux 6.12, because detail/config.hpp:825-834 reads # and that header is telling the truth about the kernel's # version and nothing about its syscall table. On that reading asio selects # epoll_reactor, and arch/wasm32/kernel/syscall_table.c has no entry for any # epoll, eventfd or timerfd number. So a stock asio compiles clean, links # clean, installs clean, and its io_context CANNOT BE CONSTRUCTED. # # THE PORT'S ANSWER IS -DASIO_DISABLE_EPOLL -DASIO_DISABLE_EVENTFD, carried in # the shipped asio.pc so a consumer inherits it, and asio/detail/reactor.hpp # then falls through to select_reactor -- which needs only select(2) and # pipe(2), both wired (syscall_table.c [23] and [22]). Section 5 is the # NEGATIVE CONTROL that makes that a measurement instead of a claim: the same # probe source built the way asio configures itself unaided, run here, and # asserted to die where the port says it dies. # # THE OTHER HALF OF THE TICKET'S QUESTION IS THREADS, and section 6 answers it. # pthread_create parks the coroutinized thread-start frame and traps # (linux-in-the-browser#85), so asio's thread-SPAWNING surface -- asio::thread, # thread_pool, a multi-threaded io_context::run, and ip::basic_resolver, which # runs its lookups on a background thread -- is unusable here whatever the # reactor does. The core is not: an io_context driven by one thread never # spawns one, and asio's mutexes and condition variables are pthread OBJECTS # rather than pthread THREADS and work fine. asio-probe-nothreads is the same # source under ASIO_DISABLE_THREADS (null_mutex, null_event, no spawning # surface at all) and section 6 asserts it reaches the same answers, which is # what makes "the select reactor survives with no threads" a result. # # SECTION 3 IS THE TRIPWIRE. It asserts the compile-time state everything after # it was measured under. A red there means the toolchain, the kernel headers or # the recipe's macro set moved -- not necessarily bad news, but every value # below would have to be re-measured rather than merely re-read. # # NO COMPILE HAPPENS ON TARGET. This image ships no compiler, so the port's # claim that the headers BUILD is established at do_compile on the build host # -- the recipe compiles the probe from ${S}/include in the same build that # installs them, which makes do_compile the port's first consumer -- and what # runs here is the resulting binary. The asio entry in # docs/brintos-test-plan.md says so; it is cited by SUBJECT and not by number, # because entry numbers are assigned at merge time and a number written into a # SHIPPED file goes stale the moment another port lands first. # # Style follows 76-span-lite and 76-libeigen: each probe is run ONCE and its # output parsed in a single fork-free loop, because the runner's per-suite # bound is a budget in process spawns rather than in cases. inc=/usr/include/asio hdr=/usr/include/asio.hpp pc=/usr/lib/pkgconfig/asio.pc PT=/usr/libexec/asio-tests # --------------------------------------------------------------------------- # 1. The payload. check asio/header test -f "$hdr" check asio/inc-dir test -d "$inc" check asio/version-h test -f "$inc/version.hpp" check asio/config-h test -f "$inc/detail/config.hpp" # asio's convenience header is the one line a consumer writes, and it is the # file most likely to be missed by a do_install that globs a subdirectory. case "$(cat "$hdr" 2>/dev/null)" in *'#include "asio/io_context.hpp"'*) ah=complete ;; *) ah=short ;; esac check_eq asio/header-includes-io-context "complete" "$ah" # The declared surface, asserted by NAME rather than by count: a header set # that installed a subset would still pass a directory-exists check, and the # names below are what a consumer's #include lines actually say. for h in io_context.hpp steady_timer.hpp ip/tcp.hpp ip/udp.hpp ip/address.hpp \ buffer.hpp error_code.hpp write.hpp read.hpp post.hpp streambuf.hpp \ ssl.hpp posix/stream_descriptor.hpp local/stream_protocol.hpp \ detail/reactor.hpp detail/select_reactor.hpp detail/epoll_reactor.hpp \ detail/throw_exception.hpp; do check "asio/hdr-$h" test -f "$inc/$h" done # The version, from the header's own macro. ASIO_VERSION is a single integer # (major*100000 + minor*100 + sub-minor), so 1.30.2 is 103002 and a port that # installed a different release would still pass every existence check above. case "$(cat "$inc/version.hpp" 2>/dev/null)" in *'#define ASIO_VERSION 103002'*) hv=103002 ;; *) hv=other ;; esac check_eq asio/header-version "103002" "$hv" # No build system and no boost under ${includedir}. asio's standalone mode is # what keeps out of the include graph, and a boost/ tree # appearing here would mean the recipe's PACKAGECONFIG = "" stopped taking. check_eq asio/no-boost-tree "0" "$(find /usr/include/boost -maxdepth 0 2>/dev/null | grep -c .)" check_eq asio/no-makefile "0" "$(find "$inc" -name 'Makefile*' 2>/dev/null | grep -c .)" # --------------------------------------------------------------------------- # 2. The integration surface -- and it is where this port's real deliverable is. # # asio.pc is not decoration here. A consumer that asks pkg-config for asio's # flags and believes the answer gets, on a stock .pc, a header set that emits # `throw` on a driver with no unwinder and an io_context whose reactor calls a # syscall this kernel does not implement. Both are facts about THIS ARM, so the # recipe extends Cflags with the arm's macro set and these cases pin it. # # A red on asio/pc-cflags-* is therefore a REGRESSION IN THE PORT, not a # cosmetic drift: it means a future consumer will be handed a configuration # that does not work, silently, at its own build time. check asio/pc-file test -f "$pc" if command -v pkg-config >/dev/null 2>&1; then check_eq asio/pc-modversion "1.30.2" "$(pkg-config --modversion asio 2>/dev/null)" cf=$(pkg-config --cflags asio 2>/dev/null) for d in ASIO_STANDALONE ASIO_NO_EXCEPTIONS ASIO_DISABLE_EPOLL ASIO_DISABLE_EVENTFD; do case "$cf" in *"-D$d"*) r=present ;; *) r=absent ;; esac check_eq "asio/pc-cflags-$d" "present" "$r" done # pkg-config drops -I/usr/include from --cflags: it strips include paths the # compiler already searches, so the default output pins that rule rather than # this file. Ask with the stripping off to read what asio.pc actually carries. ci_cf=$(PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --cflags asio 2>/dev/null) case "$ci_cf" in *-I/usr/include*) ci=present ;; *) ci=absent ;; esac check_eq asio/pc-cflags-includedir "present" "$ci" # Upstream's asio.pc.in spells the libs key `Lflags:`, which is not a key # pkg-config reads, so --libs is empty. That is the correct answer for a # header-only package and the typo is left alone rather than corrected -- # but it is PINNED, so an upstream release that fixes the spelling and # starts emitting -lssl -lcrypto shows up here as a red with this comment # attached instead of quietly changing what every consumer links. check_eq asio/pc-libs-empty "" "$(pkg-config --libs asio 2>/dev/null)" else bst_skip asio/pc-modversion "pkg-config not on this image" fi # No cmake package: upstream's autotools build ships none. Asserted as an # absence rather than skipped, so a release that starts shipping one surfaces # here instead of passing unnoticed. check_eq asio/no-cmake-package "0" "$(find /usr/lib/cmake -maxdepth 1 -iname 'asio*' 2>/dev/null | grep -c .)" # --------------------------------------------------------------------------- # 3-4. The shipped configuration. if [ ! -x "$PT/asio-probe" ]; then bst_skip asio/probe-present "asio-probe not installed on this image" exit 0 fi bst_ok asio/probe-present # Probe size is a functional property on this target, not trivia: the guest # stages an exec image as a segment list capped at WASM32_IMG_MAX_SEGS, so a # large image is refused -ENOMEM on a fragmented heap before it runs (nopoll # measured exactly that at 41 MB). Read against this arm's floor rather than on # its own -- an empty main() costs ~2.3 MB here, glibc and libc++ merged whole, # and asio is the most template-heavy header set on this image. The three # probes measured 7.03 MB, 7.05 MB and 6.82 MB at the port (2026-08-21); 12 MB # leaves real headroom for a compiler change and is well below where the cap # bites. for p in asio-probe asio-probe-epoll asio-probe-nothreads; do if [ -x "$PT/$p" ]; then sz=$(wc -c < "$PT/$p") if [ "$sz" -lt 12582912 ]; then bst_ok "asio/size-$p" else bst_notok "asio/size-$p" "$p is $sz bytes, over the 12582912 ceiling"; fi else bst_notok "asio/size-$p" "$p not installed" fi done out=$("$PT/asio-probe" 2>&1); prc=$? P_VER=; P_CXXSTD=; P_LINUX=; P_STANDALONE=; P_NOEXC= P_EPOLL=; P_EVENTFD=; P_TIMERFD=; P_IOURING=; P_THREADS=; P_PTHREADS= P_POSIXSD=; P_LOCALSOCK= P_SYSEPOLL=; P_SYSEVENTFD=; P_SYSTIMERFD= P_CTXC=; P_CTXH=; P_CTXR= P_TFIRED=; P_TEC=; P_TMS=; P_TCANCEL= P_PWROTE=; P_PREAD=; P_PEC=; P_PBYTES= P_TCPPORT=; P_TCPACC=; P_TCPCON=; P_TCPREAD=; P_TCPEC=; P_TCPBYTES=; P_TCPEOF= P_ERRREF=; P_ERRBUF=; P_THROWN= P_CHECKS=; P_FAILURES=; P_DONE= oIFS=$IFS; IFS=' ' for line in $out; do tag=${line%% *}; val=${line#* } case "$tag" in P-ASIO-VERSION) P_VER=$val ;; P-CXXSTD) P_CXXSTD=$val ;; P-LINUX) P_LINUX=$val ;; P-STANDALONE) P_STANDALONE=$val ;; P-NO-EXCEPTIONS) P_NOEXC=$val ;; P-HAS-EPOLL) P_EPOLL=$val ;; P-HAS-EVENTFD) P_EVENTFD=$val ;; P-HAS-TIMERFD) P_TIMERFD=$val ;; P-HAS-IO-URING) P_IOURING=$val ;; P-HAS-THREADS) P_THREADS=$val ;; P-HAS-PTHREADS) P_PTHREADS=$val ;; P-HAS-POSIX-SD) P_POSIXSD=$val ;; P-HAS-LOCAL-SOCKETS) P_LOCALSOCK=$val ;; P-SYS-EPOLL-CREATE1) P_SYSEPOLL=$val ;; P-SYS-EVENTFD2) P_SYSEVENTFD=$val ;; P-SYS-TIMERFD-CREATE) P_SYSTIMERFD=$val ;; P-CTX-CONSTRUCTED) P_CTXC=$val ;; P-CTX-HANDLERS) P_CTXH=$val ;; P-CTX-RAN) P_CTXR=$val ;; P-TIMER-FIRED) P_TFIRED=$val ;; P-TIMER-EC) P_TEC=$val ;; P-TIMER-MS) P_TMS=$val ;; P-TIMER-CANCEL) P_TCANCEL=$val ;; P-PIPE-WROTE) P_PWROTE=$val ;; P-PIPE-READ) P_PREAD=$val ;; P-PIPE-EC) P_PEC=$val ;; P-PIPE-BYTES) P_PBYTES=$val ;; P-TCP-PORT) P_TCPPORT=$val ;; P-TCP-ACCEPT) P_TCPACC=$val ;; P-TCP-CONNECT) P_TCPCON=$val ;; P-TCP-READ) P_TCPREAD=$val ;; P-TCP-EC) P_TCPEC=$val ;; P-TCP-BYTES) P_TCPBYTES=$val ;; P-TCP-EOF) P_TCPEOF=$val ;; P-ERR-REFUSED) P_ERRREF=$val ;; P-BUF-SIZE) P_ERRBUF=$val ;; P-THROWN) P_THROWN=$val ;; P-CHECKS) P_CHECKS=$val ;; P-FAILURES) P_FAILURES=$val ;; P-DONE) P_DONE=$val ;; esac done IFS=$oIFS # The probe must have reached its end. A truncated capture would otherwise make # every unset variable below read as an empty-string mismatch and bury the one # case that actually died. check_eq asio/probe-completed "1" "${P_DONE:-absent}" check_eq asio/probe-rc "0" "$prc" check_eq asio/probe-no-throw "" "${P_THROWN:-}" # --- 3. The tripwire: the state everything below was measured under. --------- check_eq asio/state-version "103002" "${P_VER:-absent}" check_eq asio/state-cxxstd "201703" "${P_CXXSTD:-absent}" # asio believes this is Linux, which is exactly why it reaches for epoll. check_eq asio/state-linux "1" "${P_LINUX:-absent}" check_eq asio/state-standalone "1" "${P_STANDALONE:-absent}" check_eq asio/state-no-exceptions "1" "${P_NOEXC:-absent}" # The backend gates, as the recipe leaves them. check_eq asio/state-epoll-off "0" "${P_EPOLL:-absent}" check_eq asio/state-eventfd-off "0" "${P_EVENTFD:-absent}" # timerfd needs no flag of its own: config.hpp:843-848 gates it on epoll, so it # leaves with epoll. Pinned here so a future header that decouples them is not # absorbed silently. check_eq asio/state-timerfd-off "0" "${P_TIMERFD:-absent}" check_eq asio/state-io-uring-off "0" "${P_IOURING:-absent}" check_eq asio/state-posix-stream-descriptor "1" "${P_POSIXSD:-absent}" # --- 3b. Why the flags are there, measured rather than cited. --------------- # # The three syscalls asio's Linux backend is built on, called RAW so a glibc # wrapper that is itself a stub reports the kernel's answer rather than its # own. All three are expected to answer ENOSYS (errno 38): the wasm32 syscall # table has no entry for any of them. When brintos/linux#53 lands them these # three go green-with-a-different-value, and that is the signal to drop # ASIO_DISABLE_EPOLL / ASIO_DISABLE_EVENTFD from the recipe and the .pc. case "$P_SYSEPOLL" in *errno=38*) e1=enosys ;; *) e1="$P_SYSEPOLL" ;; esac case "$P_SYSEVENTFD" in *errno=38*) e2=enosys ;; *) e2="$P_SYSEVENTFD" ;; esac case "$P_SYSTIMERFD" in *errno=38*) e3=enosys ;; *) e3="$P_SYSTIMERFD" ;; esac check_eq asio/syscall-epoll-create1-enosys "enosys" "${e1:-absent}" check_eq asio/syscall-eventfd2-enosys "enosys" "${e2:-absent}" check_eq asio/syscall-timerfd-create-enosys "enosys" "${e3:-absent}" # --- 4. The library actually running. --------------------------------------- # The io_context. On the shipped configuration this is select_reactor and it # constructs; section 5 is the same line under epoll, where it does not. check_eq asio/ctx-constructed "1" "${P_CTXC:-absent}" check_eq asio/ctx-handlers "2" "${P_CTXH:-absent}" check_eq asio/ctx-ran "2" "${P_CTXR:-absent}" # Timers. With ASIO_HAS_TIMERFD off the scheduler bounds its own wait instead # of arming a timerfd, which is a different code path -- so the elapsed time is # asserted as well as the fact of firing. A timer that returns instantly did # not wait; one that takes seconds lost its wakeup. Both are silent. check_eq asio/timer-fired "1" "${P_TFIRED:-absent}" check_eq asio/timer-ec "0" "${P_TEC:-absent}" check_eq asio/timer-cancel "125" "${P_TCANCEL:-absent}" if [ -n "$P_TMS" ] && [ "$P_TMS" -ge 25 ] && [ "$P_TMS" -lt 5000 ]; then bst_ok asio/timer-elapsed else bst_notok asio/timer-elapsed "30ms timer measured ${P_TMS:-absent}ms" fi # Descriptor I/O through the reactor. A timer proves the scheduler and never # registers a descriptor; this is the cheapest case that makes the reactor # demultiplex a real readiness event. check_eq asio/pipe-wrote "16" "${P_PWROTE:-absent}" check_eq asio/pipe-read "16" "${P_PREAD:-absent}" check_eq asio/pipe-ec "0" "${P_PEC:-absent}" check_eq asio/pipe-bytes "asio-over-a-pipe" "${P_PBYTES:-absent}" # TCP over loopback -- what asio is for, and the case with the most ways to be # silently half-working. The kernel is CONFIG_INET=y and loopback-only # (wasm32_defconfig:234-255), so a self-connect is the whole of the network # this image has; it is enough to drive accept, connect, write, read and an # orderly EOF as five separate readiness events. check_eq asio/tcp-accept "0" "${P_TCPACC:-absent}" check_eq asio/tcp-connect "0" "${P_TCPCON:-absent}" check_eq asio/tcp-read "22" "${P_TCPREAD:-absent}" check_eq asio/tcp-ec "0" "${P_TCPEC:-absent}" check_eq asio/tcp-bytes "asio-over-loopback-tcp" "${P_TCPBYTES:-absent}" # eof is asio::error::eof == 2. A read that ends in any other error is a hang # in a real protocol loop rather than a clean shutdown. check_eq asio/tcp-eof "2" "${P_TCPEOF:-absent}" if [ -n "$P_TCPPORT" ] && [ "$P_TCPPORT" -gt 0 ]; then bst_ok asio/tcp-ephemeral-port else bst_notok asio/tcp-ephemeral-port "bind to port 0 gave ${P_TCPPORT:-absent}"; fi # The non-throwing surface. Under ASIO_NO_EXCEPTIONS the error_code overloads # are the ONLY safe entry points, so the port's usability rests on them # reporting failure ACCURATELY rather than merely not throwing. case "$P_ERRREF" in 111*) er=refused ;; *) er="${P_ERRREF:-absent}" ;; esac check_eq asio/err-connection-refused "refused" "$er" check_eq asio/buf-size "64" "${P_ERRBUF:-absent}" # The probe's own tally, which covers the cases above and the several it # asserts internally without printing a value. check_eq asio/probe-failures "0" "${P_FAILURES:-absent}" # --------------------------------------------------------------------------- # 5. NEGATIVE CONTROL: asio as it configures itself, unaided. # # THIS IS THE CASE THAT MAKES THE PORT'S CENTRAL CLAIM FALSIFIABLE. Same probe # source, same compiler, same flags -- except that -DASIO_DISABLE_EPOLL and # -DASIO_DISABLE_EVENTFD are absent, so detail/config.hpp reaches the # configuration it reaches on any Linux 6.12 box and asio/detail/reactor.hpp # selects epoll_reactor. epoll_reactor's constructor calls epoll_create1, then # retries epoll_create; on this kernel both answer ENOSYS, and it routes # through asio::detail::throw_error into the definition the probe supplies, # which prints P-THROWN and exits 97. # # WHERE IT DIES IS ITSELF THE FINDING, and it is not where one would guess. # asio creates its reactor LAZILY -- io_context construction and post()/run() # never ask for one -- so the epoll build gets a working io_context, runs two # handlers through it and reports success, and only dies at the first operation # that needs the reactor, which here is the steady_timer. A consumer meeting # this unaided therefore does NOT get a refusal at start-up: it gets a program # that initialises cleanly, does some work, and then dies partway through, with # a message naming epoll long after the code that chose epoll ran. That is a # strictly worse failure than a constructor refusal and it is why the flags are # carried in asio.pc rather than left to each consumer to rediscover. # # Measured rather than reasoned: the same three arms built on x86-64 with g++ # -Os -std=c++17 -fno-exceptions all pass there, and the epoll arm reproduces # exactly this sequence -- P-CTX-RAN 2, then P-THROWN epoll, rc 97 -- when # epoll_create/epoll_create1 are made to answer ENOSYS under LD_PRELOAD. So # this section is pinned to behaviour that has been produced on two arms, not # to a prediction from the header. # # WHEN THIS CASE GOES GREEN IT IS THE PORT THAT IS STALE, not the test. # brintos/linux#53 landing epoll is exactly what turns it, and the response is # to drop the two ASIO_DISABLE_* flags from the recipe and this section with # them. if [ ! -x "$PT/asio-probe-epoll" ]; then bst_notok asio/epoll-control-present "asio-probe-epoll not installed" else eout=$("$PT/asio-probe-epoll" 2>&1); erc=$? # The control's own compile-time state: epoll ON, which is the whole point. case "$eout" in *"P-HAS-EPOLL 1"*) ce=1 ;; *) ce=0 ;; esac check_eq asio/epoll-control-has-epoll "1" "$ce" # eventfd and timerfd come back with it, which is what makes this the # unaided configuration rather than a half of one. case "$eout" in *"P-HAS-EVENTFD 1"*) cv=1 ;; *) cv=0 ;; esac check_eq asio/epoll-control-has-eventfd "1" "$cv" # It got far enough to be a control rather than a crash. case "$eout" in *"P-ASIO-VERSION 103002"*) cs=reached ;; *) cs=missing ;; esac check_eq asio/epoll-control-reached-state "reached" "$cs" # THE LAZY-REACTOR FINDING, asserted in both directions. The io_context # constructs and runs -- so a check that only looked for a start-up failure # would have found none and concluded epoll was fine. case "$eout" in *"P-CTX-RAN 2"*) cc=ran ;; *) cc=refused-early ;; esac check_eq asio/epoll-control-context-ran "ran" "$cc" # ...and then the first reactor-needing operation kills it. The timer never # fires. case "$eout" in *"P-TIMER-FIRED"*) ct=fired ;; *) ct=never ;; esac check_eq asio/epoll-control-timer-never-fired "never" "$ct" # 97 is the exit the probe's own throw_exception takes. Anything else -- a # signal, a clean 0 -- means it failed somewhere this suite has not # accounted for. check_eq asio/epoll-control-rc "97" "$erc" # asio names the failing call in the exception it constructs, so the # control identifies epoll by asio's own words rather than by inference. case "$eout" in *"P-THROWN epoll"*) cw=epoll ;; *"P-THROWN"*) cw=other-throw ;; *) cw=no-P-THROWN-line ;; esac check_eq asio/epoll-control-blamed-epoll "epoll" "$cw" fi # --------------------------------------------------------------------------- # 6. The no-threads arm -- the second half of the ticket's question. # # ASIO_DISABLE_THREADS replaces asio's pthread mutex, event and thread-local # machinery with null_mutex / null_event / null_tss_ptr and removes the # thread-spawning surface entirely. It is the configuration a consumer on this # arm should reach for if it wants the compiler to refuse the calls that # linux-in-the-browser#85 traps on, rather than discovering them at run time. # # The recipe does NOT put it in asio.pc, deliberately: it is a consumer's # choice, it changes io_context's thread-safety contract, and the core path # works either way. What the port owes instead is EVIDENCE that the choice is # available and equivalent, which is this section -- the same reactor, the same # answers, no threads at all. if [ ! -x "$PT/asio-probe-nothreads" ]; then bst_notok asio/nothreads-present "asio-probe-nothreads not installed" else nout=$("$PT/asio-probe-nothreads" 2>&1); nrc=$? case "$nout" in *"P-HAS-THREADS 0"*) nt=0 ;; *) nt=1 ;; esac check_eq asio/nothreads-has-threads "0" "$nt" case "$nout" in *"P-HAS-PTHREADS 0"*) np=0 ;; *) np=1 ;; esac check_eq asio/nothreads-has-pthreads "0" "$np" check_eq asio/nothreads-rc "0" "$nrc" case "$nout" in *"P-DONE 1"*) nd=1 ;; *) nd=0 ;; esac check_eq asio/nothreads-completed "1" "$nd" case "$nout" in *"P-FAILURES 0"*) nf=0 ;; *) nf=nonzero ;; esac check_eq asio/nothreads-failures "0" "$nf" # The three answers that carry the reactor: a constructed context, a pipe # round-trip and a loopback TCP round-trip, byte for byte the same as the # shipped arm. Equal ANSWERS under a different mutex implementation is what # "survives with no threads" has to mean. case "$nout" in *"P-CTX-RAN 2"*) n1=ok ;; *) n1=no ;; esac check_eq asio/nothreads-ctx "ok" "$n1" case "$nout" in *"P-PIPE-BYTES asio-over-a-pipe"*) n2=ok ;; *) n2=no ;; esac check_eq asio/nothreads-pipe "ok" "$n2" case "$nout" in *"P-TCP-BYTES asio-over-loopback-tcp"*) n3=ok ;; *) n3=no ;; esac check_eq asio/nothreads-tcp "ok" "$n3" fi