#!/usr/bin/env bash # Minimal test framework. Source this file; do not execute it directly. # # Globals maintained per-process: # _PASS — count of passing assertions # _FAIL — count of failing assertions _PASS=0 _FAIL=0 # Color codes (self-contained; do not rely on colors.sh) _R='\033[0;31m' # red _G='\033[0;32m' # green _Y='\033[1;33m' # yellow _B='\033[1m' # bold _D='\033[2m' # dim _N='\033[0m' # reset # suite # Print a section header for a group of related tests. suite() { printf "\n${_B}%s${_N}\n" "$1" } # pass / fail — low-level counters; prefer assert_* helpers instead pass() { _PASS=$((_PASS + 1)) printf " ${_G}✓${_N} %s\n" "$1" } fail() { _FAIL=$((_FAIL + 1)) printf " ${_R}✗${_N} %s\n" "$1" [[ -n "${2:-}" ]] && printf " expected: %s\n" "$2" [[ -n "${3:-}" ]] && printf " got: %s\n" "${3:-}" } # --------------------------------------------------------------------------- # Assertion helpers # --------------------------------------------------------------------------- assert_eq() { local expected="$1" actual="$2" msg="${3:-assert_eq}" if [[ "$expected" == "$actual" ]]; then pass "$msg" else fail "$msg" "$expected" "$actual" fi } assert_not_eq() { local unexpected="$1" actual="$2" msg="${3:-assert_not_eq}" if [[ "$unexpected" != "$actual" ]]; then pass "$msg" else fail "$msg (expected values to differ)" "$unexpected" "$actual" fi } assert_empty() { local actual="$1" msg="${2:-assert_empty}" if [[ -z "$actual" ]]; then pass "$msg" else fail "$msg (expected empty)" "" "$actual" fi } assert_not_empty() { local actual="$1" msg="${2:-assert_not_empty}" if [[ -n "$actual" ]]; then pass "$msg" else fail "$msg (expected non-empty)" fi } assert_contains() { local needle="$1" haystack="$2" msg="${3:-assert_contains}" if [[ "$haystack" == *"$needle"* ]]; then pass "$msg" else fail "$msg" "*${needle}*" "$haystack" fi } # assert_success [args...] # Passes when the command exits 0. assert_success() { local msg="$1"; shift local rc=0 "$@" 2>/dev/null || rc=$? if [[ $rc -eq 0 ]]; then pass "$msg" else fail "$msg (expected exit 0, got $rc)" fi } # assert_failure [args...] # Passes when the command exits non-zero. assert_failure() { local msg="$1"; shift local rc=0 "$@" 2>/dev/null || rc=$? if [[ $rc -ne 0 ]]; then pass "$msg" else fail "$msg (expected non-zero exit)" fi } assert_file_exists() { local file="$1" msg="${2:-assert_file_exists}" if [[ -f "$file" ]]; then pass "$msg" else fail "$msg" "" "(not found)" fi } assert_file_contains() { local pattern="$1" file="$2" msg="${3:-assert_file_contains}" if [[ ! -f "$file" ]]; then fail "$msg (file not found: $file)" return fi if grep -qF "$pattern" "$file"; then pass "$msg" else fail "$msg" "$pattern" "(not found in $file)" fi } assert_file_not_contains() { local pattern="$1" file="$2" msg="${3:-assert_file_not_contains}" if [[ ! -f "$file" ]]; then fail "$msg (file not found: $file)" return fi if ! grep -qF "$pattern" "$file"; then pass "$msg" else fail "$msg" "(should not contain $pattern)" "(found in $file)" fi } # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- # print_summary # Print pass/fail totals and exit 1 if any tests failed. print_summary() { local total=$((_PASS + _FAIL)) printf "\n" if [[ $_FAIL -eq 0 ]]; then printf "${_G}%d/%d passed${_N}\n" "$_PASS" "$total" else printf "${_R}%d failed${_N}, %d passed, %d total\n" "$_FAIL" "$_PASS" "$total" fi [[ $_FAIL -eq 0 ]] }