# BST-TIMEOUT: 600 # # librelp 1.11.0 (meta-oe recipes-extended/rsyslog, SRCREV b421f56d) -- the RELP # reliable event-logging transport. The image ships /usr/lib/librelp.a, its # header and relp.pc, plus upstream's own session drivers as /usr/bin/relp-send # and /usr/bin/relp-receive (the stock recipe builds those only under ptest, # which this distro has off; librelp_%.bbappend says why they are installed). # # THE SUBJECT IS A TRANSPORT, so the assertions are about a SESSION, not about a # binary existing. One receiver is started on a loopback TCP port and four # senders are driven against it in turn -- so the file also covers four # sequential sessions on one listener -- and what is checked is that the bytes # handed to relpCltSendSyslog come out of onSyslogRcv unchanged and in order, # that the RELP frame exchange happens (open/offers -> 200 OK -> syslog -> 200 OK # -> close -> serverclose), and that both ends shut down cleanly. # # EVERY EXPECTED STRING BELOW IS PINNED FROM AN x86-64 CONTROL, not guessed: the # identical tarball at the same SRCREV, patched with the same five patches # (meta-oe's four plus this port's ENGINE_cleanup guard) and configured with the # recipe's own switches, built natively and driven through this exact sequence on # 2026-08-21. Where a comment says "control", that is what it means. # # THE ARM IS ASSERTED, and that is the case this file exists for as much as the # round trip. relpEngineRun is compiled in one of two shapes, chosen at configure # time by HAVE_EPOLL_CREATE1 / HAVE_EPOLL_CREATE (src/relp.c:681 and :865). # configure.ac:126 settles that with AC_CHECK_FUNCS, which is a LINK test, and # this glibc ships both symbols while the syscalls behind them answer ENOSYS -- # so the probe would answer yes and select an event loop that cannot start. The # bbappend denies both cache variables, which takes upstream's own select(2) arm. # # THAT DENIAL IS LOAD-BEARING AND WAS MEASURED IN BOTH DIRECTIONS on the control, # with an LD_PRELOAD making epoll_create/epoll_create1 return ENOSYS: # epoll arm -- "epoll_create1() could not create fd" ten times, then # "giving up starting relp engine"; the listen socket exists, the # sender fails with errorcode 10007, and the outfile stays EMPTY. # librelp has no runtime fallback on that path. # select arm -- the same preload, the same sender: message delivered. # So relp-receive would be a program that listens and transports nothing if the # probe were left to answer for itself, and librelp/select-arm below is what # turns "we shipped the right arm" from an assumption into a result. It is a # POSITIVE assertion on this image; a rebuild that lost the denial reds it here # and nowhere else, which is the point. # # NOTHING HERE SLEEPS FOR A DURATION. `sleep N' does not sleep on this image # (linux-in-the-browser#120), so every bound below is an ITERATION COUNT and each # sleep is a yield that lets the peer be scheduled. # # EXEC COUNT IS DELIBERATE. Each driver statically links glibc and openssl and is # ~41 MB, and this kernel refuses to stage a large exec image under fragmentation # (brintos/linux#44), so the file spends six execs and not one more: one # receiver, four senders against it, and one sender for the negative control. # That is also why the four payloads share a single listener instead of taking a # receiver each. . "$BST_LIB" require relp-receive librelp/present require relp-send librelp/present D=${TMPDIR:-/tmp}/bst-librelp.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok librelp/workdir "cannot create $D"; exit 0; } # `kill -0' is not used to ask whether the receiver is alive: it succeeds on a # zombie, so a receiver that died at startup would read as running. Read # /proc//status, as 83-dlt-daemon does for the same reason. running() { case "$(sed -n 's/^State:[ \t]*//p' "/proc/$1/status" 2>/dev/null)" in "") return 1 ;; Z*) return 1 ;; *) return 0 ;; esac } # line -- the nth message the receiver wrote, or the empty string. onSyslogRcv # writes each message followed by one newline and nothing else, so the outfile is # the delivered sequence with no framing of its own to strip. line() { sed -n "$1p" "$OUT" 2>/dev/null; } # --------------------------------------------------------------- the artifacts # A transport library ships more than its drivers, and each half is separately # packaged (librelp / librelp-dev / librelp-staticdev) -- so each is asserted. if [ -s /usr/lib/librelp.a ]; then bst_ok librelp/static-archive else bst_notok librelp/static-archive "/usr/lib/librelp.a absent or empty"; fi if grep -q 'relpEngineConstruct' /usr/include/librelp.h 2>/dev/null; then bst_ok librelp/header else bst_notok librelp/header "/usr/include/librelp.h does not declare relpEngineConstruct" fi # The version is read from relp.pc rather than from a --version flag, because # neither driver has one. 1.11.0 is the recipe's version and a change to it # should be a deliberate recipe change, not a surprise. pcver=$(sed -n 's/^Version: *//p' /usr/lib/pkgconfig/relp.pc 2>/dev/null) check_eq librelp/pkgconfig-version "1.11.0" "$pcver" # ------------------------------------------------------------------ the payloads # Four messages, each chosen for something a weaker assertion would miss: # plain -- the baseline round trip. # counter -- five messages whose ORDER is the assertion; sent by the driver's # own -n mode, which is what upstream's own testbench drives. # long -- 1000 bytes, well past any single read, so a short or re-framed # transfer is visible. RELP is octet-counted, not line-delimited. # punct -- embedded runs of spaces, a tab and both quote characters, so a # path that went through a shell or normalised whitespace is visible. PLAIN='brintos-relp-1058 the quick brown fox' LONG=$(awk 'BEGIN{for(n=0;n<100;n++) printf "0123456789"}') PUNCT=$(printf 'a b c\td|e"f'\''g') NMSG=5 NSESS=4 # the four payloads below, one session each PORT=21058 OUT=$D/received.txt TRACE=$D/receive.trace # -v is not decoration: librelp/select-arm, librelp/ack-200-ok and # librelp/session-teardown are all read out of this trace, and it is the only # place the engine says which loop it is running. relp-receive -p "$PORT" -O "$OUT" -F "$D/receive.pid" -v -W 300 >"$D/receive.out" 2>"$TRACE" & RPID=$! # Readiness is the pidfile, not a duration: receive.c writes it AFTER # relpEngineListnerConstructFinalize and BEFORE relpEngineRun, so its appearance # means the listen socket exists. Bounded by iterations. i=0 while [ "$i" -lt 60 ]; do [ -f "$D/receive.pid" ] && break running "$RPID" || break i=$((i + 1)); sleep 1 done if [ -f "$D/receive.pid" ] && running "$RPID"; then bst_ok librelp/receiver-listens else bst_notok librelp/receiver-listens "no listener after $i yields -- $(_bst_1line "$(cat "$TRACE" 2>/dev/null)")" fi if running "$RPID"; then # send : one sender session, its streams kept for the cases # below. rc and both streams are carried out through files because each call # is a separate session and the later assertions read them all. send() { _tag=$1; shift relp-send -t 127.0.0.1 -p "$PORT" "$@" >"$D/$_tag.out" 2>"$D/$_tag.err" echo $? > "$D/$_tag.rc" } send plain -m "$PLAIN" send counter -n "$NMSG" send long -m "$LONG" send punct -m "$PUNCT" # Wait for all 1+NMSG+1+1 messages rather than for a duration. want=$((3 + NMSG)) i=0 while [ "$i" -lt 60 ]; do [ "$(wc -l < "$OUT" 2>/dev/null || echo 0)" -ge "$want" ] && break i=$((i + 1)); sleep 1 done # ------------------------------------------------------------ the session # "send: Connect DONE" is printed by send.c only after relpCltConnect has # returned RELP_RET_OK, i.e. after the open/offers exchange completed -- so it # is a statement about the RELP session, not about the TCP connect. pout=$(cat "$D/plain.out" 2>/dev/null) if bst_execfail "$pout$(cat "$D/plain.err" 2>/dev/null)"; then bst_unrun librelp/session-open "$pout$(cat "$D/plain.err" 2>/dev/null)" else check_eq librelp/session-open "send: Connect DONE" "$pout" fi # The four payload cases. Byte-exact against the string this file sent -- not # a substring, which would pass against a truncated or duplicated delivery. check_eq librelp/message-byte-exact "$PLAIN" "$(line 1)" check_eq librelp/long-message "$LONG" "$(line $((2 + NMSG)))" check_eq librelp/whitespace-preserved "$PUNCT" "$(line $((3 + NMSG)))" # The counter arm: the driver's own tally, then the ORDER as delivered. Both, # because the tally is what the sender believes it sent and the lines are what # the receiver actually got, and a transport bug lives exactly in the gap. # send.c writes the tally after a bare CR ("\r%8.8d msgs sent\n"), so it can # overwrite its own progress line, and "send: Connect DONE" precedes it on the # same stream -- so the tally is extracted rather than compared whole. tally=$(tr -d '\r' < "$D/counter.out" 2>/dev/null | sed -n 's/^\([0-9][0-9]* msgs sent\)$/\1/p') check_eq librelp/sequence-count "$(printf '%08d msgs sent' "$NMSG")" "$tally" seq_want=$(awk -v n="$NMSG" 'BEGIN{for(i=1;i<=n;i++) printf "%08d\n", i}') seq_got=$(sed -n "2,$((1 + NMSG))p" "$OUT" 2>/dev/null) check_eq librelp/sequence-in-order "$seq_want" "$seq_got" # Every sender ended by tearing its own session down through relpCltDisconnect # and relpEngineDestruct; send.c prints its final state either way, so a # non-zero state here is a teardown that did not complete. bad="" for t in plain counter long punct; do st=$(cat "$D/$t.err" 2>/dev/null) case "$st" in *"send: terminating with state 0"*) ;; *) bad="$bad $t:$(_bst_1line "$st")" ;; esac done if [ -z "$bad" ]; then bst_ok librelp/sender-clean-exit else bst_notok librelp/sender-clean-exit "$(_bst_1line "$bad")"; fi # ------------------------------------------------ the wire, from the trace # THE ARM. Positive AND negative: a trace with neither line means the engine # never entered its loop at all, which is a different failure from shipping # the wrong arm, and only asserting both tells them apart. sel=$(grep -c 'relp select returns' "$TRACE" 2>/dev/null); [ -n "$sel" ] || sel=0 epo=$(grep -c 'doing epoll_wait' "$TRACE" 2>/dev/null); [ -n "$epo" ] || epo=0 if [ "$sel" -gt 0 ] && [ "$epo" -eq 0 ]; then bst_ok librelp/select-arm elif [ "$epo" -gt 0 ]; then bst_notok librelp/select-arm "the epoll arm was compiled in ($epo epoll_wait, $sel select): the bbappend's ac_cv_func_epoll_create/epoll_create1 denials are not in effect, and epoll_create answers ENOSYS on this kernel (brintos/linux#53)" else bst_notok librelp/select-arm "the engine entered neither loop (0 select, 0 epoll_wait) -- $(_bst_1line "$(tail -3 "$TRACE" 2>/dev/null)")" fi # The acknowledgement half of the protocol: the server answers the open frame # of each session, and every syslog frame, with an rsp carrying 200 OK. Without # this a transport that swallowed messages silently would still pass the # payload cases. The close frame is answered with a bare "rsp 0" and carries no # 200 OK, so it is not counted -- four opens plus 3+NMSG messages is 12 on the # control, and that is what the arithmetic below reproduces. acks=$(grep -c "rsp .*200 OK" "$TRACE" 2>/dev/null); [ -n "$acks" ] || acks=0 want_acks=$((NSESS + 3 + NMSG)) if [ "$acks" -ge "$want_acks" ]; then bst_ok librelp/ack-200-ok else bst_notok librelp/ack-200-ok "only $acks '200 OK' responses in the trace, want at least $want_acks (one per session open, one per message)"; fi # The teardown the ticket asks about, on the RECEIVER's side and at the # protocol level: each sender's close frame is answered and the server closes # the session with a serverclose hint-frame. Four sessions, four of each. closes=$(grep -c 'serverclose' "$TRACE" 2>/dev/null); [ -n "$closes" ] || closes=0 if [ "$closes" -ge "$NSESS" ]; then bst_ok librelp/session-teardown else bst_notok librelp/session-teardown "only $closes serverclose frames for $NSESS sessions"; fi else for c in librelp/session-open librelp/message-byte-exact librelp/long-message \ librelp/whitespace-preserved librelp/sequence-count \ librelp/sequence-in-order librelp/sender-clean-exit \ librelp/select-arm librelp/ack-200-ok librelp/session-teardown; do bst_notok "$c" "no listener to drive -- see librelp/receiver-listens" done fi # ------------------------------------------------------------- the clean stop # receive.c installs a SIGTERM handler that calls relpEngineSetStop, and its # atexit handler prints EXIT HDLR after unlinking the pidfile -- so the marker in # the trace distinguishes a receiver that RAN its shutdown from one that merely # stopped existing. Signal delivery is not fully reliable on this platform # (brintos/linux#21); if that is what fails, the detail says so. kill -TERM "$RPID" 2>/dev/null i=0 while [ "$i" -lt 60 ]; do running "$RPID" || break i=$((i + 1)); sleep 1 done if running "$RPID"; then bst_notok librelp/receiver-stops "still running after SIGTERM and $i yields (brintos/linux#21 covers unreliable signal delivery here)" else bst_ok librelp/receiver-stops fi if grep -q 'receive: EXIT HDLR' "$TRACE" 2>/dev/null; then bst_ok librelp/receiver-clean-exit else bst_notok librelp/receiver-clean-exit "no 'EXIT HDLR' in the trace: the atexit handler did not run" fi # ---------------------------------------------------------- negative control # Without this the whole file could pass against a sender that reports success # for anything. Nothing listens on this port; the control measured errorcode # 10014 out of relpCltConnect and exit state 1, and what is asserted is the # refusal rather than the code, which is librelp's to change. nout=$(relp-send -t 127.0.0.1 -p 21059 -m "must not connect" 2>&1); nrc=$? if bst_execfail "$nout"; then bst_unrun librelp/no-listener-refused "$nout" elif [ "$nrc" -eq 0 ]; then bst_notok librelp/no-listener-refused "connected to a port with no listener: $(_bst_1line "$nout")" else case "$nout" in *"FAILURE in 'relpCltConnect'"*) bst_ok librelp/no-listener-refused ;; *) bst_notok librelp/no-listener-refused "rc=$nrc but not a connect failure: $(_bst_1line "$nout")" ;; esac fi rm -rf "$D"