. "$BST_LIB" # BST-TIMEOUT: 300 # 72 cases, ~68 execs of one small daemon, no loops and no # corpus. DECLARED, not yet measured: this port was built in # a batched round whose recipe phase never boots an image, so # the first on-target run is the measurement and this bound # is to be replaced by ~5x the elapsed it reports (the ratio # 99-sg3-utils.test and 99-aoetools.test use). Nothing here # waits: every pimd invocation below is bounded either by its # own refusal at init or by the `timeout` guard `run_pimd` # puts around it. For scale, the same 66 cases take 0.14s # against a native x86-64 build of pimd 2.3.2, and # 99-aoetools.test's 45 execs measured 46s on the guest. # pimd -- a PIM-SM v2 multicast routing daemon on a kernel that has no multicast # routing. # # WHAT THIS MACHINE CAN AND CANNOT TEST. pimd's whole purpose is to program the # kernel's multicast forwarding cache, which it reaches through # setsockopt(IPPROTO_IP, MRT_INIT) on a raw IGMP socket. That case is compiled # out of ip_setsockopt() unless CONFIG_IP_MROUTE is set, and this kernel does # not set it: arch/wasm32/configs/wasm32_defconfig turns on CONFIG_NET and # CONFIG_INET for a loopback-only stack and takes nothing above them (its own # comment lists PACKET, IPV6 and NET_NS as deliberate v0.1 omissions), and # IP_MROUTE depends on IP_MULTICAST, which that file never enables. So no # (S,G) route can be installed on this image and none could be. # # WHAT IS THEREFORE ASSERTED FOR REAL, and it is most of the daemon's # user-facing surface: # # * the usage and version text, byte for byte; # * argument validation -- every option that can be rejected, each documented # diagnostic, and the exit status each produces; # * THE CONFIGURATION FILE PARSER, in full. This is the part that is easy to # assume unreachable and is not. pimd reads /etc/pimd.conf inside # init_vifs(), at vif.c:116, and the multicast routing socket is not touched # until k_init_pim() at vif.c:142 -- twenty-six lines later. Every parse # diagnostic below therefore runs on this image exactly as it runs on a # router, and each is arithmetic on a line of text with no I/O in it, so a # wasm32 build that gets one wrong has no environmental excuse available. # * the documented exit code. pimd has one failure convention, debug.c: # logit() calls exit(-1) for anything at LOG_ERR or worse, which is status # 255. Both fatal paths this image can reach end there, and both are pinned. # # WHAT IS PROBED RATHER THAN ASSUMED. The forwarding half is read at runtime # from /usr/libexec/pimd-tests/pimd-probe -- the raw IGMP and PIM sockets, the # rtnetlink socket, MRT_INIT and MRT_PIM themselves, and the interface census -- # and every branch below is written against the live answer rather than against # this comment, so each skip turns back into an assertion the day the kernel # grows the missing half. # # THE CENSUS IS IN THE PROBE FOR A REASON, AND IT IS NOT TIDINESS. On an image # whose only interface is loopback, init_vifs() stops at "Cannot forward: no # enabled vifs" (vif.c:139) BEFORE it reaches k_init_pim, so pimd never asks the # kernel about multicast routing at all and its exit says nothing about # CONFIG_IP_MROUTE. The two fatals are both exit 255 and only the message tells # them apart, which is why the message is asserted and not just the status, and # why the probe reproduces pimd's own flags test (not loopback AND multicast, # config.c:172) rather than paraphrasing it through `ip link`. # # AND THE CONTROLS ARE THE POINT OF THE PROBE. "MRT_INIT was refused" is a # statement about the kernel only while this process can still open ordinary # sockets. The probe opens AF_INET stream, AF_INET datagram and AF_UNIX stream # in the same run and every skip below is gated on all three succeeding; if they # stop succeeding this suite reds rather than quietly skipping, which is the # failure mode a suite full of environmental skips is otherwise built to have. # # WHY NO NATIVE ORACLE. pimd's output is a function of the ENVIRONMENT -- which # interfaces exist, what the kernel answers -- so a build-host run would differ # from a correct target run for reasons that are not defects. The invariant text # and every diagnostic asserted below were instead read off a native # x86-64 build of pimd 2.3.2 during the port, run inside an empty network # namespace so that its interface census matched this image's, rather than # transcribed from the source. bst_ok pimd/suite-canary # first line: absorbs brintos/linux#96, which drops it # --- what is on the image, checked BEFORE `require` --------------------------- # # The order here is load-bearing. pimd's recipe inherits update-alternatives, so # do_package renames the program to ${sbindir}/pimd.pimd and ${sbindir}/pimd is a # LINK that update-alternatives creates at rootfs time -- measured on the # recipe-scope build, whose pimd_2.3.2-r0_wasm32.deb ships `./usr/sbin/pimd.pimd` # and no `pimd`. So there are two independent ways for the program to be missing # and they mean completely different things: the package did not install, or it # installed and the alternative was never linked. # # `require pimd ...` cannot tell them apart -- it asks `command -v`, and on the # second failure it would emit ONE skip reading "pimd not installed on this # image" and exit 0, taking all sixty-nine remaining cases with it and reporting # a clean run. A whole suite disappearing into a wrong explanation is the failure # this ordering exists to prevent, so both files are asserted first and the # require below can then only fire when the package genuinely is absent. for c in "sbin-pimd-real:/usr/sbin/pimd.pimd" \ "sbin-pimd:/usr/sbin/pimd" \ "etc-conf:/etc/pimd.conf"; do _id=${c%%:*}; _f=${c#*:} if [ -e "$_f" ]; then bst_ok "pimd/present/$_id" else bst_notok "pimd/present/$_id" "$_f is not installed"; fi done # The alternative specifically, as its own case, because "the program is here but # unreachable by its own name" is a distinct and silent failure. if [ -e /usr/sbin/pimd.pimd ] && [ ! -e /usr/sbin/pimd ]; then bst_notok pimd/present/alternative "/usr/sbin/pimd.pimd is installed but /usr/sbin/pimd is not -- update-alternatives did not link the program at rootfs time, so pimd is on the image and off the PATH" elif [ -e /usr/sbin/pimd ]; then bst_ok pimd/present/alternative else bst_notok pimd/present/alternative "neither /usr/sbin/pimd nor /usr/sbin/pimd.pimd is installed" fi require pimd pimd/present/on-path TMP=${TMPDIR:-/tmp} CONF=$TMP/pimd-bst.$$.conf # run_pimd -- run pimd in the foreground with a bounded lifetime, # echoing "" on the first line and its combined output after it. # # The bound matters on an image that DOES have multicast routing and a usable # interface: there pimd starts and runs forever, and an unbounded call would # hang the whole suite until the runner's timeout killed it, losing every case # after this point rather than reporting one. `timeout` is busybox's and is # always present; a run it kills reports 124 (GNU coreutils, which is what a # brintOS glibc image ships) or 143 (busybox, which reports the signal), and the # callers below accept both and read either as "pimd started" -- its own answer # rather than a failure. run_pimd() { _o=$(bst_bound 10 pimd -f "$@" 2>&1); _r=$? printf '%s\n' "$_r" printf '%s\n' "$_o" } # rc_of / out_of -- split what run_pimd returned. rc_of() { printf '%s\n' "$1" | sed -n 1p; } out_of() { printf '%s\n' "$1" | sed -n '2,$p'; } # has -- literal substring test, no regex, no exec. has() { case "$1" in *"$2"*) return 0 ;; *) return 1 ;; esac; } # --- version and usage: the invariant text ------------------------------------ # # `pimd -v` prints exactly one line, composed in main.c from a version string # the Makefile generates into vers.c. It is pinned in full rather than by a # prefix: a build that shipped the wrong tarball would still match "pimd # version". V=$(pimd -v 2>&1); vrc=$? if bst_execfail "$V"; then bst_unrun pimd/version/exact "$V" else check_eq pimd/version/exact "pimd version 2.3.2" "$V" check_eq pimd/version/rc "0" "$vrc" fi check_out pimd/version/long-opt "pimd version 2.3.2" pimd --version # `pimd -h` exits 0 and writes to STDOUT -- both are part of the contract, and # both are what a shell pipeline downstream of it depends on. H=$(pimd -h 2>/dev/null); hrc=$? if bst_execfail "$H"; then bst_unrun pimd/usage/rc "$H" else check_eq pimd/usage/rc "0" "$hrc" # The synopsis line, the option-letter set included: it is the one line that # goes stale when an option is added, and getopt's own string is asserted # separately below by exercising each letter. for n in "Usage: pimd [-fhlNqrv] [-c FILE] [-d [SYS][,SYS...]] [-s LEVEL]" \ " -c, --config=FILE Configuration file to use, default /etc/pimd.conf" \ " -d, --debug[=SYS] Debug subsystem, see below for valid systems, default all" \ " -f, --foreground Run in foreground, do not detach from calling terminal" \ " -h, --help Show this help text" \ " -l, --reload-config Tell a running pimd to reload its configuration" \ " -N, --disable-vifs Disable all virtual interfaces (phyint) by default" \ " -q, --quit-daemon Send SIGTERM to a running pimd" \ " -r, --show-routes Show state of VIFs and multicast routing tables" \ " -t, --table-id=ID Set multicast routing table ID. Allowed table ID#:" \ " 0 .. 999999999. Default: 0 (use default table)" \ " -s, --loglevel=LVL Set log level: none, err, info, notice (default), debug" \ " -v, --version Show pimd version" \ "Available subsystems for debug:" \ "Bug report address: https://github.com/troglobit/pimd/issues"; do # The id is the option letter, or a word of the line, so a red names # which line went missing rather than "usage differs". _id=$(printf '%s' "$n" | sed -n 's/^ -\([a-zA-Z]\),.*/\1/p') [ -n "$_id" ] || _id=$(printf '%s' "$n" | sed -n 's/^\([A-Za-z]*\).*/\1/p') [ -n "$_id" ] || _id=cont if has "$H" "$n"; then bst_ok "pimd/usage/$_id" else bst_notok "pimd/usage/$_id" "missing from -h output: '$n'"; fi done # The default config path is compiled in from -D_PATH_SYSCONF, which the # Makefile sets from $(sysconfdir) -- so this line is the one place the # suite can see that the recipe's --sysconfdir survived a configure script # that silently skips every option it does not recognise. if has "$H" "default /etc/pimd.conf"; then bst_ok pimd/usage/sysconfdir else bst_notok pimd/usage/sysconfdir "-h does not name /etc/pimd.conf as the default config -- the recipe's --sysconfdir did not reach -D_PATH_SYSCONF"; fi fi check_out pimd/usage/long-opt "Usage: pimd" pimd --help # --- argument validation ------------------------------------------------------ # # Every one of these returns usage(1) and is decided before pimd opens a socket, # reads a file or checks its privileges, so each is asserted unconditionally. # The exit status is 1 here and 255 for everything fatal later, which is the # distinction that makes a red below unambiguous. check_rc pimd/args/unknown-option 1 pimd -Z check_rc pimd/args/operand 1 pimd bogus-operand check_rc pimd/args/debug-unknown 1 pimd -d not_a_subsystem check_rc pimd/args/debug-unknown-in-list 1 pimd -d dvmrp,not_a_subsystem check_rc pimd/args/long-unknown 1 pimd --not-an-option # usage(1) prints to STDOUT even on the error path -- upstream's choice, and a # fact a caller redirecting only stderr would be surprised by, so it is pinned. U=$(pimd -Z 2>/dev/null) if bst_execfail "$U"; then bst_unrun pimd/args/usage-on-stdout "$U" elif has "$U" "Usage: pimd"; then bst_ok pimd/args/usage-on-stdout else bst_notok pimd/args/usage-on-stdout "the error-path usage text did not reach stdout: '$(_bst_1line "$U")'"; fi # -t: strtonum(3) over 0..999999999, and each of its three failure words is a # distinct message. Asserting the word and not just the status is what separates # "the bound is wrong" from "the parse is wrong". for c in "abc:Table ID invalid!" \ "1000000000:Table ID too large!" \ "-1:Table ID too small!" \ "1e9:Table ID invalid!" \ "blank= :Table ID invalid!"; do _spec=${c%%:*}; _want=${c#*:} case "$_spec" in *=*) _id=${_spec%%=*}; _arg=${_spec#*=} ;; *) _id=$_spec; _arg=$_spec ;; esac _e=$(pimd -t "$_arg" 2>&1 >/dev/null); _r=$? if bst_execfail "$_e"; then bst_unrun "pimd/args/table-id/$_id" "$_e" elif [ "$_r" -eq 1 ] && has "$_e" "$_want"; then bst_ok "pimd/args/table-id/$_id" else bst_notok "pimd/args/table-id/$_id" "rc=$_r want=1 want:'$_want' got:'$(_bst_1line "$_e")'"; fi done # ...and the two ENDS OF THE DOCUMENTED RANGE, asserted as ACCEPTED. The five # cases above only pin the rejections, and a bound that moved inward would still # reject every one of them -- caught by mutation: narrowing strtonum's upper # limit from 999999999 to 99999999 changed nothing any of them could see, while # `pimd -h` went on advertising "0 .. 999999999". The boundary values are the # only two inputs that can tell the documented range from a different one. # # Bounded and with -N for the same reason as the -s case below: an ACCEPTED # option means pimd carries on into init_vifs(), and on an image that can forward # it would daemonise. for b in 0 999999999; do : > "$CONF" _res=$(run_pimd -N -t "$b" -c "$CONF"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun "pimd/args/table-id/accepted-$b" "$_o" elif has "$_o" "Usage: pimd"; then bst_notok "pimd/args/table-id/accepted-$b" "-t $b was rejected, but -h documents the range as 0 .. 999999999" else bst_ok "pimd/args/table-id/accepted-$b"; fi done # -s takes any word: loglvl() falls through to atoi(3), so an unrecognised level # is accepted as 0 rather than rejected. That is upstream's behaviour and it is # asserted rather than corrected -- a suite that expected a usage error here # would red on a correct build. atoi("-1") is -1, which IS the one value loglvl # reports as invalid, so the two sit side by side. # # Run through run_pimd, and with -N, for a reason worth stating: an ACCEPTED # option means pimd carries on into init_vifs(), and on an image that can # actually forward it would fork, daemonise and still be running when the next # case looks for a pid file. -N disables every vif the kernel offered, so # init_vifs() always stops at "Cannot forward: no enabled vifs" and pimd never # reaches the fork -- which makes this assertion about option parsing on every # image rather than only on one that cannot forward. Caught by running this # suite against a native pimd 2.3.2 with two multicast interfaces, where the # unbounded form left a daemon behind and reddened pimd/signal/r three cases # later. : > "$CONF" _res=$(run_pimd -N -s not_a_level -c "$CONF"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun pimd/args/loglevel/word-accepted "$_o" elif has "$_o" "Usage: pimd"; then bst_notok pimd/args/loglevel/word-accepted "an unrecognised -s level was rejected with the usage text, but loglvl() falls through to atoi(3) and should accept it as level 0" else bst_ok pimd/args/loglevel/word-accepted; fi check_rc pimd/args/loglevel/negative 1 pimd -s -1 # --- the three signalling options --------------------------------------------- # # -q, -r and -l return from inside the option loop, before the privilege check # and before any socket, so they are the only pimd invocations on this image # that neither need root nor touch the kernel. With no pimd running, # daemon_pid() finds no pid file, killshow() sends nothing and returns 0 -- a # silent success that is easy to mistake for a stub, so it is pinned in both # halves: the status AND the absence of output. for o in q r l; do _o=$(pimd -$o 2>&1); _r=$? if bst_execfail "$_o"; then bst_unrun "pimd/signal/$o" "$_o" elif [ "$_r" -eq 0 ] && [ -z "$_o" ]; then bst_ok "pimd/signal/$o" else bst_notok "pimd/signal/$o" "rc=$_r out:'$(_bst_1line "$_o")' -- with no pimd running this must be a silent 0"; fi done # --- the probe ---------------------------------------------------------------- P=/usr/libexec/pimd-tests/pimd-probe if [ ! -x "$P" ]; then bst_notok pimd/probe/present "no probe at $P -- pimd-tests is not installed, so nothing below can be attributed" rm -f "$CONF" exit 0 fi PO=$("$P" 2>&1); prc=$? if bst_execfail "$PO"; then bst_unrun pimd/probe/runs "$PO" rm -f "$CONF" exit 0 fi case "$PO" in *"probe end"*) bst_ok pimd/probe/runs ;; *) bst_notok pimd/probe/runs "rc=$prc, no 'probe end' -- the probe did not finish, so its values below are partial: $(_bst_1line "$PO")" rm -f "$CONF" exit 0 ;; esac # field -- the value of on the probe's # line. Reads from a variable, never a redirect: a top-level stdin # redirect on the virtiofs root makes dup2(2) return 513 and ash exits, ending # the suite silently (brintos/linux#73). field() { printf '%s\n' "$PO" | sed -n "s/^$1 / /p" \ | sed -n "s/.*[ ]$2=\\([^ ]*\\).*/\\1/p" | sed -n 1p } # pstate -- the third word of the probe's `proc present|absent`. pstate() { printf '%s\n' "$PO" | sed -n "s|^proc $1 \\([a-z]*\\)\$|\\1|p" | sed -n 1p } # Everything the probe says the daemon's own diagnostics have to be read against. euid=$(printf '%s\n' "$PO" | sed -n 's/^euid //p' | sed -n 1p) igmp_rc=$(field raw-igmp rc); igmp_name=$(field raw-igmp name) pim_rc=$(field raw-pim rc) nl_rc=$(field netlink-route rc) cens_rc=$(field ifcensus rc); cand=$(field ifcensus cand) mrt_rc=$(field mrt-init rc); mrt_name=$(field mrt-init name) mrt_err=$(printf '%s\n' "$PO" | sed -n 's/^mrt-init .*strerror=//p' | sed -n 1p) mrtpim_rc=$(field mrt-pim rc) # --- the controls ------------------------------------------------------------- # # Asserted, not merely recorded. Every skip further down claims "the kernel # refused X"; that claim is only meaningful while these three succeed, so if one # of them fails the suite reds here and the attributions below stop being made. ctl_ok=1 for c in ctl-inet-stream ctl-inet-dgram ctl-unix-stream; do _r=$(field "$c" rc) if [ "$_r" = "0" ]; then bst_ok "pimd/control/$c" else ctl_ok=0 bst_notok "pimd/control/$c" "rc=$_r name=$(field "$c" name) -- a socket this kernel must carry was refused, so no refusal below can be attributed to a missing kernel feature" fi done # root, because pimd's own answer to everything below changes without it: # main.c exits 1 at "Need root privileges to start." before opening a file, and # a raw socket would be refused EPERM rather than by the kernel's configuration. if [ "$euid" = "0" ]; then bst_ok pimd/control/root else bst_notok pimd/control/root "euid=$euid -- pimd refuses to start as non-root and every refusal below would be about privilege, not about the kernel"; fi # --- the multicast routing table, read directly ------------------------------- # # Asserted in BOTH directions rather than skipped. Two answers are correct on a # brintOS image: MRT_INIT refused with ENOPROTOOPT, which is what a kernel built # without CONFIG_IP_MROUTE returns because the MRT_* cases are compiled out of # ip_setsockopt(); or MRT_INIT accepted, which means the image grew multicast # routing and is a finding rather than a failure. Anything else is neither -- # EPERM says this is about privilege, EADDRINUSE says another multicast router # already holds the table, and both would otherwise be attributed to a kernel # option that has nothing to do with them. if [ "$igmp_rc" != "0" ]; then bst_skip pimd/kernel/mrt-init "no raw IGMP socket to ask on (raw-igmp $igmp_name), so the multicast routing table cannot be reached from userspace at all" elif [ "$mrt_rc" = "0" ]; then bst_ok pimd/kernel/mrt-init elif [ "$mrt_name" = "ENOPROTOOPT" ]; then bst_ok pimd/kernel/mrt-init elif [ "$ctl_ok" = "1" ]; then bst_notok pimd/kernel/mrt-init "MRT_INIT refused with $mrt_name ($mrt_err) -- neither ENOPROTOOPT (kernel built without CONFIG_IP_MROUTE) nor success, so this is not the gap this port documents" else bst_skip pimd/kernel/mrt-init "controls failed; not attributing $mrt_name" fi # The kernel's own witness, and the reason it is here: ip_mr_init() registers # /proc/net/ip_mr_vif and /proc/net/ip_mr_cache only under CONFIG_IP_MROUTE, so # it answers the same question through a completely different mechanism. A # disagreement between the two means one of them is lying, which is worth a red # on an image where both are cheap to read. mrvif=$(pstate /proc/net/ip_mr_vif) mrcache=$(pstate /proc/net/ip_mr_cache) if [ "$igmp_rc" != "0" ]; then bst_skip pimd/kernel/mrt-corroborated "MRT_INIT was not asked (no raw IGMP socket), so there is nothing to corroborate" elif [ "$mrt_rc" = "0" ] && [ "$mrvif" = "present" ]; then bst_ok pimd/kernel/mrt-corroborated elif [ "$mrt_rc" != "0" ] && [ "$mrvif" = "absent" ] && [ "$mrcache" = "absent" ]; then bst_ok pimd/kernel/mrt-corroborated else bst_notok pimd/kernel/mrt-corroborated "MRT_INIT rc=$mrt_rc but /proc/net/ip_mr_vif=$mrvif /proc/net/ip_mr_cache=$mrcache -- setsockopt and procfs disagree about whether this kernel has multicast routing" fi # MRT_PIM (aka MRT_ASSERT) is the second half k_init_pim() sets, and pimd treats # its refusal as fatal in its own right. It can only be meaningful once MRT_INIT # took. if [ "$mrt_rc" != "0" ]; then bst_skip pimd/kernel/mrt-pim "MRT_INIT did not take ($mrt_name), so the PIM assert flag cannot be set either" elif [ "$mrtpim_rc" = "0" ]; then bst_ok pimd/kernel/mrt-pim else bst_notok pimd/kernel/mrt-pim "MRT_INIT took but MRT_PIM was refused with $(field mrt-pim name) -- a kernel with multicast routing but no PIM assert support stops pimd at k_init_pim()" fi # The census, recorded as its own case because every branch in the daemon half # below reads it. It is not a pass/fail about the image -- both answers are # legitimate -- so what is asserted is that pimd's own ioctl pair worked at all. if [ "$cens_rc" = "0" ]; then bst_ok pimd/kernel/ifcensus else bst_notok pimd/kernel/ifcensus "SIOCGIFCONF failed with $(field ifcensus name) -- config_vifs_from_kernel() calls logit(LOG_ERR) on this and pimd exits 255 before reading its configuration"; fi printf '#diag: pimd probe: %s\n' "$(printf '%s\n' "$PO" | sed -n '2,$p' | tr '\n' '|')" # --- the configuration file parser -------------------------------------------- # # Reachable only once the three sockets pimd opens before init_vifs() have # opened. That is a fact about the image, so it is read from the probe rather # than assumed, and the whole block below is gated on it in one place. parser_ok=1 if [ "$igmp_rc" != "0" ]; then parser_ok=0; parser_why="the raw IGMP socket was refused ($igmp_name), so init_igmp() calls logit(LOG_ERR) and pimd exits 255 before init_vifs() opens the config file" elif [ "$pim_rc" != "0" ]; then parser_ok=0; parser_why="the raw PIM socket was refused ($(field raw-pim name)), so init_pim() ends the daemon before the config file is opened" elif [ "$nl_rc" != "0" ]; then parser_ok=0; parser_why="the rtnetlink socket was refused ($(field netlink-route name)), so init_routesock() ends the daemon before the config file is opened" elif [ "$cens_rc" != "0" ]; then parser_ok=0; parser_why="SIOCGIFCONF failed, so config_vifs_from_kernel() ends the daemon before config_vifs_from_file() runs" elif [ "$euid" != "0" ]; then parser_ok=0; parser_why="euid=$euid; pimd exits 1 at the privilege check before opening anything" fi # conf_case -- write , run pimd against it, # and require in what pimd said. # # Each needle is a literal diagnostic read off a native pimd 2.3.2 run, INCLUDING # the ": - " prefix where the parser emits one, because that prefix # is where lineno is asserted. Two of pimd's messages deliberately carry no # prefix -- they are emitted by logit() directly rather than through config.c's # WARN macro -- and asserting the difference is what would catch a refactor that # unified them. conf_case() { _id=$1; _body=$2; _needle=$3 if [ "$parser_ok" != "1" ]; then bst_skip "pimd/conf/$_id" "$parser_why" return fi printf '%s\n' "$_body" > "$CONF" _res=$(run_pimd -c "$CONF"); _r=$(rc_of "$_res"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun "pimd/conf/$_id" "$_o" elif has "$_o" "$_needle"; then bst_ok "pimd/conf/$_id" else bst_notok "pimd/conf/$_id" "rc=$_r want:'$_needle' got:'$(_bst_1line "$_o")'"; fi } # An unrecognised keyword is the parser's own error path and the only one that # raises error_flag, so it is also the only one that ends in "Syntax error". Both # lines are asserted, and the second one is what pins exit 255 to a PARSE failure # rather than to the forwarding failure every other case here ends in. conf_case unknown-command "not_a_pimd_keyword foo" "$CONF:1 - Unknown command 'not_a_pimd_keyword'" conf_case syntax-error "not_a_pimd_keyword foo" "$CONF:1 - Syntax error" # lineno, asserted on its own. The parser counts every line it reads including # blanks and comments, so a keyword on line 4 must be reported as line 4 -- an # off-by-one here is invisible in every other case above, which all use line 1. conf_case lineno "# a comment # another not_a_pimd_keyword foo" "$CONF:5 - Unknown command 'not_a_pimd_keyword'" # The remaining diagnostics are warnings: each names the value it rejected and # the default it substituted, so asserting the whole sentence asserts the # substitution too. None of them sets error_flag, so none of them ends the run # by itself -- which is exactly why they are worth pinning: a build that turned # one into a fatal would still exit 255 and would still look right on status. conf_case phyint-bad-address "phyint eth9 enable" "$CONF:1 - Invalid phyint address 'eth9'" conf_case bsr-priority-clamp "bsr-candidate priority 300" "$CONF:1 - Too high Cand-BSR priority 300, defaulting to 255" conf_case group-prefix-masklen "group-prefix 224.0.0.0 masklen 3" "$CONF:1 - Too small masklen 3. Defaulting to 4" conf_case spt-threshold-rate "spt-threshold rate abc" "$CONF:1 - Invalid spt-threshold rate abc, defaulting to 0" # The three that carry NO file:line prefix, because parse_hello_interval() and # parse_rp_address() call logit() directly instead of going through config.c's # WARN macro. conf_case hello-interval-missing "hello-interval" "Missing hello-interval value; defaulting to 30" conf_case hello-interval-invalid "hello-interval abc" "Invalid hello-interval abc; defaulting to 30" conf_case rp-address-missing "rp-address" "Missing rp-address argument" # And the absence of that prefix, asserted separately -- because a substring test # for the message alone would still match a message that had GAINED one, which is # precisely the refactor these three exist to notice. Found by mutation: the # claim that they pin the difference was not true of the cases above. conf_noprefix() { _id=$1; _body=$2; _needle=$3 if [ "$parser_ok" != "1" ]; then bst_skip "pimd/conf/$_id" "$parser_why" return fi printf '%s\n' "$_body" > "$CONF" _res=$(run_pimd -c "$CONF"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun "pimd/conf/$_id" "$_o" elif ! has "$_o" "$_needle"; then bst_notok "pimd/conf/$_id" "want:'$_needle' got:'$(_bst_1line "$_o")'" elif has "$_o" "$CONF:1 - $_needle"; then bst_notok "pimd/conf/$_id" "'$_needle' now carries a ': - ' prefix -- it is emitted through logit() directly, not through config.c's WARN macro, and the two must stay distinguishable" else bst_ok "pimd/conf/$_id"; fi } conf_noprefix hello-interval-missing-noprefix "hello-interval" "Missing hello-interval value; defaulting to 30" conf_noprefix rp-address-missing-noprefix "rp-address" "Missing rp-address argument" # A two-line diagnostic whose second line is arithmetic on the values it just # read -- 3 x 12 + 10 / 2 = 41 -- which is the closest thing in this parser to a # computation a miscompiled build could get wrong. conf_case igmp-querier-timeout "igmp-query-interval 12 igmp-querier-timeout 1" "must be larger than the query interval 12, forcing default!" conf_case igmp-querier-arithmetic "igmp-query-interval 12 igmp-querier-timeout 1" "3 x 12 + 10 / 2 = 41" # A configuration file that is entirely valid must produce NO diagnostic at all. # Without this case every one above would pass on a build whose parser warned # unconditionally. if [ "$parser_ok" != "1" ]; then bst_skip pimd/conf/valid-is-quiet "$parser_why" else printf '%s\n' "# a valid pimd.conf default-route-metric 1024 default-route-distance 101 hello-interval 30 igmp-query-interval 12 spt-threshold rate 0 interval 100 bsr-candidate priority 5" > "$CONF" _res=$(run_pimd -c "$CONF"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun pimd/conf/valid-is-quiet "$_o" else _noise=$(printf '%s\n' "$_o" | grep -v 'Cannot forward' | grep -v 'Cannot enable multicast routing' | grep -v '^$') if [ -z "$_noise" ]; then bst_ok pimd/conf/valid-is-quiet else bst_notok pimd/conf/valid-is-quiet "a valid configuration produced a diagnostic: '$(_bst_1line "$_noise")'"; fi fi fi # A config file that is not there is a WARNING, not an error: pimd says so and # falls back to a built-in configuration. The distinction is worth pinning # because a build that made it fatal would still exit 255. if [ "$parser_ok" != "1" ]; then bst_skip pimd/conf/missing-file "$parser_why" else _res=$(run_pimd -c /nonexistent-pimd.conf); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun pimd/conf/missing-file "$_o" elif has "$_o" "Cannot open configuration file /nonexistent-pimd.conf: No such file or directory"; then bst_ok pimd/conf/missing-file else bst_notok pimd/conf/missing-file "got:'$(_bst_1line "$_o")'"; fi fi # --- the documented exit code ------------------------------------------------- # # pimd has exactly one fatal convention: debug.c's logit() calls exit(-1) for # anything at LOG_ERR, which the shell reports as 255. Asserted on the parse # failure, where the cause is entirely inside pimd and owes nothing to the # kernel. if [ "$parser_ok" != "1" ]; then bst_skip pimd/exit/syntax-error-255 "$parser_why" else printf '%s\n' "not_a_pimd_keyword foo" > "$CONF" _res=$(run_pimd -c "$CONF"); _r=$(rc_of "$_res") check_eq pimd/exit/syntax-error-255 "255" "$_r" fi # -N disables every interface the kernel offered, so with a configuration that # enables none back, enabled_vifs is 0 whatever this image's interfaces are. # That makes it the one forwarding-path assertion here that needs no branch: the # message and the status are the same on a router and on this image. if [ "$parser_ok" != "1" ]; then bst_skip pimd/exit/disable-vifs "$parser_why" else : > "$CONF" _res=$(run_pimd -N -c "$CONF"); _r=$(rc_of "$_res"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun pimd/exit/disable-vifs "$_o" elif [ "$_r" = "255" ] && has "$_o" "Cannot forward: no enabled vifs"; then bst_ok pimd/exit/disable-vifs else bst_notok pimd/exit/disable-vifs "rc=$_r out:'$(_bst_1line "$_o")' -- with every vif disabled init_vifs() must stop at 'Cannot forward: no enabled vifs' and exit 255"; fi fi # And the branch the probe exists for. With a valid configuration and no -N, # WHICH fatal pimd reaches is decided by the interface census, not by a comment: # # cand == 0 -> init_vifs() stops at "Cannot forward: no enabled vifs" # (vif.c:139) and never asks the kernel about multicast routing. # This is the loopback-only brintOS image, and the multicast # routing gap is asserted directly by pimd/kernel/mrt-init above # instead -- it cannot be observed through pimd here, which is # precisely what the probe is for. # cand >= 1 -> init_vifs() reaches k_init_pim() and the MRT_INIT refusal # becomes pimd's own message, pinned against the probe's # strerror(3) reading so the case survives a libc whose wording # changes. # # Both halves are assertions. Nothing here is skipped on the kernel's account. if [ "$parser_ok" != "1" ]; then bst_skip pimd/exit/forwarding "$parser_why" else : > "$CONF" _res=$(run_pimd -c "$CONF"); _r=$(rc_of "$_res"); _o=$(out_of "$_res") if bst_execfail "$_o"; then bst_unrun pimd/exit/forwarding "$_o" elif [ "$_r" = "124" ] || [ "$_r" = "143" ]; then # timeout(1) killed it: pimd started and stayed up, which is only # possible on an image that has both a usable interface and multicast # routing. A correct outcome, and a different image from the one this # port documents. if [ "$cand" != "0" ] && [ "$mrt_rc" = "0" ]; then bst_ok pimd/exit/forwarding else bst_notok pimd/exit/forwarding "pimd ran to the 10s bound, but the probe says cand=$cand mrt-init rc=$mrt_rc -- it should not have been able to start"; fi elif [ "$cand" = "0" ]; then if [ "$_r" = "255" ] && has "$_o" "Cannot forward: no enabled vifs"; then bst_ok pimd/exit/forwarding else bst_notok pimd/exit/forwarding "rc=$_r out:'$(_bst_1line "$_o")' -- the census found no multicast-capable non-loopback interface, so init_vifs() must stop at 'Cannot forward: no enabled vifs' with 255"; fi elif [ "$mrt_rc" != "0" ]; then if [ "$_r" = "255" ] && has "$_o" "Cannot enable multicast routing in kernel" && has "$_o" "$mrt_err"; then bst_ok pimd/exit/forwarding else bst_notok pimd/exit/forwarding "rc=$_r out:'$(_bst_1line "$_o")' -- with $cand vif candidate(s) and MRT_INIT refused '$mrt_err', k_init_pim() must report 'Cannot enable multicast routing in kernel' and exit 255"; fi else bst_notok pimd/exit/forwarding "rc=$_r out:'$(_bst_1line "$_o")' -- the probe says this image can forward (cand=$cand, MRT_INIT ok), so pimd should have stayed up until the 10s bound" fi fi rm -f "$CONF"