#!/usr/bin/env bash # lib/assert.sh — self-asserting shell helper library (C11 core) # # Foundation library sourced by every test workflow (T4-T13). Provides uniform # [PASS]/[FAIL] logging to stderr and explicit non-zero return on failure. # # Design contract: # * All assert_* functions log [PASS]/[FAIL] to STDERR (never stdout). # * On success: print [PASS] to stderr and `return 0` (caller continues). # * On failure: print [FAIL] to stderr and `return 1` (caller decides to exit). # * sha256_of is the ONLY function that writes to stdout (the bare hash). # * The library never sets `set -e`; the caller owns flow control. # * Missing arguments always fail loudly — assertions never silently pass. # # Usage: # source lib/assert.sh # assert_eq "$got" "$want" "my note" # if ! assert_exists "./build/out"; then exit 1; fi # --------------------------------------------------------------------------- # Bootstrap: this library relies on bash-specific features (`[[ ]]`, `=~`). # Detect bash early; if absent, fail loudly. This guard itself is written in # POSIX-compatible syntax so it parses under any shell. # --------------------------------------------------------------------------- if [ -z "${BASH_VERSION:-}" ]; then printf '[FAIL] assert.sh: requires bash, but BASH_VERSION is unset (current shell is not bash)\n' >&2 exit 1 fi # --------------------------------------------------------------------------- # Internal logging helpers. Centralised so the [PASS]/[FAIL] format is uniform # and grep-friendly across every assert function. # $1 - assert function name (e.g. assert_eq) # $2 - caller-supplied note # $3 - human-readable detail (actual vs expected, etc.) # --------------------------------------------------------------------------- __assert_pass() { printf '[PASS] %s: %s (%s)\n' "$1" "$2" "$3" >&2 } __assert_fail() { printf '[FAIL] %s: %s (%s)\n' "$1" "$2" "$3" >&2 } # --------------------------------------------------------------------------- # assert_eq # Pass when the two strings are byte-identical. # --------------------------------------------------------------------------- assert_eq() { if [ "$#" -lt 3 ]; then printf '[FAIL] assert_eq: missing arguments (usage: assert_eq ; got %d args)\n' "$#" >&2 return 1 fi local actual="$1" expected="$2" note="$3" if [ "$actual" = "$expected" ]; then __assert_pass "assert_eq" "$note" "'$actual' == '$expected'" return 0 fi __assert_fail "assert_eq" "$note" "got '$actual', expected '$expected'" return 1 } # --------------------------------------------------------------------------- # assert_contains # Pass when appears as a substring of . # --------------------------------------------------------------------------- assert_contains() { if [ "$#" -lt 3 ]; then printf '[FAIL] assert_contains: missing arguments (usage: assert_contains ; got %d args)\n' "$#" >&2 return 1 fi local haystack="$1" needle="$2" note="$3" if [[ "$haystack" == *"$needle"* ]]; then __assert_pass "assert_contains" "$note" "'$needle' found in '$haystack'" return 0 fi __assert_fail "assert_contains" "$note" "'$needle' not found in '$haystack'" return 1 } # --------------------------------------------------------------------------- # assert_exists [note] # Pass when exists on the filesystem (any type: file/dir/symlink). # --------------------------------------------------------------------------- assert_exists() { if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then printf '[FAIL] assert_exists: missing arguments (usage: assert_exists ; got %d args)\n' "$#" >&2 return 1 fi local path="$1" local note="${2:-$path}" if [ -e "$path" ]; then __assert_pass "assert_exists" "$note" "'$path' exists" return 0 fi __assert_fail "assert_exists" "$note" "'$path' does not exist" return 1 } # --------------------------------------------------------------------------- # assert_not_exists [note] # Pass when does NOT exist on the filesystem. # --------------------------------------------------------------------------- assert_not_exists() { if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then printf '[FAIL] assert_not_exists: missing arguments (usage: assert_not_exists ; got %d args)\n' "$#" >&2 return 1 fi local path="$1" local note="${2:-$path}" if [ ! -e "$path" ]; then __assert_pass "assert_not_exists" "$note" "'$path' absent" return 0 fi __assert_fail "assert_not_exists" "$note" "'$path' exists (expected absent)" return 1 } # --------------------------------------------------------------------------- # assert_match # Pass when matches the bash extended regex via `[[ =~ ]]`. # Bash-specific — hence the bootstrap guard at the top of this file. # --------------------------------------------------------------------------- assert_match() { if [ "$#" -lt 3 ]; then printf '[FAIL] assert_match: missing arguments (usage: assert_match ; got %d args)\n' "$#" >&2 return 1 fi local actual="$1" regex="$2" note="$3" if [[ "$actual" =~ $regex ]]; then __assert_pass "assert_match" "$note" "'$actual' =~ /$regex/" return 0 fi __assert_fail "assert_match" "$note" "'$actual' !~ /$regex/" return 1 } # --------------------------------------------------------------------------- # sha256_of # Print JUST the sha256 hex digest of to stdout (capturable via # `$(sha256_of file)`). Prefers sha256sum, falls back to `shasum -a 256`. # This is the only function that writes to stdout; all errors go to stderr. # --------------------------------------------------------------------------- sha256_of() { if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then printf '[FAIL] sha256_of: missing arguments (usage: sha256_of ; got %d args)\n' "$#" >&2 return 1 fi local path="$1" if [ ! -f "$path" ]; then printf '[FAIL] sha256_of: not a regular file: '%s'\n' "$path" >&2 return 1 fi local line if command -v sha256sum >/dev/null 2>&1; then line=$(sha256sum "$path") || { printf '[FAIL] sha256_of: sha256sum failed for '%s'\n' "$path" >&2; return 1; } elif command -v shasum >/dev/null 2>&1; then line=$(shasum -a 256 "$path") || { printf '[FAIL] sha256_of: shasum failed for '%s'\n' "$path" >&2; return 1; } else printf '[FAIL] sha256_of: neither sha256sum nor shasum is available\n' >&2 return 1 fi # sha256sum/shasum both emit " "; strip everything from the # first space onward to yield the bare digest. printf '%s\n' "${line%% *}" return 0 }