. "$BST_LIB" # BST-TIMEOUT: 900 # ~55 cases, and the cost is EXECS rather than work: about # # 25 fresh wasm module instantiations, one of them lldpad at # # 3.4 MB. 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 byte-identical. # # openlldp 1.1.1+git (SRCREV f1dd9eb9, meta-openembedded # meta-networking/recipes-protocols/openlldp_1.1.1.bb, `inherit pkgconfig # autotools', DEPENDS = libnl libconfig readline, plus a one-line meta-brintos # bbappend adding flex-native and no patch) on the wasm32-hwjs glibc image -- # Intel's Linux LLDP agent. Four programs ship, all in /usr/sbin: lldpad, the # daemon that speaks IEEE 802.1AB and DCBX; and lldptool, dcbtool and vdptool, # which drive it over an abstract AF_UNIX datagram socket. # # ============================================================================ # THERE IS NO NEIGHBOUR AND NO DCB DEVICE HERE, AND THAT IS THE SUBJECT. # ============================================================================ # # arch/wasm32/configs/wasm32_defconfig sets CONFIG_NET=y, CONFIG_INET=y and # CONFIG_UNIX=y and leaves CONFIG_PACKET UNSET. So AF_PACKET does not exist and # no LLDP frame could reach a wire even if there were one. lldpad never tries: # init_ports() walks if_nameindex() through is_valid_lldp_device(), whose first # test is is_loopback(), and `lo' is the only interface on this machine -- so no # port is added and lldp/l2_packet_linux.c is never reached. The absence of a # neighbour is therefore NOT what this suite measures around; it is a deeper # absence, one layer down, that stops the daemon first. # # ============================================================================ # THE DAEMON STOPS AT ITS SHARED-MEMORY SEGMENT -- MEASURED, AND NOT FOR THE # REASON THIS SUITE FIRST PREDICTED. # ============================================================================ # # lldpad_shm_getpid() runs before the event loop and is the only routine that # creates the segment: shm_open("/lldpad.state"), ftruncate to 4096, then # mmap(PROT_READ|PROT_WRITE, MAP_SHARED). main() takes # # LLDPAD_ERR("error getting shm pid"); goto out_fail; # # and exits 1 if any of the three fails. In the foreground log_message() prints # to STDOUT whenever loglvl (LOG_WARNING by default) is at or above the level, # so LOG_ERR is visible without a syslogd -- this image ships none. # # WHICH of the three fails is the part that was got wrong first time round. The # prediction was mmap: this kernel refuses a file-backed MAP_SHARED whenever # PROT_WRITE is asked for (brintos/linux#30, found on gdbm and cited by # 99-haveged for sem_open's identical 16-byte mapping), so the daemon's stop was # attributed to #30 and a `shm-control' case was written to assert that /dev/shm # was present and writable, making that attribution safe. # # The control did its job by REFUSING it. Measured on the batch-2048 image # (2026-09-02): the daemon does stop at `error getting shm pid', but /dev/shm # does not exist at all, so shm_open(3) fails first and mmap is never reached. # The stock /etc/fstab in machine-stage mounts tmpfs on /run and /var/volatile # only, carries no /dev/shm line, and the shipped /dev is empty -- so glibc's # shm_open, which is openat(2) under /dev/shm, cannot succeed here. # # So the daemon's stop is attributable to the ABSENT SHM FILESYSTEM -- a fact # about how this image is composed, of the same class as a kernel config -- and # NOT to #30, which this image gives no evidence either way about. `no-dev-shm' # below states that absence as a machine fact rather than reddening over it, and # is the tripwire: the day the image mounts /dev/shm it goes red, and only then # is there anything here to say about #30. # # ============================================================================ # THE ENTIRE CLIENT SURFACE IS GATED BEHIND THAT DAEMON. THAT IS UPSTREAM'S # DESIGN, AND IT IS ASSERTED RATHER THAN WORKED AROUND. # ============================================================================ # # lldptool, dcbtool and vdptool call clif_open() in main() BEFORE parsing a # single argument. The getopt loop, the -V/-c TLV parser, the -i interface # lookup and the TLV-name table all live inside request(), which runs only on a # connected socket. So on a machine with no lldpad, `lldptool -h', `lldptool -i # lo -t' and `lldptool -V no-such-tlv -i lo -t' produce the IDENTICAL answer: # # connect: Connection refused # Failed to connect to lldpad - clif_open: Connection refused # # and `return -1' from main, which is exit status 255. The `gated-*' cases # assert that identity deliberately. They are a tripwire: the day #30 closes and # lldpad stays resident they must diverge, and that is where real TLV-parser # coverage gets written. A suite that claimed to be exercising the parser here # would be measuring nothing at all. # # dcbtool is the one client with a pre-connection surface: its getopt loop runs # first and `-h' and `-v' return from main before clif_open. Those two cases are # ordinary userland C on this arm and are asserted exactly. # # `connect: Connection refused' IS ITSELF AN ASSERTION, not incidental text. It # says the abstract AF_UNIX datagram socket was created and bound and only the # peer is missing; a `socket:' or a `bind:' line there would mean something much # worse about CONFIG_UNIX. `clif-depth' scores which of the three happened # rather than scoring "it failed". # # ============================================================================ # WHAT IS PREDICTED AND WHAT IS MEASURED. # ============================================================================ # # THERE IS NO NATIVE CONTROL FOR THIS PORT. Unlike 93-wpan-tools, this suite was # not first run against an x86_64 build of the same source -- the build host has # no pkg-config and no libnl/libconfig/readline headers, so no control could be # made. Every expectation below therefore comes from the source at SRCREV # f1dd9eb9, EXCEPT the four version strings, which were read out of the SHIPPED # wasm modules with strings(1) at recipe time and are measured facts. # # Two cases WERE predictions when this suite was written -- `daemon-branch' (that # the daemon stops at its shm segment rather than somewhere earlier) and # `clif-depth' (that the client reaches connect(2) rather than failing at # socket(2) or bind(2)). Both have since been MEASURED on the batch-2048 image # (2026-09-02) and both hold, so they are results here and not guesses. What did # NOT survive was the reason given for the first: see the shm block above -- the # branch is right, the cause named for it was wrong, and the control written to # guard that cause is what caught it. # # ============================================================================ # BOUNDING, AND WHY NOT timeout(1). # ============================================================================ # # 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. `ol_bounded' 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, because bst_probe # scores rc 0 as the pass and every invocation here is expected to exit # non-zero. Bare `lldptool' and bare `vdptool' with NO arguments are interactive # mode, which retries clif_open() in an unbounded `sleep 1' loop by design; the # ticket scopes this port to the command line and this suite never runs them. PATH="$PATH:/usr/sbin:/sbin" export PATH require lldptool openlldp/present W=${TMPDIR:-/tmp}/bst-openlldp.$$ mkdir -p "$W" || { bst_notok openlldp/workdir "cannot create $W"; exit 0; } OL_BOUND=60 # per invocation; every path here either answers on the # first syscall that fails or does not answer at all, so # this is a hang detector rather than a budget. # ol_bounded -- run with stdout at $W/out, stderr at # $W/err and no result line of its own. Sets OL_RC to the exit status, or 124 # when the bound expired (the child is killed). ol_bounded() { _ol_bound=$1; shift rm -f "$W/out" "$W/err" "$W/rc" { "$@" >"$W/out" 2>"$W/err"; echo $? >"$W/rc"; } & _ol_pid=$! _ol_t0= if [ -r /proc/uptime ] && read -r _ol_u _ < /proc/uptime; then _ol_t0=${_ol_u%.*}; fi _ol_ticks=0 while [ ! -e "$W/rc" ]; do _ol_d=$_ol_ticks if [ -n "$_ol_t0" ] && read -r _ol_u _ < /proc/uptime; then _ol_d=$(( ${_ol_u%.*} - _ol_t0 )) fi if [ "$_ol_d" -ge "$_ol_bound" ]; then kill -9 "$_ol_pid" 2>/dev/null OL_RC=124 return 124 fi sleep 1 _ol_ticks=$((_ol_ticks + 1)) done OL_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 OL_RC < "$W/rc" 2>/dev/null case "$OL_RC" in ''|*[!0-9]*) OL_RC=0 ;; esac return 0 } # ol_fail -- the pure-userland form, for the # paths that return before any socket is opened. They signal through the exit # status as much as through the text, which check_out cannot express (it # requires rc 0). ol_fail() { _of_id=$1; _of_want=$2; _of_needle=$3; shift 3 _of_out=$("$@" 2>&1); _of_rc=$? case "$_of_out" in *"$_of_needle"*) _of_hit=1 ;; *) _of_hit=0 ;; esac if [ "$_of_rc" -eq "$_of_want" ] && [ "$_of_hit" -eq 1 ]; then bst_ok "$_of_id" elif bst_execfail "$_of_out"; then bst_unrun "$_of_id" "$_of_out" else bst_notok "$_of_id" "rc=$_of_rc want=$_of_want needle:'$_of_needle' got:'$(_bst_1line "$_of_out")'"; fi } # ol_nodaemon -- score the run ol_bounded just did as "stopped because no # lldpad is listening": it returned inside the bound, exited 255 (main's # `return -1'), and perror'd upstream's one line. OL_DEPTH records how deep # clif_open got, so `clif-depth' below can name it once for all of them, and # OL_MIXED trips if two invocations disagree. OL_DEPTH= OL_MIXED=0 ol_nodaemon() { _on_id=$1 _on_o=$(cat "$W/out" "$W/err" 2>/dev/null) case "$_on_o" in *"connect: "*) _on_d=connect ;; *"bind: "*) _on_d=bind ;; *"socket: "*) _on_d=socket ;; *) _on_d= ;; esac if [ -z "$OL_DEPTH" ]; then OL_DEPTH=$_on_d elif [ -n "$_on_d" ] && [ "$_on_d" != "$OL_DEPTH" ]; then OL_MIXED=1; fi case "$_on_o" in *"Failed to connect to lldpad - clif_open"*) _on_hit=1 ;; *) _on_hit=0 ;; esac if [ "$OL_RC" = 124 ]; then bst_notok "$_on_id" "did not return within ${OL_BOUND}s -- it blocked on the client socket rather than reporting the absent daemon" elif bst_execfail "$_on_o"; then bst_unrun "$_on_id" "$_on_o" elif [ "$_on_hit" -eq 0 ]; then bst_notok "$_on_id" "rc=$OL_RC, not upstream's clif_open diagnostic: '$(_bst_1line "$_on_o")'" elif [ "$OL_RC" != 255 ]; then bst_notok "$_on_id" "said clif_open failed but rc=$OL_RC, want 255 (main returns -1)" else bst_ok "$_on_id" fi } # ------------------------------------------------------- what shipped -------- # All four programs are sbin_PROGRAMS upstream, so they land in /usr/sbin and # not /usr/bin. The paths are asserted directly: a caller resolving them through # a PATH without /usr/sbin finds nothing and reads the port as ABSENT rather # than broken, which is a failure that looks like coverage. for b in lldpad lldptool dcbtool vdptool; do check_rc "openlldp/sbin-$b" 0 test -x "/usr/sbin/$b" check_rc "openlldp/not-in-bin-$b" 1 test -e "/usr/bin/$b" done # dpkg KNOWS the package -- every case above would read the same if some other # recipe had dropped these four names into /usr/sbin. sed -n '/^Package: openlldp$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check_out openlldp/pkg-installed "Status: install ok installed" cat "$W/st" check_out openlldp/pkg-version "Version: 1.1.1+git0+f1dd9eb961-r0" cat "$W/st" # The one runtime dependency this recipe's packaging resolves to on this distro. check_out openlldp/pkg-depends "brintos-libc-so" cat "$W/st" # ...and the one RRECOMMENDS the stock recipe carries. Unlike vpnc's # kernel-module-tun, iproute2-tc EXISTS on this image, so it needed no removal # and do_rootfs resolved it -- which is why this is asserted rather than # overridden. check_out openlldp/pkg-recommends "iproute2-tc" cat "$W/st" # The packaged file list, from dpkg's side. A .list names directories too and # without a trailing slash, so each name is matched whole. PKGLIST=/var/lib/dpkg/info/openlldp.list check_rc openlldp/pkg-list-present 0 test -s "$PKGLIST" for b in lldpad lldptool dcbtool vdptool; do check "openlldp/pkg-list-sbin-$b" grep -qx "/usr/sbin/$b" "$PKGLIST" done # The bash-completion scripts are in the MAIN package here -- this recipe has no # -bash-completion split -- and lldpad/lldptool repeat as basenames, so these two # ids name the directory as well. for b in lldpad lldptool; do check "openlldp/pkg-list-completion-$b" grep -qx "/etc/bash_completion.d/$b" "$PKGLIST" done # Upstream's install-data-hook creates an empty state directory, and it is where # lldpad's default config file would live (DEFAULT_CFG_FILE is # /var/lib/lldpad/lldpad.conf). The package ships the directory and no file. check_rc openlldp/state-dir 0 test -d /var/lib/lldpad check_rc openlldp/no-stock-conf 1 test -e /var/lib/lldpad/lldpad.conf # The three split packages. -dev is the client headers plus two .pc files, # -staticdev is the single convenience archive, -doc the man pages. check_rc openlldp/dev-header 0 test -s /usr/include/lldpad/clif.h check_rc openlldp/dev-sock-header 0 test -s /usr/include/lldpad/clif_sock.h check_rc openlldp/dev-pc-lldpad 0 test -s /usr/lib/pkgconfig/lldpad.pc check_rc openlldp/dev-pc-clif 0 test -s /usr/lib/pkgconfig/liblldp_clif.pc check_rc openlldp/staticdev-archive 0 test -s /usr/lib/liblldp_clif.a check_rc openlldp/doc-lldptool 0 test -s /usr/share/man/man8/lldptool.8 check_rc openlldp/doc-lldpad 0 test -s /usr/share/man/man8/lldpad.8 # The distro is static-only for this recipe: liblldp_clif is a libtool library # and configure answered "whether to build shared libraries... no". bst_lib_shape # passes vacuously on a package that ships none, and catches the dangling-link # failure the day this recipe is converted. bst_lib_shape openlldp/lib-shape "/usr/lib/liblldp_clif.so*" # ------------------------------------------- the machine, stated as fact ----- # CONFIG_PACKET is unset, so the AF_PACKET protocol family never registers and # /proc/net/packet -- which af_packet.c creates in its init -- does not exist. # This is the absence lldpad would meet if `lo' were not excluded first, stated # here as a property of the machine rather than inferred from a tool's silence. check_rc openlldp/no-af-packet 1 test -e /proc/net/packet # ...and `lo' IS a loopback device, which is why init_ports() skips it. /sys is # the oracle: type 772 is ARPHRD_LOOPBACK. check_out openlldp/lo-is-loopback "772" cat /sys/class/net/lo/type # WHAT STOPS THE DAEMON, STATED AS A MACHINE FACT. There is no shm filesystem # here: the stock fstab mounts tmpfs on /run and /var/volatile only and names no # /dev/shm, and /dev ships empty. glibc's shm_open(3) is openat(2) under # /dev/shm, so it cannot succeed -- which is why `daemon-branch' below is `shm' # and why that branch says NOTHING about the MAP_SHARED+PROT_WRITE refusal # (brintos/linux#30): shm_open fails first and mmap is never reached. # # This is the tripwire, not a control that must hold. It reds the day the image # grows a /dev/shm mount, and that red is the signal to re-derive which of # shm_open/ftruncate/mmap stops lldpad -- only then is #30 in the frame at all. if [ -d /dev/shm ]; then bst_notok openlldp/no-dev-shm "/dev/shm now exists -- re-derive the daemon's shm branch; #30 is only testable from here" else bst_ok openlldp/no-dev-shm fi # ------------------------------------------- the pre-connection userland ----- # lldpad parses its options before it opens anything, so these four cases would # still hold on a machine where the daemon runs. The version strings were read # out of the SHIPPED wasm modules with strings(1), not guessed. check_out openlldp/lldpad-version "lldpad v1.1.1" lldpad -v # usage() writes to STDERR and ends in a hardcoded exit(1). Asserting rc 0 for # -h is the reflex, and it reds against every build of this package. ol_fail openlldp/lldpad-usage 1 "usage: lldpad [-hdksptv]" lldpad -h # The getopt '?' arm falls through to the same usage(). A short flag that is not # in "hdksptvf:RV:" rather than a long one: plain getopt(3) would read `--x' as # the short options `-', `x', which is the same answer by a longer route and # would make the case about glibc rather than about lldpad. ol_fail openlldp/lldpad-badopt 1 "usage: lldpad [-hdksptv]" lldpad -Z # `optind < argc' after the loop -- a positional argument is not accepted. ol_fail openlldp/lldpad-extra-arg 1 "usage: lldpad [-hdksptv]" lldpad spurious # -f given twice takes the `if (cfg_file_name)' arm. Upstream wrote a `break' # after that usage() call as though usage() returned; it does not -- it ends in # a hardcoded exit(1) -- so the second -f is refused on the spot and the break # is dead code. Pinned because the source reads as though it continues. ol_fail openlldp/lldpad-double-f 1 "usage: lldpad [-hdksptv]" lldpad -f /dev/null -f /dev/null # dcbtool is the ONE client whose getopt loop runs before clif_open, so it has a # pre-connection surface of two cases and the other two clients have none. check_out openlldp/dcbtool-version "dcbtool v1.1.1" dcbtool -v check_out openlldp/dcbtool-usage "Usage:" dcbtool -h # Its usage() prints the version banner first, which is how these two are told # apart from a stub. check_out openlldp/dcbtool-usage-banner "dcbtool v1.1.1" dcbtool -h # ------------------------------------------- the daemon, bounded ------------- # A config-file fixture the test writes, so the daemon is not made to create one # under /var/lib on a read-only-ish rootfs and so `version' is absent from it -- # init_cfg() treats a config carrying a `version' string as stale, removes the # file and rebuilds, which is a different path from the one under test. printf 'lldp :\n{\n};\n' > "$W/lldpad.conf" check_rc openlldp/conf-fixture 0 test -s "$W/lldpad.conf" # -p omits the pid file (so /var/run's writability is not part of this # assertion), -t omits the timestamp prefix, and no -d keeps it in the # foreground where log_message() prints to stdout instead of syslog. It must # come back rather than stay resident. ol_bounded "$OL_BOUND" lldpad -p -t -f "$W/lldpad.conf" OL_DAEMON_OUT=$(cat "$W/out" "$W/err" 2>/dev/null) case "$OL_DAEMON_OUT" in *"error getting shm pid"*) OL_DBRANCH=shm ;; *"failed to initialize configuration file"*) OL_DBRANCH=cfg ;; *"failed to register client interface"*) OL_DBRANCH=clif ;; *"failed to register user space event interface"*) OL_DBRANCH=netlink ;; *"failed to initialize event loop"*) OL_DBRANCH=eloop ;; *) OL_DBRANCH= ;; esac # HALF ONE, WHICH HOLDS WHICHEVER WAY THE PREDICTION LANDS: it terminated inside # the bound instead of sitting in eloop_run(). This is the case the ticket asks # for in its own words. if [ "$OL_RC" = 124 ]; then bst_notok openlldp/daemon-stops "stayed resident for ${OL_BOUND}s -- it reached eloop_run() rather than reporting what it could not open" elif bst_execfail "$OL_DAEMON_OUT"; then bst_unrun openlldp/daemon-stops "$OL_DAEMON_OUT" elif [ -z "$OL_DBRANCH" ]; then bst_notok openlldp/daemon-stops "rc=$OL_RC and no documented startup diagnostic: '$(_bst_1line "$OL_DAEMON_OUT")'" elif [ "$OL_RC" != 1 ]; then bst_notok openlldp/daemon-stops "stopped at '$OL_DBRANCH' but rc=$OL_RC, want 1" else bst_ok openlldp/daemon-stops fi # HALF TWO. `shm' means the daemon got past its config file, its event loop and # its client socket and stopped at its shared-memory segment -- which on this # image it cannot open, because there is no /dev/shm for shm_open(3) to reach # (`no-dev-shm' above states that absence). MEASURED on batch-2048, not # predicted: this is where it stops. Any other branch means the reasoning in the # header is wrong about WHERE it stops, not that the package is broken, and the # red names the branch it saw. check_eq openlldp/daemon-branch shm "$OL_DBRANCH" # It left no pid file behind: -p was given, and out_fail only unlinks when it # did write one. check_rc openlldp/daemon-no-pidfile 1 test -e /var/run/lldpad.pid # ------------------------------------------- the clients, bounded ------------ # Every one of these is expected to give the SAME answer, because clif_open() # runs before any of them parses an argument. The ids say what each invocation # WOULD have exercised, so that when #30 closes and these diverge the file # already names the coverage that has to be written. # What the ticket names: a get-tlv and a statistics request on the only # interface this machine has. ol_bounded "$OL_BOUND" lldptool -i lo -t ol_nodaemon openlldp/lldptool-get-tlv-lo ol_bounded "$OL_BOUND" lldptool -i lo -S ol_nodaemon openlldp/lldptool-stats-lo # An interface that does not exist. The `-i' argument is not even read until # request(), so this is the same stop -- and that identity is the point. ol_bounded "$OL_BOUND" lldptool -i nosuchdev0 -t ol_nodaemon openlldp/gated-absent-iface # THE TLV-PARSER TRIPWIRE. A well-formed -V/-c pair and two malformed ones on # the same argument positions. All three stop at the missing daemon today; the # day lldpad stays resident they must diverge, and these three become real # parser coverage. ol_bounded "$OL_BOUND" lldptool -i lo -t -V sysName ol_nodaemon openlldp/gated-tlv-name-valid ol_bounded "$OL_BOUND" lldptool -i lo -t -V no-such-tlv-name ol_nodaemon openlldp/gated-tlv-name-unknown ol_bounded "$OL_BOUND" lldptool -i lo -T -V 8 -c enableTx=yes ol_nodaemon openlldp/gated-tlv-config-valid ol_bounded "$OL_BOUND" lldptool -i lo -T -V 0xZZZZ -c enableTx=yes ol_nodaemon openlldp/gated-tlv-id-malformed # `-h' and `-v' ARE GATED TOO, and that is the quirk most worth pinning: unlike # every other tool in this image, lldptool cannot print its own usage without a # daemon, because main() connects before it reads argv. A reader would guess # both of these exit 0. ol_bounded "$OL_BOUND" lldptool -h ol_nodaemon openlldp/gated-lldptool-help ol_bounded "$OL_BOUND" lldptool -v ol_nodaemon openlldp/gated-lldptool-version # vdptool has the identical main(), one module id apart. ol_bounded "$OL_BOUND" vdptool -i lo -t ol_nodaemon openlldp/vdptool-get-tlv-lo # dcbtool's getopt loop DOES run first, but a command still needs the socket. ol_bounded "$OL_BOUND" dcbtool gc lo dcb ol_nodaemon openlldp/dcbtool-gc-lo # EVERY client invocation agreed. Eleven independent runs giving one answer is # what separates "no daemon is listening" from an intermittent socket, and it # holds whichever depth clif_open reached. if [ "$OL_MIXED" = 1 ]; then bst_notok openlldp/clients-agree "the client invocations did not agree; the first depth seen was '$OL_DEPTH'" elif [ -z "$OL_DEPTH" ]; then bst_notok openlldp/clients-agree "no invocation reported which syscall clif_open stopped at" else bst_ok openlldp/clients-agree fi # THE SECOND AND LAST PREDICTION. `connect' means the abstract AF_UNIX datagram # socket was created and bound and only the peer is missing -- CONFIG_UNIX # works and the port is one running daemon away from its whole client surface. # `socket' or `bind' would say something much worse about AF_UNIX on this # kernel, and the red would be about the platform rather than about openlldp. check_eq openlldp/clif-depth connect "$OL_DEPTH" rm -rf "$W"