. "$BST_LIB" # BST-TIMEOUT: 300 # 59 cases, each one fork of a 60 kB binary. NOT ONE of # # them waits on a socket: every case ends in an argv # # refusal, a config-parse refusal, an ioctl(SIOCGIFADDR) # # refusal, a cache-file decision, or a connect(2) that # # fails in the resolver or with ECONNREFUSED on lo. The # # per-case bound (20 s in ez_run) is what actually stops a # # hang; 300 is the ticket-gate/nightly default. MEASURED # # on the batch-2048 image, 2026-09-03: elapsed=45s against # # bound=300s, 59 declared and 59 captured, 59 green. # # ez-ipupdate 3.0.11b7 (SourceForge, packaged by meta-openembedded/meta-networking # recipes-connectivity/ez-ipupdate) on wasm32-hwjs, glibc arm. One binary, # /usr/bin/ez-ipupdate, plus the state directory /var/lib/ez-ipupdate the recipe # creates. NO BBAPPEND AND NO PATCH: the upstream recipe builds clean here -- # 886 tasks, all succeeded, zero ERROR lines -- and the two do_package_qa notes # it raises (`configure was passed unrecognised options: --disable-shared' and # `rdepends on brintos-libc-so, but it isn't a build dependency') are both the # distro's own arms working as intended and already carry precedent in the image # recipe's roster. # # --------------------------------------------------------------------------- # NOTHING HERE TALKS TO A DYNAMIC-DNS SERVICE, AND THAT IS THE DESIGN # # There is no dynamic-DNS provider reachable from this machine: it has lo and # nothing else, no /etc/resolv.conf, and no libnss_dns.so. So the suite asserts # the whole of ez-ipupdate that runs BEFORE a packet -- argv, the configuration # file parser, the interface lookup and the cache file -- and asserts the two # network legs as the FAILURES they must be, bounded and fast: # # * an unresolvable server, which dies in gethostbyname before a socket is # connected; and # * localhost:45999, which resolves out of /etc/hosts and is refused by the # kernel. That second one is the positive half: reaching ECONNREFUSED is the # proof the name resolved and the socket was really opened, the same # reasoning 61-netcat.test's resolver cases use. # # --------------------------------------------------------------------------- # THE ROUTE THAT KEEPS EVERY CONFIG CASE OFF THE NETWORK # # ez-ipupdate parses argv, then the config file, then -- in the non-daemon path # of main() -- reads the cache file, and only then resolves the address it is # about to send. With `interface=nosuchif0' and no address it never gets that # far: get_if_addr() fails at the first ioctl, main() prints # # could not resolve ip address for nosuchif0. # # and exit(1)s. That line is therefore a POSITIVE result for everything upstream # of it -- it can only be reached once the config file parsed and the service # type was accepted, because parse_service() exit(1)s on an unknown one before # any of this. It is what the 16 per-service cases assert. # # --------------------------------------------------------------------------- # WHERE THE EXPECTED STRINGS COME FROM, AND THREE THINGS THE TICKET GOT WRONG # # Every string and status below was read off an x86-64 build of the SAME # 3.0.11b7 sources with the SAME six meta-networking patches applied, on this # build host (Linux 6.8, glibc, gcc -w), 2026-09-02. Three of BRINTOS-2076's # own assertions did not survive that oracle and are narrowed here rather than # carried forward: # # * `ez-ipupdate -h' IS NOT HELP. -h is --host, and takes an argument; a bare # -h is a getopt error. Help is --help. The ticket's "`ez-ipupdate -h' and # its version output" is asserted as --help/--version/-V instead, and # help/host-takes-an-argument pins the option that was mistaken for it. # * THE CACHE FILE CANNOT BE CREATED ON THIS MACHINE. write_cache_file() is # reached only after a SUCCESSFUL update (`retval == 0 && cache_file'), and # no update can succeed with no provider to reach. So the ticket's "create, # read back and refuse" narrows to read back and refuse, plus # ez-ipupdate/cache-untouched-on-failure -- that a failed update leaves the file alone, # which is the half of the write path that IS observable here. # * "AN UNKNOWN service-type AND A MALFORMED KEY" are two different refusals # with two different messages, not one shape: an unknown service type is # rejected by parse_service() with `unknown service type: ' and the # supported list, while an unknown config key is rejected by the conf-file # reader with `,: unknown command: ' and the command table. # Both are asserted, separately. # # WHAT THE ORACLE COULD NOT SPEAK FOR. Both were predictions when this suite was # written; both have now been MEASURED on the batch-2048 image, 2026-09-03, and # what stands below is the measurement: # # * the errno text in `ioctl(SIOCGIFADDR): No such device'. The host answered # ENODEV; MEASURED, the wasm32 kernel answers ENODEV too, so the prediction # held and ez-ipupdate/iface-absent-ioctl asserts it as written. The MESSAGE # that matters -- `could not resolve ip address for ...' -- is printed by # ez-ipupdate itself either way, and is a separate case for exactly that reason. # * `gethostbyname: Unknown host' was predicted -- herror(3) for HOST_NOT_FOUND, # which is what a files-only NSS was expected to give an unresolvable name. # MEASURED 2026-09-03: this image answers TRY_AGAIN, `gethostbyname: Host name # lookup failure'. The prediction was wrong about the resolver and right about # which case to trust: the verdict is carried by # ez-ipupdate/net-unresolvable-server, whose string ez-ipupdate prints for ANY # do_connect() failure, and the herror case now asserts only that herror fired. EZ=/usr/bin/ez-ipupdate STATEDIR=/var/lib/ez-ipupdate require "$EZ" ez-ipupdate/present W=${TMPDIR:-/tmp}/ez-ipupdate.$$ # A fallback to the bare temp dir would be handed to `rm -rf' at the end of this # file, so the fallback is to NOT own a directory rather than to own /tmp. if mkdir -p "$W" 2>/dev/null; then _ezown=1; else W=${TMPDIR:-/tmp}; _ezown=0; fi _ezseq=0 # ez_run : one bounded fork; sets _ez_rc and _ez_out. # # Bounded rather than run bare because three of the paths below can block if the # platform behaves unexpectedly -- the `null' service and gnudip PROMPT on stdin, # and any leg that reached a real socket would wait on it -- and a suite that # wedges the guest produces no cases at all, which in a batched image costs every # other member the round rather than just this one. "$_ezof" 2>&1 "$_ezrf"; } & _ezpid=$! _ezt0= if [ -r /proc/uptime ] && read -r _ezu _ < /proc/uptime; then _ezt0=${_ezu%.*}; fi _eztk=0 while [ ! -e "$_ezrf" ]; do _ezdt=$_eztk if [ -n "$_ezt0" ] && read -r _ezu _ < /proc/uptime; then _ezdt=$(( ${_ezu%.*} - _ezt0 )); fi if [ "$_ezdt" -ge "$_ezb" ]; then kill -9 "$_ezpid" 2>/dev/null _ez_rc=timeout _ez_bound=$_ezb _ez_out=$(tail -n 1 "$_ezof" 2>/dev/null) return 124 fi sleep 1 _eztk=$((_eztk + 1)) done _ez_rc=0 # BST-REDIRECT-OK: $W is ${TMPDIR:-/tmp}, tmpfs, never the virtiofs root [ -s "$_ezrf" ] && read -r _ez_rc < "$_ezrf" 2>/dev/null case "$_ez_rc" in ''|*[!0-9]*) _ez_rc=0 ;; esac _ez_out=$(cat "$_ezof" 2>/dev/null) return 0 } # ez_said : assert the LAST ez_run's status and that its # combined output contains . The two together, never separately: this # tool prints its diagnostics and then exits 1 on almost every path here, so a # status alone would pass for any refusal at all and a string alone would pass # for one printed on the way to a hang. ez_said() { if [ "$_ez_rc" = timeout ]; then bst_notok "$1" "did not return within ${_ez_bound}s -- this tool is supposed to exit, not retry; last line: $(_bst_1line "$_ez_out")" return 1 fi if bst_execfail "$_ez_out"; then bst_unrun "$1" "$_ez_out"; return 1; fi case "$_ez_out" in *"$3"*) _ezhit=1 ;; *) _ezhit=0 ;; esac if [ "$_ez_rc" -eq "$2" ] && [ "$_ezhit" -eq 1 ]; then bst_ok "$1" else bst_notok "$1" "rc=$_ez_rc want=$2 want:'$3' got:'$(_bst_1line "$_ez_out")'"; fi } # ez_said_not : the same, for a string that must be # ABSENT. One caller -- an absent cache file is documented as NOT an error # (read_cache_file returns 0 on ENOENT), and the only way to say that is that the # run printed no cache complaint. ez_said_not() { if [ "$_ez_rc" = timeout ]; then bst_notok "$1" "did not return within ${_ez_bound}s; last line: $(_bst_1line "$_ez_out")" return 1 fi if bst_execfail "$_ez_out"; then bst_unrun "$1" "$_ez_out"; return 1; fi case "$_ez_out" in *"$3"*) _ezhit=1 ;; *) _ezhit=0 ;; esac if [ "$_ez_rc" -eq "$2" ] && [ "$_ezhit" -eq 0 ]; then bst_ok "$1" else bst_notok "$1" "rc=$_ez_rc want=$2 want-absent:'$3' got:'$(_bst_1line "$_ez_out")'"; fi } # ez_case : the one-assertion form. ez_case() { _ezcid=$1; _ezcb=$2; _ezcrc=$3; _ezcn=$4; shift 4 ez_run "$_ezcb" "$@" ez_said "$_ezcid" "$_ezcrc" "$_ezcn" } # ------------------------------------------------------------------ packaging if [ -x "$EZ" ]; then bst_ok ez-ipupdate/installed else bst_notok ez-ipupdate/installed "$EZ is not an executable file"; fi # The recipe's do_install:append creates this and FILES:${PN} claims it. It is # where a packaged deployment would keep its cache file; nothing in this suite # writes to it, because the write path is unreachable here (see the header). if [ -d "$STATEDIR" ]; then bst_ok ez-ipupdate/statedir else bst_notok ez-ipupdate/statedir "$STATEDIR is not a directory"; fi # ----------------------------------------------------------------- argv, help # One fork, four assertions. `usage:' carries argv[0], which is how the tool # prints it, so this also pins that the binary is being run under its packaged # path rather than a busybox applet of the same name. ez_run 20 "$EZ" --help ez_said ez-ipupdate/help-usage 0 'usage: /usr/bin/ez-ipupdate [options]' ez_said ez-ipupdate/help-service-list 0 'dyndns-static dyndns-custom' # THE OPTION THE TICKET MISTOOK FOR HELP. -h is --host and takes an argument. ez_said ez-ipupdate/help-host-option 0 '-h, --host ' ez_said ez-ipupdate/help-config-hint 0 'echo help | /usr/bin/ez-ipupdate -c -' # The version string is printed by both spellings and carries the $Id$ of the # 2001 source it was cut from, which is the strongest available statement that # the tree was not silently substituted. ez_run 20 "$EZ" --version ez_said ez-ipupdate/version-long 0 '- 3.0.11b7 - $Id: ez-ipupdate.c,v 1.48' ez_case ez-ipupdate/version-short 20 0 '- 3.0.11b7 -' "$EZ" -V ez_case ez-ipupdate/credits 20 0 'Angus Mackay' "$EZ" --credits ez_case ez-ipupdate/signalhelp 20 0 'HUP causes it to re-read its config file' "$EZ" --signalhelp # getopt's own refusals. A bare -h is the one that matters: it is an ERROR, not # help, and a build that silently accepted it would mean the option table had # been rewritten. ez_run 20 "$EZ" -h ez_said ez-ipupdate/argv-h-needs-arg 1 "option requires an argument -- 'h'" ez_said ez-ipupdate/argv-try-help 1 "Try \`/usr/bin/ez-ipupdate --help' for more information" ez_case ez-ipupdate/argv-unknown-long 20 1 "unrecognized option '--nosuchopt'" "$EZ" --nosuchopt # No arguments at all. DEF_SERVICE is `null', whose check_info() prompts on # stdin for a service type; with stdin at /dev/null it reads nothing and # parse_service("") refuses. Asserted because it is the shape a user hits first, # and because it is the one path that could BLOCK if stdin were a terminal. ez_run 20 "$EZ" ez_said ez-ipupdate/noargs-banner 1 'ez-ipupdate Version 3.0.11b7' ez_said ez-ipupdate/noargs-refused 1 'unknown service type:' # -------------------------------------------------- the config-file parser # `echo help | ez-ipupdate -c -' is the tool's own documented way to list the # config commands (the --help text says so, asserted above). It goes through a # pipe, so the redirect is the forked child's, never this shell's. ez_run 20 sh -c 'printf "%s\n" help | /usr/bin/ez-ipupdate -c -' ez_said ez-ipupdate/conf-help-unknown-command 1 '-,1: unknown command: help' ez_said ez-ipupdate/conf-help-command-table 1 'service-type usage: service-type=' ez_said ez-ipupdate/conf-help-names-the-file 1 'error parsing config file "-"' # An unknown KEY is the conf-file reader's refusal, and it names file and line. ez_run 20 sh -c 'printf "%s\n" not-a-command=1 | /usr/bin/ez-ipupdate -c -' ez_said ez-ipupdate/conf-unknown-key 1 '-,1: unknown command: not-a-command' # A known key with its argument missing is a DIFFERENT refusal, by name. ez_case ez-ipupdate/conf-key-needs-argument 20 1 'option "host" requires an argument' \ sh -c 'printf "%s\n" host | /usr/bin/ez-ipupdate -c -' # An unknown SERVICE TYPE is parse_service()'s refusal, and it prints the # supported list. Both halves, because the list is the part that would change if # a service were dropped in the port. ez_run 20 sh -c 'printf "%s\n" service-type=bogus | /usr/bin/ez-ipupdate -c -' ez_said ez-ipupdate/conf-unknown-service-type 1 'unknown service type: bogus' ez_said ez-ipupdate/conf-service-type-list 1 'null ezip pgpow dhs dyndns dyndns-static dyndns-custom ods' # A config file that is not there is reported as such, with strerror(3) text, # rather than treated as an empty one. ez_case ez-ipupdate/conf-absent-file 20 1 "could not open config file \"$W/nope\": No such file or directory" \ "$EZ" -c "$W/nope" # A well-formed file: a comment, a blank line, a leading-whitespace key. It # reaches the interface refusal, which is only possible once the whole file # parsed -- see the header. printf '# a comment\n\nservice-type=dyndns\n host=h.example.invalid\nuser=u:p\ninterface=nosuchif0\nquiet\n' > "$W/good.conf" ez_case ez-ipupdate/conf-comments-and-blanks 20 1 'could not resolve ip address for nosuchif0.' \ "$EZ" -c "$W/good.conf" # The same content on stdin through `-c -', which is a different reader path. ez_case ez-ipupdate/conf-stdin-dash 20 1 'could not resolve ip address for nosuchif0.' \ sh -c 'printf "%s\n" service-type=dyndns host=h.example.invalid user=u:p interface=nosuchif0 quiet | /usr/bin/ez-ipupdate -c -' # ------------------------------------------- every service type the build has # The list is --help's, which is services[] in ez-ipupdate.c. Each is written # into a config file and accepted -- proved by the run reaching the interface # refusal, which is downstream of parse_service(). No packet is sent for any of # them. for _st in ezip pgpow dhs dyndns dyndns-static dyndns-custom ods tzo \ easydns easydns-partner gnudip justlinux dyns hn zoneedit heipv6tb; do printf 'service-type=%s\nuser=u:p\nhost=h.example.invalid\ninterface=nosuchif0\nquiet\n' "$_st" > "$W/svc.conf" ez_case "ez-ipupdate/conf-accepts-$_st" 20 1 'could not resolve ip address for nosuchif0.' \ "$EZ" -c "$W/svc.conf" done # `null' is in the list and is NOT one of those: it is the no-service sentinel, # whose check_info() asks for a real service type. Its own case, with its own # expected refusal, rather than a hole in the loop above. printf 'service-type=null\nuser=u:p\nhost=h.example.invalid\ninterface=nosuchif0\nquiet\n' > "$W/null.conf" ez_case ez-ipupdate/conf-accepts-null 20 1 'unknown service type:' "$EZ" -c "$W/null.conf" # ------------------------------------------------------- the interface lookup # An interface that does not exist. Two cases from one run: the ioctl's own # perror line, whose errno text is the kernel's (MEASURED -- see the header: # the wasm32 kernel does answer ENODEV), and ez-ipupdate's own message, which # is the verdict. ez_run 20 "$EZ" -q -S dyndns -u u:p -h h.example.invalid -i nosuchif0 ez_said ez-ipupdate/iface-absent-ioctl 1 'ioctl(SIOCGIFADDR): No such device' ez_said ez-ipupdate/iface-absent-message 1 'could not resolve ip address for nosuchif0.' # lo IS THE POSITIVE HALF, and it is the only interface this machine has: the # kernel brings it up and auto-assigns 127.0.0.1/8, and no virtio-net device is # registered (the same fact 60-net-tools.test's ifconfig cases rest on). With a # cache file already holding 127.0.0.1, main() reads the cache, resolves lo # through SIOCGIFADDR, finds the two equal and declines to update -- so this one # case says the ioctl WORKED, the cache was read back, and the comparison that # is the whole point of a dynamic-DNS client came out right. rc is 1: retval is # initialised to 1 and this branch never clears it, which is upstream's # behaviour and is pinned rather than corrected. printf '1700000000,127.0.0.1\n' > "$W/lo.cache" ez_run 20 "$EZ" -q -S dyndns -u u:p -h h.example.invalid -i lo -b "$W/lo.cache" ez_said ez-ipupdate/iface-lo-resolved 1 'no update needed at this time' ez_said_not ez-ipupdate/iface-lo-no-ioctl-error 1 'ioctl(SIOCGIFADDR)' # ----------------------------------------------------------- the cache file # read_cache_file() wants `,'. A line with no comma is # refused in two voices -- the reader's own, and main()'s attribution of it -- # and both are asserted because the second one is what a user actually sees. # The attribution's SUFFIX is not asserted: main() prints # `errno == 0 ? "malformed entry" : strerror(errno)' on a path that never sets # errno, so what lands there is whatever the last libc call left behind. MEASURED # 2026-09-03: the x86-64 oracle carried errno 0 and said `malformed entry'; this # image carried ENOMEM and said `Cannot allocate memory'. Same branch, so the # case asserts the part that identifies it -- the file it could not read. # The run then continues to the interface refusal, which is what keeps it off # the network. printf 'this-line-has-no-comma\n' > "$W/bad.cache" ez_run 20 "$EZ" -q -S dyndns -u u:p -h h.example.invalid -b "$W/bad.cache" -i nosuchif0 ez_said ez-ipupdate/cache-malformed-reader 1 'malformed cache file' ez_said ez-ipupdate/cache-malformed-attributed 1 "error reading cache file \"$W/bad.cache\":" # AN ABSENT CACHE IS NOT AN ERROR. read_cache_file() returns 0 on ENOENT, so the # first run of a freshly installed ez-ipupdate must not complain about the file # it is about to create. The only way to say that is that nothing was said. rm -f "$W/absent.cache" ez_run 20 "$EZ" -q -S dyndns -u u:p -h h.example.invalid -b "$W/absent.cache" -i nosuchif0 ez_said_not ez-ipupdate/cache-absent-is-quiet 1 'error reading cache file' ez_said ez-ipupdate/cache-absent-proceeds 1 'could not resolve ip address for nosuchif0.' # A FAILED UPDATE LEAVES THE CACHE ALONE. write_cache_file() is guarded by # `retval == 0', so the file must be byte-identical after a run that could not # reach a provider. This is the half of the write path that is observable on a # machine with no dynamic-DNS service; creation is not, and the header says so. printf '1700000000,192.0.2.9\n' > "$W/stale.cache" ez_run 20 "$EZ" -S dyndns -u u:p -h h.example.invalid -i lo -b "$W/stale.cache" \ -s no-such-host.invalid -r 1 ez_said ez-ipupdate/cache-stale-triggers-update 1 'error connecting to no-such-host.invalid:80' check_eq ez-ipupdate/cache-untouched-on-failure '1700000000,192.0.2.9' "$(cat "$W/stale.cache" 2>/dev/null)" # ----------------------------------------------------------- the network legs # Neither of these sends a dynamic-DNS update; both assert that a run which # CANNOT ends promptly and says why. -q is deliberately absent: do_connect()'s # diagnostics are suppressed by it. # # 1. A name nothing resolves. This machine has no /etc/resolv.conf and no # libnss_dns.so, so no socket is ever connected. The herror(3) wording is the # resolver's, not ez-ipupdate's, and is therefore not asserted: MEASURED # 2026-09-03 this image answers TRY_AGAIN (`Host name lookup failure'), not # the HOST_NOT_FOUND (`Unknown host') a files-only NSS was predicted to give. # The case asserts that herror fired at all; ez-ipupdate's own line is printed # for any do_connect() failure and is what carries the verdict. ez_run 20 "$EZ" -S dyndns -u u:p -h h.example.invalid -a 192.0.2.1 \ -s no-such-host.invalid -r 1 ez_said ez-ipupdate/net-unresolvable-server 1 'error connecting to no-such-host.invalid:80' ez_said ez-ipupdate/net-unresolvable-herror 1 'gethostbyname:' # 2. A name that DOES resolve, to a port nothing listens on. Reaching # ECONNREFUSED is the proof that /etc/hosts answered and a real socket was # opened and connected -- the same reasoning 61-netcat.test uses. 45999 is # the port the sibling suites already treat as reliably dead. ez_run 20 "$EZ" -S dyndns -u u:p -h h.example.invalid -a 192.0.2.1 \ -s localhost:45999 -r 1 ez_said ez-ipupdate/net-resolved-then-refused 1 'connect: Connection refused' ez_said ez-ipupdate/net-refused-attributed 1 'error connecting to localhost:45999' # ------------------------------------------------------------- daemon refusals # Daemon mode is NOT entered: it forks, closes its descriptors and loops for # ever. What is asserted is the two argument checks that run before any of that, # each of which exits 1 with a documented message. ez_run 20 "$EZ" -d -S dyndns -u u:p -h h.example.invalid ez_said ez-ipupdate/daemon-needs-an-address 1 'you must provide either an interface or an address' ez_said ez-ipupdate/daemon-invalid-data 1 'invalid data to perform requested action.' ez_case ez-ipupdate/daemon-null-service 20 1 \ 'no compile time default service was set therefor you must specify a service type.' \ "$EZ" -d [ "$_ezown" = 1 ] && rm -rf "$W"