#!/usr/bin/env bash set -euo pipefail : "${TEST_REPO_ROOT:=${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}" : "${TEST_SHURIKEN:=${SHURIKEN:-$TEST_REPO_ROOT/bin/shuriken}}" : "${TEST_IMAGEMAGICK:=magick}" : "${TEST_TMPDIR:=}" test::setup() { TEST_TMPDIR=$(mktemp -d) } test::teardown() { if [ -n "$TEST_TMPDIR" ]; then rm -rf "$TEST_TMPDIR" TEST_TMPDIR='' fi } test::run_case() { local -r description="$1"; shift local output_file local -i status=0 output_file=$(mktemp) set +e ( set -euo pipefail; trap test::teardown EXIT; "$@" ) \ > "$output_file" 2>&1 status=$? set -e if (( status != 0 )); then echo "FAIL: $description" >&2 cat "$output_file" >&2 rm -f "$output_file" exit 1 fi rm -f "$output_file" } # Runs a shuriken action body in an isolated subshell and returns its status. # # Production runs action bodies in-process (see run_configured_action_body in # src/lib/action.source.sh). A few action bodies (notably generate_staged) flip # errexit back on internally and "return" a non-zero status while errexit is # active, which aborts the *caller's* shell on failure. In production that is # correct: main runs under "set -euo pipefail" and a generation failure should # end the process. A handful of tests, however, need to capture that failure # status and keep asserting afterwards. They used to get isolation for free from # the old serialized-subprocess runner; now that production runs in-process, this # test-only shim provides the minimal "run it where an abort can't kill me" # wrapper. The subshell catches any errexit/trap-driven abort, so the caller can # read the status with "$?". test::run_action_isolated() { ( "$@" ) } test::assert_failure() { local -r description="$1"; shift local output_file local -i status=0 output_file=$(mktemp) set +e "$@" > "$output_file" 2>&1 status=$? set -e if (( status == 0 )); then echo "FAIL: $description" >&2 cat "$output_file" >&2 rm -f "$output_file" exit 1 fi rm -f "$output_file" } test::capture_failure_output() { local output local output_file local -i status=0 output_file=$(mktemp) set +e "$@" > "$output_file" 2>&1 status=$? set -e if (( status == 0 )); then echo 'FAIL: expected command to fail' >&2 output=$(<"$output_file") echo "$output" >&2 rm -f "$output_file" exit 1 fi output=$(<"$output_file") rm -f "$output_file" printf '%s\n' "$output" } test::assert_contains() { local -r needle="$1"; shift local -r haystack="$1"; shift if [[ "$haystack" != *"$needle"* ]]; then echo "FAIL: expected output to contain $needle" >&2 echo "$haystack" >&2 exit 1 fi } test::assert_not_contains() { local -r needle="$1"; shift local -r haystack="$1"; shift if [[ "$haystack" == *"$needle"* ]]; then echo "FAIL: expected output not to contain $needle" >&2 echo "$haystack" >&2 exit 1 fi } test::assert_contains_before() { local -r first="$1"; shift local -r second="$1"; shift local -r haystack="$1"; shift local before_first before_first="${haystack%%"$first"*}" if [[ "$haystack" != *"$first"* \ || "$haystack" != *"$second"* \ || "$before_first" == *"$second"* ]]; then echo "FAIL: expected $first to appear before $second" >&2 echo "$haystack" >&2 exit 1 fi } test::assert_file_exists() { local -r path="$1"; shift if [ ! -f "$path" ]; then echo "FAIL: expected file $path to exist" >&2 exit 1 fi } test::assert_dir_exists() { local -r path="$1"; shift if [ ! -d "$path" ]; then echo "FAIL: expected directory $path to exist" >&2 exit 1 fi } test::assert_path_absent() { local -r path="$1"; shift if [ -e "$path" ]; then echo "FAIL: expected $path to be absent" >&2 exit 1 fi } test::assert_find_count() { local -r expected="$1"; shift local -r dir="$1"; shift local -r name="$1"; shift local actual actual=$(find "$dir" -maxdepth 1 -type f -name "$name" | wc -l) if [ "$actual" -ne "$expected" ]; then echo "FAIL: expected $expected files matching $name in $dir" >&2 echo "found $actual" >&2 exit 1 fi } test::assert_no_staging_dirs() { local -r dir="$1"; shift local found found=$(find "$dir" -type d -name '.shuriken.*' -print -quit) if [ -n "$found" ]; then echo "FAIL: expected no staging directories under $dir" >&2 echo "found $found" >&2 exit 1 fi } test::run_shuriken() { "$TEST_SHURIKEN" "$@" 2>&1 } test::install_fake_imagemagick() { local -r bin_dir="$1"; shift mkdir -p "$bin_dir" TEST_IMAGEMAGICK="$bin_dir/magick" cat > "$bin_dir/magick" <<'MAGICK' #!/usr/bin/env bash set -euo pipefail if [ "${1:-}" = identify ]; then if [ -n "${TEST_IMAGEMAGICK_IDENTIFY_LOG:-}" ]; then printf '%s\n' "$*" >> "$TEST_IMAGEMAGICK_IDENTIFY_LOG" fi # Simulate a corrupt photo / identify failure when the fixture matches the # configured failing target (or "*" for any photo). Emit an error to stderr # and exit non-zero so callers can exercise the failure path. if [ -n "${TEST_IMAGEMAGICK_IDENTIFY_FAIL:-}" ]; then target="${@: -1}" if [ "$TEST_IMAGEMAGICK_IDENTIFY_FAIL" = '*' ] \ || [[ "$target" == *"$TEST_IMAGEMAGICK_IDENTIFY_FAIL"* ]]; then printf 'identify: corrupt image\n' >&2 exit 1 fi fi if [ -n "${TEST_IMAGEMAGICK_IDENTIFY_OUTPUT:-}" ]; then printf '%s\n' "$TEST_IMAGEMAGICK_IDENTIFY_OUTPUT" fi exit 0 fi dest="${@: -1}" mkdir -p "$(dirname "$dest")" { printf 'fake image\n' printf 'args:' printf ' %q' "$@" printf '\n' } > "$dest" MAGICK chmod 0755 "$bin_dir/magick" cp "$bin_dir/magick" "$bin_dir/convert" } test::install_parallel_imagemagick_spy() { local -r bin_dir="$1"; shift mkdir -p "$bin_dir" cat > "$bin_dir/magick" <<'MAGICK' #!/usr/bin/env bash set -euo pipefail if [ "${1:-}" = identify ]; then exit 0 fi lock_file="${TEST_PARALLEL_MAGICK_LOCK:?}" active_file="${TEST_PARALLEL_MAGICK_ACTIVE:?}" max_file="${TEST_PARALLEL_MAGICK_MAX:?}" log_file="${TEST_PARALLEL_MAGICK_LOG:?}" ( flock 9 active=0 if [ -f "$active_file" ]; then active=$(<"$active_file") fi active=$(( active + 1 )) printf '%s\n' "$active" > "$active_file" max=0 if [ -f "$max_file" ]; then max=$(<"$max_file") fi if (( active > max )); then printf '%s\n' "$active" > "$max_file" fi printf 'start %s %s\n' "$active" "$*" >> "$log_file" ) 9>"$lock_file" sleep 0.1 dest="${@: -1}" mkdir -p "$(dirname "$dest")" { printf 'fake image\n' printf 'args:' printf ' %q' "$@" printf '\n' } > "$dest" ( flock 9 active=$(<"$active_file") active=$(( active - 1 )) printf '%s\n' "$active" > "$active_file" printf 'finish %s %s\n' "$active" "$*" >> "$log_file" ) 9>"$lock_file" MAGICK chmod 0755 "$bin_dir/magick" cp "$bin_dir/magick" "$bin_dir/convert" } test::install_wait_n_imagemagick_spy() { local -r bin_dir="$1"; shift mkdir -p "$bin_dir" cat > "$bin_dir/magick" <<'MAGICK' #!/usr/bin/env bash set -euo pipefail if [ "${1:-}" = identify ]; then exit 0 fi lock_file="${TEST_WAIT_N_MAGICK_LOCK:?}" log_file="${TEST_WAIT_N_MAGICK_LOG:?}" dest="${@: -1}" dest_name="$(basename "$dest")" dest_dir="$(basename "$(dirname "$dest")")" dest_label="$dest_dir/$dest_name" ( flock 9 printf 'start %s\n' "$dest_label" >> "$log_file" ) 9>"$lock_file" if [ "$dest_label" = 'photos/01.jpg' ]; then sleep "${TEST_WAIT_N_SLOW_SECONDS:-0.4}" else sleep "${TEST_WAIT_N_FAST_SECONDS:-0.02}" fi mkdir -p "$(dirname "$dest")" { printf 'fake image\n' printf 'args:' printf ' %q' "$@" printf '\n' } > "$dest" ( flock 9 printf 'finish %s\n' "$dest_label" >> "$log_file" ) 9>"$lock_file" MAGICK chmod 0755 "$bin_dir/magick" cp "$bin_dir/magick" "$bin_dir/convert" } test::install_parallel_identify_spy() { local -r bin_dir="$1"; shift mkdir -p "$bin_dir" cat > "$bin_dir/magick" <<'MAGICK' #!/usr/bin/env bash set -euo pipefail if [ "${1:-}" = identify ]; then lock_file="${TEST_PARALLEL_IDENTIFY_LOCK:?}" active_file="${TEST_PARALLEL_IDENTIFY_ACTIVE:?}" max_file="${TEST_PARALLEL_IDENTIFY_MAX:?}" log_file="${TEST_PARALLEL_IDENTIFY_LOG:?}" ( flock 9 active=0 if [ -f "$active_file" ]; then active=$(<"$active_file") fi active=$(( active + 1 )) printf '%s\n' "$active" > "$active_file" max=0 if [ -f "$max_file" ]; then max=$(<"$max_file") fi if (( active > max )); then printf '%s\n' "$active" > "$max_file" fi printf 'start %s %s\n' "$active" "$*" >> "$log_file" ) 9>"$lock_file" sleep 0.1 printf ' exif:Make: ParallelCam\n' ( flock 9 active=$(<"$active_file") active=$(( active - 1 )) printf '%s\n' "$active" > "$active_file" printf 'finish %s %s\n' "$active" "$*" >> "$log_file" ) 9>"$lock_file" exit 0 fi dest="${@: -1}" mkdir -p "$(dirname "$dest")" { printf 'fake image\n' printf 'args:' printf ' %q' "$@" printf '\n' } > "$dest" MAGICK chmod 0755 "$bin_dir/magick" cp "$bin_dir/magick" "$bin_dir/convert" } test::write_parallel_template_spy_file() { local -r template_path="$1"; shift local -r template_label="$1"; shift local -r lock_file="$1"; shift local -r active_file="$1"; shift local -r max_file="$1"; shift local -r log_file="$1"; shift { printf 'template_label=%q\n' "$template_label" printf 'lock_file=%q\n' "$lock_file" printf 'active_file=%q\n' "$active_file" printf 'max_file=%q\n' "$max_file" printf 'log_file=%q\n' "$log_file" cat <<'TEMPLATE' ( flock 9 active=0 if [ -f "$active_file" ]; then active=$(<"$active_file") fi active=$(( active + 1 )) printf '%s\n' "$active" > "$active_file" max=0 if [ -f "$max_file" ]; then max=$(<"$max_file") fi if (( active > max )); then printf '%s\n' "$active" > "$max_file" fi printf 'start %s %s %s\n' \ "$active" "$template_label" "$render_photo_html" >> "$log_file" ) 9>"$lock_file" sleep "${TEST_TEMPLATE_SPY_SLEEP_SECONDS:-0.1}" printf '