# BST-TIMEOUT: 600 # ~70 cases; three clock-bounded smsd runs, 32s in total . "$BST_LIB" # smstools3 3.1.21 (meta-oe recipes-connectivity/smstools3) -- the SMS Server # Tools gateway. smsd watches a spool directory and moves messages through a GSM # modem on a serial port. # # THERE IS NO GSM MODEM ON THIS IMAGE, AND MOST OF smsd IS NOT MODEM CODE. It is # a spool-directory state machine wrapped around a serial dialogue, and the state # machine runs with nothing on the other end of the port -- so what is asserted # below is the real thing, not a stand-in. Three whole classes need no device at # all: # # 1. THE CLI AND THE STARTUP VALIDATOR. -h/-V and the START/STOP refusal are # handled inside parsearguments() and exit before the config file is even # opened. The validator runs one step later: readcfg() collects every # complaint into startup_err_str, check_directory() probes the spool dirs, # and main() prints the lot and exit(1)s BEFORE forking anything. # # 2. THE GSM 7-BIT PACKED CODEC. -E and -D reach encode_7bit_packed() / # decode_7bit_packed() in charset.c and exit(0) -- pure text transforms # with no device and no fork. Note they run AFTER readcfg(), so they double # as the assertion that /etc/smsd.conf itself parses. # # 3. THE SHELL TOOLS. sendsms only writes a spool file; sms2unicode, # unicode2sms and sms2html are text filters. # # ONE CASE CLASS DOES NEED THE DAEMON, and it is the one the package exists for: # the outgoing -> checked transition. It is section 8, and it is LAST ON PURPOSE. # smsd forks an outgoing-file checker and a modem handler, and a fork whose child # is woken while the binary's first exec-load is still in flight trips # hwjs-cc#69's set-over-set park and wedges the guest before any case is emitted # (measured on 88-p910nd, whose section 1 carries the same note). Every case in # sections 1-7 either avoids the fork outright or forks a small script, so by the # time section 8 runs, smsd has been exec'd ~40 times without forking. # # `[ -x ]' IS NOT AN ORACLE ON THE VIRTIOFS ROOT -- the rootfs files are # owned 1000:1001 while the guest runs as root, so busybox test falls through to # the supplementary-group check and getgroups(2) answers ENOSYS. Held by # brintos/linux#19. The mode cases below read `ls -l' instead. require smsd smstools3/present W=${TMPDIR:-/tmp}/bst-smstools3.$$ rm -rf "$W"; mkdir -p "$W" || { bst_notok smstools3/stage "cannot create $W"; return 2>/dev/null || exit 0; } # check_fail -- the startup validator signals # through the exit status as much as the diagnostic, and check_out cannot # express it (it requires rc 0). check_fail() { _id=$1; _want=$2; _needle=$3; shift 3 _out=$("$@" 2>&1); _rc=$? case "$_out" in *"$_needle"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_rc" -eq "$_want" ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$_out"; then bst_unrun "$_id" "$_out" else bst_notok "$_id" "rc=$_rc want=$_want needle:'$_needle' got:'$(_bst_1line "$_out")'"; fi } # has -- substring assertion over a value already read. # The execfail arm matters here: this suite exec's a 7.9 MB smsd ~40 times and # reads several of its outputs into a variable first, so a run the kernel could # not stage would otherwise land as an ordinary red naming the shell's own # diagnostic. check_eq takes the same arm for the same reason. has() { case "$3" in *"$2"*) bst_ok "$1"; return ;; esac if bst_execfail "$3"; then bst_unrun "$1" "$3" else bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'"; fi } # BOUNDING AN smsd RUN THAT NEVER RETURNS. Every smsd invocation that gets PAST # readcfg() reaches main()'s daemon path and stays in the foreground under -t, # and it CANNOT be bounded with timeout(1): relative timers do not fire on this # runtime, so timeout blocks forever and sleep is a no-op # (brintos/linux-in-the-browser#120) -- brintos-selftest says exactly this and # bounds every suite by polling /proc/uptime instead. # `timeout 20 smsd ...' is therefore a second hang, not a bound. So: run it in # the background with its output on a FILE -- never a command substitution, # which waits on every inheritor of the pipe and blocks even after smsd itself # exits -- poll the wall clock with builtins, and kill -9 by name at the end. bg_bounded() { # $1 = outfile, $2 = seconds; rest = cmd _bo=$1; _bmax=$2; shift 2 : > "$_bo"; rm -f "$_bo.done" { "$@" > "$_bo" 2>&1; : > "$_bo.done"; } & read -r _bt0 _bt1 < /proc/uptime; _bt0=${_bt0%%.*} while [ ! -f "$_bo.done" ]; do read -r _bn _bt1 < /proc/uptime; _bn=${_bn%%.*} [ $(( _bn - _bt0 )) -ge "$_bmax" ] && break done for _q in $(ps 2>/dev/null | grep '[s]msd' | awk '{print $1}'); do kill -9 "$_q" 2>/dev/null; done rm -f "$_bo.done" } # isfile / isdir -- presence without [ -x ]. isfile() { if [ -f "$2" ]; then bst_ok "$1"; else bst_notok "$1" "$2 absent"; fi; } isdir() { if [ -d "$2" ]; then bst_ok "$1"; else bst_notok "$1" "$2 absent"; fi; } # conf -- write a config whose spool, log, # info and pid paths all point INSIDE $W, so nothing here touches /var. conf() { _c=$1; _g=$2; _m=$3 mkdir -p "$W/spool/incoming" "$W/spool/outgoing" "$W/spool/checked" { printf 'devices = GSM1\n' printf 'logfile = %s/smsd.log\n' "$W" printf 'loglevel = 7\n' printf 'outgoing = %s/spool/outgoing\n' "$W" printf 'incoming = %s/spool/incoming\n' "$W" printf 'checked = %s/spool/checked\n' "$W" printf 'infofile = %s/smsd.working\n' "$W" printf 'pidfile = %s/smsd.pid\n' "$W" [ -n "$_g" ] && printf '%s\n' "$_g" printf '\n[GSM1]\n' printf 'device = /dev/ttyS0\n' printf 'incoming = yes\n' [ -n "$_m" ] && printf '%s\n' "$_m" } > "$_c" } # --- 1. identity and the CLI contract ---------------------------------------- # NOTHING IN THIS SECTION READS A CONFIG FILE. -V and help() both exit(0) from # inside parsearguments(), which runs before initcfg()/readcfg() -- so these hold # even if /etc/smsd.conf were missing, and they are the first exec of the binary. _v=$(smsd -V 2>&1) has smstools3/version "Version 3.1.21, Copyright (c) Keijo Kasvi" "$_v" has smstools3/version-contact "smstools3@kekekasvi.com" "$_v" has smstools3/version-support "Support: http://smstools3.kekekasvi.com" "$_v" has smstools3/version-sms2 "Based on SMS Server Tools 2" "$_v" check_rc smstools3/version-rc 0 smsd -V _h=$(smsd -h 2>&1) has smstools3/help-banner "smsd spools incoming and outgoing sms." "$_h" has smstools3/help-usage "smsd [options]" "$_h" has smstools3/help-opt-c "-cx set config file to x" "$_h" has smstools3/help-opt-D "-Dx decode GSM 7bit Packed string x" "$_h" has smstools3/help-opt-E "-Ex encode string x to GSM 7bit Packed format" "$_h" has smstools3/help-opt-t "-t run smsd in terminal" "$_h" has smstools3/help-opt-V "-V print copyright and version" "$_h" # help() names the compiled-in DEFAULT_CONFIGFILE, which is the path the recipe # installs -- so this case pins the two to each other. has smstools3/help-default-conf "All other options are set by the file /etc/smsd.conf." "$_h" check_rc smstools3/help-rc 0 smsd -h check_out smstools3/help-question "smsd spools incoming and outgoing sms." smsd -? # getopt's ':' and '?' both fall through to help() and exit(0), so an unknown # option and a missing option-argument are NOT errors here. Asserted as found. check_out smstools3/unknown-opt-shows-help "smsd spools incoming and outgoing sms." smsd -Z check_out smstools3/missing-optarg-shows-help "smsd spools incoming and outgoing sms." smsd -c # START/STOP are refused by name, before getopt, with exit 0 -- the daemon # deliberately leaves those to the init script. strcasecmp, so case is ignored. _s="Use the sms3 script (or equivalent) for start and stop operations." check_out smstools3/refuses-START "Invalid option for smsd: START. $_s" smsd START check_out smstools3/refuses-STOP "Invalid option for smsd: STOP. $_s" smsd STOP check_out smstools3/refuses-start-lower "Invalid option for smsd: start. $_s" smsd start # --- 2. the shipped file inventory ------------------------------------------- # Measured from the built package: 7 files and 3 empty spool directories, and # nothing else. -dev and -dbg are packaged EMPTY; -doc, -locale, -staticdev and # -src are not produced at all. isfile smstools3/bin-smsd /usr/bin/smsd isfile smstools3/bin-sendsms /usr/bin/sendsms isfile smstools3/bin-sms2html /usr/bin/sms2html isfile smstools3/bin-sms2unicode /usr/bin/sms2unicode isfile smstools3/bin-unicode2sms /usr/bin/unicode2sms isfile smstools3/etc-smsd-conf /etc/smsd.conf isfile smstools3/etc-init-sms3 /etc/init.d/sms3 isdir smstools3/spool-incoming /var/spool/sms/incoming isdir smstools3/spool-outgoing /var/spool/sms/outgoing isdir smstools3/spool-checked /var/spool/sms/checked # The recipe ships NO failed/ directory: `failed' is an optional config key and # smsd only moves a message there when a send actually fails at the modem. With # no modem the message stops at checked/, which is what section 8 asserts. if [ -d /var/spool/sms/failed ]; then bst_notok smstools3/spool-no-failed "/var/spool/sms/failed exists; the recipe does not install it" else bst_ok smstools3/spool-no-failed; fi has smstools3/init-mode-0755 "-rwxr-xr-x" "$(ls -l /etc/init.d/sms3 2>&1)" # --- 3. the startup validator ------------------------------------------------ # EVERY CASE HERE EXITS BEFORE ANY fork(). readcfg() accumulates its complaints # and main() prints them with the check_directory() results, then exit(1)s. conf "$W/good.conf" "" "" check_fail smstools3/conf-missing 1 \ "Cannot open config file $W/absent.conf for read: No such file or directory" \ smsd -c "$W/absent.conf" -E hi conf "$W/badglobal.conf" "nosuchkey = 1" "" _bg=$(smsd -c "$W/badglobal.conf" -t 2>&1); _bgrc=$? has smstools3/conf-error-header "There was 1 error while reading the config file:" "$_bg" has smstools3/conf-unknown-global "- Unknown global setting: nosuchkey" "$_bg" has smstools3/conf-major-singular "There was 1 major problem found." "$_bg" has smstools3/conf-cannot-start "Cannot start. See the log file for details." "$_bg" check_eq smstools3/conf-bad-rc 1 "$_bgrc" conf "$W/badmodem.conf" "" "nosuchmodemkey = 1" check_fail smstools3/conf-unknown-modem 1 \ "- Unknown setting for modem GSM1: nosuchmodemkey" smsd -c "$W/badmodem.conf" -t # A config with no devices key at all: the per-device section is what `devices' # selects, so dropping it is a config error rather than a quiet no-op. # NOT check_fail: "no devices" is a startuperror(), which readcfg() only # ACCUMULATES -- so this is the first invocation in the suite that survives # readcfg() and enters main()'s daemon path, where it never returns here. It is # read off a file under bg_bounded for that reason; the exit status is not # available and is not asserted. printf 'logfile = %s/smsd.log\n' "$W" > "$W/nodev.conf" bg_bounded "$W/nodev.out" 6 smsd -c "$W/nodev.conf" -t has smstools3/conf-no-devices "- There are no devices specified." "$(cat "$W/nodev.out" 2>&1)" # check_directory() is a separate pass from readcfg(): a spool path that parses # fine but cannot be opened is a MAJOR PROBLEM, reported by name. conf "$W/missdir.conf" "" "" sed "s|^outgoing = .*|outgoing = $W/absent-spool|" "$W/good.conf" > "$W/missdir.conf" bg_bounded "$W/missdir.out" 6 smsd -c "$W/missdir.conf" -t has smstools3/conf-spool-unopenable \ "Spool directory $W/absent-spool cannot be opened." "$(cat "$W/missdir.out" 2>&1)" # --- 4. the GSM 7-bit packed codec ------------------------------------------- # -E/-D run after readcfg() and exit(0) without forking. Because they need the # config to parse first, the DEFAULT-CONFIG case below is also the assertion # that the /etc/smsd.conf the recipe ships is valid as installed. check_out smstools3/default-conf-parses "E8329BFD06" smsd -E hello # Vectors measured against the 3.1.21 codec; each is a true round trip. check_out smstools3/encode-hello "E8329BFD06" smsd -c "$W/good.conf" -E "hello" check_out smstools3/encode-hellohello "E8329BFD4697D9EC37" smsd -c "$W/good.conf" -E "hellohello" check_out smstools3/encode-A "41" smsd -c "$W/good.conf" -E "A" check_out smstools3/encode-msg "F4F29C0E6A97E7F3F0B90C" smsd -c "$W/good.conf" -E "test message" check_out smstools3/encode-digits "B0986C46ABD96EB81C" smsd -c "$W/good.conf" -E "0123456789" check_out smstools3/decode-hello "hello" smsd -c "$W/good.conf" -D "E8329BFD06" check_out smstools3/decode-hellohello "hellohello" smsd -c "$W/good.conf" -D "E8329BFD4697D9EC37" check_out smstools3/decode-msg "test message" smsd -c "$W/good.conf" -D "F4F29C0E6A97E7F3F0B90C" check_out smstools3/decode-digits "0123456789" smsd -c "$W/good.conf" -D "B0986C46ABD96EB81C" # Round trip through both directions in one line, so a codec that is # self-consistently wrong is still caught by the vectors above. check_eq smstools3/codec-roundtrip "brintOS" "$(smsd -c "$W/good.conf" -D "$(smsd -c "$W/good.conf" -E 'brintOS')" 2>&1)" # --- 5. sendsms writes a spool file ------------------------------------------ # sendsms is a /bin/sh script (meta-oe's scripts_no_bash.patch) that only creates # a file in the outgoing queue -- no daemon, no device. It writes to the # HARDCODED /var/spool/sms/outgoing, so this section needs that directory # writable and says so rather than guessing. if ( : > /var/spool/sms/outgoing/.bst-probe ) 2>/dev/null; then rm -f /var/spool/sms/outgoing/.bst-probe bst_ok smstools3/outgoing-writable _before=$(ls /var/spool/sms/outgoing 2>/dev/null | wc -l) _so=$(sendsms 358401234567 "brintOS selftest" 2>&1); _sorc=$? check_eq smstools3/sendsms-rc 0 "$_sorc" has smstools3/sendsms-echoes-text "Text: brintOS selftest" "$_so" has smstools3/sendsms-echoes-dest "To: 358401234567" "$_so" _after=$(ls /var/spool/sms/outgoing 2>/dev/null | wc -l) check_eq smstools3/sendsms-queued-one "$(( _before + 1 ))" "$_after" _sf=$(ls -t /var/spool/sms/outgoing 2>/dev/null | head -1) case "$_sf" in send_*) bst_ok smstools3/sendsms-name-prefix ;; *) bst_notok smstools3/sendsms-name-prefix "want send_*, got '$_sf'" ;; esac # Exact bytes: `To: ', a blank line, then the text with NO trailing # newline -- the format smsd's outgoing checker parses. if [ -n "$_sf" ]; then cp "/var/spool/sms/outgoing/$_sf" "$W/queued" 2>/dev/null printf 'To: 358401234567\n\nbrintOS selftest' > "$W/queued.want" if cmp -s "$W/queued.want" "$W/queued"; then bst_ok smstools3/sendsms-file-bytes else bst_notok smstools3/sendsms-file-bytes "got:'$(_bst_1line "$(cat "$W/queued" 2>&1)")'"; fi rm -f "/var/spool/sms/outgoing/$_sf" fi else bst_skip smstools3/outgoing-writable "/var/spool/sms/outgoing not writable on this root" fi # --- 6. the text transforms -------------------------------------------------- # Pure filters. The fixture is GENERATED under $TMPDIR rather than shipped, so # nothing here redirects stdin from the virtiofs root (brintos/linux#73). F=$W/fixture.sms printf 'From: 358401234567\nSent: 26-08-27 09:00:00\nSubject: GSM1\nAlphabet: ISO8859-15\n\nbrintOS smstools3 selftest\n' > "$F" check_fail smstools3/sms2unicode-usage 1 "Usage: sms2unicode filename" sms2unicode check_fail smstools3/unicode2sms-usage 1 "Usage: unicode2sms filename" unicode2sms check_fail smstools3/sms2html-usage 1 "Usage: sms2html filename" sms2html sms2unicode "$F" > "$W/fix.u" 2>"$W/fix.u.err"; _u2rc=$? check_eq smstools3/sms2unicode-rc 0 "$_u2rc" # UTF-16BE with a big-endian BOM: the script's first act is `echo -en "\xFE\xFF"'. check_eq smstools3/sms2unicode-bom "fe ff" "$(od -An -tx1 -N2 "$W/fix.u" 2>&1 | tr -s ' ' | sed 's/^ //;s/ $//')" unicode2sms "$W/fix.u" > "$W/fix.back" 2>/dev/null; _b2rc=$? check_eq smstools3/unicode2sms-rc 0 "$_b2rc" # THE ROUND TRIP IS EXACT MODULO CRLF, AND THAT IS A PROPERTY OF THE PAIR, not a # defect: sms2unicode emits \x00\x0d before every \x00\x0a, and unicode2sms only # strips the high byte -- so the CR it introduced comes back. Asserting the raw # bytes would pin a round trip that never held; asserting it after `tr -d "\r"' # pins the one that does. tr -d '\r' < "$W/fix.back" > "$W/fix.back.nocr" 2>/dev/null if cmp -s "$F" "$W/fix.back.nocr"; then bst_ok smstools3/roundtrip-after-cr-strip else bst_notok smstools3/roundtrip-after-cr-strip "differs from the original"; fi # Two bytes per character plus the two-byte BOM -- so the UCS form's size is # fixed by the CR-expanded text's, which is the arithmetic the pair implies. check_eq smstools3/roundtrip-sizes \ "$(( 2 + 2 * $(wc -c < "$W/fix.back") ))" "$(wc -c < "$W/fix.u")" # The CR expansion itself, stated as a number: one per line of the original. check_eq smstools3/roundtrip-cr-count \ "$(( $(wc -c < "$F") + $(wc -l < "$F") ))" "$(wc -c < "$W/fix.back")" _html=$(sms2html "$F" 2>&1) has smstools3/sms2html-open "" "$_html" has smstools3/sms2html-close "" "$_html" has smstools3/sms2html-hdr-from "From: 358401234567
" "$_html" has smstools3/sms2html-hdr-subj "Subject: GSM1
" "$_html" has smstools3/sms2html-hdr-alpha "Alphabet: ISO8859-15
" "$_html" # THE BODY IS formail's, NOT THE SCRIPT'S. sms2html's non-UCS path is # `text=`formail -I "" < $1`', and formail comes from procmail -- an UNDECLARED # runtime dependency: the package carries no Depends field at all. procmail is # on this image, so the body is asserted; the probe is what tells a body that is # missing because formail is gone from a future image apart from one the script # got wrong. if command -v formail >/dev/null 2>&1; then bst_ok smstools3/sms2html-formail-present has smstools3/sms2html-body "brintOS smstools3 selftest" "$_html" else bst_skip smstools3/sms2html-formail-present "formail (procmail) not installed" bst_skip smstools3/sms2html-body "sms2html's non-UCS body path needs formail" fi # --- 7. the shipped init script ---------------------------------------------- # sms3 is `#! /bin/sh' with no bashisms, and meta-oe's sms_binpath.patch already # points DAEMON at /usr/bin/smsd -- so unlike several other ported init scripts # this one is runnable here. `status' is not one of its verbs, which makes the # usage branch a fork-free oracle for the whole case statement. check_fail smstools3/sms3-usage 1 "{start|stop|restart|force-stop|reset}" /etc/init.d/sms3 status has smstools3/sms3-daemon-path "DAEMON=/usr/bin/smsd" "$(grep '^DAEMON=' /etc/init.d/sms3 2>&1)" # --- 8. the spool state machine ---------------------------------------------- # THE ONLY SECTION THAT FORKS, AND IT IS LAST FOR THAT REASON (see the header). # smsd -t stays in the foreground and forks an outgoing-file checker plus one # modem handler. With no modem the handler's AT handshake times out and the # handler dies -- but the CHECKER runs first and independently, and moving a # queued message from outgoing/ to checked/ is its whole job. That move is the # package's core transition and it needs no device. # # The message stops at checked/ and goes no further: `failed' is where a message # lands when a send FAILS at the modem, and with no modem no send is ever # attempted. conf "$W/run.conf" "" "" printf 'To: 358401234567\n\nbrintOS spool selftest' > "$W/spool/outgoing/send_bst1" # The run is bounded by bg_bounded, not timeout(1) -- see the note beside it. # How the run ended is not asserted and is not knowable here; the MOVE is. bg_bounded "$W/run.out" 20 smsd -c "$W/run.conf" -t if [ -f "$W/spool/checked/send_bst1" ]; then bst_ok smstools3/spool-moved-to-checked else bst_notok smstools3/spool-moved-to-checked \ "still in outgoing:$(ls "$W/spool/outgoing" 2>&1 | tr '\n' ' ') checked:$(ls "$W/spool/checked" 2>&1 | tr '\n' ' ')"; fi if [ -f "$W/spool/outgoing/send_bst1" ]; then bst_notok smstools3/spool-left-outgoing "the message is still queued in outgoing/" else bst_ok smstools3/spool-left-outgoing; fi # The moved file is the queued file, byte for byte -- the checker relays it, it # does not rewrite it. printf 'To: 358401234567\n\nbrintOS spool selftest' > "$W/spool.want" if cmp -s "$W/spool.want" "$W/spool/checked/send_bst1" 2>/dev/null; then bst_ok smstools3/spool-content-preserved else bst_notok smstools3/spool-content-preserved "checked/ copy differs from the queued file"; fi _log=$(cat "$W/smsd.log" 2>&1) has smstools3/log-started "Smsd v3.1.21 started." "$_log" has smstools3/log-terminal "Running in terminal mode." "$_log" has smstools3/log-checker "Outgoing file checker has started." "$_log" has smstools3/log-move "SMS To: 358401234567. Moved file" "$_log" has smstools3/log-modem "Modem handler 0 has started." "$_log" # THE MODEM DIALOGUE IS PROBED, NOT ASSUMED. /dev/ttyS0 is what the config # names; if a serial device ever appears the skips below go stale and the live # arm needs writing. if [ -c /dev/ttyS0 ]; then bst_notok smstools3/probe-no-modem-device \ "/dev/ttyS0 exists -- the modem dialogue is now reachable and needs a live arm" else bst_ok smstools3/probe-no-modem-device bst_skip smstools3/modem-at-handshake "no serial device on this image" bst_skip smstools3/modem-pdu-submit "no serial device on this image" fi # THE HANDLER'S AT TIMEOUT NEVER ELAPSES HERE. put_command() waits for the # modem's answer on the relative-timer path, and relative timers do not fire on # this runtime (brintos/linux-in-the-browser#120) -- so the handler starts, which # is asserted above, and then stays in that wait instead of reporting "No answer". # Kept as a probe rather than dropped: the day #120 lands, the line appears and # this goes green by itself. case "$_log" in *"No answer, put_command expected"*) bst_ok smstools3/modem-no-answer ;; *) bst_skip smstools3/modem-no-answer "the handler's AT timeout is a relative timer and never elapses here (brintos/linux-in-the-browser#120); it started but never reported" ;; esac for _q in $(ps 2>/dev/null | grep '[s]msd' | awk '{print $1}'); do kill -9 "$_q" 2>/dev/null; done rm -rf "$W"