# BST-TIMEOUT: 900 # 172 cases, 138 of them a separate wasm process spawn; # # measured 226 s (fra2, 2026-08-06), so ~4x headroom # # libosip2 5.3.1 -- the GNU oSIP library, the reference SIP (RFC 3261) parser. # # THIS PORT INSTALLS NOT ONE EXECUTABLE OF ITS OWN. Without upstream's test # programs nothing on this image can call into libosipparser2 at all, and the # strongest thing a suite could say would be "libosip2.a exists". So the bbappend # flips --enable-test (upstream defaults it off) and ships src/test/'s eleven # programs plus upstream's res/ corpus, and this file is what runs them. # # THE EVIDENCE IS OF THREE KINDS. # # 1. UPSTREAM'S OWN CORPUS, RUN ON TARGET. torture_test over 121 messages -- 98 # well-formed and 23 malformed, each carrying its own `expected_error:` line so # the corpus asserts REJECTION as precisely as acceptance -- plus ten # per-header parsers over their own corpora. One case per message: the corpus # has to be walked message by message either way, so precise attribution # ("sip37 regressed") costs nothing over an aggregate count. # # 2. CROSS-ARM AGREEMENT ON WHOLE OUTPUT, not on a summary line. Nine of the ten # header programs exit 0 no matter what they parsed, so their exit status # asserts almost nothing; what they DO is print each input and the header # re-serialised from the parse tree. That text is a pure function of the corpus # bytes, so data/libosip2-ref-.txt is what an x86_64 build of the same # tarball printed and this arm must reproduce it byte for byte. A field # silently dropped here is a diff, not a rounding error. # # 3. ONE ASSERTION THAT MUST *DIFFER* BETWEEN THE ARMS -- section 6. The parser's # Content-Length handling is a function of sizeof(long) and the two arms cannot # agree above 2^31. That is the case an x86_64 binary packaged into the image # by mistake could not satisfy; everything else here is arch-independent by # design and would pass just as happily against one. # # WHAT IS NOT COVERED, DELIBERATELY: the osip2 half -- the ict/nict/ist/nist # transaction FSMs, and with them this package's only pthread and select() call # sites. Driving them wants a network peer and a clock to step timers through, # which the gate does not have. The test-plan entry states the gap rather than # papering it over with a case that would only assert that a connection failed. # Upstream does not test them either: all eleven of its programs link # libosipparser2 only. # # ON SPAWN COST. A process spawn is the expensive operation on this guest, so the # structure below is written to make exactly one per assertion that needs one: # the 121 verdicts come from 121 runs and no per-message sed, the diagnostics are # accumulated into a single file and normalised in one pass at the end, and the # expected verdicts are eval'd into shell variables up front rather than grepped # out of the control 121 times. Written naively this suite spawns ~750 processes # instead of ~200. . "$BST_LIB" NAT="$BST_DATA/libosip2-native.txt" REF_TORT="$BST_DATA/libosip2-ref-torture.txt" REF_DIAG="$BST_DATA/libosip2-ref-torture-diag.txt" for f in "$NAT" "$REF_TORT" "$REF_DIAG"; do [ -r "$f" ] || { bst_skip libosip2/payload "missing control $f"; exit 0; } done # Read one key out of the x86_64 control. Values may contain spaces. nat() { sed -n "s/^$1 //p" "$NAT" | head -1; } # The single normalisation, character for character the expression # src/libosip2_mknative.sh used to build the control: osip's trace lines carry a # wall-clock timestamp and nothing else in this output varies run to run. The # level, the source location and the message text are KEPT -- those are exactly # the diagnostics a bad port gets wrong. norm() { sed -e 's/^\(|[ ]*[A-Z][A-Z]*[ ]*| \)[0-9][0-9-]* [0-9:.][0-9:.]* /\1/'; } # Name the first line at which a capture diverges from its control. On a 718-line # reference the line number is most of the diagnosis, and "output differs" is not. firstdiff() { awk 'NR==FNR{a[++n]=$0;next} {m++; if(!d && $0!=a[m]){d=m; w=a[m]; g=$0}} END{if(d) printf "line %d: want[%s] got[%s]\n", d, w, g; else if(m!=n) printf "line count %d vs control %d\n", m+0, n+0}' "$1" "$2" } W=${TMPDIR:-/tmp}/bst-libosip2.$$ mkdir -p "$W" 2>/dev/null || { bst_notok libosip2/workdir "cannot mkdir $W"; exit 0; } trap 'rm -rf "$W"' EXIT # The suite locates libexecdir rather than hardcoding it: a layer that moves it # should make these cases fail with a clear reason, not report a broken library. LOT= for d in /usr/libexec/libosip2-tests /usr/lib/libosip2-tests; do [ -d "$d" ] && { LOT=$d; break; } done RES="$LOT/res" HDR_PROGS="turl tfrom tto tcontact tvia tcallid tcontentt trecordr troute twwwa" corpus_for() { case "$1" in turl) echo urls.txt ;; tfrom) echo froms.txt ;; tto) echo tos.txt ;; tcontact) echo contacts.txt ;; tvia) echo vias.txt ;; tcallid) echo callids.txt ;; tcontentt) echo contenttypes.txt ;; trecordr) echo recordroutes.txt ;; troute) echo routes.txt ;; twwwa) echo wwwas.txt ;; esac } # --------------------------------------------------------------------------- # 0. Is the deliverable there at all? # # NOT A SKIP. The bbappend installs these, so their absence is a packaging # regression of this very port, and a skip would hide exactly the thing the # ticket exists to add. `require` is for a tool the image legitimately does not # carry; this image is required to carry these. if [ -z "$LOT" ]; then bst_notok libosip2/tests-dir "libosip2-tests not installed (looked in /usr/libexec, /usr/lib) -- everything below is unrunnable" exit 0 fi bst_ok libosip2/tests-dir for p in torture_test $HDR_PROGS; do [ -x "$LOT/$p" ] || bst_notok "libosip2/installed-$p" "not installed in $LOT" done # THE ARM CHECK. Every other assertion here was designed to be arch-independent # so that a disagreement means the PARSER moved; this one says what the shipped # artifact actually is. A wasm module starts with the four-byte magic \0asm; an # x86_64 ELF starts \177ELF. if command -v od >/dev/null 2>&1; then magic=$(od -An -c -N4 "$LOT/torture_test" 2>/dev/null | tr -s ' ') case "$magic" in *'\0 a s m'*) bst_ok libosip2/tests-are-wasm ;; *) bst_notok libosip2/tests-are-wasm "first 4 bytes of torture_test are [$magic], not a wasm module" ;; esac else bst_skip libosip2/tests-are-wasm "no od on this image to read the file magic" fi # --------------------------------------------------------------------------- # 1. The corpus arrived intact and complete. # # Everything below compares against numbers measured from THESE files on x86_64, # so a truncated or partial corpus would produce a page of mismatches whose real # cause is the payload rather than the parser. Check it once, first. wf=$(ls "$RES" 2>/dev/null | grep -c '^sip[0-9]*$') mf=$(ls "$RES" 2>/dev/null | grep -c '^sip-malformed[0-9]*$') check_eq libosip2/corpus-wellformed "$(nat LOS-CORPUS-WELLFORMED)" "$wf" check_eq libosip2/corpus-malformed "$(nat LOS-CORPUS-MALFORMED)" "$mf" for p in $HDR_PROGS; do c=$(corpus_for "$p") check_eq "libosip2/corpus-bytes-$c" "$(nat "LOS-CORPUS-BYTES-$c")" \ "$(wc -c < "$RES/$c" 2>/dev/null | tr -d ' ')" done # --------------------------------------------------------------------------- # 2. The ten header parsers: exit status, then the whole of what they printed. # # Two cases each because they fail for different reasons and the names should say # which. `upstream-

` is upstream's own verdict; `xarm-

` is this arm # reproducing the x86_64 output exactly. The second is the one with teeth -- nine # of the ten return 0 unconditionally, so `upstream-

` alone would stay green # against a parser that had quietly stopped parsing. for p in $HDR_PROGS; do c="$RES/$(corpus_for "$p")" ref="$BST_DATA/libosip2-ref-$p.txt" if [ ! -x "$LOT/$p" ]; then bst_notok "libosip2/upstream-$p" "not installed" bst_notok "libosip2/xarm-$p" "not installed" continue fi "$LOT/$p" "$c" > "$W/$p.raw" 2>&1 && rc=0 || rc=$? if bst_execfail "$(cat "$W/$p.raw")"; then bst_unrun "libosip2/upstream-$p" "$(cat "$W/$p.raw")" bst_unrun "libosip2/xarm-$p" "$(cat "$W/$p.raw")" continue fi check_eq "libosip2/upstream-$p" "$(nat "LOS-RC-$p")" "$rc" if [ ! -r "$ref" ]; then bst_notok "libosip2/xarm-$p" "missing control $ref" continue fi norm < "$W/$p.raw" > "$W/$p.norm" if cmp -s "$W/$p.norm" "$ref"; then bst_ok "libosip2/xarm-$p" else bst_notok "libosip2/xarm-$p" "$(_bst_1line "$(firstdiff "$ref" "$W/$p.norm")")" fi done # --------------------------------------------------------------------------- # 3. torture_test over upstream's 121 messages, ONE CASE EACH. # # Each res/sipN and res/sip-malformedN begins with its own `expected_error:` line, # so the program's verdict is upstream's own expectation for that message: a # malformed message CORRECTLY rejected is a pass, and one this arm happens to # accept is a fail. That is why the malformed half is worth as much as the # well-formed half. # # The expected verdicts are loaded into shell variables in one awk pass -- the # obvious `sed -n` per message would triple this section's process count. Names # are mapped sip-malformedN -> sipmN so they are valid shell identifiers, with no # spawn (the substitution is a `case` and a `${}`). eval "$(awk '/ rc=/{n=$1; sub(/^sip-malformed/,"sipm",n); printf "__v_%s=\"%s %s\"\n", n, $2, $3}' "$REF_TORT")" : > "$W/torture.v" : > "$W/torture.raw" # LC_ALL=C matches the generator's sort exactly. The diagnostics comparison below # is line for line, so the ORDER is part of the control: a UTF-8 locale ignores # the '-' at the first collation level and puts sip-malformed0 after sip9, while C # puts it before sip0. The target's busybox sort is C-only, so the mismatch would # have been a build-host artifact showing up as a target red. for f in $(ls "$RES" 2>/dev/null | grep -E '^sip[0-9]+$|^sip-malformed[0-9]+$' | LC_ALL=C sort); do out=$("$LOT/torture_test" "$RES/$f" -c 2>&1) && rc=0 || rc=$? case "$f" in sip-malformed*) k="sipm${f#sip-malformed}" ;; *) k=$f ;; esac eval "want=\$__v_$k" if bst_execfail "$out"; then bst_unrun "libosip2/torture-$f" "$out" continue fi case "$out" in *error_code=*) code=${out##*error_code=}; code=${code%%)*} ;; *) code=none ;; esac printf '%s rc=%s code=%s\n' "$f" "$rc" "$code" >> "$W/torture.v" printf '=== %s\n%s\n' "$f" "$out" >> "$W/torture.raw" check_eq "libosip2/torture-$f" "${want:-MISSING-FROM-CONTROL}" "rc=$rc code=$code" done # THE DIAGNOSTICS THEMSELVES, not just the verdicts. A parser can reach the right # rc down the wrong path -- and on this arm that is not hypothetical: section 6 is # a case where exactly that happens. This compares every trace line the 121 runs # produced (level, source location, message text, and the byte counts embedded in # them) against the control, so a changed error path surfaces here even when every # verdict still matches. norm < "$W/torture.raw" | awk '/^=== /{f=$2;next} /^test /{next} {print f" "$0}' > "$W/torture.diag" if cmp -s "$W/torture.diag" "$REF_DIAG"; then bst_ok libosip2/torture-diagnostics else bst_notok libosip2/torture-diagnostics "$(_bst_1line "$(firstdiff "$REF_DIAG" "$W/torture.diag")")" fi # --------------------------------------------------------------------------- # 4. THE READ PATH ACROSS TWO FILESYSTEMS. # # The corpus lives on the virtiofs root; a tmpfs copy is the control. psmisc found # the virtiofs root refusing an operation tmpfs allowed and libmaxminddb set the # precedent of checking both, so the class is real here rather than hypothetical. # turl is the probe because urls.txt is the largest header corpus and its whole # output is already pinned above -- a short read shows up as a text diff rather # than as an error. cp "$RES/urls.txt" "$W/urls.txt" 2>/dev/null if [ -r "$W/urls.txt" ] && [ -r "$W/turl.norm" ]; then "$LOT/turl" "$W/urls.txt" 2>&1 | norm > "$W/turl-tmpfs.norm" if cmp -s "$W/turl-tmpfs.norm" "$W/turl.norm"; then bst_ok libosip2/read-virtiofs-vs-tmpfs else bst_notok libosip2/read-virtiofs-vs-tmpfs \ "turl output differs between the virtiofs root and a tmpfs copy of the same corpus" fi check_eq libosip2/read-tmpfs-lines "$(nat LOS-LINES-turl)" \ "$(wc -l < "$W/turl-tmpfs.norm" | tr -d ' ')" else bst_notok libosip2/read-virtiofs-vs-tmpfs "could not stage a tmpfs copy of urls.txt in $W" fi # --------------------------------------------------------------------------- # 5. CONTENT-LENGTH: THE ONE PLACE THE ARMS MUST NOT AGREE. # # osip_message_parse() decides WHETHER a body exists with libc atoi() # (osip_message_parse.c:909) and then how LONG it is with osip_atoi() (line 719). # osip_atoi is not atoi: it runs strtol and returns OSIP_UNDEFINED_ERROR (-1) when # the result lands on LONG_MIN or LONG_MAX. So the two disagree over a range whose # lower bound is LONG_MAX -- a function of sizeof(long) and nothing else: # # x86_64 (LP64) Content-Length >= 2^31 truncates to <= 0 inside atoi(), the # parser concludes there is NO BODY, and returns OSIP_SUCCESS. # The header is silently ignored. # wasm32 (ILP32) strtol saturates at LONG_MAX = 2^31-1, so atoi returns a # POSITIVE number and a body IS expected; osip_atoi then reports # LONG_MAX as an error, the (size_t) cast makes the expected # length enormous, and the "message was not received entirely" # check rejects with OSIP_SYNTAXERROR. # # Below 2^31 the two agree and the suite asserts EQUALITY against the control. At # and above it they cannot, and the suite asserts the DIVERGENCE explicitly -- # pinning what THIS arm does rather than exempting the range. This is the only # assertion here whose expected value is written into this file instead of read # from the control, because the control is what the other arm did. # # Note which side is safer: wasm32 REJECTS a message the control accepts. This is # an upstream portability defect, recorded in the design doc and deliberately NOT # patched here -- "fixing" it would change what the reference SIP parser accepts, # which is not something a package port should smuggle in. cl_msg() { printf 'MESSAGE sip:a@b SIP/2.0\r\nVia: SIP/2.0/UDP h;branch=z9hG4bK1\r\nFrom: ;tag=1\r\nTo: \r\nCall-ID: 1@h\r\nCSeq: 1 MESSAGE\r\nContent-Type: text/plain\r\nContent-Length: %s\r\n\r\nhi\r\n' "$1" } for v in 2 100 2147483646 2147483647 2147483648 4294967295 9223372036854775807; do cl_msg "$v" > "$W/cl.sip" out=$("$LOT/torture_test" "$W/cl.sip" 2>&1) && rc=0 || rc=$? if bst_execfail "$out"; then bst_unrun "libosip2/contentlength-$v" "$out"; continue; fi case "$out" in *error_code=*) code=${out##*error_code=}; code=${code%%)*} ;; *) code=none ;; esac got="rc=$rc code=$code" want=$(nat "LOS-CL-$v") case "$v" in 2|100|2147483646|2147483647) # Below LONG_MAX on BOTH arms -- must agree. The two small values are also # what catches a drift between this cl_msg() and the generator's: if the # templates ever diverge these go red on an assertion that is supposed to be # arm-independent, which is where the mistake would actually be. check_eq "libosip2/contentlength-$v" "$want" "$got" ;; *) if [ "$got" = "rc=1 code=-5" ]; then bst_ok "libosip2/contentlength-$v" else bst_notok "libosip2/contentlength-$v" \ "ILP32 must reject Content-Length $v with rc=1 code=-5 (x86_64 control: $want); got $got" fi ;; esac done # --------------------------------------------------------------------------- # 6. Packaging: what a consumer of this port actually links against, which # nothing above exercises. # # THIS ARM IS STATIC ONLY -- the distro builds no shared libraries for the guest. # Asserting a .so would be asserting a distro property that is false; asserting # the .a files is asserting the deliverable. Both are checked because the split is # the package's whole architecture: libosipparser2 is the parser (everything # above), libosip2 is the transaction layer on top -- untested here by design, but # it must still ship. for a in libosipparser2 libosip2; do if [ -f "/usr/lib/$a.a" ]; then bst_ok "libosip2/staticlib-$a" else bst_notok "libosip2/staticlib-$a" "no /usr/lib/$a.a" fi done # Pinned deliberately rather than left unstated: if this distro ever gains shared # libraries this turns red and someone re-reads the port, instead of discovering # the change through a link failure downstream. if ls /usr/lib/libosip*.so* >/dev/null 2>&1; then bst_notok libosip2/no-shared-lib "a libosip*.so appeared -- this arm was static-only when the port landed" else bst_ok libosip2/no-shared-lib fi check libosip2/pkgconfig-installed test -f /usr/lib/pkgconfig/libosip2.pc check libosip2/header-parser test -f /usr/include/osipparser2/osip_parser.h check libosip2/header-osip2 test -f /usr/include/osip2/osip.h check libosip2/header-sdp test -f /usr/include/osipparser2/sdp_message.h