#!/usr/bin/env bash # Tests for newest_matching_file() in tools/install.sh. # # The function answers one question -- "which is the most recent backup / tarball # here?" -- and its callers act destructively on the answer. Three ways of getting it # wrong have already been found, and each has a case below: # # * `ls -1t | head -1` returns 141 under `set -o pipefail` once the listing fills a # pipe buffer (~1600 names), because head closes the pipe and ls takes SIGPIPE. # Callers assign it at top level under `set -e`, so a 141 aborts the run. # * `mapfile` is a Bash 4 builtin. macOS ships Bash 3.2 and the installer supports # Darwin, so the whole lookup was unavailable there -- and an empty answer is what # sends the uninstaller down its delete-the-destination branch. # * Any line-based parse of `ls` splits a filename containing a newline into two # wrong answers. # # The large-population and newline cases are the point: with two or three ordinary # names every version of this function passes, which is why the first two went # unnoticed. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-newest-match-test-XXXXXX")" trap 'rm -rf "$TMP"' EXIT # Load the function under test and the mtime helper it depends on, with the same # shell options install.sh runs under. eval "$(sed -n '/^_MTIME_STYLE=/,/^}/p' "$ROOT/tools/install.sh")" eval "$(sed -n '/^newest_matching_file()/,/^}/p' "$ROOT/tools/install.sh")" POPULATED="$TMP/many" mkdir -p "$POPULATED" # Enough names to overflow a 64 KiB pipe buffer several times over. for i in $(seq 1 5000); do : > "$POPULATED/mosaicstack-mosaic-0.0.${i}.tgz" done sleep 1 : > "$POPULATED/mosaicstack-mosaic-9.9.9.tgz" echo "[test] the newest match is returned from a directory large enough to fill a pipe" GOT="$(newest_matching_file "$POPULATED" 'mosaicstack-mosaic-*.tgz')" [[ "$(basename "$GOT")" == "mosaicstack-mosaic-9.9.9.tgz" ]] || { echo "expected the newest tarball, got '${GOT}'" >&2 exit 1 } echo "[test] a large population does not make the lookup fail" set +e newest_matching_file "$POPULATED" 'mosaicstack-mosaic-*.tgz' >/dev/null RC=$? set -e [[ "$RC" -eq 0 ]] || { echo "expected rc=0, got ${RC} (141 means the SIGPIPE regression is back)" >&2; exit 1; } echo "[test] a small population still works" SMALL="$TMP/few" mkdir -p "$SMALL" : > "$SMALL/mosaicstack-gateway-0.0.1.tgz" sleep 1 : > "$SMALL/mosaicstack-gateway-0.0.2.tgz" GOT="$(newest_matching_file "$SMALL" 'mosaicstack-gateway-*.tgz')" [[ "$(basename "$GOT")" == "mosaicstack-gateway-0.0.2.tgz" ]] || { echo "expected the newer gateway tarball, got '${GOT}'" >&2 exit 1 } echo "[test] a name containing a space is returned whole" SPACED="$TMP/spaced" mkdir -p "$SPACED" : > "$SPACED/agents.md.mosaic-bak-one two" GOT="$(newest_matching_file "$SPACED" 'agents.md.mosaic-bak-*')" [[ "$GOT" == "$SPACED/agents.md.mosaic-bak-one two" ]] || { echo "expected the spaced name intact, got '${GOT}'" >&2 exit 1 } echo "[test] a name containing a newline is returned whole, not split" # The old `ls -1t` parse reported this file as two separate shorter names, neither of # which exists -- so the caller saw a backup path that could not be restored. NEWLINE="$TMP/newline" mkdir -p "$NEWLINE" WEIRD="$NEWLINE/agents.md.mosaic-bak-$(printf 'a\nb')" : > "$WEIRD" GOT="$(newest_matching_file "$NEWLINE" 'agents.md.mosaic-bak-*')" [[ "$GOT" == "$WEIRD" ]] || { echo "expected the newline-containing name intact, got '${GOT}'" >&2 exit 1 } [[ -f "$GOT" ]] || { echo "the returned path does not name a real file" >&2; exit 1; } echo "[test] no match is an empty answer, not an error" EMPTY="$TMP/none" mkdir -p "$EMPTY" set +e GOT="$(newest_matching_file "$EMPTY" 'nothing-*.tgz')" RC=$? set -e [[ "$RC" -eq 0 && -z "$GOT" ]] || { echo "expected empty output and rc=0, got '${GOT}' rc=${RC}" >&2; exit 1; } echo "[test] a directory that does not exist is an empty answer, not an error" set +e GOT="$(newest_matching_file "$TMP/absent" 'nothing-*.tgz')" RC=$? set -e [[ "$RC" -eq 0 && -z "$GOT" ]] || { echo "expected empty output and rc=0, got '${GOT}' rc=${RC}" >&2; exit 1; } echo "[test] an unanswerable lookup fails loudly instead of reporting no match" # This is the distinction the uninstaller depends on. "No backup exists" is licence to # delete the destination; "I could not tell" must never reach that branch. _MTIME_STYLE=none set +e GOT="$(newest_matching_file "$SMALL" 'mosaicstack-gateway-*.tgz')" RC=$? set -e _MTIME_STYLE="" [[ "$RC" -ne 0 ]] || { echo "expected a non-zero rc when no mtime source is usable, got rc=0 output '${GOT}'" >&2 exit 1 } echo "[test] the installer uses no Bash 4 syntax" # A lint, not an execution test: this host has no Bash 3.2 to run under. It is still # the thing that stops the regression, because every Bash 4 construct that has broken # macOS here was introduced by someone who never ran the script there either. # Comments are stripped first -- the ones above name these constructs on purpose. BASH4_HITS="$( sed 's/#.*$//' "$ROOT/tools/install.sh" \ | grep -nE '(^|[^[:alnum:]_])(mapfile|readarray)([^[:alnum:]_]|$)|declare[[:space:]]+-[a-zA-Z]*A|local[[:space:]]+-[a-zA-Z]*A|\$\{[A-Za-z_][A-Za-z0-9_]*(\^\^|,,)' \ || true )" [[ -z "$BASH4_HITS" ]] || { echo "tools/install.sh uses Bash 4+ syntax, which macOS's Bash 3.2 cannot run:" >&2 echo "$BASH4_HITS" >&2 exit 1 } echo "[test] newest_matching_file tests passed"