# BST-TIMEOUT: 600 # ~55 cases, two loopback jobs and three bounded settles . "$BST_LIB" # p910nd 0.97 (meta-oe recipes-support/p910nd, kenyapcomau git 57ebc07a) -- the # port 9100+n printer daemon. It listens on TCP 9100+n and copies the job # straight to /dev/lp, or to whatever -f names, so with -f pointing at a # regular file BOTH ENDS ARE REACHABLE ON THIS IMAGE and the transfer below is a # real one, not a stand-in. # # TWO THINGS ABOUT THE SHIPPED PACKAGE DECIDE THE SHAPE OF THIS FILE. # # 1. p910nd.8 DOCUMENTS NO EXIT CODES -- there is no EXIT STATUS section and no # value is named anywhere in it. What is asserted below is therefore the codes # the SOURCE defines: usage(), a failed get_lock(), a failed getaddrinfo() and # a failed accept() are each exit(1), and the daemonising parent is exit(0), # so 1 is the only non-zero status p910nd can produce. An UNOPENABLE DEVICE is # not among them at all -- open_printer() is retried forever # (`while ((lp = open_printer(n)) == -1) sleep(10);`), so there is no code to # assert and section 6 asserts the retry instead. # # 2. THE IMAGE PROVISIONS NO /var/lock, and get_lock() runs before the daemon # binds anything. /var/lock is a symlink to ../run/lock and nothing creates # the target, so every p910nd run refuses at once with # `/var/lock/p910d: No such file or directory` and exit 1. That is asserted # in its own right in section 2 and then PROVISIONED in section 3, the same # treatment 99-lockdev.test gives the same gap -- so what the rest measures is # p910nd rather than the image. Sections 1 and 2 need no provisioning and hold # on a read-only root; sections 4-6 are skipped when the mkdir is refused. # # THE REFUSAL DOUBLES AS THE ARGUMENT-PARSING ORACLE. The lock name carries the # resolved printer number, so `p910nd -d ` reports which number getopt and # the positional handling settled on -- without opening a socket, on any root. require p910nd p910nd/present W=${TMPDIR:-/tmp}/bst-p910nd.$$ rm -rf "$W"; mkdir -p "$W" || { bst_notok p910nd/stage "cannot create $W"; return 2>/dev/null || exit 0; } # check_fail -- p910nd 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" else bst_notok "$_id" "rc=$_rc want=$_want needle:'$_needle' got:'$(_bst_1line "$_out")'"; fi } # has -- substring assertion over a value already read. has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'" ;; esac } # lock -- run p910nd with /var/lock absent and # assert it refuses naming the lock of the printer number it parsed. lock() { _id=$1; _n=$2; shift 2 check_fail "$_id" 1 "/var/lock/p910${_n}d: No such file or directory" p910nd -d "$@" } reap() { for _q in $(ps 2>/dev/null | grep '[p]910nd' | awk '{print $1}'); do kill -9 "$_q" 2>/dev/null; done; } # --- 1. identity and the CLI contract ---------------------------------------- # NOTHING IN THIS SECTION OR THE NEXT MAY FORK, AND THAT IS DELIBERATE. `p910nd` # without -d reaches server(), which forks; 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 whole guest before any case is emitted. Measured on this # image: with the bare `-v` first, 4 of 8 landing boots wedged at 0 cases, while # 88-vblade -- same shape, same tree, back to back -- went 3 for 3 clean. With # the forking case moved to the END of section 2, behind ~20 non-forking execs of # the same binary, it stops. Every run below therefore either passes -d (which # skips the fork block outright) or ends inside getopt at usage(). # # `-v -z` is how the version text is read without forking: getopt handles 'v' # (show_version writes to STDOUT) and then 'z' (usage writes to STDERR and # exits 1), so stdout alone carries exactly the version line -- which is a # stronger assertion than a substring, since usage() prints the same words with # the copyright appended. _vo=$(p910nd -v -z 2>/dev/null); _vrc=$? check_eq p910nd/version "p910nd Version 0.97" "$_vo" check_eq p910nd/version-then-usage-rc 1 "$_vrc" check_fail p910nd/usage-rc 1 "Usage: p910nd [-f device] [-i bindaddr] [-bvd] [0|1|2]" p910nd -z check_fail p910nd/usage-getopt 1 "invalid option -- 'z'" p910nd -z check_fail p910nd/usage-banner 1 "Copyright (c) 2008-2014 Ken Yap and others, GPLv2" p910nd -z check_fail p910nd/usage-missing-arg 1 "option requires an argument -- 'f'" p910nd -d -f check_fail p910nd/usage-missing-i 1 "option requires an argument -- 'i'" p910nd -d -i # --- 2. the lock wall, and the printer number it names ------------------------ rm -rf /run/lock 2>/dev/null if [ -e /run/lock ]; then bst_skip p910nd/no-lockdir "/run/lock exists and could not be removed" LOCKGATE=0 else bst_ok p910nd/no-lockdir LOCKGATE=1 fi if [ "$LOCKGATE" -eq 1 ]; then lock p910nd/lock-refuses 0 lock p910nd/arg-default 0 lock p910nd/arg-0 0 0 lock p910nd/arg-1 1 1 lock p910nd/arg-2 2 2 # isdigit(argv[0][0]) accepts every digit, not the 0|1|2 the man page names, # and reads only the FIRST character of the argument. lock p910nd/arg-digit-beyond-2 7 7 lock p910nd/arg-first-char-only 3 3x # a non-digit is not rejected, it is ignored -- lpnumber keeps its '0' default. lock p910nd/arg-nondigit-ignored 0 zzz # each option is carried past getopt into get_lock rather than into usage(). : > "$W/dev" lock p910nd/opt-f-accepted 1 -f "$W/dev" 1 lock p910nd/opt-b-accepted 1 -b 1 lock p910nd/opt-i-accepted 1 -i 127.0.0.1 1 lock p910nd/opt-v-accepted 1 -v 1 lock p910nd/opt-all-accepted 1 -b -v -f "$W/dev" -i 127.0.0.1 1 # -v DOES NOT EXIT: show_version() has no exit and getopt moves on, so a bare # `p910nd -v` falls through to server(), forks, and returns the DAEMONISING # PARENT's 0 -- not a version tool's. It is the only forking run in sections # 1-2 and it is last on purpose (see the note at the top of section 1). It sits # inside this branch because the lock directory is still absent here, so the # child it forks dies at once instead of being left holding port 9100. _v=$(p910nd -v 2>&1); _vrc=$? check_eq p910nd/version-does-not-exit 0 "$_vrc" has p910nd/version-daemonises "p910nd Version 0.97" "$_v" else for c in lock-refuses arg-default arg-0 arg-1 arg-2 arg-digit-beyond-2 \ arg-first-char-only arg-nondigit-ignored opt-f-accepted opt-b-accepted \ opt-i-accepted opt-v-accepted opt-all-accepted \ version-does-not-exit version-daemonises; do bst_skip "p910nd/$c" "the /var/lock refusal could not be posed" done fi # --- 3. provision the lock directory the image does not create ---------------- mkdir -p /run/lock 2>"$W/mkdir.err" if [ -d /run/lock ]; then bst_ok p910nd/provision-lockdir; NET=1 else bst_notok p910nd/provision-lockdir "$(_bst_1line "$(cat "$W/mkdir.err" 2>/dev/null)")"; NET=0; fi # --- 4. the loopback job, and everything else that needs a socket ------------- if [ "$NET" -eq 1 ]; then P910ND_W=$W; export P910ND_W tclsh "$BST_DATA/p910nd-loopback.tcl" > "$W/net.out" 2>&1 kv() { sed -n "s/^P-$1=//p" "$W/net.out" | head -1; } if [ "$(kv done)" != "1" ]; then bst_notok p910nd/net-payload "p910nd-loopback.tcl did not finish: $(_bst_1line "$(tail -3 "$W/net.out")")" else bst_ok p910nd/net-payload fi # the unidirectional job -- 1 MiB over TCP 9105 into the file -f names check_eq p910nd/job-size "$(kv uni-want)" "$(kv uni-size)" check_eq p910nd/job-identical 1 "$(kv uni-identical)" check p910nd/job-cmp cmp -s "$W/sent-uni.bin" "$W/out-uni.bin" check_eq p910nd/job-sha "$(sha256sum < "$W/sent-uni.bin" | cut -d' ' -f1)" \ "$(sha256sum < "$W/out-uni.bin" | cut -d' ' -f1)" # nothing comes back on a unidirectional job check_eq p910nd/job-no-return 0 "$(kv uni-back)" # -d's own log, which only becomes visible at this payload size: glibc # block-buffers vfprintf(stdout) and the daemon never exits to flush it. check_eq p910nd/job-log-connection 1 "$(kv uni-log-conn)" check_eq p910nd/job-log-read8192 1 "$(kv uni-log-read)" check_eq p910nd/job-log-wrote8192 1 "$(kv uni-log-wrote)" # -b: the printer end is opened O_RDWR|O_NONBLOCK and read back to the network. # A regular file answers every read with EOF, so the job still terminates and # the copy is still exact -- and nothing is returned. check_eq p910nd/bidir-size "$(kv bidir-want)" "$(kv bidir-size)" check_eq p910nd/bidir-identical 1 "$(kv bidir-identical)" check p910nd/bidir-cmp cmp -s "$W/sent-bidir.bin" "$W/out-bidir.bin" check_eq p910nd/bidir-no-return 0 "$(kv bidir-back)" # where each daemon binds check_eq p910nd/bind-default-any 1 "$(kv bind-default)" check_eq p910nd/bind-i-loopback 1 "$(kv bind-loopback)" # an occupied port: bind(2) fails on every addrinfo, the loop falls out with # netfd closed, and accept(2) on it is what actually ends the run. check_eq p910nd/occupied-rc 1 "$(kv occupied-rc)" check_eq p910nd/occupied-bind 1 "$(kv occupied-bind)" check_eq p910nd/occupied-accept 1 "$(kv occupied-accept)" # an unresolvable -i. getaddrinfo(3) does not set errno, so upstream's "%m" # prints whatever errno happened to hold -- the prefix is the assertable part. check_eq p910nd/badaddr-rc 1 "$(kv badaddr-rc)" check_eq p910nd/badaddr-msg 1 "$(kv badaddr-msg)" # one printer number, one daemon: the second blocks in fcntl(F_SETLKW) on the # lock file and never reaches bind, so there is exactly one listener. check_eq p910nd/single-listener 1 "$(kv second-listeners)" check_eq p910nd/single-first-alive 1 "$(kv second-a-alive)" check_eq p910nd/single-second-alive 1 "$(kv second-b-alive)" check_eq p910nd/single-second-silent 0 "$(kv second-b-logsize)" # an unopenable -f target: NOT an exit code. The daemon accepts, fails the # open, and retries it for as long as it is left running. check_eq p910nd/badev-no-exit 1 "$(kv badev-alive)" check_eq p910nd/badev-still-listening 1 "$(kv badev-listen)" check_eq p910nd/badev-nothing-created 0 "$(kv badev-target)" else for c in net-payload job-size job-identical job-cmp job-sha job-no-return \ job-log-connection job-log-read8192 job-log-wrote8192 \ bidir-size bidir-identical bidir-cmp bidir-no-return \ bind-default-any bind-i-loopback occupied-rc occupied-bind \ occupied-accept badaddr-rc badaddr-msg single-listener \ single-first-alive single-second-alive single-second-silent \ badev-no-exit badev-still-listening badev-nothing-created; do bst_skip "p910nd/$c" "/var/lock (-> /run/lock) could not be provisioned on a read-only root" done fi reap # --- 4b. daemon mode, which does not complete on this kernel ------------------ # Without -d, server() forks, the parent exit(0)s, and the child is supposed to # close every fd up to RLIMIT_NOFILE, setsid(), and write /var/run/p910d.pid. # getrlimit(2) is ENOSYS here (brintos/linux#20), and it is the FIRST thing the child does # and is checked -- `if (getrlimit(...) < 0) { dolog(...); exit(1); }` -- so the # child dies at once, with its diagnostic going to a syslog nothing on this image # reads. The evidence is that the forked child is a zombie whose session id never # changed, i.e. it never reached setsid(), and neither the pidfile nor the lock # nor a listener ever appears. -d skips the whole block, which is why every case # above uses it. # The probe is TWO INDEPENDENT ANSWERS THAT MUST AGREE, and neither costs an # exec: /proc//limits is the kernel's own text and reports real values, # while the shell's `ulimit` builtin goes through getrlimit(2) and reports 0 # because the call is unwired. When they disagree the syscall is not answering. # (An earlier version asked python3 for resource.getrlimit and read errno 38. # That is a better question and a worse instrument: by this point in the run the # guest cannot exec a 30 MB interpreter -- `python3: Cannot allocate memory`, # memory is not reclaimed on process exit, brintos/linux#25 -- so the probe # reported the platform as FIXED and reddened itself.) _lim=$(awk '/^Max open files/{print $4}' /proc/self/limits 2>/dev/null) _ul=$(ulimit -n 2>/dev/null) case "$_lim" in ''|*[!0-9]*) RLGATE=0; _why="/proc/self/limits gave no numeric soft NOFILE ('$_lim')" ;; *) if [ "$_ul" = "$_lim" ]; then RLGATE=0; _why="ulimit -n and /proc/self/limits agree at $_lim -- getrlimit(2) answers now" else RLGATE=1; fi ;; esac if [ "$RLGATE" -eq 1 ]; then bst_ok p910nd/probe-getrlimit-enosys else bst_notok p910nd/probe-getrlimit-enosys "$_why -- daemon mode may now work, write the live arm" fi : > "$W/dev9" _dout=$(p910nd -f "$W/dev9" 9 2>&1); _drc=$? check_eq p910nd/daemon-parent-exit0 0 "$_drc" check_eq p910nd/daemon-parent-silent "" "$_dout" # 300 polls of /proc/uptime, the only clock this target answers; the -d daemons # above were reachable within 18 connect attempts, so this is not a short wait. _i=0; while [ "$_i" -lt 300 ]; do _i=$((_i+1)); [ -e /var/run/p9109d.pid ] && break; cat /proc/uptime >/dev/null 2>&1; done if [ "$RLGATE" -eq 1 ]; then check p910nd/daemon-no-pidfile test ! -e /var/run/p9109d.pid check p910nd/daemon-no-lockfile test ! -e /var/lock/p9109d _ls=$(netstat -ltn 2>/dev/null | grep -c 9109) check_eq p910nd/daemon-no-listener 0 "$_ls" else for c in daemon-no-pidfile daemon-no-lockfile daemon-no-listener; do bst_skip "p910nd/$c" "getrlimit(2) now answers; daemon mode needs its live arm" done fi reap # --- 5. the shipped configuration and init script ----------------------------- # The package ships /etc/sysconfig/p910nd.conf and /etc/init.d/p910nd. Neither is # usable as shipped on this image, and each reason is its own case rather than a # note: the recipe's do_install renames the config, the script is bash-only, and # the SUSE helpers it calls are not here. check p910nd/conf-installed test -f /etc/sysconfig/p910nd.conf check p910nd/init-installed test -f /etc/init.d/p910nd # do_install uses `install -m 0644` for the init script, so it ships without an # execute bit. Asserted off the MODE STRING rather than with `test -x`: the # shell builtin answers `getgroups: Function not implemented` on this kernel for # a file on the virtiofs root -- getgroups(2) is unwired (brintos/linux#19, nr # 115 in its table) -- so `[ -x ... ]` is not an oracle here. check_eq p910nd/init-not-executable "-rw-r--r--" "$(ls -l /etc/init.d/p910nd | cut -c1-10)" check_eq p910nd/binary-executable "-rwxr-xr-x" "$(ls -l /usr/sbin/p910nd | cut -c1-10)" cp /etc/sysconfig/p910nd.conf "$W/sysconfig" 2>/dev/null check p910nd/conf-copy-identical cmp -s /etc/sysconfig/p910nd.conf "$W/sysconfig" ( . "$W/sysconfig"; printf 'NUM=[%s] OPTS=[%s]\n' "$P910ND_NUM" "$P910ND_OPTS" ) > "$W/conf.out" 2>&1 check_out p910nd/conf-parses-empty "NUM=[] OPTS=[]" cat "$W/conf.out" # The script composes its command line as `$P910ND_BIN $P910ND_OPTS $P910ND_NUM` # -- the number LAST, after the options. Driven against a recording stub. mkdir -p "$W/bin" printf '#!/bin/sh\nprintf "ARGV[%%s]" "$*" > %s/argv.rec\n' "$W" > "$W/bin/p910nd" chmod 755 "$W/bin/p910nd" printf 'P910ND_NUM="1"\nP910ND_OPTS="-b -f /tmp/dev"\n' > "$W/sysconfig.filled" ( . "$W/sysconfig.filled"; "$W/bin/p910nd" $P910ND_OPTS $P910ND_NUM ) check_out p910nd/conf-argv-order "ARGV[-b -f /tmp/dev 1]" cat "$W/argv.rec" ( . "$W/sysconfig"; "$W/bin/p910nd" $P910ND_OPTS $P910ND_NUM ) check_out p910nd/conf-argv-empty "ARGV[]" cat "$W/argv.rec" # THE CONFIG PATH DOES NOT MATCH. p910nd.init reads /etc/sysconfig/p910nd; the # recipe installs /etc/sysconfig/p910nd.conf. So `start` takes the LSB # "not configured" exit before it does anything at all, and `stop` is the # LSB-conformant 0 the script writes for the same state. check_fail p910nd/init-config-mismatch 6 "/etc/sysconfig/p910nd not found" sh /etc/init.d/p910nd start check_fail p910nd/init-stop-lsb-zero 0 "/etc/sysconfig/p910nd not found" sh /etc/init.d/p910nd stop # Past that check the script is bash-only -- `_SMSG=(running dead ...)` is a bash # array. Posed against a copy whose config path is redirected into $TMPDIR, so # the read-only rootfs is not written to. sed "s|^P910ND_CONFIG=.*|P910ND_CONFIG=$W/sysconfig|" /etc/init.d/p910nd > "$W/init" check_fail p910nd/init-bash-only 2 "syntax error" sh "$W/init" start # And under bash it reaches the SUSE helpers, which this image does not carry. if command -v bash >/dev/null 2>&1; then check_fail p910nd/init-needs-startproc 127 "startproc: command not found" bash "$W/init" start else bst_skip p910nd/init-needs-startproc "no bash on this image" fi reap # --- 6. the printer device the daemon exists to drive ------------------------- # The -f arm above is the whole of what this image can exercise. The DEFAULT # target -- /dev/lp -- and a USB printer are probed rather than assumed # absent, and the skip below is gated on that probe: the day a device appears, # probe-no-printer-device goes red and the live arm needs writing. LP=$(ls -d /dev/lp0 /dev/lp1 /dev/lp2 /dev/usb/lp0 2>/dev/null | tr '\n' ' ') if [ -z "$LP" ]; then bst_ok p910nd/probe-no-printer-device bst_skip p910nd/default-device-job "no /dev/lp* and no /dev/usb/lp* on this machine (probed)" bst_skip p910nd/default-device-open "no /dev/lp* and no /dev/usb/lp* on this machine (probed)" else bst_notok p910nd/probe-no-printer-device "a printer device appeared: $LP" fi rm -rf "$W" /run/lock