#!/usr/bin/env bash
# src/system/index.sh

# src/system/check_os.sh

_BASHUNIT_OS="Unknown"
_BASHUNIT_DISTRO="Unknown"

function bashunit::check_os::init() {
  _BASHUNIT_UNAME="$(uname)"
  if bashunit::check_os::is_linux; then
    _BASHUNIT_OS="Linux"
    if bashunit::check_os::is_ubuntu; then
      _BASHUNIT_DISTRO="Ubuntu"
    elif bashunit::check_os::is_alpine; then
      _BASHUNIT_DISTRO="Alpine"
    elif bashunit::check_os::is_nixos; then
      _BASHUNIT_DISTRO="NixOS"
    else
      _BASHUNIT_DISTRO="Other"
    fi
  elif bashunit::check_os::is_macos; then
    _BASHUNIT_OS="OSX"
  elif bashunit::check_os::is_windows; then
    _BASHUNIT_OS="Windows"
  else
    _BASHUNIT_OS="Unknown"
    _BASHUNIT_DISTRO="Unknown"
  fi
}

function bashunit::check_os::is_ubuntu() {
  command -v apt >/dev/null 2>&1
}

function bashunit::check_os::is_alpine() {
  command -v apk >/dev/null 2>&1
}

function bashunit::check_os::is_nixos() {
  [ -f /etc/NIXOS ] && return 0
  grep -q '^ID=nixos' /etc/os-release 2>/dev/null
}

function bashunit::check_os::is_linux() {
  [ "$_BASHUNIT_UNAME" = "Linux" ]
}

function bashunit::check_os::is_macos() {
  [ "$_BASHUNIT_UNAME" = "Darwin" ]
}

function bashunit::check_os::is_windows() {
  case "$_BASHUNIT_UNAME" in
  *MINGW* | *MSYS* | *CYGWIN*)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function bashunit::check_os::nproc() {
  local cores=""
  cores="$(nproc 2>/dev/null)" || cores=""
  if [ -z "$cores" ]; then
    cores="$(sysctl -n hw.ncpu 2>/dev/null)" || cores=""
  fi
  if [ -z "$cores" ]; then
    cores="$(getconf _NPROCESSORS_ONLN 2>/dev/null)" || cores=""
  fi
  cores="${cores%% *}"
  case "$cores" in
  '' | *[!0-9]*) cores=4 ;;
  esac
  [ "$cores" -lt 1 ] && cores=4
  echo "$cores"
}

function bashunit::check_os::is_busybox() {

  case "$_BASHUNIT_DISTRO" in

  "Alpine")
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

bashunit::check_os::init

export _BASHUNIT_OS
export _BASHUNIT_DISTRO
export -f bashunit::check_os::nproc
export -f bashunit::check_os::is_alpine
export -f bashunit::check_os::is_busybox
export -f bashunit::check_os::is_ubuntu
export -f bashunit::check_os::is_nixos

# src/system/dependencies.sh
set -euo pipefail

function bashunit::dependencies::has_perl() {
  command -v perl >/dev/null 2>&1
}

function bashunit::dependencies::has_powershell() {
  command -v powershell >/dev/null 2>&1
}

function bashunit::dependencies::has_bc() {
  command -v bc >/dev/null 2>&1
}

function bashunit::dependencies::has_awk() {
  command -v awk >/dev/null 2>&1
}

function bashunit::dependencies::has_git() {
  command -v git >/dev/null 2>&1
}

function bashunit::dependencies::has_curl() {
  command -v curl >/dev/null 2>&1
}

function bashunit::dependencies::has_wget() {
  command -v wget >/dev/null 2>&1
}

function bashunit::dependencies::has_python() {
  command -v python >/dev/null 2>&1
}

function bashunit::dependencies::has_node() {
  command -v node >/dev/null 2>&1
}

function bashunit::dependencies::has_tput() {
  command -v tput >/dev/null 2>&1
}

# src/system/io.sh

function bashunit::io::file_size() {
  local bytes
  bytes=$(wc -c <"$1" 2>/dev/null | tr -d ' ') || bytes=""
  if [ -z "$bytes" ]; then
    bytes="unknown"
  fi
  printf '%s' "$bytes"
}

function bashunit::io::clear_screen() {
  if bashunit::dependencies::has_tput; then
    local out
    out=$(tput clear 2>/dev/null)
    if [ -n "$out" ]; then
      printf '%s' "$out"
      return
    fi
  fi
  printf '\033[2J\033[H'
}

function bashunit::io::download_to() {
  local url="$1"
  local output="$2"
  if bashunit::dependencies::has_curl; then
    curl -fsSL -o "$output" "$url"
  elif bashunit::dependencies::has_wget; then
    wget -q -O "$output" "$url"
  else
    echo "no curl or wget available" >&2
    return 1
  fi
}

# src/util/index.sh

# src/util/str.sh

_BASHUNIT_STR_STRIPPED_OUT=""

function bashunit::random_str() {
  local length=${1:-6}
  local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  local str=''
  local i
  for ((i = 0; i < length; i++)); do
    str="$str${chars:RANDOM%${#chars}:1}"
  done
  echo "$str"
}

function bashunit::str::strip_ansi_to_slot() {
  local input="$1"

  case "$input" in
  *\\* | *[[:cntrl:]]*) ;;
  *)
    _BASHUNIT_STR_STRIPPED_OUT=$input
    return
    ;;
  esac

  case "$input" in
  *\\*) ;;
  *)
    if [ "${#input}" -le 1024 ]; then
      local out="" rest="$input" params
      while :; do
        case "$rest" in
        *$'\x1b'\[*)
          out="$out${rest%%$'\x1b'\[*}"
          rest="${rest#*$'\x1b'\[}"
          params=""
          while :; do
            case "$rest" in
            [0-9\;]*)
              params="$params${rest%"${rest#?}"}"
              rest="${rest#?}"
              ;;
            *) break ;;
            esac
          done
          case "$rest" in

          m* | K*) rest="${rest#?}" ;;
          *) out="${out}[${params}" ;;
          esac
          ;;
        *)
          out="$out$rest"
          break
          ;;
        esac
      done
      _BASHUNIT_STR_STRIPPED_OUT=${out//[[:cntrl:]]/}
      return
    fi
    ;;
  esac

  _BASHUNIT_STR_STRIPPED_OUT=$(printf '%s' "$input" | sed -E 's/\x1B\[[0-9;]*[mK]//g; s/[[:cntrl:]]//g')
}

function bashunit::str::strip_ansi() {
  bashunit::str::strip_ansi_to_slot "$1"
  echo "$_BASHUNIT_STR_STRIPPED_OUT"
}

function bashunit::str::rpad() {
  local left_text="$1"
  local right_word="$2"
  local width_padding="${3:-$TERMINAL_WIDTH}"

  local padding=$((width_padding - ${#right_word} - 1))
  if ((padding < 0)); then
    padding=0
  fi

  bashunit::str::strip_ansi_to_slot "$left_text"
  local clean_left_text=$_BASHUNIT_STR_STRIPPED_OUT

  local is_truncated=false

  if [ ${#clean_left_text} -gt $padding ]; then
    local truncation_length=$((padding < 3 ? 0 : padding - 3))
    clean_left_text="${clean_left_text:0:$truncation_length}"
    is_truncated=true
  fi

  local result_left_text
  local remaining_space
  if $is_truncated; then

    result_left_text=""
    local i=0
    local j=0
    while [ $i -lt ${#clean_left_text} ] && [ $j -lt ${#left_text} ]; do
      local char="${clean_left_text:$i:1}"
      local original_char="${left_text:$j:1}"

      if [ "$original_char" = $'\x1b' ]; then
        while [ "${left_text:$j:1}" != "m" ] && [ $j -lt ${#left_text} ]; do
          result_left_text="$result_left_text${left_text:$j:1}"
          ((++j))
        done
        result_left_text="$result_left_text${left_text:$j:1}"
        ((++j))
      elif [ "$char" = "$original_char" ]; then

        result_left_text="$result_left_text$char"
        ((++i))
        ((++j))
      else
        ((++j))
      fi
    done
    result_left_text="$result_left_text..."

    remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1 - 3))
  else

    result_left_text="$left_text"
    remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1))
  fi

  if [ $remaining_space -lt 0 ]; then
    remaining_space=0
  fi

  printf "%s%${remaining_space}s %s\n" "$result_left_text" "" "$right_word"
}

function bashunit::str::html_escape() {
  printf '%s' "$1" | awk '{
    gsub(/&/, "\\&amp;")
    gsub(/</, "\\&lt;")
    gsub(/>/, "\\&gt;")
    gsub(/"/, "\\&quot;")
    print
  }'
}

# src/util/math.sh

function bashunit::math::calculate() {
  local expr="$*"

  if bashunit::dependencies::has_bc; then
    echo "$expr" | bc
    return
  fi

  case "$expr" in
  *.*)
    if bashunit::dependencies::has_awk; then
      awk "BEGIN { print ($expr) }"
      return
    fi

    expr=$(echo "$expr" | sed -E 's/([0-9]+)\.[0-9]+/\1/g')
    ;;
  esac

  case "$expr" in
  0[0-9]* | *[!0-9.]0[0-9]*)
    expr=$(echo "$expr" | sed -E 's/\b0*([1-9][0-9]*)/\1/g')
    ;;
  esac

  local result=$((expr))
  echo "$result"
}

_BASHUNIT_MATH_PADDED_OUT=""

function bashunit::math::pad_to_slot() {
  local value=$1
  local places=$2

  case "$value" in
  *.*) ;;
  *) value="${value}." ;;
  esac

  local frac=${value#*.}
  while [ ${#frac} -lt "$places" ]; do
    frac="${frac}0"
  done

  _BASHUNIT_MATH_PADDED_OUT="${value%%.*}.$frac"
}

_BASHUNIT_MATH_DECIMALS_OUT=0

function bashunit::math::decimals_to_slot() {
  local frac
  case "$1" in
  *.*)
    frac=${1#*.}
    _BASHUNIT_MATH_DECIMALS_OUT=${#frac}
    ;;
  *) _BASHUNIT_MATH_DECIMALS_OUT=0 ;;
  esac
}

_BASHUNIT_MATH_SCALED_L_OUT=""
_BASHUNIT_MATH_SCALED_R_OUT=""

function bashunit::math::scale_pair_to_slots() {
  local left=$1 right=$2

  case "$left$right" in
  '' | *[!0-9.+-]* | *e* | *E*) return 1 ;;
  esac

  local left_sign=1 right_sign=1
  case "$left" in
  -*) left_sign=-1 left=${left#-} ;;
  +*) left=${left#+} ;;
  esac
  case "$right" in
  -*) right_sign=-1 right=${right#-} ;;
  +*) right=${right#+} ;;
  esac

  case "$left$right" in
  *-* | *+*) return 1 ;;
  esac

  local left_int left_frac right_int right_frac
  case "$left" in
  *.*) left_int=${left%%.*} left_frac=${left#*.} ;;
  *) left_int=$left left_frac="" ;;
  esac
  case "$right" in
  *.*) right_int=${right%%.*} right_frac=${right#*.} ;;
  *) right_int=$right right_frac="" ;;
  esac

  case "$left_int$left_frac$right_int$right_frac" in
  *.*) return 1 ;;
  esac

  left_int=${left_int:-0}
  right_int=${right_int:-0}

  while [ ${#left_frac} -lt ${#right_frac} ]; do left_frac="${left_frac}0"; done
  while [ ${#right_frac} -lt ${#left_frac} ]; do right_frac="${right_frac}0"; done

  if [ $((${#left_int} + ${#left_frac})) -gt 18 ] ||
    [ $((${#right_int} + ${#right_frac})) -gt 18 ]; then
    return 1
  fi

  while [ ${#left_int} -gt 1 ]; do
    case "$left_int" in 0*) left_int=${left_int#0} ;; *) break ;; esac
  done
  while [ ${#right_int} -gt 1 ]; do
    case "$right_int" in 0*) right_int=${right_int#0} ;; *) break ;; esac
  done
  local left_frac_value=${left_frac:-0} right_frac_value=${right_frac:-0}
  while [ ${#left_frac_value} -gt 1 ]; do
    case "$left_frac_value" in 0*) left_frac_value=${left_frac_value#0} ;; *) break ;; esac
  done
  while [ ${#right_frac_value} -gt 1 ]; do
    case "$right_frac_value" in 0*) right_frac_value=${right_frac_value#0} ;; *) break ;; esac
  done

  local power=1 i=0
  while [ "$i" -lt ${#left_frac} ]; do
    power=$((power * 10))
    i=$((i + 1))
  done

  _BASHUNIT_MATH_SCALED_L_OUT=$((left_sign * (left_int * power + left_frac_value)))
  _BASHUNIT_MATH_SCALED_R_OUT=$((right_sign * (right_int * power + right_frac_value)))
}

function bashunit::math::is_le() {
  local left="$1"
  local right="$2"

  if bashunit::math::scale_pair_to_slots "$left" "$right"; then
    [ "$_BASHUNIT_MATH_SCALED_L_OUT" -le "$_BASHUNIT_MATH_SCALED_R_OUT" ]
    return
  fi

  left=${left#+}
  right=${right#+}

  if bashunit::dependencies::has_bc; then
    [ "$(echo "$left <= $right" | bc)" = "1" ]
    return
  fi

  if bashunit::dependencies::has_awk; then
    awk -v a="$left" -v b="$right" 'BEGIN { exit !(a <= b) }'
    return
  fi

  left=$(echo "$left" | sed -E 's/([0-9]+)\.[0-9]+/\1/g')
  right=$(echo "$right" | sed -E 's/([0-9]+)\.[0-9]+/\1/g')
  [ "$left" -le "$right" ]
}

function bashunit::math::shuffle() {
  local seed=$1
  case "$seed" in '' | *[!0-9]*) seed=0 ;; esac
  local state=$((seed & 2147483647))

  local -a items=()
  local n=0
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    items[n]=$line
    n=$((n + 1))
  done

  local i j tmp
  i=$((n - 1))
  while [ "$i" -gt 0 ]; do
    state=$(((1103515245 * state + 12345) & 2147483647))
    j=$((state % (i + 1)))
    tmp=${items[i]}
    items[i]=${items[j]}
    items[j]=$tmp
    i=$((i - 1))
  done

  local k=0
  while [ "$k" -lt "$n" ]; do
    printf '%s\n' "${items[k]}"
    k=$((k + 1))
  done
}

# src/util/clock.sh

_BASHUNIT_CLOCK_NOW_IMPL=""

function bashunit::clock::_choose_impl() {
  local shell_time

  local attempts_count=0
  local attempts

  attempts[attempts_count]="EPOCHREALTIME"
  attempts_count=$((attempts_count + 1))
  if shell_time="$(bashunit::clock::shell_time)"; then
    _BASHUNIT_CLOCK_NOW_IMPL="shell"
    return 0
  fi

  attempts[attempts_count]="date"
  attempts_count=$((attempts_count + 1))
  if ! bashunit::check_os::is_macos && ! bashunit::check_os::is_alpine; then
    local result
    result=$(date +%s%N 2>/dev/null)

    case "$result" in
    '' | *[!0-9]*) ;;
    *)
      _BASHUNIT_CLOCK_NOW_IMPL="date"
      return 0
      ;;
    esac
  fi

  attempts[attempts_count]="Perl"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_perl; then
    local perl_now
    perl_now="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)' 2>/dev/null)"
    case "$perl_now" in
    '' | *[!0-9]*) ;;
    *)
      _BASHUNIT_CLOCK_NOW_IMPL="perl"
      _BASHUNIT_CLOCK_NOW_OUT="$perl_now"
      return 0
      ;;
    esac
  fi

  attempts[attempts_count]="Python"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_python; then
    _BASHUNIT_CLOCK_NOW_IMPL="python"
    return 0
  fi

  attempts[attempts_count]="Node"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_node; then
    _BASHUNIT_CLOCK_NOW_IMPL="node"
    return 0
  fi

  attempts[attempts_count]="PowerShell"
  attempts_count=$((attempts_count + 1))
  if bashunit::check_os::is_windows && bashunit::dependencies::has_powershell; then
    _BASHUNIT_CLOCK_NOW_IMPL="powershell"
    return 0
  fi

  attempts[attempts_count]="date-seconds"
  attempts_count=$((attempts_count + 1))
  if date +%s &>/dev/null; then
    _BASHUNIT_CLOCK_NOW_IMPL="date-seconds"
    return 0
  fi

  printf "bashunit::clock::now implementations tried: %s\n" "${attempts[*]}" >&2
  echo ""
  return 1
}

function bashunit::clock::is_expensive() {
  [ -n "$_BASHUNIT_CLOCK_NOW_IMPL" ] || bashunit::clock::_choose_impl >/dev/null 2>&1 || true
  case "$_BASHUNIT_CLOCK_NOW_IMPL" in
  perl | python | node | powershell) return 0 ;;
  *) return 1 ;;
  esac
}

_BASHUNIT_CLOCK_NOW_OUT=""

function bashunit::clock::now_to_slot() {
  if [ -z "$_BASHUNIT_CLOCK_NOW_IMPL" ]; then
    _BASHUNIT_CLOCK_NOW_OUT=""
    bashunit::clock::_choose_impl || return 1

    if [ -n "$_BASHUNIT_CLOCK_NOW_OUT" ]; then
      return 0
    fi
  fi

  case "$_BASHUNIT_CLOCK_NOW_IMPL" in
  perl)
    _BASHUNIT_CLOCK_NOW_OUT="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)')"
    ;;
  python)
    _BASHUNIT_CLOCK_NOW_OUT="$(
      python - <<'EOF'
import time, sys
sys.stdout.write(str(int(time.time() * 1000000000)))
EOF
    )"
    ;;
  node)
    _BASHUNIT_CLOCK_NOW_OUT="$(node -e 'process.stdout.write((BigInt(Date.now()) * 1000000n).toString())')"
    ;;
  powershell)
    _BASHUNIT_CLOCK_NOW_OUT="$(powershell -Command "\
        \$unixEpoch = [DateTime]'1970-01-01 00:00:00';\
        \$now = [DateTime]::UtcNow;\
        \$ticksSinceEpoch = (\$now - \$unixEpoch).Ticks;\
        \$nanosecondsSinceEpoch = \$ticksSinceEpoch * 100;\
        Write-Output \$nanosecondsSinceEpoch\
      ")"
    ;;
  date)
    _BASHUNIT_CLOCK_NOW_OUT="$(date +%s%N)"
    ;;
  date-seconds)
    local seconds
    seconds=$(date +%s)
    _BASHUNIT_CLOCK_NOW_OUT="$((seconds * 1000000000))"
    ;;
  shell)

    local shell_time="${EPOCHREALTIME:-}"
    local seconds="${shell_time%%[.,]*}"
    local microseconds="${shell_time#*[.,]}"
    if [ "$seconds" = "$shell_time" ]; then
      microseconds=""
    fi

    microseconds="${microseconds}000000"
    microseconds="${microseconds:0:6}"
    microseconds="${microseconds#"${microseconds%%[!0]*}"}"
    microseconds="${microseconds:-0}"
    _BASHUNIT_CLOCK_NOW_OUT="$(((seconds * 1000000000) + (microseconds * 1000)))"
    ;;
  *)
    bashunit::clock::_choose_impl || return 1
    bashunit::clock::now_to_slot
    ;;
  esac
}

function bashunit::clock::now() {
  bashunit::clock::now_to_slot || return 1
  echo "$_BASHUNIT_CLOCK_NOW_OUT"
}

function bashunit::clock::shell_time() {

  [ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
  local end_time
  end_time=$(bashunit::clock::now)
  if [ -n "$end_time" ]; then
    bashunit::math::calculate "($end_time - $_BASHUNIT_START_TIME) / 1000000"
  else
    echo ""
  fi
}

function bashunit::clock::init() {
  _BASHUNIT_START_TIME=$(bashunit::clock::now)
}

# src/api/index.sh

# src/api/globals.sh
set -euo pipefail

function bashunit::current_dir() {
  dirname "${BASH_SOURCE[1]}"
}

function bashunit::current_filename() {
  basename "${BASH_SOURCE[1]}"
}

function bashunit::caller_filename() {
  dirname "${BASH_SOURCE[2]}"
}

function bashunit::caller_line() {
  echo "${BASH_LINENO[1]}"
}

function bashunit::is_command_available() {
  command -v "$1" >/dev/null 2>&1
}

function bashunit::temp_file() {
  local prefix=${1:-bashunit}
  local test_prefix=""
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then

    test_prefix="${BASHUNIT_CURRENT_TEST_ID}_"
  elif [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then

    test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
  fi
  "$MKTEMP" "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

function bashunit::temp_dir() {
  local prefix=${1:-bashunit}
  local test_prefix=""
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then

    test_prefix="${BASHUNIT_CURRENT_TEST_ID}_"
  elif [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then

    test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
  fi
  "$MKTEMP" -d "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

function bashunit::cleanup_testcase_temp_files() {
  bashunit::internal_log "cleanup_testcase_temp_files"
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then

    local matches
    matches=("$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}"_*)

    if [ -e "${matches[0]:-}" ]; then
      rm -rf "${matches[@]}"
    fi
  fi
}

function bashunit::cleanup_script_temp_files() {
  bashunit::internal_log "cleanup_script_temp_files"
  if [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then
    rm -rf "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_SCRIPT_ID}"_*
  fi
}

function bashunit::print_line() {
  local length="${1:-70}"
  local char="${2:--}"
  printf '%*s\n' "$length" '' | tr ' ' "$char"
}

function bashunit::data_set() {
  local arg
  local first=true

  for arg in "$@"; do
    if [ "$first" = true ]; then

      if [ -z "$arg" ]; then
        printf "''"
      else
        printf '%q' "$arg"
      fi
      first=false
    else
      if [ -z "$arg" ]; then
        printf " ''"
      else
        printf ' %q' "$arg"
      fi
    fi
  done

  printf " ''\n"
}

# src/api/skip_todo.sh

function bashunit::skip::__mark() {
  local reason=${1-}
  local depth=${2:-2}
  local label

  label="$(bashunit::helper::normalize_test_function_name "${FUNCNAME[$depth]:-}")"

  bashunit::skip::__mark_with_label "$label" "$reason"
}

function bashunit::skip::__mark_with_label() {
  bashunit::console_results::print_skipped_test "${1}" "${2-}"

  bashunit::state::add_assertions_skipped
}

function bashunit::skip::__mark_and_stop() {
  bashunit::skip::__mark "${1-}" 3
  exit 0
}

function bashunit::skip() {
  bashunit::skip::__mark "${1-}" 2
}

function bashunit::skip_if() {
  local condition=${1-}
  local reason=${2-}

  if eval "$condition"; then
    bashunit::skip::__mark_and_stop "$reason"
  fi
}

function bashunit::skip_unless() {
  local condition=${1-}
  local reason=${2-}

  if eval "$condition"; then
    return 0
  fi

  bashunit::skip::__mark_and_stop "$reason"
}

function bashunit::skip_unless_command() {
  local cmd
  for cmd in "$@"; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
      bashunit::skip::__mark_and_stop "requires $cmd"
    fi
  done
}

function bashunit::skip_on() {
  local os=${1-}
  local reason=${2-}
  local matches=false

  case "$os" in
  windows)
    if bashunit::check_os::is_windows; then
      matches=true
    fi
    ;;
  macos)
    if bashunit::check_os::is_macos; then
      matches=true
    fi
    ;;
  linux)
    if bashunit::check_os::is_linux; then
      matches=true
    fi
    ;;
  *)

    bashunit::assert::usage_error_detail "bashunit::skip_on" \
      "accepts windows, macos or linux, got '$os'"
    return 1
    ;;
  esac

  if [ "$matches" = true ]; then
    bashunit::skip::__mark_and_stop "$reason"
  fi
}

function bashunit::todo() {
  local pending=${1-}
  local label
  label="$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")"

  bashunit::console_results::print_incomplete_test "${label}" "${pending}"

  bashunit::state::add_assertions_incomplete
}

# src/api/test_title.sh

function bashunit::set_test_title() {
  bashunit::state::set_test_title "$1"
}

# src/api/bashunit.sh

function bashunit::assertion_failed() {
  bashunit::assert::should_skip && return 0

  local expected=$1
  local actual=$2
  local failure_condition_message=${3:-"but got "}
  local label=${4:-}

  bashunit::assert::fail_with "$label" "${expected}" \
    "$failure_condition_message" "${actual}"
}

function bashunit::assertion_passed() {
  bashunit::assert::should_skip && return 0

  bashunit::state::add_assertions_passed
}

function bashunit::assert_that() {
  bashunit::assert::should_skip && return 0

  local expected=$1
  local actual=$2
  shift 2

  if "$@"; then
    bashunit::state::add_assertions_passed
    return 0
  fi

  bashunit::assert::fail_with "" "$expected" "but got " "$actual"
  return 1
}

# src/config/index.sh

# src/config/parallel.sh

function bashunit::parallel::mark_stop_on_failure() {
  touch "$TEMP_FILE_PARALLEL_STOP_ON_FAILURE"
}

function bashunit::parallel::must_stop_on_failure() {
  [ -f "$TEMP_FILE_PARALLEL_STOP_ON_FAILURE" ]
}

function bashunit::parallel::cleanup() {
  local target="$TEMP_DIR_PARALLEL_TEST_SUITE"

  target="${target%/}"
  case "$target" in
  */bashunit/parallel/*/?*)
    rm -rf "$target"
    return 0
    ;;
  *)
    bashunit::internal_log "parallel::cleanup" "refused unsafe path:$target"
    return 1
    ;;
  esac
}

function bashunit::parallel::init() {
  bashunit::parallel::cleanup
  mkdir -p "$TEMP_DIR_PARALLEL_TEST_SUITE"
}

_BASHUNIT_PARALLEL_ENABLED=""

function bashunit::parallel::_compute_enabled() {
  bashunit::env::is_parallel_run_enabled &&
    (bashunit::check_os::is_macos || bashunit::check_os::is_ubuntu ||
      bashunit::check_os::is_alpine || bashunit::check_os::is_windows)
}

function bashunit::parallel::resolve_enabled() {
  if bashunit::parallel::_compute_enabled; then
    _BASHUNIT_PARALLEL_ENABLED=true
  else
    _BASHUNIT_PARALLEL_ENABLED=false
  fi
  bashunit::internal_log "bashunit::parallel::resolve_enabled" \
    "requested:$BASHUNIT_PARALLEL_RUN" "os:${_BASHUNIT_OS:-Unknown}" \
    "enabled:$_BASHUNIT_PARALLEL_ENABLED"
}

function bashunit::parallel::is_enabled() {
  case "$_BASHUNIT_PARALLEL_ENABLED" in
  true) return 0 ;;
  false) return 1 ;;
  esac

  bashunit::parallel::_compute_enabled
}

# src/config/env.sh

function bashunit::env::load_config_file() {
  local file=$1
  [ -f "$file" ] || return 0

  local line key val
  while IFS= read -r line || [ -n "$line" ]; do

    line=${line#"${line%%[![:space:]]*}"}
    case "$line" in
    '' | '#'*) continue ;;
    esac
    case "$line" in export\ *) line=${line#export } ;; esac
    case "$line" in
    *=*) ;;
    *) continue ;;
    esac

    key=${line%%=*}
    val=${line#*=}

    case "$key" in
    '' | *[!A-Za-z0-9_]* | [0-9]*) continue ;;
    esac

    case "$val" in
    \"*\") val=${val#\"} val=${val%\"} ;;
    \'*\') val=${val#\'} val=${val%\'} ;;
    esac

    eval "export $key=\"\${$key:-\$val}\""
  done <"$file"
}

function bashunit::env::warn_deprecated() {
  [ "${BASHUNIT_NO_DEPRECATION_WARNINGS:-false}" = "true" ] && return 0

  printf "%sDeprecated: %s. Use %s instead.%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED:-}" "$1" "$2" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
}

function bashunit::env::positive_int_or_default() {
  local value="$1"
  local default="$2"
  case "$value" in
  '' | *[!0-9]* | 0) echo "$default" ;;
  *) echo "$value" ;;
  esac
}

if [ "${BASHUNIT_SKIP_ENV_FILE:-false}" != "true" ]; then
  bashunit::env::load_config_file ".bashunitrc"

  if [ -f ".env" ]; then

    _bashunit_env_preserved=""
    for _bashunit_env_name in $(compgen -v BASHUNIT_ 2>/dev/null || true); do
      eval "_bashunit_env_value=\${$_bashunit_env_name}"
      if [ -n "$_bashunit_env_value" ]; then
        _bashunit_env_preserved="$_bashunit_env_preserved $_bashunit_env_name"
        eval "_bashunit_env_saved_$_bashunit_env_name=\$_bashunit_env_value"
      fi
    done

    set -o allexport

    source .env
    set +o allexport

    for _bashunit_env_name in $_bashunit_env_preserved; do
      eval "_bashunit_env_value=\${$_bashunit_env_name}"
      if [ -z "$_bashunit_env_value" ]; then
        eval "export $_bashunit_env_name=\$_bashunit_env_saved_$_bashunit_env_name"
      fi
      eval "unset _bashunit_env_saved_$_bashunit_env_name"
    done

    unset _bashunit_env_preserved _bashunit_env_name _bashunit_env_value
  fi
fi

_BASHUNIT_DEPRECATED_ALIASES="DEFAULT_PATH DEV_LOG BOOTSTRAP BOOTSTRAP_ARGS
LOG_JUNIT LOG_GHA REPORT_HTML REPORT_TAP REPORT_JSON WATCH_INTERVAL COVERAGE
COVERAGE_PATHS COVERAGE_EXCLUDE COVERAGE_REPORT COVERAGE_REPORT_HTML
COVERAGE_MIN COVERAGE_THRESHOLD_LOW COVERAGE_THRESHOLD_HIGH PARALLEL_RUN
SHOW_HEADER HEADER_ASCII_ART SIMPLE_OUTPUT STOP_ON_FAILURE SHOW_EXECUTION_TIME
VERBOSE BENCH_MODE NO_OUTPUT INTERNAL_LOG SHOW_SKIPPED SHOW_INCOMPLETE
STRICT_MODE STOP_ON_ASSERTION_FAILURE SKIP_ENV_FILE LOGIN_SHELL FAILURES_ONLY
SHOW_OUTPUT_ON_FAILURE NO_DIFF NO_PROGRESS OUTPUT_FORMAT FAIL_ON_RISKY PROFILE
PROFILE_COUNT TEST_TIMEOUT"

_bashunit_deprecated_in_use=""
for _bashunit_alias in $_BASHUNIT_DEPRECATED_ALIASES; do
  eval "_bashunit_alias_prefixed=\${BASHUNIT_$_bashunit_alias+set}"
  [ -n "$_bashunit_alias_prefixed" ] && continue
  eval "_bashunit_alias_value=\${$_bashunit_alias:-}"
  [ -n "$_bashunit_alias_value" ] &&
    _bashunit_deprecated_in_use="$_bashunit_deprecated_in_use $_bashunit_alias"
done
unset _bashunit_alias _bashunit_alias_prefixed _bashunit_alias_value

if [ -n "$_bashunit_deprecated_in_use" ]; then
  for _bashunit_alias in $_bashunit_deprecated_in_use; do
    bashunit::env::warn_deprecated \
      "the unprefixed \`$_bashunit_alias\`" "\`BASHUNIT_$_bashunit_alias\`"
  done
  unset _bashunit_alias
fi

_BASHUNIT_DEFAULT_DEFAULT_PATH="tests"
_BASHUNIT_DEFAULT_BOOTSTRAP="tests/bootstrap.sh"
_BASHUNIT_DEFAULT_DEV_LOG=""
_BASHUNIT_DEFAULT_LOG_JUNIT=""
_BASHUNIT_DEFAULT_LOG_GHA=""
_BASHUNIT_DEFAULT_REPORT_HTML=""
_BASHUNIT_DEFAULT_REPORT_TAP=""
_BASHUNIT_DEFAULT_REPORT_JSON=""
_BASHUNIT_DEFAULT_REPORT_MD=""

_BASHUNIT_DEFAULT_COVERAGE="false"
_BASHUNIT_DEFAULT_COVERAGE_PATHS=""
_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh"
_BASHUNIT_DEFAULT_COVERAGE_REPORT="coverage/lcov.info"
_BASHUNIT_DEFAULT_COVERAGE_REPORT_HTML=""
_BASHUNIT_DEFAULT_COVERAGE_REPORT_COBERTURA=""
_BASHUNIT_DEFAULT_COVERAGE_MIN=""
_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW="50"
_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH="80"

_BASHUNIT_DEFAULT_COVERAGE_SHOW_LINE_HITS="false"

_BASHUNIT_DEFAULT_COVERAGE_SHOW_FUNCTIONS="false"
_BASHUNIT_DEFAULT_COVERAGE_SHOW_UNCOVERED="false"

_BASHUNIT_DEFAULT_COVERAGE_ENGINE="auto"

_BASHUNIT_DEFAULT_COVERAGE_DIFF=""

: "${BASHUNIT_DEFAULT_PATH:=${DEFAULT_PATH:=$_BASHUNIT_DEFAULT_DEFAULT_PATH}}"
: "${BASHUNIT_DEV_LOG:=${DEV_LOG:=$_BASHUNIT_DEFAULT_DEV_LOG}}"
: "${BASHUNIT_BOOTSTRAP:=${BOOTSTRAP:=$_BASHUNIT_DEFAULT_BOOTSTRAP}}"
: "${BASHUNIT_BOOTSTRAP_ARGS:=${BOOTSTRAP_ARGS:=}}"
: "${BASHUNIT_LOG_JUNIT:=${LOG_JUNIT:=$_BASHUNIT_DEFAULT_LOG_JUNIT}}"
: "${BASHUNIT_LOG_GHA:=${LOG_GHA:=$_BASHUNIT_DEFAULT_LOG_GHA}}"
: "${BASHUNIT_REPORT_HTML:=${REPORT_HTML:=$_BASHUNIT_DEFAULT_REPORT_HTML}}"
: "${BASHUNIT_REPORT_TAP:=${REPORT_TAP:=$_BASHUNIT_DEFAULT_REPORT_TAP}}"
: "${BASHUNIT_REPORT_JSON:=${REPORT_JSON:=$_BASHUNIT_DEFAULT_REPORT_JSON}}"

_BASHUNIT_DEFAULT_WATCH_INTERVAL="2"
: "${BASHUNIT_WATCH_INTERVAL:=${WATCH_INTERVAL:=$_BASHUNIT_DEFAULT_WATCH_INTERVAL}}"
BASHUNIT_WATCH_INTERVAL=$(bashunit::env::positive_int_or_default \
  "$BASHUNIT_WATCH_INTERVAL" "$_BASHUNIT_DEFAULT_WATCH_INTERVAL")

: "${BASHUNIT_COVERAGE:=${COVERAGE:=$_BASHUNIT_DEFAULT_COVERAGE}}"
: "${BASHUNIT_COVERAGE_PATHS:=${COVERAGE_PATHS:=$_BASHUNIT_DEFAULT_COVERAGE_PATHS}}"
: "${BASHUNIT_COVERAGE_EXCLUDE:=${COVERAGE_EXCLUDE:=$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE}}"
: "${BASHUNIT_COVERAGE_REPORT:=${COVERAGE_REPORT:=$_BASHUNIT_DEFAULT_COVERAGE_REPORT}}"
: "${BASHUNIT_COVERAGE_REPORT_HTML:=${COVERAGE_REPORT_HTML:=$_BASHUNIT_DEFAULT_COVERAGE_REPORT_HTML}}"
: "${BASHUNIT_COVERAGE_MIN:=${COVERAGE_MIN:=$_BASHUNIT_DEFAULT_COVERAGE_MIN}}"
: "${BASHUNIT_COVERAGE_THRESHOLD_LOW:=${COVERAGE_THRESHOLD_LOW:=$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}}"
: "${BASHUNIT_COVERAGE_THRESHOLD_HIGH:=${COVERAGE_THRESHOLD_HIGH:=$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}}"

: "${BASHUNIT_COVERAGE_SHOW_LINE_HITS:=$_BASHUNIT_DEFAULT_COVERAGE_SHOW_LINE_HITS}"

: "${BASHUNIT_COVERAGE_SHOW_FUNCTIONS:=$_BASHUNIT_DEFAULT_COVERAGE_SHOW_FUNCTIONS}"
: "${BASHUNIT_COVERAGE_SHOW_UNCOVERED:=$_BASHUNIT_DEFAULT_COVERAGE_SHOW_UNCOVERED}"

: "${BASHUNIT_COVERAGE_ENGINE:=$_BASHUNIT_DEFAULT_COVERAGE_ENGINE}"

: "${BASHUNIT_COVERAGE_REPORT_COBERTURA:=$_BASHUNIT_DEFAULT_COVERAGE_REPORT_COBERTURA}"

: "${BASHUNIT_COVERAGE_DIFF:=$_BASHUNIT_DEFAULT_COVERAGE_DIFF}"

_BASHUNIT_DEFAULT_PARALLEL_RUN="false"

_BASHUNIT_DEFAULT_PARALLEL_JOBS="0"
_BASHUNIT_DEFAULT_SHOW_HEADER="true"
_BASHUNIT_DEFAULT_HEADER_ASCII_ART="false"
_BASHUNIT_DEFAULT_SIMPLE_OUTPUT="false"
_BASHUNIT_DEFAULT_STOP_ON_FAILURE="false"

_BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME="auto"
_BASHUNIT_DEFAULT_VERBOSE="false"
_BASHUNIT_DEFAULT_BENCH_MODE="false"
_BASHUNIT_DEFAULT_NO_OUTPUT="false"
_BASHUNIT_DEFAULT_INTERNAL_LOG="false"
_BASHUNIT_DEFAULT_SHOW_SKIPPED="false"
_BASHUNIT_DEFAULT_SHOW_INCOMPLETE="false"
_BASHUNIT_DEFAULT_STRICT_MODE="false"
_BASHUNIT_DEFAULT_STOP_ON_ASSERTION_FAILURE="true"
_BASHUNIT_DEFAULT_SKIP_ENV_FILE="false"
_BASHUNIT_DEFAULT_LOGIN_SHELL="false"
_BASHUNIT_DEFAULT_FAILURES_ONLY="false"
_BASHUNIT_DEFAULT_NO_COLOR="false"
_BASHUNIT_DEFAULT_NO_DIFF="false"
_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE="true"
_BASHUNIT_DEFAULT_NO_PROGRESS="false"
_BASHUNIT_DEFAULT_OUTPUT_FORMAT=""
_BASHUNIT_DEFAULT_FAIL_ON_RISKY="false"
_BASHUNIT_DEFAULT_SNAPSHOT_PRUNE="false"
_BASHUNIT_DEFAULT_BENCH_BASELINE=""
_BASHUNIT_DEFAULT_BENCH_BASELINE_TOLERANCE="10"
_BASHUNIT_DEFAULT_BENCH_BASELINE_UPDATE=""
_BASHUNIT_DEFAULT_BENCH_REPORT_JSON=""
_BASHUNIT_DEFAULT_BENCH_REPORT_JUNIT=""
_BASHUNIT_DEFAULT_SANDBOX="false"
_BASHUNIT_DEFAULT_SANDBOX_ALLOW=""
_BASHUNIT_DEFAULT_PROFILE="false"
_BASHUNIT_DEFAULT_PROFILE_COUNT="10"

_BASHUNIT_DEFAULT_TEST_TIMEOUT="0"

_BASHUNIT_DEFAULT_RETRY="0"

_BASHUNIT_DEFAULT_RANDOM_ORDER="false"

_BASHUNIT_DEFAULT_SEED=""

_BASHUNIT_DEFAULT_SHARD_INDEX=""
_BASHUNIT_DEFAULT_SHARD_TOTAL=""

_BASHUNIT_DEFAULT_RERUN_FAILED="false"

_BASHUNIT_DEFAULT_GHA_ANNOTATIONS="auto"

_BASHUNIT_DEFAULT_REPEAT="1"

_BASHUNIT_DEFAULT_FAIL_ON_FLAKY="false"

_BASHUNIT_DEFAULT_ORDER_BY="defined"

_BASHUNIT_DEFAULT_CHANGED="false"

_BASHUNIT_DEFAULT_CHANGED_REF=""

_BASHUNIT_DEFAULT_EXCLUDE_FILTER=""

_BASHUNIT_DEFAULT_LIST_TESTS="false"

_BASHUNIT_DEFAULT_LIST_FORMAT="text"

_BASHUNIT_DEFAULT_SNAPSHOT_UPDATE="false"

_BASHUNIT_DEFAULT_SNAPSHOT_CREATE="true"

_BASHUNIT_DEFAULT_SNAPSHOT_REPORT_UNUSED="false"

: "${BASHUNIT_PARALLEL_RUN:=${PARALLEL_RUN:=$_BASHUNIT_DEFAULT_PARALLEL_RUN}}"
: "${BASHUNIT_PARALLEL_JOBS:=$_BASHUNIT_DEFAULT_PARALLEL_JOBS}"
: "${BASHUNIT_SHOW_HEADER:=${SHOW_HEADER:=$_BASHUNIT_DEFAULT_SHOW_HEADER}}"
: "${BASHUNIT_HEADER_ASCII_ART:=${HEADER_ASCII_ART:=$_BASHUNIT_DEFAULT_HEADER_ASCII_ART}}"
: "${BASHUNIT_SIMPLE_OUTPUT:=${SIMPLE_OUTPUT:=$_BASHUNIT_DEFAULT_SIMPLE_OUTPUT}}"
: "${BASHUNIT_STOP_ON_FAILURE:=${STOP_ON_FAILURE:=$_BASHUNIT_DEFAULT_STOP_ON_FAILURE}}"
: "${BASHUNIT_SHOW_EXECUTION_TIME:=${SHOW_EXECUTION_TIME:=$_BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME}}"
: "${BASHUNIT_VERBOSE:=${VERBOSE:=$_BASHUNIT_DEFAULT_VERBOSE}}"
: "${BASHUNIT_BENCH_MODE:=${BENCH_MODE:=$_BASHUNIT_DEFAULT_BENCH_MODE}}"
: "${BASHUNIT_NO_OUTPUT:=${NO_OUTPUT:=$_BASHUNIT_DEFAULT_NO_OUTPUT}}"
: "${BASHUNIT_INTERNAL_LOG:=${INTERNAL_LOG:=$_BASHUNIT_DEFAULT_INTERNAL_LOG}}"
: "${BASHUNIT_SHOW_SKIPPED:=${SHOW_SKIPPED:=$_BASHUNIT_DEFAULT_SHOW_SKIPPED}}"
: "${BASHUNIT_SHOW_INCOMPLETE:=${SHOW_INCOMPLETE:=$_BASHUNIT_DEFAULT_SHOW_INCOMPLETE}}"
: "${BASHUNIT_STRICT_MODE:=${STRICT_MODE:=$_BASHUNIT_DEFAULT_STRICT_MODE}}"
: "${BASHUNIT_STOP_ON_ASSERTION_FAILURE:=${STOP_ON_ASSERTION_FAILURE:=$_BASHUNIT_DEFAULT_STOP_ON_ASSERTION_FAILURE}}"
: "${BASHUNIT_SKIP_ENV_FILE:=${SKIP_ENV_FILE:=$_BASHUNIT_DEFAULT_SKIP_ENV_FILE}}"
: "${BASHUNIT_LOGIN_SHELL:=${LOGIN_SHELL:=$_BASHUNIT_DEFAULT_LOGIN_SHELL}}"
: "${BASHUNIT_FAILURES_ONLY:=${FAILURES_ONLY:=$_BASHUNIT_DEFAULT_FAILURES_ONLY}}"
: "${BASHUNIT_SHOW_OUTPUT_ON_FAILURE:=${SHOW_OUTPUT_ON_FAILURE:=$_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE}}"
: "${BASHUNIT_NO_DIFF:=${NO_DIFF:=$_BASHUNIT_DEFAULT_NO_DIFF}}"
: "${BASHUNIT_NO_PROGRESS:=${NO_PROGRESS:=$_BASHUNIT_DEFAULT_NO_PROGRESS}}"
: "${BASHUNIT_OUTPUT_FORMAT:=${OUTPUT_FORMAT:=$_BASHUNIT_DEFAULT_OUTPUT_FORMAT}}"
: "${BASHUNIT_FAIL_ON_RISKY:=${FAIL_ON_RISKY:=$_BASHUNIT_DEFAULT_FAIL_ON_RISKY}}"

: "${BASHUNIT_SNAPSHOT_PRUNE:=$_BASHUNIT_DEFAULT_SNAPSHOT_PRUNE}"
: "${BASHUNIT_BENCH_BASELINE:=$_BASHUNIT_DEFAULT_BENCH_BASELINE}"
: "${BASHUNIT_BENCH_BASELINE_TOLERANCE:=$_BASHUNIT_DEFAULT_BENCH_BASELINE_TOLERANCE}"
: "${BASHUNIT_BENCH_BASELINE_UPDATE:=$_BASHUNIT_DEFAULT_BENCH_BASELINE_UPDATE}"
: "${BASHUNIT_BENCH_REPORT_JSON:=$_BASHUNIT_DEFAULT_BENCH_REPORT_JSON}"
: "${BASHUNIT_BENCH_REPORT_JUNIT:=$_BASHUNIT_DEFAULT_BENCH_REPORT_JUNIT}"
: "${BASHUNIT_SANDBOX:=$_BASHUNIT_DEFAULT_SANDBOX}"
: "${BASHUNIT_SANDBOX_ALLOW:=$_BASHUNIT_DEFAULT_SANDBOX_ALLOW}"
: "${BASHUNIT_PROFILE:=${PROFILE:=$_BASHUNIT_DEFAULT_PROFILE}}"
: "${BASHUNIT_PROFILE_COUNT:=${PROFILE_COUNT:=$_BASHUNIT_DEFAULT_PROFILE_COUNT}}"
: "${BASHUNIT_TEST_TIMEOUT:=${TEST_TIMEOUT:=$_BASHUNIT_DEFAULT_TEST_TIMEOUT}}"

: "${BASHUNIT_RETRY:=$_BASHUNIT_DEFAULT_RETRY}"

: "${BASHUNIT_RANDOM_ORDER:=$_BASHUNIT_DEFAULT_RANDOM_ORDER}"
: "${BASHUNIT_SEED:=$_BASHUNIT_DEFAULT_SEED}"

: "${BASHUNIT_ORDER_BY:=$_BASHUNIT_DEFAULT_ORDER_BY}"
: "${BASHUNIT_FAIL_ON_FLAKY:=$_BASHUNIT_DEFAULT_FAIL_ON_FLAKY}"
: "${BASHUNIT_REPEAT:=$_BASHUNIT_DEFAULT_REPEAT}"
: "${BASHUNIT_GHA_ANNOTATIONS:=$_BASHUNIT_DEFAULT_GHA_ANNOTATIONS}"

: "${BASHUNIT_REPORT_MD:=$_BASHUNIT_DEFAULT_REPORT_MD}"

if [ -n "${_BASHUNIT_GHA_ANNOTATIONS_CLAIMED:-}" ]; then
  _BASHUNIT_IS_OUTERMOST_RUN=false
else
  _BASHUNIT_IS_OUTERMOST_RUN=true
fi
export _BASHUNIT_GHA_ANNOTATIONS_CLAIMED=1
: "${BASHUNIT_SHARD_INDEX:=$_BASHUNIT_DEFAULT_SHARD_INDEX}"
: "${BASHUNIT_SHARD_TOTAL:=$_BASHUNIT_DEFAULT_SHARD_TOTAL}"

: "${BASHUNIT_RERUN_FAILED:=$_BASHUNIT_DEFAULT_RERUN_FAILED}"

: "${BASHUNIT_CHANGED:=$_BASHUNIT_DEFAULT_CHANGED}"
: "${BASHUNIT_CHANGED_REF:=$_BASHUNIT_DEFAULT_CHANGED_REF}"

: "${BASHUNIT_EXCLUDE_FILTER:=$_BASHUNIT_DEFAULT_EXCLUDE_FILTER}"
: "${BASHUNIT_LIST_TESTS:=$_BASHUNIT_DEFAULT_LIST_TESTS}"
: "${BASHUNIT_LIST_FORMAT:=$_BASHUNIT_DEFAULT_LIST_FORMAT}"

: "${BASHUNIT_SNAPSHOT_UPDATE:=$_BASHUNIT_DEFAULT_SNAPSHOT_UPDATE}"
: "${BASHUNIT_SNAPSHOT_CREATE:=$_BASHUNIT_DEFAULT_SNAPSHOT_CREATE}"
: "${BASHUNIT_SNAPSHOT_REPORT_UNUSED:=$_BASHUNIT_DEFAULT_SNAPSHOT_REPORT_UNUSED}"

if [ -n "${NO_COLOR:-}" ]; then
  BASHUNIT_NO_COLOR="true"
else
  : "${BASHUNIT_NO_COLOR:=$_BASHUNIT_DEFAULT_NO_COLOR}"
fi

function bashunit::env::is_parallel_run_enabled() {
  [ "$BASHUNIT_PARALLEL_RUN" = "true" ]
}

function bashunit::env::is_test_timeout_enabled() {
  case "${BASHUNIT_TEST_TIMEOUT:-0}" in
  '' | *[!0-9]*) return 1 ;;
  esac
  [ "${BASHUNIT_TEST_TIMEOUT:-0}" -gt 0 ]
}

function bashunit::env::test_timeout_secs() {
  printf '%s' "${BASHUNIT_TEST_TIMEOUT:-0}"
}

_BASHUNIT_RETRY_VALIDATED=0
_BASHUNIT_REPEAT_VALIDATED=1

function bashunit::env::resolve_repeat_count() {
  case "${BASHUNIT_REPEAT:-1}" in
  '' | *[!0-9]* | 0) _BASHUNIT_REPEAT_VALIDATED=1 ;;
  *) _BASHUNIT_REPEAT_VALIDATED="${BASHUNIT_REPEAT:-1}" ;;
  esac
}

function bashunit::env::resolve_retry_count() {
  case "${BASHUNIT_RETRY:-0}" in
  '' | *[!0-9]*) _BASHUNIT_RETRY_VALIDATED=0 ;;
  *) _BASHUNIT_RETRY_VALIDATED="${BASHUNIT_RETRY:-0}" ;;
  esac
}

function bashunit::env::retry_count() {
  bashunit::env::resolve_retry_count
  printf '%s' "$_BASHUNIT_RETRY_VALIDATED"
}

function bashunit::env::is_random_order_enabled() {
  [ "$BASHUNIT_RANDOM_ORDER" = "true" ] || [ "${BASHUNIT_ORDER_BY:-defined}" = "random" ]
}

function bashunit::env::is_defects_order_enabled() {
  [ "${BASHUNIT_ORDER_BY:-defined}" = "defects" ]
}

function bashunit::env::seed() {
  printf '%s' "${BASHUNIT_SEED:-}"
}

function bashunit::env::is_shard_enabled() {
  [ -n "${BASHUNIT_SHARD_INDEX:-}" ] && [ -n "${BASHUNIT_SHARD_TOTAL:-}" ]
}

function bashunit::env::is_changed_enabled() {
  [ "${BASHUNIT_CHANGED:-false}" = "true" ]
}

function bashunit::env::shard_index() {
  printf '%s' "${BASHUNIT_SHARD_INDEX:-}"
}

function bashunit::env::shard_total() {
  printf '%s' "${BASHUNIT_SHARD_TOTAL:-}"
}

function bashunit::env::is_show_header_enabled() {
  [ "$BASHUNIT_SHOW_HEADER" = "true" ]
}

function bashunit::env::is_header_ascii_art_enabled() {
  [ "$BASHUNIT_HEADER_ASCII_ART" = "true" ]
}

function bashunit::env::is_simple_output_enabled() {
  [ "$BASHUNIT_SIMPLE_OUTPUT" = "true" ]
}

function bashunit::env::is_stop_on_failure_enabled() {
  [ "$BASHUNIT_STOP_ON_FAILURE" = "true" ]
}

function bashunit::env::is_show_execution_time_enabled() {
  case "$BASHUNIT_SHOW_EXECUTION_TIME" in
  true) return 0 ;;
  auto) ! bashunit::clock::is_expensive ;;
  *) return 1 ;;
  esac
}

function bashunit::env::is_total_execution_time_enabled() {
  [ "$BASHUNIT_SHOW_EXECUTION_TIME" != "false" ]
}

function bashunit::env::is_dev_mode_enabled() {
  [ -n "$BASHUNIT_DEV_LOG" ]
}

function bashunit::env::is_internal_log_enabled() {
  [ "$BASHUNIT_INTERNAL_LOG" = "true" ]
}

function bashunit::current_timestamp() {
  date +"%Y-%m-%d %H:%M:%S"
}

function bashunit::log() {
  if ! bashunit::env::is_dev_mode_enabled; then
    return
  fi

  local level="$1"
  shift

  case "$level" in
  info | INFO) level="INFO" ;;
  debug | DEBUG) level="DEBUG" ;;
  warning | WARNING) level="WARNING" ;;
  critical | CRITICAL) level="CRITICAL" ;;
  error | ERROR) level="ERROR" ;;
  *)
    set -- "$level $@"
    level="INFO"
    ;;
  esac

  echo "$(bashunit::current_timestamp) [$level]: $* #${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >>"$BASHUNIT_DEV_LOG"
}

function bashunit::internal_log() {
  if ! bashunit::env::is_dev_mode_enabled || ! bashunit::env::is_internal_log_enabled; then
    return
  fi

  echo "$(bashunit::current_timestamp) [INTERNAL]: $* #${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >>"$BASHUNIT_DEV_LOG"
}

function bashunit::env::is_verbose_enabled() {
  [ "$BASHUNIT_VERBOSE" = "true" ]
}

function bashunit::env::is_bench_mode_enabled() {
  [ "$BASHUNIT_BENCH_MODE" = "true" ]
}

function bashunit::env::is_no_output_enabled() {
  [ "$BASHUNIT_NO_OUTPUT" = "true" ]
}

function bashunit::env::is_show_skipped_enabled() {
  [ "$BASHUNIT_SHOW_SKIPPED" = "true" ]
}

function bashunit::env::is_show_incomplete_enabled() {
  [ "$BASHUNIT_SHOW_INCOMPLETE" = "true" ]
}

function bashunit::env::is_strict_mode_enabled() {
  [ "$BASHUNIT_STRICT_MODE" = "true" ]
}

function bashunit::env::is_stop_on_assertion_failure_enabled() {
  [ "$BASHUNIT_STOP_ON_ASSERTION_FAILURE" = "true" ]
}

function bashunit::env::is_skip_env_file_enabled() {
  [ "$BASHUNIT_SKIP_ENV_FILE" = "true" ]
}

function bashunit::env::is_login_shell_enabled() {
  [ "$BASHUNIT_LOGIN_SHELL" = "true" ]
}

function bashunit::env::is_failures_only_enabled() {
  [ "$BASHUNIT_FAILURES_ONLY" = "true" ]
}

function bashunit::env::is_show_output_on_failure_enabled() {
  [ "$BASHUNIT_SHOW_OUTPUT_ON_FAILURE" = "true" ]
}

function bashunit::env::is_no_progress_enabled() {
  [ "$BASHUNIT_NO_PROGRESS" = "true" ]
}

function bashunit::env::is_no_color_enabled() {
  [ "$BASHUNIT_NO_COLOR" = "true" ]
}

function bashunit::env::is_diff_enabled() {

  [ "${BASHUNIT_NO_DIFF:-}" != "true" ]
}

function bashunit::env::supports_color() {
  if [ "${TERM:-}" = "dumb" ]; then
    return 1
  fi

  if ! bashunit::dependencies::has_tput; then
    return 0
  fi

  local n
  n=$(tput colors 2>/dev/null)
  case "$n" in
  '' | *[!0-9]*)
    return 0
    ;;
  *)
    [ "$n" -ge 8 ]
    ;;
  esac
}

function bashunit::env::is_coverage_enabled() {
  [ "$BASHUNIT_COVERAGE" = "true" ]
}

function bashunit::env::is_tap_output_enabled() {
  [ "$BASHUNIT_OUTPUT_FORMAT" = "tap" ]
}

function bashunit::env::is_json_output_enabled() {
  [ "$BASHUNIT_OUTPUT_FORMAT" = "json" ]
}

function bashunit::env::is_junit_output_enabled() {
  [ "$BASHUNIT_OUTPUT_FORMAT" = "junit" ]
}

function bashunit::env::is_machine_output_enabled() {
  case "$BASHUNIT_OUTPUT_FORMAT" in
  tap | json | junit) return 0 ;;
  esac
  return 1
}

function bashunit::env::is_snapshot_prune_enabled() {
  [ "${BASHUNIT_SNAPSHOT_PRUNE:-false}" = "true" ]
}

function bashunit::env::is_snapshot_report_unused_enabled() {
  [ "$BASHUNIT_SNAPSHOT_REPORT_UNUSED" = "true" ]
}

function bashunit::env::is_snapshot_create_enabled() {
  [ "$BASHUNIT_SNAPSHOT_CREATE" = "true" ]
}

function bashunit::env::is_snapshot_update_enabled() {
  [ "$BASHUNIT_SNAPSHOT_UPDATE" = "true" ]
}

function bashunit::env::is_list_enabled() {
  [ "$BASHUNIT_LIST_TESTS" = "true" ]
}

function bashunit::env::is_fail_on_risky_enabled() {
  [ "$BASHUNIT_FAIL_ON_RISKY" = "true" ]
}

function bashunit::env::is_sandbox_enabled() {
  [ "${BASHUNIT_SANDBOX:-false}" = "true" ]
}

function bashunit::env::should_print_gha_annotations() {
  case "${BASHUNIT_GHA_ANNOTATIONS:-auto}" in
  never) return 1 ;;
  always) return 0 ;;
  esac

  [ "${_BASHUNIT_IS_OUTERMOST_RUN:-true}" = true ] &&
    [ "${GITHUB_ACTIONS:-}" = "true" ] &&
    ! bashunit::env::is_machine_output_enabled
}

function bashunit::env::should_append_step_summary() {
  [ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 1

  [ "${_BASHUNIT_IS_OUTERMOST_RUN:-true}" = true ]
}

function bashunit::env::is_fail_on_flaky_enabled() {
  [ "${BASHUNIT_FAIL_ON_FLAKY:-false}" = "true" ]
}

function bashunit::env::is_profile_enabled() {
  [ "$BASHUNIT_PROFILE" = "true" ]
}

function bashunit::env::active_internet_connection() {
  if [ "${BASHUNIT_NO_NETWORK:-}" = "true" ]; then
    return 1
  fi

  if command -v curl >/dev/null 2>&1; then
    curl -sfI https://github.com >/dev/null 2>&1 && return 0
  elif command -v wget >/dev/null 2>&1; then
    wget -q --spider https://github.com && return 0
  fi

  if ping -c 1 -W 3 google.com &>/dev/null; then
    return 0
  fi

  return 1
}

function bashunit::env::find_terminal_width() {
  local cols=""

  if command -v tput >/dev/null; then
    cols=$(tput cols 2>/dev/null)
  fi

  if [ -z "$cols" ] && command -v stty >/dev/null; then
    cols=$(stty size 2>/dev/null | cut -d' ' -f2)
  fi

  echo "${cols:-100}"
}

function bashunit::env::print_verbose() {
  bashunit::internal_log "Printing verbose environment variables"
  local IFS=$' \t\n'

  local keys
  keys=(
    "BASHUNIT_DEFAULT_PATH"
    "BASHUNIT_DEV_LOG"
    "BASHUNIT_BOOTSTRAP"
    "BASHUNIT_BOOTSTRAP_ARGS"
    "BASHUNIT_LOG_JUNIT"
    "BASHUNIT_LOG_GHA"
    "BASHUNIT_REPORT_HTML"
    "BASHUNIT_REPORT_TAP"
    "BASHUNIT_PARALLEL_RUN"
    "BASHUNIT_SHOW_HEADER"
    "BASHUNIT_HEADER_ASCII_ART"
    "BASHUNIT_SIMPLE_OUTPUT"
    "BASHUNIT_STOP_ON_FAILURE"
    "BASHUNIT_SHOW_EXECUTION_TIME"
    "BASHUNIT_VERBOSE"
    "BASHUNIT_STRICT_MODE"
    "BASHUNIT_STOP_ON_ASSERTION_FAILURE"
    "BASHUNIT_SKIP_ENV_FILE"
    "BASHUNIT_LOGIN_SHELL"
    "BASHUNIT_COVERAGE"
    "BASHUNIT_COVERAGE_PATHS"
    "BASHUNIT_COVERAGE_EXCLUDE"
    "BASHUNIT_COVERAGE_REPORT"
    "BASHUNIT_COVERAGE_REPORT_HTML"
    "BASHUNIT_COVERAGE_MIN"
    "BASHUNIT_COVERAGE_ENGINE"
  )

  local max_length=0

  local key
  for key in "${keys[@]+"${keys[@]}"}"; do
    if ((${#key} > max_length)); then
      max_length=${#key}
    fi
  done

  for key in "${keys[@]+"${keys[@]}"}"; do
    bashunit::internal_log "$key=${!key}"
    printf "%s:%*s%s\n" "$key" $((max_length - ${#key} + 1)) "" "${!key}"
  done
}

EXIT_CODE_STOP_ON_FAILURE=4

TEMP_DIR_PARALLEL_TEST_SUITE="${TMPDIR:-/tmp}/bashunit/parallel/${_BASHUNIT_OS:-Unknown}/$(bashunit::random_str 8)"
TEMP_FILE_PARALLEL_STOP_ON_FAILURE="$TEMP_DIR_PARALLEL_TEST_SUITE/.stop-on-failure"
TERMINAL_WIDTH="$(bashunit::env::find_terminal_width)"
CAT="$(command -v cat)"
GREP="$(command -v grep)"
MKTEMP="$(command -v mktemp)"
AWK="$(command -v awk)"

_BASHUNIT_RUN_OUTPUT_DIR="${TMPDIR:-/tmp}/bashunit/run/${_BASHUNIT_OS:-Unknown}/$(bashunit::random_str 8)"

_BASHUNIT_RUN_DIR_VANISHED=false
FAILURES_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/failures"
SKIPPED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/skipped"
INCOMPLETE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/incomplete"
RISKY_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/risky"
PROFILE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/profile"

WORKER_STDERR_OUTPUT_PREFIX="$_BASHUNIT_RUN_OUTPUT_DIR/worker-stderr"

RERUN_FAILED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/rerun-failed"

SNAPSHOT_USED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/snapshots-used"
REPORTS_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/reports"

BASHUNIT_TEMP_DIR="${TMPDIR:-/tmp}/bashunit/tmp"

function bashunit::env::create_scratch_dirs() {
  local run_dir=$1
  local temp_dir=$2

  mkdir -p "$run_dir" "$temp_dir" || true

  local dir
  for dir in "$run_dir" "$temp_dir"; do
    if [ ! -d "$dir" ]; then
      printf 'bashunit: cannot create the scratch directory: %s\n' "$dir" >&2
      printf 'bashunit: set TMPDIR to a writable location and try again.\n' >&2
      return 1
    fi
  done
}

bashunit::env::create_scratch_dirs "$_BASHUNIT_RUN_OUTPUT_DIR" "$BASHUNIT_TEMP_DIR" || exit 1

function bashunit::env::cleanup_run_output_dir() {
  local target="$_BASHUNIT_RUN_OUTPUT_DIR"

  target="${target%/}"
  case "$target" in
  */bashunit/run/*/?*)
    rm -rf "$target"
    return 0
    ;;
  *)
    bashunit::internal_log "env::cleanup_run_output_dir" "refused unsafe path:$target"
    return 1
    ;;
  esac
}

_BASHUNIT_LOADING_BOOTSTRAP=""

function bashunit::env::report_unfinished_bootstrap() {
  if [ -n "${_BASHUNIT_LOADING_BOOTSTRAP:-}" ]; then
    printf "%sError: the bootstrap file did not load: '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$_BASHUNIT_LOADING_BOOTSTRAP" \
      "${_BASHUNIT_COLOR_DEFAULT}" >&2
    printf "%s\n" "It ended the shell before any test ran (a syntax error, or an 'exit')." >&2
    _BASHUNIT_LOADING_BOOTSTRAP=""
    exit 1
  fi
}

trap 'bashunit::env::report_unfinished_bootstrap; bashunit::env::cleanup_run_output_dir' EXIT

if bashunit::env::is_dev_mode_enabled; then
  bashunit::internal_log "info" "Dev log enabled" "file:$BASHUNIT_DEV_LOG"
fi

# src/config/rerun.sh

_BASHUNIT_RERUN_ENTRIES=""

function bashunit::rerun::cache_file() {
  echo "${BASHUNIT_RERUN_CACHE_DIR:-.bashunit}/last-failed"
}

function bashunit::rerun::is_enabled() {
  [ "${BASHUNIT_RERUN_FAILED:-false}" = true ]
}

function bashunit::rerun::record() {
  local test_file=$1
  local fn_name=$2
  [ -n "${RERUN_FAILED_OUTPUT_PATH:-}" ] || return 0
  printf '%s:%s\n' "$test_file" "$fn_name" >>"$RERUN_FAILED_OUTPUT_PATH" 2>/dev/null || true
}

function bashunit::rerun::persist() {
  local cache
  cache="$(bashunit::rerun::cache_file)"
  local collected="${RERUN_FAILED_OUTPUT_PATH:-}"

  if [ -n "$collected" ] && [ -s "$collected" ]; then
    local dir="${cache%/*}"
    if [ "$dir" != "$cache" ]; then
      mkdir -p "$dir" 2>/dev/null || return 0
    fi
    awk '!seen[$0]++' "$collected" >"$cache" 2>/dev/null || true
  elif [ -f "$cache" ]; then
    : >"$cache" 2>/dev/null || true
  fi
}

function bashunit::rerun::load() {
  local cache
  cache="$(bashunit::rerun::cache_file)"
  _BASHUNIT_RERUN_ENTRIES=""
  [ -f "$cache" ] || return 0
  _BASHUNIT_RERUN_ENTRIES="$(cat "$cache" 2>/dev/null)"
}

function bashunit::rerun::has_entries() {
  [ -n "$_BASHUNIT_RERUN_ENTRIES" ]
}

function bashunit::rerun::files() {
  [ -n "$_BASHUNIT_RERUN_ENTRIES" ] || return 0
  printf '%s\n' "$_BASHUNIT_RERUN_ENTRIES" | awk '
    NF {
      file = $0
      sub(/:[^:]*$/, "", file)
      if (!seen[file]++) print file
    }'
}

function bashunit::rerun::allows() {
  local file=$1
  local fn=$2
  case "
$_BASHUNIT_RERUN_ENTRIES
" in
  *"
$file:$fn
"*) return 0 ;;
  esac
  return 1
}

function bashunit::rerun::order_files() {
  local recorded_files
  recorded_files="$(bashunit::rerun::files)"
  if [ -z "$recorded_files" ]; then
    [ "$#" -gt 0 ] && printf '%s\n' "$@"
    return 0
  fi

  local file recorded

  while IFS= read -r recorded; do
    [ -z "$recorded" ] && continue
    for file in "$@"; do
      if [ "$file" = "$recorded" ]; then
        printf '%s\n' "$file"
        break
      fi
    done
  done <<EOF
$recorded_files
EOF

  for file in "$@"; do
    case "
$recorded_files
" in
    *"
$file
"*) ;;
    *) printf '%s\n' "$file" ;;
    esac
  done
}

function bashunit::rerun::order_functions() {
  local file=$1
  local functions=$2
  local ordered=""
  local entry recorded_fn fn

  while IFS= read -r entry; do
    case "$entry" in
    "$file":*) recorded_fn="${entry#"$file":}" ;;
    *) continue ;;
    esac
    for fn in $functions; do
      if [ "$fn" = "$recorded_fn" ]; then
        ordered="$ordered $fn"
        break
      fi
    done
  done <<EOF
$_BASHUNIT_RERUN_ENTRIES
EOF

  for fn in $functions; do
    if ! bashunit::rerun::allows "$file" "$fn"; then
      ordered="$ordered $fn"
    fi
  done

  echo "${ordered# }"
}

function bashunit::rerun::filter_functions() {
  local file=$1
  local functions=$2
  local kept=""
  local fn
  for fn in $functions; do
    if bashunit::rerun::allows "$file" "$fn"; then
      kept="$kept $fn"
    fi
  done
  echo "${kept# }"
}

# src/config/suites.sh

_BASHUNIT_SUITE_NAMES=()
_BASHUNIT_SUITE_PATHS=()
_BASHUNIT_SUITE_ARGS=()
_BASHUNIT_SUITES_LOADED_FILE=""

_BASHUNIT_SUITE_PATHS_OUT=""
_BASHUNIT_SUITE_ARGS_OUT=""

function bashunit::suites::_abort() {
  printf "%sError: %s in %s: '%s'.%s\n" \
    "${_BASHUNIT_COLOR_FAILED:-}" "$2" "$1" "$3" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
  exit 1
}

function bashunit::suites::load() {
  local file=${1:-.bashunitrc}

  if [ "$file" = "$_BASHUNIT_SUITES_LOADED_FILE" ]; then
    return 0
  fi
  _BASHUNIT_SUITES_LOADED_FILE="$file"
  _BASHUNIT_SUITE_NAMES=()
  _BASHUNIT_SUITE_PATHS=()
  _BASHUNIT_SUITE_ARGS=()

  [ -f "$file" ] || return 0

  local line raw key val index=-1

  while IFS= read -r line || [ -n "$line" ]; do
    raw=$line
    line=${line#"${line%%[![:space:]]*}"}
    line=${line%"${line##*[![:space:]]}"}

    case "$line" in
    '' | '#'* | ';'*) continue ;;
    esac

    case "$line" in
    '['*']')
      case "$line" in
      '[suite:'*']')
        local name=${line#\[suite:}
        name=${name%\]}
        name=${name#"${name%%[![:space:]]*}"}
        name=${name%"${name##*[![:space:]]}"}
        if [ -z "$name" ]; then
          bashunit::suites::_abort "$file" "a suite section needs a name" "$raw"
        fi
        index=$((${#_BASHUNIT_SUITE_NAMES[@]}))
        _BASHUNIT_SUITE_NAMES[index]="$name"
        _BASHUNIT_SUITE_PATHS[index]=""
        _BASHUNIT_SUITE_ARGS[index]=""
        ;;
      *) index=-1 ;;
      esac
      continue
      ;;
    esac

    if [ "$index" -lt 0 ]; then
      continue
    fi

    case "$line" in
    *=*) ;;
    *) bashunit::suites::_abort "$file" "a suite entry must be 'key = value'" "$raw" ;;
    esac

    key=${line%%=*}
    val=${line#*=}
    key=${key%"${key##*[![:space:]]}"}
    val=${val#"${val%%[![:space:]]*}"}
    val=${val%"${val##*[![:space:]]}"}
    case "$val" in
    \"*\") val=${val#\"} val=${val%\"} ;;
    \'*\') val=${val#\'} val=${val%\'} ;;
    esac

    if [ "$key" = "paths" ] || [ "$key" = "path" ]; then
      _BASHUNIT_SUITE_PATHS[index]="$val"
      continue
    fi

    key=$(printf '%s' "$key" | tr '_' '-')
    case "$key" in
    '' | *[!a-z0-9-]*)
      bashunit::suites::_abort "$file" "a suite option must be a long flag name" "$raw"
      ;;
    esac

    case "$val" in
    false) continue ;;
    true) bashunit::suites::_append "$index" "--$key" ;;
    *) bashunit::suites::_append "$index" "--$key" "$val" ;;
    esac
  done <"$file"
}

function bashunit::suites::_append() {
  local index=$1
  shift
  local current=${_BASHUNIT_SUITE_ARGS[index]}
  local entry
  for entry in "$@"; do
    if [ -z "$current" ]; then
      current="$entry"
    else
      current="$current
$entry"
    fi
  done
  _BASHUNIT_SUITE_ARGS[index]="$current"
}

function bashunit::suites::names() {
  local i=0
  local total=${#_BASHUNIT_SUITE_NAMES[@]}

  local seen=""
  local name
  while [ "$i" -lt "$total" ]; do
    name="${_BASHUNIT_SUITE_NAMES[i]}"
    case "$seen" in
    *"|$name|"*) ;;
    *)
      seen="$seen|$name|"
      printf '%s\n' "$name"
      ;;
    esac
    i=$((i + 1))
  done
}

function bashunit::suites::resolve() {
  local wanted=$1
  local i=0
  local total=${#_BASHUNIT_SUITE_NAMES[@]}

  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_SUITE_NAMES[i]}" = "$wanted" ]; then
      _BASHUNIT_SUITE_PATHS_OUT="${_BASHUNIT_SUITE_PATHS[i]}"
      _BASHUNIT_SUITE_ARGS_OUT="${_BASHUNIT_SUITE_ARGS[i]}"
      return 0
    fi
    i=$((i + 1))
  done

  local defined
  defined=$(bashunit::suites::names | tr '\n' ' ')
  defined=${defined%" "}
  if [ -z "$defined" ]; then
    defined="none defined in .bashunitrc"
  fi
  printf "%sError: unknown suite '%s'. Defined: %s.%s\n" \
    "${_BASHUNIT_COLOR_FAILED:-}" "$wanted" "$defined" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
  exit 1
}

# src/coverage/index.sh

# src/coverage/config.sh

_BASHUNIT_COVERAGE_DATA_FILE="${_BASHUNIT_COVERAGE_DATA_FILE:-}"
_BASHUNIT_COVERAGE_TRACKED_FILES="${_BASHUNIT_COVERAGE_TRACKED_FILES:-}"

_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="${_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE:-}"

_BASHUNIT_COVERAGE_TEST_HITS_FILE="${_BASHUNIT_COVERAGE_TEST_HITS_FILE:-}"

_BASHUNIT_COVERAGE_ENGINE_RESOLVED="${_BASHUNIT_COVERAGE_ENGINE_RESOLVED:-}"

_BASHUNIT_COVERAGE_IS_PARALLEL=""

function bashunit::coverage::auto_discover_paths() {
  local project_root
  project_root="$(pwd)"
  local -a discovered_paths=()
  local discovered_paths_count=0
  local test_file

  for test_file in "$@"; do

    local file_basename
    file_basename=$(basename "$test_file")

    local source_name="${file_basename%_test.sh}"
    [ "$source_name" = "$file_basename" ] && source_name="${file_basename%Test.sh}"
    [ "$source_name" = "$file_basename" ] && continue

    local found_file
    while IFS= read -r -d '' found_file; do

      case "$found_file" in
      *test* | *Test* | *vendor* | *node_modules*) continue ;;
      esac
      discovered_paths[discovered_paths_count]="$found_file"
      discovered_paths_count=$((discovered_paths_count + 1))
    done < <(find "$project_root" -name "${source_name}*.sh" -type f -print0 2>/dev/null)
  done

  if [ "$discovered_paths_count" -gt 0 ]; then
    printf '%s\n' "${discovered_paths[@]}" | sort -u | tr '\n' ',' | sed 's/,$//'
  fi
}

function bashunit::coverage::init() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  if [ -n "${_BASHUNIT_COVERAGE_DATA_FILE:-}" ]; then
    export BASHUNIT_COVERAGE=false
    return 0
  fi

  local coverage_dir
  coverage_dir=$("${MKTEMP:-mktemp}" -d "${BASHUNIT_TEMP_DIR:-${TMPDIR:-/tmp}}/bashunit-coverage.XXXXXXXX")

  _BASHUNIT_COVERAGE_DATA_FILE="${coverage_dir}/hits.dat"
  _BASHUNIT_COVERAGE_TRACKED_FILES="${coverage_dir}/files.dat"
  _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="${coverage_dir}/cache.dat"
  _BASHUNIT_COVERAGE_TEST_HITS_FILE="${coverage_dir}/test_hits.dat"

  : >"$_BASHUNIT_COVERAGE_DATA_FILE"
  : >"$_BASHUNIT_COVERAGE_TRACKED_FILES"
  _BASHUNIT_COVERAGE_SEEDED=false
  : >"$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE"
  : >"$_BASHUNIT_COVERAGE_TEST_HITS_FILE"

  _BASHUNIT_COVERAGE_DISKCACHE_FILE=""
  _BASHUNIT_COVERAGE_DISKCACHE_KEYS=()
  _BASHUNIT_COVERAGE_DISKCACHE_VALUES=()
  _BASHUNIT_COVERAGE_DISKCACHE_COUNT=0

  bashunit::coverage::build_trap_glob
  export _BASHUNIT_COVERAGE_TRAP_GLOB
  bashunit::coverage::invalidate_hits_aggregation
  bashunit::coverage::reset_lookup_namespace "_BASHUNIT_COVLOOKUP_FILE_"
  _BASHUNIT_COVERAGE_IS_PARALLEL=""
  _BASHUNIT_COVERAGE_STATS_FILES=()
  _BASHUNIT_COVERAGE_STATS_EXEC=()
  _BASHUNIT_COVERAGE_STATS_HIT=()
  _BASHUNIT_COVERAGE_STATS_PCT=()
  _BASHUNIT_COVERAGE_STATS_CLASS=()
  _BASHUNIT_COVERAGE_STATS_COUNT=0
  bashunit::coverage::reset_lookup_namespace "_BASHUNIT_COVLOOKUP_STATS_"

  _BASHUNIT_COVERAGE_ENGINE_RESOLVED=$(bashunit::coverage::resolve_engine)

  export _BASHUNIT_COVERAGE_DATA_FILE
  export _BASHUNIT_COVERAGE_TRACKED_FILES
  export _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE
  export _BASHUNIT_COVERAGE_TEST_HITS_FILE
  export _BASHUNIT_COVERAGE_ENGINE_RESOLVED
}

function bashunit::coverage::xtrace_is_supported() {
  if [ "${BASH_VERSINFO[0]}" -gt 4 ]; then
    return 0
  fi
  [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 1 ]
}

function bashunit::coverage::resolve_engine() {
  case "${BASHUNIT_COVERAGE_ENGINE:-auto}" in
  xtrace | auto)
    if bashunit::coverage::xtrace_is_supported; then
      echo "xtrace"
    else
      echo "trap"
    fi
    ;;
  *) echo "trap" ;;
  esac
}

function bashunit::coverage::engine_in_use() {
  if [ -n "${_BASHUNIT_COVERAGE_ENGINE_RESOLVED:-}" ]; then
    echo "$_BASHUNIT_COVERAGE_ENGINE_RESOLVED"
    return 0
  fi
  bashunit::coverage::resolve_engine
}

function bashunit::coverage::engine_was_downgraded() {
  if [ "${BASHUNIT_COVERAGE_ENGINE:-auto}" != "xtrace" ]; then
    return 1
  fi
  if bashunit::coverage::xtrace_is_supported; then
    return 1
  fi
  return 0
}

# src/coverage/paths.sh

_BASHUNIT_COVERAGE_LOOKUP_OUT=""

_BASHUNIT_COVERAGE_LOOKUP_KEY_OUT=""

function bashunit::coverage::lookup_key_to_slot() {
  _BASHUNIT_COVERAGE_LOOKUP_KEY_OUT="$1${2//[^a-zA-Z0-9]/_}"
}

function bashunit::coverage::lookup_put() {
  bashunit::coverage::lookup_key_to_slot "$1" "$2"
  local key=$_BASHUNIT_COVERAGE_LOOKUP_KEY_OUT
  eval "${key}_PATH=\$2"
  eval "${key}_VALUE=\$3"
}

function bashunit::coverage::lookup_get() {
  bashunit::coverage::lookup_key_to_slot "$1" "$2"
  local path_var="${_BASHUNIT_COVERAGE_LOOKUP_KEY_OUT}_PATH"
  local value_var="${_BASHUNIT_COVERAGE_LOOKUP_KEY_OUT}_VALUE"

  _BASHUNIT_COVERAGE_LOOKUP_OUT=""
  if [ "${!path_var:-}" != "$2" ]; then
    return 1
  fi
  _BASHUNIT_COVERAGE_LOOKUP_OUT="${!value_var:-}"
  return 0
}

function bashunit::coverage::reset_lookup_namespace() {
  local name
  for name in $(compgen -v "$1" 2>/dev/null || true); do
    unset "$name"
  done
}

function bashunit::coverage::normalize_path() {
  local file="$1"

  [ -f "$file" ] || {

    builtin printf '%s' "$file"
    return 0
  }

  local dir="${file%/*}" base="${file##*/}"
  if [ "$dir" = "$file" ]; then
    dir="."
  elif [ -z "$dir" ]; then
    dir="/"
  fi

  builtin printf '%s/%s' "$(cd "$dir" 2>/dev/null && pwd)" "$base"
}

function bashunit::coverage::get_tracked_files() {
  if [ ! -f "$_BASHUNIT_COVERAGE_TRACKED_FILES" ]; then
    return
  fi
  sort -u "$_BASHUNIT_COVERAGE_TRACKED_FILES"
}

_BASHUNIT_COVERAGE_TRAP_GLOB=""

function bashunit::coverage::_single_quote() {
  local value=$1

  local q="'"
  local esc="$q\\$q$q"
  printf "%s%s%s" "$q" "${value//$q/$esc}" "$q"
}

function bashunit::coverage::build_trap_glob() {
  _BASHUNIT_COVERAGE_TRAP_GLOB=""

  [ -n "${BASHUNIT_COVERAGE_PATHS:-}" ] || return 0

  local glob=""
  local cwd
  cwd=$(pwd)
  local old_ifs="$IFS"
  IFS=','
  local path resolved relative
  for path in $BASHUNIT_COVERAGE_PATHS; do
    [ -n "$path" ] || continue
    case "$path" in
    /*) resolved="$path" ;;
    *) resolved="$cwd/$path" ;;
    esac

    relative="$path"
    case "$relative" in
    "$cwd"/*) relative="${relative#"$cwd"/}" ;;
    esac

    local q_resolved q_relative
    q_resolved=$(bashunit::coverage::_single_quote "$resolved")
    q_relative=$(bashunit::coverage::_single_quote "$relative")

    if [ -n "$glob" ]; then
      glob="$glob|"
    fi
    glob="$glob${q_resolved}*|${q_relative}*|./${q_relative}*|*/${q_relative}/*|*/${q_relative}"
  done
  IFS="$old_ifs"

  _BASHUNIT_COVERAGE_TRAP_GLOB="$glob"
}

_BASHUNIT_COVERAGE_SEEDED=false

function bashunit::coverage::seed_tracked_files() {

  if [ "$_BASHUNIT_COVERAGE_SEEDED" = true ]; then
    return 0
  fi
  _BASHUNIT_COVERAGE_SEEDED=true

  [ -n "${BASHUNIT_COVERAGE_PATHS:-}" ] || return 0
  [ -n "${_BASHUNIT_COVERAGE_TRACKED_FILES:-}" ] || return 0

  local old_ifs="$IFS"
  IFS=','
  local path
  for path in $BASHUNIT_COVERAGE_PATHS; do
    [ -n "$path" ] || continue
    IFS="$old_ifs"
    bashunit::coverage::_seed_one_path "$path"
    IFS=','
  done
  IFS="$old_ifs"
}

function bashunit::coverage::_seed_one_path() {
  local path="$1"
  local root

  case "$path" in
  /*) root="$path" ;;
  *) root="$(pwd)/$path" ;;
  esac

  if [ -f "$root" ]; then
    root="$(bashunit::coverage::normalize_path "$root")"
    bashunit::coverage::_seed_emit "$root"
    return 0
  fi
  [ -d "$root" ] || return 0
  root="$(cd "$root" && pwd)"

  local file
  while IFS= read -r file; do
    [ -n "$file" ] || continue
    bashunit::coverage::_seed_emit "$file"
  done < <(find "$root" -type f -name '*.sh' 2>/dev/null)
}

function bashunit::coverage::_seed_emit() {
  local file="$1"
  local old_ifs="$IFS"
  IFS=','
  local pattern
  for pattern in ${BASHUNIT_COVERAGE_EXCLUDE:-}; do
    case "$file" in
    *$pattern*)
      IFS="$old_ifs"
      return 0
      ;;
    esac
  done
  IFS="$old_ifs"

  printf '%s\n' "$file" >>"$_BASHUNIT_COVERAGE_TRACKED_FILES"
}

_BASHUNIT_COVERAGE_DISKCACHE_FILE=""
_BASHUNIT_COVERAGE_DISKCACHE_KEYS=()
_BASHUNIT_COVERAGE_DISKCACHE_VALUES=()
_BASHUNIT_COVERAGE_DISKCACHE_COUNT=0

function bashunit::coverage::_diskcache_load() {
  local cache_file="$1"
  [ "$_BASHUNIT_COVERAGE_DISKCACHE_FILE" = "$cache_file" ] && return 0

  _BASHUNIT_COVERAGE_DISKCACHE_FILE="$cache_file"
  _BASHUNIT_COVERAGE_DISKCACHE_KEYS=()
  _BASHUNIT_COVERAGE_DISKCACHE_VALUES=()
  _BASHUNIT_COVERAGE_DISKCACHE_COUNT=0
  [ -f "$cache_file" ] || return 0

  local line idx=0
  while IFS= read -r line || [ -n "$line" ]; do
    [ -n "$line" ] || continue
    _BASHUNIT_COVERAGE_DISKCACHE_KEYS[idx]="${line%:*}"
    _BASHUNIT_COVERAGE_DISKCACHE_VALUES[idx]="${line##*:}"
    idx=$((idx + 1))
  done <"$cache_file"
  _BASHUNIT_COVERAGE_DISKCACHE_COUNT=$idx
}

_BASHUNIT_COVERAGE_DISKCACHE_OUT=""

function bashunit::coverage::_diskcache_get() {
  local file="$1" idx=0
  while [ "$idx" -lt "$_BASHUNIT_COVERAGE_DISKCACHE_COUNT" ]; do
    if [ "${_BASHUNIT_COVERAGE_DISKCACHE_KEYS[idx]}" = "$file" ]; then
      _BASHUNIT_COVERAGE_DISKCACHE_OUT="${_BASHUNIT_COVERAGE_DISKCACHE_VALUES[idx]}"
      return 0
    fi
    idx=$((idx + 1))
  done
  return 1
}

function bashunit::coverage::_diskcache_put() {
  local cache_file="$1" file="$2" decision="$3"
  { [ -n "$cache_file" ] && [ -f "$cache_file" ]; } || return 0

  echo "${file}:${decision}" >>"$cache_file"
  local idx="$_BASHUNIT_COVERAGE_DISKCACHE_COUNT"
  _BASHUNIT_COVERAGE_DISKCACHE_KEYS[idx]="$file"
  _BASHUNIT_COVERAGE_DISKCACHE_VALUES[idx]="$decision"
  _BASHUNIT_COVERAGE_DISKCACHE_COUNT=$((idx + 1))
}

function bashunit::coverage::should_track() {
  local file="$1"

  [ -z "$file" ] && return 1

  [ -z "$_BASHUNIT_COVERAGE_TRACKED_FILES" ] && return 1

  local cache_file="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE"
  if bashunit::parallel::is_enabled && [ -n "$cache_file" ]; then
    cache_file="${cache_file}.$$"

    if [ ! -f "$cache_file" ] && [ -d "${cache_file%/*}" ]; then
      : >"$cache_file"
    fi
  fi
  if [ -n "$cache_file" ]; then
    bashunit::coverage::_diskcache_load "$cache_file"
    if bashunit::coverage::_diskcache_get "$file"; then
      [ "$_BASHUNIT_COVERAGE_DISKCACHE_OUT" = "1" ] && return 0 || return 1
    fi
  fi

  local normalized_file
  normalized_file=$(bashunit::coverage::normalize_path "$file")

  local old_ifs="$IFS"
  IFS=','
  local pattern
  for pattern in $BASHUNIT_COVERAGE_EXCLUDE; do
    case "$normalized_file" in
    *$pattern*)
      IFS="$old_ifs"

      bashunit::coverage::_diskcache_put "$cache_file" "$file" "0"
      return 1
      ;;
    esac
  done

  local matched=false
  local path
  for path in $BASHUNIT_COVERAGE_PATHS; do

    local resolved_path
    case "$path" in
    /*)
      resolved_path="$path"
      ;;
    *)
      resolved_path="$(pwd)/$path"
      ;;
    esac

    while [ "$resolved_path" != "${resolved_path//\/\//\/}" ]; do
      resolved_path="${resolved_path//\/\//\/}"
    done

    case "$normalized_file" in
    "$resolved_path"*)
      matched=true
      break
      ;;
    esac
  done
  IFS="$old_ifs"

  if [ "$matched" = "false" ]; then

    bashunit::coverage::_diskcache_put "$cache_file" "$file" "0"
    return 1
  fi

  bashunit::coverage::_diskcache_put "$cache_file" "$file" "1"

  local tracked_file="$_BASHUNIT_COVERAGE_TRACKED_FILES"
  if bashunit::parallel::is_enabled; then
    tracked_file="${_BASHUNIT_COVERAGE_TRACKED_FILES}.$$"
  fi

  if [ -d "$(dirname "$tracked_file")" ]; then

    if ! grep -q "^${normalized_file}$" "$tracked_file" 2>/dev/null; then
      echo "$normalized_file" >>"$tracked_file"
    fi
  fi

  return 0
}

_BASHUNIT_COVERAGE_SAFE_NAME_OUT=""

function bashunit::coverage::path_to_filename_to_slot() {
  local file="$1"
  local display_file="${file#"$PWD"/}"

  local safe_name="${display_file//\//_}"
  _BASHUNIT_COVERAGE_SAFE_NAME_OUT="${safe_name//./_}"
}

function bashunit::coverage::path_to_filename() {
  bashunit::coverage::path_to_filename_to_slot "$1"
  echo "$_BASHUNIT_COVERAGE_SAFE_NAME_OUT"
}

# src/coverage/rules_awk.sh

_BASHUNIT_COVERAGE_AWK_RULES='
# Whether a source line counts as executable. Mirrors
# bashunit::coverage::is_executable_line, quirk for quirk.
function bu_is_executable(line,   tmp, stripped, trimmed, first, rest, fn_rest, fn_name, cp_before, cp_after, i, c) {
  # Empty means "nothing but SPACES": the reference strips spaces only, so a
  # line of tabs is not empty and goes on to the rules below.
  tmp = line
  gsub(/ /, "", tmp)
  if (tmp == "") { return 0 }

  stripped = line
  sub(/^[ \t]+/, "", stripped)
  trimmed = stripped
  sub(/[ \t]+$/, "", trimmed)

  if (substr(trimmed, 1, 1) == "#") { return 0 }
  if (trimmed == "{" || trimmed == "}" || trimmed == "\\") { return 0 }

  # The first token ends at whitespace or at a `#`, so `done#note` still reads
  # as the keyword `done`.
  first = trimmed
  sub(/[ \t].*$/, "", first)
  sub(/#.*$/, "", first)

  if (first == "then" || first == "else" || first == "fi" || first == "do" ||
      first == "done" || first == "esac" || first == "in" || first == ";;" ||
      first == ";;&" || first == ";&" || first == ")") {
    rest = substr(trimmed, length(first) + 1)
    sub(/^[ \t]+/, "", rest)
    if (rest == "" || substr(rest, 1, 1) == "#") { return 0 }
    # A loop terminator still terminates the loop when a redirection or a pipe
    # follows: `done < file`, `done | sort`.
    if (first == "done") { return 0 }
  }

  # Function declarations: `[function ]name()` with an optional trailing `{`,
  # and no trailing comment.
  if (index(trimmed, "()") > 0) {
    fn_rest = trimmed
    if (fn_rest ~ /^function[ \t]/) {
      sub(/^function/, "", fn_rest)
      sub(/^[ \t]+/, "", fn_rest)
    }
    if (substr(fn_rest, length(fn_rest), 1) == "{") {
      fn_rest = substr(fn_rest, 1, length(fn_rest) - 1)
      sub(/[ \t]+$/, "", fn_rest)
    }
    if (length(fn_rest) >= 2 && substr(fn_rest, length(fn_rest) - 1) == "()") {
      fn_name = substr(fn_rest, 1, length(fn_rest) - 2)
      sub(/[ \t]+$/, "", fn_name)
      if (fn_name ~ /^[a-zA-Z_]/) {
        # Every character after the first must be a name character; the
        # reference accepts `:` so bashunit::fn() reads as a declaration.
        for (i = 2; i <= length(fn_name); i++) {
          c = substr(fn_name, i, 1)
          if (c !~ /[a-zA-Z0-9_:]/) { return 1 }
        }
        return 0
      }
    }
  }

  # Case arms: something, then `)`, then end of line or a comment. The `)` only
  # closes an arm when no `(` opened earlier on the line, so `x=$(foo)`,
  # `((i++))` and `cmd <(sub)` stay statements (#1055).
  if (index(trimmed, ")") > 0) {
    cp_before = trimmed
    sub(/\).*$/, "", cp_before)
    if (cp_before != "" && index(cp_before, "(") == 0) {
      cp_after = substr(trimmed, length(cp_before) + 2)
      sub(/^[ \t]+/, "", cp_after)
      if (cp_after == "" || substr(cp_after, 1, 1) == "#") { return 0 }
    }
  }

  return 1
}

# Whether a source line ends with a line continuation: an odd number of
# trailing backslashes, and not a comment. Lives here because both the LCOV
# emitter and the stats pass propagate hits along a continuation chain (#722).
function bu_ends_with_continuation(line,   lead, i, n) {
  lead = line
  sub(/^[ \t]+/, "", lead)
  if (substr(lead, 1, 1) == "#") { return 0 }
  n = 0
  for (i = length(line); i >= 1; i--) {
    if (substr(line, i, 1) == "\\") { n++ } else { break }
  }
  return (n % 2) == 1
}
'

_BASHUNIT_COVERAGE_AWK_LCOV='
# The guard is FILENAME, not the usual `FNR == NR`: a run with no recorded hits
# passes an EMPTY first file, and `FNR == NR` is then true for the first record
# of the SECOND file, which would swallow the source line 1.
FILENAME == hitsfile {
  # The hits block: "<lineno> <count>".
  hits[$1] = $2
  next
}

{
  total++
  src[total] = $0
}

END {
  carry = 0
  for (ln = 1; ln <= total; ln++) {
    h = (ln in hits) ? hits[ln] + 0 : 0
    if (carry > 0 && h < carry) { h = carry; hits[ln] = h }
    if (h > 0 && bu_ends_with_continuation(src[ln])) { carry = h } else { carry = 0 }
  }

  executable = 0
  hit = 0
  for (ln = 1; ln <= total; ln++) {
    if (!bu_is_executable(src[ln])) { continue }
    executable++
    h = (ln in hits) ? hits[ln] + 0 : 0
    if (h > 0) { hit++ }
    printf "DA:%s,%s\n", ln, h
  }
  printf "LF:%s\n", executable
  printf "LH:%s\n", hit
}
'

_BASHUNIT_COVERAGE_AWK_STATS='
{
  hitsfile = $0
  sub(/\t.*$/, "", hitsfile)
  src = $0
  sub(/^[^\t]*\t/, "", src)

  split("", hits)
  if (hitsfile != "") {
    while ((getline hline < hitsfile) > 0) {
      split(hline, hp, " ")
      hits[hp[1] + 0] = hp[2] + 0
    }
    close(hitsfile)
  }

  total = 0
  split("", sl)
  while ((getline sline < src) > 0) {
    total++
    sl[total] = sline
  }
  close(src)

  # The DEBUG trap attributes a multi-line statement to its starting line, so
  # the count carries forward across the backslash chain (#722).
  carry = 0
  for (ln = 1; ln <= total; ln++) {
    h = (ln in hits) ? hits[ln] : 0
    if (carry > 0 && h < carry) { h = carry; hits[ln] = h }
    if (h > 0 && bu_ends_with_continuation(sl[ln])) { carry = h } else { carry = 0 }
  }

  executable = 0
  hit = 0
  for (ln = 1; ln <= total; ln++) {
    if (!bu_is_executable(sl[ln])) { continue }
    executable++
    if ((ln in hits) && hits[ln] > 0) { hit++ }
  }

  print executable "\t" hit "\t" src
}
'

_BASHUNIT_COVERAGE_AWK_LCOV_ALL='
# An arm ran as often as its FIRST executable line did (#1061).
function bu_arm_taken(s, e,   ln) {
  for (ln = s; ln <= e; ln++) {
    if (!bu_is_executable(sl[ln])) { continue }
    return (ln in hits) ? hits[ln] : 0
  }
  return 0
}

BEGIN { print "TN:" }

{
  hitsfile = $0
  sub(/\t.*$/, "", hitsfile)
  src = $0
  sub(/^[^\t]*\t/, "", src)

  split("", hits)
  if (hitsfile != "") {
    while ((getline hline < hitsfile) > 0) {
      split(hline, hp, " ")
      hits[hp[1] + 0] = hp[2] + 0
    }
    close(hitsfile)
  }

  total = 0
  split("", sl)
  while ((getline sline < src) > 0) {
    total++
    sl[total] = sline
  }
  close(src)

  # The DEBUG trap attributes a multi-line statement to its starting line, so
  # the count carries forward across the backslash chain (#722).
  carry = 0
  for (ln = 1; ln <= total; ln++) {
    h = (ln in hits) ? hits[ln] : 0
    if (carry > 0 && h < carry) { h = carry; hits[ln] = h }
    if (h > 0 && bu_ends_with_continuation(sl[ln])) { carry = h } else { carry = 0 }
  }

  bu_fn_reset()
  bu_br_reset()
  for (ln = 1; ln <= total; ln++) {
    bu_fn_line(sl[ln], ln)
    bu_br_line(sl[ln], ln)
  }
  bu_fn_finish(total)

  print "SF:" src

  # FN lines as we walk, the matching FNDA lines after them, per LCOV
  # convention.
  fn_hit = 0
  fnda = ""
  for (i = 1; i <= fn_count; i++) {
    print "FN:" fns[i] "," fnn[i]
    any = 0
    for (ln = fns[i]; ln <= fne[i]; ln++) {
      if ((ln in hits) && hits[ln] > 0) { any = 1; break }
    }
    fnda = fnda "FNDA:" any "," fnn[i] "\n"
    if (any == 1) { fn_hit++ }
  }
  printf "%s", fnda
  print "FNF:" fn_count
  print "FNH:" fn_hit

  br_total = 0
  br_hit = 0
  for (i = 1; i <= br_count; i++) {
    n = split(br_arms[i], arms, ",")
    for (a = 1; a <= n; a++) {
      split(arms[a], se, ":")
      taken = bu_arm_taken(se[1] + 0, se[2] + 0)
      print "BRDA:" br_dec[i] "," (i - 1) "," (a - 1) "," taken
      br_total++
      if (taken > 0) { br_hit++ }
    }
  }
  print "BRF:" br_total
  print "BRH:" br_hit

  executable = 0
  hit = 0
  for (ln = 1; ln <= total; ln++) {
    if (!bu_is_executable(sl[ln])) { continue }
    executable++
    h = (ln in hits) ? hits[ln] : 0
    if (h > 0) { hit++ }
    print "DA:" ln "," h
  }
  print "LF:" executable
  print "LH:" hit
  print "end_of_record"
}
'

function bashunit::coverage::awk_rules() {
  printf '%s' "$_BASHUNIT_COVERAGE_AWK_RULES"
}

function bashunit::coverage::awk_lcov_lines() {
  local file="$1"

  bashunit::coverage::ensure_hits_aggregated
  bashunit::coverage::hits_file_for "$file"
  local hits_file="$_BASHUNIT_COVERAGE_HITS_FILE_OUT"
  if [ -z "$hits_file" ] || [ ! -f "$hits_file" ]; then

    hits_file="/dev/null"
  fi

  env LC_ALL=C "$AWK" -v hitsfile="$hits_file" \
    "${_BASHUNIT_COVERAGE_AWK_RULES}${_BASHUNIT_COVERAGE_AWK_LCOV}" \
    "$hits_file" "$file"
}

function bashunit::coverage::awk_file_stats() {
  env LC_ALL=C "$AWK" \
    "${_BASHUNIT_COVERAGE_AWK_RULES}${_BASHUNIT_COVERAGE_AWK_STATS}" \
    "$1"
}

function bashunit::coverage::awk_lcov_report() {
  env LC_ALL=C "$AWK" \
    "${_BASHUNIT_COVERAGE_AWK_RULES}${_BASHUNIT_COVERAGE_AWK_FUNCTIONS}\
${_BASHUNIT_COVERAGE_AWK_BRANCHES}${_BASHUNIT_COVERAGE_AWK_LCOV_ALL}" \
    "$1"
}

# src/coverage/lines.sh

function bashunit::coverage::is_executable_line() {
  local line="$1"
  local lineno="$2"

  : "$lineno"

  [ -z "${line// /}" ] && return 1

  local stripped="${line#"${line%%[![:space:]]*}"}"
  local _trail="${stripped##*[![:space:]]}"
  local trimmed="${stripped%"$_trail"}"

  case "$trimmed" in
  '#'*) return 1 ;;
  '{' | '}' | [\\]) return 1 ;;
  esac

  local first="${trimmed%%[[:space:]]*}"
  first="${first%%'#'*}"
  case "$first" in
  'then' | 'else' | 'fi' | 'do' | 'done' | 'esac' | 'in' | ';;' | ';;&' | ';&' | ')')
    local rest="${trimmed#"$first"}"
    local _rl="${rest%%[![:space:]]*}"
    rest="${rest#"$_rl"}"
    case "$rest" in '' | '#'*) return 1 ;; esac

    if [ "$first" = 'done' ]; then
      return 1
    fi
    ;;
  esac

  case "$trimmed" in
  *'()'*)
    local fn_rest="$trimmed"
    case "$fn_rest" in
    'function'[[:space:]]*)
      fn_rest="${fn_rest#function}"
      fn_rest="${fn_rest#"${fn_rest%%[![:space:]]*}"}"
      ;;
    esac
    case "$fn_rest" in
    *'{')
      fn_rest="${fn_rest%'{'}"
      fn_rest="${fn_rest%"${fn_rest##*[![:space:]]}"}"
      ;;
    esac
    case "$fn_rest" in
    *'()')
      local fn_name="${fn_rest%'()'}"
      fn_name="${fn_name%"${fn_name##*[![:space:]]}"}"
      case "$fn_name" in
      [a-zA-Z_]*)
        case "${fn_name#?}" in
        *[!a-zA-Z0-9_:]*) : ;;
        *) return 1 ;;
        esac
        ;;
      esac
      ;;
    esac
    ;;
  esac

  case "$line" in
  *')'*)
    local cp_before="${line%%')'*}"
    case "$cp_before" in
    '' | *'('*) : ;;
    *)
      local cp_after="${line#*')'}"
      cp_after="${cp_after#"${cp_after%%[![:space:]]*}"}"
      case "$cp_after" in '' | '#'*) return 1 ;; esac
      ;;
    esac
    ;;
  esac

  return 0
}

function bashunit::coverage::get_executable_lines() {
  local file="$1"
  local count=0
  local lineno=0
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    ((++lineno))
    bashunit::coverage::is_executable_line "$line" "$lineno" && ((++count))
  done <"$file"

  echo "$count"
}

function bashunit::coverage::get_hit_lines() {
  local file="$1"

  if [ ! -f "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    echo "0"
    return
  fi

  local hit_lines
  hit_lines=$( (grep "^${file}:" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null || true) |
    cut -d: -f2 | sort -u)

  if [ -z "$hit_lines" ]; then
    echo "0"
    return
  fi

  local -a file_lines=()
  local _idx=0 _fl
  while IFS= read -r _fl || [ -n "$_fl" ]; do
    file_lines[_idx]="$_fl"
    ((++_idx))
  done <"$file"

  local count=0
  local line_num
  for line_num in $hit_lines; do
    local line_content="${file_lines[$((line_num - 1))]:-}"
    [ -z "$line_content" ] && continue
    if bashunit::coverage::is_executable_line "$line_content" "$line_num"; then
      ((++count))
    fi
  done

  echo "$count"
}

function bashunit::coverage::compute_file_coverage() {
  local file="$1"

  bashunit::coverage::load_hits_by_line "$file"

  local executable=0 hit=0 lineno=0 line line_hits
  local -a cv_lines=()
  local _cli=0 _cl
  while IFS= read -r _cl || [ -n "$_cl" ]; do
    cv_lines[_cli]="$_cl"
    ((++_cli))
  done <"$file"

  for line in "${cv_lines[@]}"; do
    ((++lineno))
    bashunit::coverage::is_executable_line "$line" "$lineno" || continue
    ((++executable))
    line_hits=${_BASHUNIT_COVERAGE_HITS_BY_LINE[lineno]:-0}
    [ "$line_hits" -gt 0 ] && ((++hit))
  done

  echo "${executable}:${hit}"
}

function bashunit::coverage::_ends_with_continuation() {
  local line="$1"
  local lead="${line#"${line%%[![:space:]]*}"}"
  case "$lead" in '#'*) return 1 ;; esac
  local trailing="${line##*[!\\]}"
  case "$line" in *[!\\]*) : ;; *) trailing="$line" ;; esac
  [ $((${#trailing} % 2)) -eq 1 ]
}

_BASHUNIT_COVERAGE_HITS_AGGREGATED=false
_BASHUNIT_COVERAGE_HITS_FILE_OUT=""

function bashunit::coverage::hits_file_for() {
  _BASHUNIT_COVERAGE_HITS_FILE_OUT=""
  [ -n "${_BASHUNIT_COVERAGE_DATA_FILE:-}" ] || return 0

  local dir="${_BASHUNIT_COVERAGE_DATA_FILE%/*}/hits"
  local name="${1//[^a-zA-Z0-9]/_}"
  _BASHUNIT_COVERAGE_HITS_FILE_OUT="$dir/$name"
}

_BASHUNIT_COVERAGE_MANIFEST_OUT=""

function bashunit::coverage::write_batch_manifest() {
  _BASHUNIT_COVERAGE_MANIFEST_OUT=""

  local data_dir="${_BASHUNIT_COVERAGE_DATA_FILE%/*}"
  { [ -n "${_BASHUNIT_COVERAGE_DATA_FILE:-}" ] && [ -d "$data_dir" ]; } || return 1

  bashunit::coverage::ensure_hits_aggregated

  local manifest="$data_dir/$1"
  local tracked=0 file
  {
    while IFS= read -r file; do
      { [ -z "$file" ] || [ ! -f "$file" ]; } && continue
      tracked=$((tracked + 1))
      bashunit::coverage::hits_file_for "$file"
      printf '%s\t%s\n' "$_BASHUNIT_COVERAGE_HITS_FILE_OUT" "$file"
    done < <(bashunit::coverage::get_tracked_files)
  } >"$manifest" 2>/dev/null || return 1

  if [ "$tracked" -gt 0 ]; then
    _BASHUNIT_COVERAGE_MANIFEST_OUT="$manifest"
  fi
  return 0
}

function bashunit::coverage::invalidate_hits_aggregation() {
  _BASHUNIT_COVERAGE_HITS_AGGREGATED=false

  _BASHUNIT_COVERAGE_HITS_BY_LINE_FILE=""
}

function bashunit::coverage::ensure_hits_aggregated() {
  if [ "$_BASHUNIT_COVERAGE_HITS_AGGREGATED" = true ]; then
    return 0
  fi
  _BASHUNIT_COVERAGE_HITS_AGGREGATED=true

  [ -n "${_BASHUNIT_COVERAGE_DATA_FILE:-}" ] || return 0
  [ -f "$_BASHUNIT_COVERAGE_DATA_FILE" ] || return 0

  local dir="${_BASHUNIT_COVERAGE_DATA_FILE%/*}/hits"
  rm -rf "$dir" 2>/dev/null || true
  mkdir -p "$dir" 2>/dev/null || return 0

  env LC_ALL=C awk -v dir="$dir" '
    {
      i = length($0)
      while (i > 0 && substr($0, i, 1) != ":") { i-- }
      if (i == 0) { next }
      path = substr($0, 1, i - 1)
      line = substr($0, i + 1)
      if (line !~ /^[0-9]+$/) { next }
      key = path SUBSEP line
      if (!(key in counts)) { order[++n] = key }
      counts[key]++
    }
    END {
      for (j = 1; j <= n; j++) {
        split(order[j], parts, SUBSEP)
        name = parts[1]
        gsub(/[^a-zA-Z0-9]/, "_", name)
        print parts[2], counts[order[j]] > (dir "/" name)
      }
      for (j = 1; j <= n; j++) {
        split(order[j], parts, SUBSEP)
        name = parts[1]
        gsub(/[^a-zA-Z0-9]/, "_", name)
        close(dir "/" name)
      }
    }
  ' "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null || true
}

function bashunit::coverage::get_all_line_hits() {
  local file="$1"

  if [ ! -f "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    return
  fi

  bashunit::coverage::ensure_hits_aggregated

  local -a counts=()
  local count lineno maxln=0
  local hits_file
  bashunit::coverage::hits_file_for "$file"
  hits_file=$_BASHUNIT_COVERAGE_HITS_FILE_OUT

  if [ -n "$hits_file" ] && [ -f "$hits_file" ]; then
    while read -r lineno count; do
      if [ -n "$count" ]; then
        counts[lineno]=$count
        [ "$lineno" -gt "$maxln" ] && maxln=$lineno
      fi
    done <"$hits_file"
  fi

  if [ "$maxln" -eq 0 ]; then
    return
  fi

  local -a src=()
  local _i=0 _l
  while IFS= read -r _l || [ -n "$_l" ]; do
    src[_i]="$_l"
    ((++_i))
  done <"$file"

  local total=$_i
  [ "$maxln" -gt "$total" ] && total=$maxln

  local carry=0 idx h
  for ((idx = 1; idx <= total; idx++)); do
    h=${counts[idx]:-0}
    if [ "$carry" -gt 0 ] && [ "$h" -lt "$carry" ]; then
      h=$carry
      counts[idx]=$h
    fi
    if [ "$h" -gt 0 ] && bashunit::coverage::_ends_with_continuation "${src[idx - 1]:-}"; then
      carry=$h
    else
      carry=0
    fi
  done

  local ln
  for ((ln = 1; ln <= total; ln++)); do
    [ "${counts[ln]:-0}" -gt 0 ] && echo "${ln}:${counts[ln]}"
  done

  return 0
}

declare -a _BASHUNIT_COVERAGE_HITS_BY_LINE

_BASHUNIT_COVERAGE_HITS_BY_LINE_FILE=""

function bashunit::coverage::load_hits_by_line() {
  local file="$1"

  if [ -n "$file" ] && [ "$file" = "$_BASHUNIT_COVERAGE_HITS_BY_LINE_FILE" ]; then
    return 0
  fi

  bashunit::coverage::ensure_hits_aggregated

  _BASHUNIT_COVERAGE_HITS_BY_LINE=()
  _BASHUNIT_COVERAGE_HITS_BY_LINE_FILE="$file"
  local hl_lineno hl_count
  while IFS=: read -r hl_lineno hl_count; do
    [ -n "$hl_lineno" ] && _BASHUNIT_COVERAGE_HITS_BY_LINE[hl_lineno]=$hl_count
  done < <(bashunit::coverage::get_all_line_hits "$file")
}

function bashunit::coverage::get_all_line_tests() {
  local file="$1"

  if [ ! -f "${_BASHUNIT_COVERAGE_TEST_HITS_FILE:-}" ]; then
    return
  fi

  grep "^${file}:" "$_BASHUNIT_COVERAGE_TEST_HITS_FILE" 2>/dev/null |
    sed "s|^${file}:||" | sort -u
}

# src/coverage/functions.sh

_BASHUNIT_COVERAGE_AWK_FUNCTIONS='
BEGIN { SQ = sprintf("%c", 39) }

# Scans one line under the quote and heredoc state carried over from the lines
# before it -- a string or a heredoc body can span lines, so per-line state is
# not enough. Sets nopen/nclose to the braces that are code, and code_start to
# 1 when the line begins outside any string or heredoc, which is the only place
# a declaration can start.
function bu_scan(line,   i, n, c, rest, delim, q) {
  nopen = 0
  nclose = 0
  code_start = (in_s == 0 && in_d == 0 && hd == "")

  if (hd != "") {
    rest = line
    if (hd_strip) { sub(/^\t+/, "", rest) }
    if (rest == hd) { hd = "" }
    return
  }

  n = length(line)
  for (i = 1; i <= n; i++) {
    c = substr(line, i, 1)

    if (in_s) {
      # Single quotes take no escapes: the next one always closes.
      if (c == SQ) { in_s = 0 }
      continue
    }
    if (in_d) {
      if (c == "\\") { i++; continue }
      if (c == "\"") { in_d = 0 }
      continue
    }
    if (c == "\\") { i++; continue }
    if (c == SQ) { in_s = 1; continue }
    if (c == "\"") { in_d = 1; continue }

    # A `#` opens a comment only where bash opens one, at the start of a word,
    # so ${x#foo} and a#b keep their braces.
    if (c == "#") {
      if (i == 1) { return }
      q = substr(line, i - 1, 1)
      if (q == " " || q == "\t" || q == ";" || q == "&" || q == "|" || q == "(") { return }
      continue
    }

    if (c == "<" && substr(line, i + 1, 1) == "<") {
      # `<<<` is a here-string: one line, no body. Consume all three so the
      # second `<` cannot read as the start of a heredoc and swallow the file.
      if (substr(line, i + 2, 1) == "<") { i = i + 2; continue }

      rest = substr(line, i + 2)
      hd_strip = 0
      if (substr(rest, 1, 1) == "-") { hd_strip = 1; rest = substr(rest, 2) }
      sub(/^[ \t]+/, "", rest)
      q = substr(rest, 1, 1)
      if (q == SQ || q == "\"") {
        delim = substr(rest, 2)
        if (index(delim, q) == 0) {
          delim = ""
        } else {
          sub(q ".*$", "", delim)
        }
      } else {
        delim = rest
        sub(/[ \t;)&|<>].*$/, "", delim)
      }
      # The body starts on the next line, so nothing after the operator on this
      # one can close the function.
      if (delim != "") { hd = delim; return }
      continue
    }

    if (c == "{") { nopen++ } else if (c == "}") { nclose++ }
  }
}

# Feeds one line to the scanner, recording a declaration when it opens and its
# span when the braces balance. A caller reads the records out of fnn/fns/fne,
# which is what lets the per-file API below and the batch report share this.
function bu_fn_line(line, lineno,   stripped, name, after, ok) {
  bu_scan(line)

  if (in_function == 0 && code_start) {
    # Pattern 1: function name() { or function name {
    # Pattern 2: name() { or name () {
    stripped = line
    sub(/^[ \t]+/, "", stripped)
    if (stripped ~ /^function[ \t]/) {
      sub(/^function/, "", stripped)
      sub(/^[ \t]+/, "", stripped)
    }

    # The candidate name is the first word, ending at whitespace, `(` or `{`.
    name = stripped
    sub(/[ \t({].*$/, "", name)

    if (name != "") {
      ok = 1
      # A candidate holding anything outside the identifier alphabet is not a
      # function name. Cutting at the first `{` means `VAR="x${Y}"` yields
      # `VAR="x$`, whose trailing `{Y}"` then looks like a body opener -- every
      # such assignment became a phantom FN record, and one containing the
      # record separator `|` shifted the fields and crashed the arithmetic in
      # report_lcov (#936). Checking only the first character let all of that
      # through.
      if (name ~ /[^a-zA-Z0-9_:]/) {
        ok = 0
      } else if (name !~ /^[a-zA-Z_]/) {
        ok = 0
      } else {
        # A declaration continues with `()` or `{`; a call does not.
        after = substr(stripped, length(name) + 1)
        sub(/^[ \t]+/, "", after)
        if (substr(after, 1, 2) != "()" && substr(after, 1, 1) != "{") { ok = 0 }
      }

      if (ok) {
        in_function = 1
        current_fn = name
        fn_start = lineno
        brace_count = nopen - nclose
        # Single-line function: braces balance on the same line, both present.
        if (brace_count == 0 && nopen > 0 && nclose > 0) { bu_fn_emit(lineno) }
        return
      }
    }
  }

  if (in_function == 1) {
    brace_count = brace_count + nopen - nclose
    if (brace_count <= 0) { bu_fn_emit(lineno) }
  }
}

function bu_fn_emit(endline) {
  fn_count++
  fnn[fn_count] = current_fn
  fns[fn_count] = fn_start
  fne[fn_count] = endline
  in_function = 0
  current_fn = ""
  brace_count = 0
}

# An unclosed function (should not happen in valid code) still gets a record,
# ending at the last line, so a truncated file cannot drop one silently.
function bu_fn_finish(lastline) {
  if (in_function == 1 && current_fn != "") { bu_fn_emit(lastline) }
}

# Clears the per-file state: quote and heredoc carry-over, the open
# declaration, and the records collected so far.
function bu_fn_reset() {
  in_s = 0; in_d = 0; hd = ""; hd_strip = 0
  in_function = 0; current_fn = ""; brace_count = 0
  fn_count = 0
}
'

_BASHUNIT_COVERAGE_AWK_FUNCTIONS_MAIN='
{ bu_fn_line($0, NR) }

END {
  bu_fn_finish(NR)
  for (bu_i = 1; bu_i <= fn_count; bu_i++) {
    print fnn[bu_i] "|" fns[bu_i] "|" fne[bu_i]
  }
}
'

function bashunit::coverage::extract_functions() {
  env LC_ALL=C "$AWK" \
    "${_BASHUNIT_COVERAGE_AWK_FUNCTIONS}${_BASHUNIT_COVERAGE_AWK_FUNCTIONS_MAIN}" "$1"
}

# src/coverage/engine.sh

_BASHUNIT_COVERAGE_XTRACE_FS=$'\034'

_BASHUNIT_COVERAGE_XTRACE_PS4='@|${BASH_SOURCE}'"$_BASHUNIT_COVERAGE_XTRACE_FS"
_BASHUNIT_COVERAGE_XTRACE_PS4="$_BASHUNIT_COVERAGE_XTRACE_PS4"'${LINENO}'"$_BASHUNIT_COVERAGE_XTRACE_FS"' '

_BASHUNIT_COVERAGE_XTRACE_FD=""
_BASHUNIT_COVERAGE_XTRACE_FILE=""
_BASHUNIT_COVERAGE_XTRACE_SAVED_PS4=""

_BASHUNIT_COVERAGE_DATA_TARGET_OUT=""
_BASHUNIT_COVERAGE_HITS_TARGET_OUT=""

function bashunit::coverage::enable_trap() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  local engine="${_BASHUNIT_COVERAGE_ENGINE_RESOLVED:-}"
  if [ -z "$engine" ]; then
    engine=$(bashunit::coverage::resolve_engine)
  fi

  if [ "$engine" = "xtrace" ]; then
    bashunit::coverage::_enable_xtrace
    return 0
  fi

  set -T

  local record='bashunit::coverage::record_line "${BASH_SOURCE[0]:-}" "${LINENO:-}"'
  if [ -n "${_BASHUNIT_COVERAGE_TRAP_GLOB:-}" ]; then

    local guarded='case "${BASH_SOURCE[0]:-}" in '
    guarded="$guarded${_BASHUNIT_COVERAGE_TRAP_GLOB}) $record ;; esac"

    trap "$guarded" DEBUG
  else

    trap "$record" DEBUG
  fi
}

function bashunit::coverage::disable_trap() {
  if [ -n "$_BASHUNIT_COVERAGE_XTRACE_FD" ]; then
    bashunit::coverage::_disable_xtrace
    return 0
  fi

  trap - DEBUG
  set +T
}

function bashunit::coverage::_enable_xtrace() {
  local coverage_dir="${_BASHUNIT_COVERAGE_DATA_FILE%/*}"
  _BASHUNIT_COVERAGE_XTRACE_FILE="${coverage_dir}/xtrace.$$.${_BASHUNIT_RUNNER_RESULT_ORDINAL:-0}.trace"

  exec {_BASHUNIT_COVERAGE_XTRACE_FD}>>"$_BASHUNIT_COVERAGE_XTRACE_FILE"

  local test_ctx=""
  if [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FILE:-}" ] &&
    [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FN:-}" ]; then
    test_ctx="${_BASHUNIT_COVERAGE_CURRENT_TEST_FILE}:${_BASHUNIT_COVERAGE_CURRENT_TEST_FN}"
  fi
  builtin printf '%sTEST%s%s\n' "$_BASHUNIT_COVERAGE_XTRACE_FS" \
    "$_BASHUNIT_COVERAGE_XTRACE_FS" "$test_ctx" >&"$_BASHUNIT_COVERAGE_XTRACE_FD"

  _BASHUNIT_COVERAGE_XTRACE_SAVED_PS4="${PS4:-}"
  BASH_XTRACEFD=$_BASHUNIT_COVERAGE_XTRACE_FD
  PS4=$_BASHUNIT_COVERAGE_XTRACE_PS4
  set -x
}

function bashunit::coverage::_disable_xtrace() {
  set +x
  PS4=$_BASHUNIT_COVERAGE_XTRACE_SAVED_PS4

  local fd="$_BASHUNIT_COVERAGE_XTRACE_FD"
  _BASHUNIT_COVERAGE_XTRACE_FD=""
  _BASHUNIT_COVERAGE_XTRACE_FILE=""

  unset BASH_XTRACEFD
  exec {fd}>&-
}

_BASHUNIT_COVERAGE_XTRACE_PATHS_AWK='
{
  if (substr($0, 1, 1) != "@") { next }
  i = 1
  while (substr($0, i, 1) == "@") { i++ }
  if (substr($0, i, 1) != "|") { next }
  rest = substr($0, i + 1)
  p = index(rest, fsc)
  if (p == 0) { next }
  path = substr(rest, 1, p - 1)
  if (path != "" && !(path in seen)) { seen[path] = 1; print path }
}
'

_BASHUNIT_COVERAGE_XTRACE_EMIT_AWK='
FILENAME == map_file {
  tab = index($0, "\t")
  if (tab > 0) {
    norm = substr($0, tab + 1)
    if (norm != "-") { tracked[substr($0, 1, tab - 1)] = norm }
  }
  next
}
substr($0, 1, 1) == fsc {
  head = substr($0, 2)
  if (substr(head, 1, 5) == "TEST" fsc) { ctx = substr(head, 6); next }
}
{
  if (substr($0, 1, 1) != "@") { next }
  i = 1
  while (substr($0, i, 1) == "@") { i++ }
  if (substr($0, i, 1) != "|") { next }
  rest = substr($0, i + 1)
  p = index(rest, fsc)
  if (p == 0) { next }
  path = substr(rest, 1, p - 1)
  if (!(path in tracked)) { next }
  tail = substr(rest, p + 1)
  q = index(tail, fsc)
  if (q == 0) { next }
  line = substr(tail, 1, q - 1)
  if (line == "") { next }
  record = tracked[path] ":" line
  print record >> data_out
  if (ctx != "") { print record "|" ctx >> hits_out }
}
'

function bashunit::coverage::finalize() {
  bashunit::coverage::invalidate_hits_aggregation
  if [ "${_BASHUNIT_COVERAGE_ENGINE_RESOLVED:-}" != "xtrace" ]; then
    return 0
  fi
  [ -n "$_BASHUNIT_COVERAGE_DATA_FILE" ] || return 0

  local coverage_dir="${_BASHUNIT_COVERAGE_DATA_FILE%/*}"
  local -a traces=()
  local trace
  for trace in "$coverage_dir"/xtrace.*.trace; do
    [ -f "$trace" ] || continue
    traces[${#traces[@]}]="$trace"
  done
  [ "${#traces[@]}" -gt 0 ] || return 0

  local map_file="${coverage_dir}/xtrace-map.dat"
  local paths_file="${coverage_dir}/xtrace-paths.dat"
  : >"$map_file"

  "$AWK" -v fsc="$_BASHUNIT_COVERAGE_XTRACE_FS" \
    "$_BASHUNIT_COVERAGE_XTRACE_PATHS_AWK" "${traces[@]}" >"$paths_file"

  local path
  while IFS= read -r path; do
    [ -n "$path" ] || continue
    if bashunit::coverage::should_track "$path"; then
      builtin printf '%s\t%s\n' "$path" \
        "$(bashunit::coverage::normalize_path "$path")" >>"$map_file"
    else
      builtin printf '%s\t-\n' "$path" >>"$map_file"
    fi
  done <"$paths_file"

  "$AWK" \
    -v map_file="$map_file" \
    -v fsc="$_BASHUNIT_COVERAGE_XTRACE_FS" \
    -v data_out="$_BASHUNIT_COVERAGE_DATA_FILE" \
    -v hits_out="$_BASHUNIT_COVERAGE_TEST_HITS_FILE" \
    "$_BASHUNIT_COVERAGE_XTRACE_EMIT_AWK" "$map_file" "${traces[@]}"

  rm -f "$paths_file" "${traces[@]}"
}

function bashunit::coverage::record_line() {
  local file="$1"
  local lineno="$2"

  { [ -z "$file" ] || [ -z "$lineno" ]; } && return 0

  [ -z "$_BASHUNIT_COVERAGE_DATA_FILE" ] && return 0

  local entry="" decision="" normalized_file=""
  if bashunit::coverage::lookup_get "_BASHUNIT_COVLOOKUP_FILE_" "$file"; then
    entry="$_BASHUNIT_COVERAGE_LOOKUP_OUT"
    decision="${entry%% *}"
    [ "$decision" = "0" ] && return 0
    normalized_file="${entry#* }"
  else
    if bashunit::coverage::should_track "$file"; then
      decision=1
      normalized_file=$(bashunit::coverage::normalize_path "$file")
      bashunit::coverage::lookup_put "_BASHUNIT_COVLOOKUP_FILE_" "$file" \
        "1 $normalized_file"
    else
      bashunit::coverage::lookup_put "_BASHUNIT_COVLOOKUP_FILE_" "$file" "0"
      return 0
    fi
  fi

  bashunit::coverage::_resolve_output_files
  builtin printf '%s:%s\n' "$normalized_file" "$lineno" \
    >>"$_BASHUNIT_COVERAGE_DATA_TARGET_OUT"

  if [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FILE:-}" ] &&
    [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FN:-}" ]; then
    builtin printf '%s:%s|%s:%s\n' "$normalized_file" "$lineno" \
      "$_BASHUNIT_COVERAGE_CURRENT_TEST_FILE" "$_BASHUNIT_COVERAGE_CURRENT_TEST_FN" \
      >>"$_BASHUNIT_COVERAGE_HITS_TARGET_OUT"
  fi

  _BASHUNIT_COVERAGE_HITS_AGGREGATED=false
  _BASHUNIT_COVERAGE_HITS_BY_LINE_FILE=""
}

function bashunit::coverage::_resolve_output_files() {

  if [ -z "$_BASHUNIT_COVERAGE_IS_PARALLEL" ]; then
    if bashunit::parallel::is_enabled; then
      _BASHUNIT_COVERAGE_IS_PARALLEL="yes"
    else
      _BASHUNIT_COVERAGE_IS_PARALLEL="no"
    fi
  fi

  if [ "$_BASHUNIT_COVERAGE_IS_PARALLEL" = "yes" ]; then
    _BASHUNIT_COVERAGE_DATA_TARGET_OUT="${_BASHUNIT_COVERAGE_DATA_FILE}.$$"
    _BASHUNIT_COVERAGE_HITS_TARGET_OUT="${_BASHUNIT_COVERAGE_TEST_HITS_FILE}.$$"
  else
    _BASHUNIT_COVERAGE_DATA_TARGET_OUT="$_BASHUNIT_COVERAGE_DATA_FILE"
    _BASHUNIT_COVERAGE_HITS_TARGET_OUT="$_BASHUNIT_COVERAGE_TEST_HITS_FILE"
  fi
}

function bashunit::coverage::flush_buffer() {
  bashunit::coverage::invalidate_hits_aggregation
}

function bashunit::coverage::aggregate_parallel() {
  bashunit::coverage::invalidate_hits_aggregation

  local base_file="$_BASHUNIT_COVERAGE_DATA_FILE"
  local tracked_base="$_BASHUNIT_COVERAGE_TRACKED_FILES"
  local test_hits_base="$_BASHUNIT_COVERAGE_TEST_HITS_FILE"

  local pid_files pid_file
  pid_files=$(ls -1 "${base_file}."* 2>/dev/null) || true
  if [ -n "$pid_files" ]; then
    while IFS= read -r pid_file; do
      [ -f "$pid_file" ] || continue
      cat "$pid_file" >>"$base_file"
      rm -f "$pid_file"
    done <<<"$pid_files"
  fi

  pid_files=$(ls -1 "${tracked_base}."* 2>/dev/null) || true
  if [ -n "$pid_files" ]; then
    while IFS= read -r pid_file; do
      [ -f "$pid_file" ] || continue
      cat "$pid_file" >>"$tracked_base"
      rm -f "$pid_file"
    done <<<"$pid_files"
  fi

  if [ -n "$test_hits_base" ]; then
    pid_files=$(ls -1 "${test_hits_base}."* 2>/dev/null) || true
    if [ -n "$pid_files" ]; then
      while IFS= read -r pid_file; do
        [ -f "$pid_file" ] || continue
        cat "$pid_file" >>"$test_hits_base"
        rm -f "$pid_file"
      done <<<"$pid_files"
    fi
  fi

  if [ -f "$tracked_base" ]; then
    sort -u "$tracked_base" -o "$tracked_base"
  fi
}

function bashunit::coverage::cleanup() {
  if [ -n "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    local coverage_dir
    coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE")
    rm -rf "$coverage_dir"
  fi
}

# src/coverage/stats.sh

_BASHUNIT_COVERAGE_CLASS_OUT=""
_BASHUNIT_COVERAGE_COLOR_OUT=""

_BASHUNIT_COVERAGE_TOTAL_EXEC_OUT=0
_BASHUNIT_COVERAGE_TOTAL_HIT_OUT=0

function bashunit::coverage::class_to_slot() {
  local pct="$1"
  if [ "$pct" -ge "${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}" ]; then
    _BASHUNIT_COVERAGE_CLASS_OUT="high"
  elif [ "$pct" -ge "${BASHUNIT_COVERAGE_THRESHOLD_LOW:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}" ]; then
    _BASHUNIT_COVERAGE_CLASS_OUT="medium"
  else
    _BASHUNIT_COVERAGE_CLASS_OUT="low"
  fi
}

function bashunit::coverage::color_to_slot() {
  case "$1" in
  high) _BASHUNIT_COVERAGE_COLOR_OUT="$_BASHUNIT_COLOR_PASSED" ;;
  medium) _BASHUNIT_COVERAGE_COLOR_OUT="$_BASHUNIT_COLOR_SKIPPED" ;;
  low) _BASHUNIT_COVERAGE_COLOR_OUT="$_BASHUNIT_COLOR_FAILED" ;;
  *) _BASHUNIT_COVERAGE_COLOR_OUT="" ;;
  esac
}

function bashunit::coverage::get_coverage_class() {
  bashunit::coverage::class_to_slot "$1"
  echo "$_BASHUNIT_COVERAGE_CLASS_OUT"
}

function bashunit::coverage::get_color_for_class() {
  bashunit::coverage::color_to_slot "$1"
  printf '%s' "$_BASHUNIT_COVERAGE_COLOR_OUT"
}

function bashunit::coverage::calculate_percentage() {
  local hit="$1"
  local executable="$2"
  if [ "$executable" -gt 0 ]; then
    echo $((hit * 100 / executable))
  else
    echo "0"
  fi
}

_BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT=""
_BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT=""
_BASHUNIT_COVERAGE_FILE_STATS_PCT_OUT=""
_BASHUNIT_COVERAGE_FILE_STATS_CLASS_OUT=""

function bashunit::coverage::_compute_file_stats() {
  local file="$1"
  local stats
  stats=$(bashunit::coverage::compute_file_coverage "$file")
  bashunit::coverage::_derive_file_stats "${stats%%:*}" "${stats##*:}"
}

function bashunit::coverage::_derive_file_stats() {
  local executable="$1" hit="$2"
  _BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT="$executable"
  _BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT="$hit"

  local pct=0
  if [ "$executable" -gt 0 ]; then
    pct=$((hit * 100 / executable))
  fi
  _BASHUNIT_COVERAGE_FILE_STATS_PCT_OUT="$pct"

  bashunit::coverage::class_to_slot "$pct"
  _BASHUNIT_COVERAGE_FILE_STATS_CLASS_OUT="$_BASHUNIT_COVERAGE_CLASS_OUT"
}

function bashunit::coverage::get_file_stats() {
  bashunit::coverage::_compute_file_stats "$1"
  echo "${_BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT}:${_BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT}:\
${_BASHUNIT_COVERAGE_FILE_STATS_PCT_OUT}:${_BASHUNIT_COVERAGE_FILE_STATS_CLASS_OUT}"
}

_BASHUNIT_COVERAGE_STATS_FILES=()
_BASHUNIT_COVERAGE_STATS_EXEC=()
_BASHUNIT_COVERAGE_STATS_HIT=()
_BASHUNIT_COVERAGE_STATS_PCT=()
_BASHUNIT_COVERAGE_STATS_CLASS=()
_BASHUNIT_COVERAGE_STATS_COUNT=0

function bashunit::coverage::precompute_file_stats() {

  bashunit::coverage::seed_tracked_files

  _BASHUNIT_COVERAGE_STATS_FILES=()
  _BASHUNIT_COVERAGE_STATS_EXEC=()
  _BASHUNIT_COVERAGE_STATS_HIT=()
  _BASHUNIT_COVERAGE_STATS_PCT=()
  _BASHUNIT_COVERAGE_STATS_CLASS=()
  _BASHUNIT_COVERAGE_STATS_COUNT=0
  bashunit::coverage::reset_lookup_namespace "_BASHUNIT_COVLOOKUP_STATS_"

  if bashunit::coverage::_precompute_batch; then
    return 0
  fi

  local file
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    bashunit::coverage::_compute_file_stats "$file"
    bashunit::coverage::_record_file_stats "$file" \
      "$_BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT" "$_BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT"
  done < <(bashunit::coverage::get_tracked_files)
}

function bashunit::coverage::_record_file_stats() {
  local file="$1"
  bashunit::coverage::_derive_file_stats "$2" "$3"

  local idx="$_BASHUNIT_COVERAGE_STATS_COUNT"
  _BASHUNIT_COVERAGE_STATS_FILES[idx]="$file"
  _BASHUNIT_COVERAGE_STATS_EXEC[idx]="$_BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT"
  _BASHUNIT_COVERAGE_STATS_HIT[idx]="$_BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT"
  _BASHUNIT_COVERAGE_STATS_PCT[idx]="$_BASHUNIT_COVERAGE_FILE_STATS_PCT_OUT"
  _BASHUNIT_COVERAGE_STATS_CLASS[idx]="$_BASHUNIT_COVERAGE_FILE_STATS_CLASS_OUT"
  _BASHUNIT_COVERAGE_STATS_COUNT=$((idx + 1))
  bashunit::coverage::lookup_put "_BASHUNIT_COVLOOKUP_STATS_" "$file" "$idx"
}

function bashunit::coverage::_precompute_batch() {
  bashunit::coverage::write_batch_manifest "stats-manifest" || return 1
  local manifest="$_BASHUNIT_COVERAGE_MANIFEST_OUT"

  if [ -z "$manifest" ]; then

    return 0
  fi

  local executable hit file
  while IFS="$(printf '\t')" read -r executable hit file; do
    [ -n "$file" ] || continue
    bashunit::coverage::_record_file_stats "$file" "$executable" "$hit"
  done < <(bashunit::coverage::awk_file_stats "$manifest" 2>/dev/null)

  [ "$_BASHUNIT_COVERAGE_STATS_COUNT" -gt 0 ]
}

function bashunit::coverage::cached_stats_to_slots() {
  local file="$1"

  if bashunit::coverage::lookup_get "_BASHUNIT_COVLOOKUP_STATS_" "$file"; then
    local idx="$_BASHUNIT_COVERAGE_LOOKUP_OUT"
    _BASHUNIT_COVERAGE_SPLIT_EXEC_OUT="${_BASHUNIT_COVERAGE_STATS_EXEC[idx]}"
    _BASHUNIT_COVERAGE_SPLIT_HIT_OUT="${_BASHUNIT_COVERAGE_STATS_HIT[idx]}"
    _BASHUNIT_COVERAGE_SPLIT_PCT_OUT="${_BASHUNIT_COVERAGE_STATS_PCT[idx]}"
    _BASHUNIT_COVERAGE_SPLIT_CLASS_OUT="${_BASHUNIT_COVERAGE_STATS_CLASS[idx]}"
    return 0
  fi

  bashunit::coverage::_compute_file_stats "$file"
  _BASHUNIT_COVERAGE_SPLIT_EXEC_OUT="$_BASHUNIT_COVERAGE_FILE_STATS_EXEC_OUT"
  _BASHUNIT_COVERAGE_SPLIT_HIT_OUT="$_BASHUNIT_COVERAGE_FILE_STATS_HIT_OUT"
  _BASHUNIT_COVERAGE_SPLIT_PCT_OUT="$_BASHUNIT_COVERAGE_FILE_STATS_PCT_OUT"
  _BASHUNIT_COVERAGE_SPLIT_CLASS_OUT="$_BASHUNIT_COVERAGE_FILE_STATS_CLASS_OUT"
}

function bashunit::coverage::get_cached_stats() {
  local file="$1"

  if bashunit::coverage::lookup_get "_BASHUNIT_COVLOOKUP_STATS_" "$file"; then
    local idx="$_BASHUNIT_COVERAGE_LOOKUP_OUT"
    echo "${_BASHUNIT_COVERAGE_STATS_EXEC[idx]}:${_BASHUNIT_COVERAGE_STATS_HIT[idx]}\
:${_BASHUNIT_COVERAGE_STATS_PCT[idx]}:${_BASHUNIT_COVERAGE_STATS_CLASS[idx]}"
    return 0
  fi

  bashunit::coverage::get_file_stats "$file"
}

_BASHUNIT_COVERAGE_SPLIT_EXEC_OUT=""
_BASHUNIT_COVERAGE_SPLIT_HIT_OUT=""
_BASHUNIT_COVERAGE_SPLIT_PCT_OUT=""
_BASHUNIT_COVERAGE_SPLIT_CLASS_OUT=""

function bashunit::coverage::split_stats() {
  local stats="$1" rest
  _BASHUNIT_COVERAGE_SPLIT_EXEC_OUT="${stats%%:*}"
  rest="${stats#*:}"
  _BASHUNIT_COVERAGE_SPLIT_HIT_OUT="${rest%%:*}"
  rest="${rest#*:}"
  _BASHUNIT_COVERAGE_SPLIT_PCT_OUT="${rest%%:*}"
  _BASHUNIT_COVERAGE_SPLIT_CLASS_OUT="${rest#*:}"
}

function bashunit::coverage::totals_to_slots() {
  local total_executable=0
  local total_hit=0

  if [ "$_BASHUNIT_COVERAGE_STATS_COUNT" -gt 0 ]; then
    local i
    for ((i = 0; i < _BASHUNIT_COVERAGE_STATS_COUNT; i++)); do
      total_executable=$((total_executable + _BASHUNIT_COVERAGE_STATS_EXEC[i]))
      total_hit=$((total_hit + _BASHUNIT_COVERAGE_STATS_HIT[i]))
    done
  else
    while IFS= read -r file; do
      { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

      local executable hit
      executable=$(bashunit::coverage::get_executable_lines "$file")
      hit=$(bashunit::coverage::get_hit_lines "$file")

      total_executable=$((total_executable + executable))
      total_hit=$((total_hit + hit))
    done < <(bashunit::coverage::get_tracked_files)
  fi

  _BASHUNIT_COVERAGE_TOTAL_EXEC_OUT=$total_executable
  _BASHUNIT_COVERAGE_TOTAL_HIT_OUT=$total_hit
}

function bashunit::coverage::get_percentage() {
  bashunit::coverage::totals_to_slots
  bashunit::coverage::calculate_percentage \
    "$_BASHUNIT_COVERAGE_TOTAL_HIT_OUT" "$_BASHUNIT_COVERAGE_TOTAL_EXEC_OUT"
}

function bashunit::coverage::check_threshold() {
  if [ -z "$BASHUNIT_COVERAGE_MIN" ]; then
    return 0
  fi

  local pct

  if bashunit::coverage::is_diff_enabled && [ -n "$_BASHUNIT_COVERAGE_DIFF_PCT_OUT" ]; then
    pct="$_BASHUNIT_COVERAGE_DIFF_PCT_OUT"
  else

    bashunit::coverage::totals_to_slots
    pct=$(bashunit::coverage::calculate_percentage \
      "$_BASHUNIT_COVERAGE_TOTAL_HIT_OUT" "$_BASHUNIT_COVERAGE_TOTAL_EXEC_OUT")
  fi

  if [ "$pct" -lt "$BASHUNIT_COVERAGE_MIN" ]; then
    local message
    if [ "${_BASHUNIT_COVERAGE_TOTAL_EXEC_OUT:-0}" -eq 0 ]; then

      message=$(printf "%sCoverage gate failed: no executable lines were tracked%s\n%s" \
        "$_BASHUNIT_COLOR_FAILED" "$_BASHUNIT_COLOR_DEFAULT" \
        "Check --coverage-paths (BASHUNIT_COVERAGE_PATHS): it matched no shell file with executable code.")
    else
      message=$(printf "%sCoverage %d%% is below minimum %d%%%s" \
        "$_BASHUNIT_COLOR_FAILED" "$pct" "$BASHUNIT_COVERAGE_MIN" "$_BASHUNIT_COLOR_DEFAULT")
    fi

    if bashunit::env::is_machine_output_enabled; then
      printf "%s\n" "$message" >&2
    else
      printf "%s\n" "$message"
    fi
    return 1
  fi

  return 0
}

# src/coverage/branches.sh

_BASHUNIT_BRANCH_ARMS_OUT=""

function bashunit::coverage::_append_arm() {
  local existing="$1" arm_start="$2" arm_end="$3"
  if [ -z "$existing" ]; then
    _BASHUNIT_BRANCH_ARMS_OUT="${arm_start}:${arm_end}"
  else
    _BASHUNIT_BRANCH_ARMS_OUT="${existing},${arm_start}:${arm_end}"
  fi
}

function bashunit::coverage::_is_case_pattern_line() {
  local trimmed="$1"
  case "$trimmed" in
  *')'*) ;;
  *) return 1 ;;
  esac

  local before_paren="${trimmed%%')'*}"
  local after="${trimmed#"$before_paren"}"
  after="${after#)}"
  after="${after#"${after%%[![:space:]]*}"}"
  case "$after" in
  '' | '#'*) return 0 ;;
  esac
  return 1
}

function bashunit::coverage::_branch_push_if() {
  local lineno=$1
  if_decision_line[if_depth]=$lineno
  if_arms[if_depth]=""
  if_arm_start[if_depth]=$((lineno + 1))
  if_depth=$((if_depth + 1))
}

function bashunit::coverage::_branch_close_if_arm() {
  local lineno=$1 idx=$((if_depth - 1))
  bashunit::coverage::_append_arm \
    "${if_arms[$idx]}" "${if_arm_start[$idx]}" "$((lineno - 1))"
  if_arms[idx]="$_BASHUNIT_BRANCH_ARMS_OUT"
  if_arm_start[idx]=$((lineno + 1))
}

function bashunit::coverage::_branch_emit_if() {
  local lineno=$1 idx=$((if_depth - 1))
  bashunit::coverage::_append_arm \
    "${if_arms[$idx]}" "${if_arm_start[$idx]}" "$((lineno - 1))"
  echo "${if_decision_line[$idx]}|if|${_BASHUNIT_BRANCH_ARMS_OUT}"
  if_depth=$idx
}

function bashunit::coverage::_branch_push_case() {
  local lineno=$1
  case_decision_line[case_depth]=$lineno
  case_arms[case_depth]=""
  case_arm_start[case_depth]=0
  case_in_pattern[case_depth]=0
  case_depth=$((case_depth + 1))
}

function bashunit::coverage::_branch_close_case_arm() {
  local lineno=$1 idx=$((case_depth - 1))
  [ "${case_in_pattern[$idx]}" = "1" ] || return 0
  bashunit::coverage::_append_arm \
    "${case_arms[$idx]}" "${case_arm_start[$idx]}" "$((lineno - 1))"
  case_arms[idx]="$_BASHUNIT_BRANCH_ARMS_OUT"
  case_in_pattern[idx]=0
}

function bashunit::coverage::_branch_emit_case() {
  local lineno=$1 idx=$((case_depth - 1))
  bashunit::coverage::_branch_close_case_arm "$lineno"
  if [ -n "${case_arms[$idx]}" ]; then
    echo "${case_decision_line[$idx]}|case|${case_arms[$idx]}"
  fi
  case_depth=$idx
}

function bashunit::coverage::_branch_open_case_pattern() {
  local lineno=$1 idx=$((case_depth - 1))
  case_arm_start[idx]=$((lineno + 1))
  case_in_pattern[idx]=1
}

function bashunit::coverage::_branch_push_loop() {
  local lineno=$1
  loop_decision_line[loop_depth]=$lineno
  loop_arm_start[loop_depth]=$((lineno + 1))
  loop_depth=$((loop_depth + 1))
}

function bashunit::coverage::_branch_emit_loop() {
  local lineno=$1 idx=$((loop_depth - 1))
  echo "${loop_decision_line[$idx]}|loop|${loop_arm_start[$idx]}:$((lineno - 1))"
  loop_depth=$idx
}

_BASHUNIT_COVERAGE_AWK_BRANCHES='
function bu_br_append_arm(existing, s, e) {
  return (existing == "") ? (s ":" e) : (existing "," s ":" e)
}

# A case-pattern opener ends with `)`, optionally followed by a comment. This
# does not exclude a `(` earlier on the line -- the reference does not either.
function bu_br_is_case_pattern(t,   before, after) {
  if (index(t, ")") == 0) { return 0 }
  before = t
  sub(/\).*$/, "", before)
  after = substr(t, length(before) + 2)
  sub(/^[ \t]+/, "", after)
  return (after == "" || substr(after, 1, 1) == "#")
}

function bu_br_add(decision, kind, arms) {
  br_count++
  br_dec[br_count] = decision
  br_kind[br_count] = kind
  br_arms[br_count] = arms
}

function bu_br_line(line, lineno,   trimmed, first, idx) {
  trimmed = line
  sub(/^[ \t]+/, "", trimmed)
  if (trimmed == "" || substr(trimmed, 1, 1) == "#") { return }

  first = trimmed
  sub(/[ \t;].*$/, "", first)

  if (first == "if") {
    if_decision_line[if_depth] = lineno
    if_arms[if_depth] = ""
    if_arm_start[if_depth] = lineno + 1
    if_depth++
  } else if (first == "elif" || first == "else") {
    if (if_depth > 0) {
      idx = if_depth - 1
      if_arms[idx] = bu_br_append_arm(if_arms[idx], if_arm_start[idx], lineno - 1)
      if_arm_start[idx] = lineno + 1
    }
  } else if (first == "fi") {
    if (if_depth > 0) {
      idx = if_depth - 1
      bu_br_add(if_decision_line[idx], "if", bu_br_append_arm(if_arms[idx], if_arm_start[idx], lineno - 1))
      if_depth = idx
    }
  } else if (first == "case") {
    case_decision_line[case_depth] = lineno
    case_arms[case_depth] = ""
    case_arm_start[case_depth] = 0
    case_in_pattern[case_depth] = 0
    case_depth++
  } else if (first == "esac") {
    if (case_depth > 0) {
      idx = case_depth - 1
      if (case_in_pattern[idx] == 1) {
        case_arms[idx] = bu_br_append_arm(case_arms[idx], case_arm_start[idx], lineno - 1)
        case_in_pattern[idx] = 0
      }
      if (case_arms[idx] != "") { bu_br_add(case_decision_line[idx], "case", case_arms[idx]) }
      case_depth = idx
    }
  } else if (first == "while" || first == "until" || first == "for" || first == "select") {
    loop_decision_line[loop_depth] = lineno
    loop_arm_start[loop_depth] = lineno + 1
    loop_depth++
  } else if (first == "done") {
    if (loop_depth > 0) {
      idx = loop_depth - 1
      bu_br_add(loop_decision_line[idx], "loop", loop_arm_start[idx] ":" (lineno - 1))
      loop_depth = idx
    }
  } else if (case_depth > 0) {
    idx = case_depth - 1
    if (trimmed ~ /^;;&/ || trimmed ~ /^;;/ || trimmed ~ /^;&/) {
      if (case_in_pattern[idx] == 1) {
        case_arms[idx] = bu_br_append_arm(case_arms[idx], case_arm_start[idx], lineno - 1)
        case_in_pattern[idx] = 0
      }
    } else if (bu_br_is_case_pattern(trimmed)) {
      case_arm_start[idx] = lineno + 1
      case_in_pattern[idx] = 1
    }
  }
}

# The depths must be numeric before they index anything: an uninitialised awk
# variable is the empty string as a subscript, so the first push would land in
# arr[""] and the matching pop would read arr[0].
function bu_br_reset() {
  if_depth = 0
  case_depth = 0
  loop_depth = 0
  br_count = 0
}
'

function bashunit::coverage::extract_branches() {
  local file="$1"

  local -a lines=()
  local _i=0 _l
  while IFS= read -r _l || [ -n "$_l" ]; do
    lines[_i]="$_l"
    ((++_i))
  done <"$file"
  local total_lines=$_i

  local -a if_decision_line=() if_arms=() if_arm_start=()
  local if_depth=0
  local -a case_decision_line=() case_arms=() case_arm_start=() case_in_pattern=()
  local case_depth=0
  local -a loop_decision_line=() loop_arm_start=()
  local loop_depth=0

  local lineno=0 line trimmed first
  while [ "$lineno" -lt "$total_lines" ]; do
    line="${lines[$lineno]}"
    lineno=$((lineno + 1))

    trimmed="${line#"${line%%[![:space:]]*}"}"
    case "$trimmed" in '' | '#'*) continue ;; esac
    first="${trimmed%%[[:space:]\;]*}"

    case "$first" in
    'if') bashunit::coverage::_branch_push_if "$lineno" ;;
    'elif' | 'else')
      [ "$if_depth" -gt 0 ] && bashunit::coverage::_branch_close_if_arm "$lineno"
      ;;
    'fi')
      [ "$if_depth" -gt 0 ] && bashunit::coverage::_branch_emit_if "$lineno"
      ;;
    'case') bashunit::coverage::_branch_push_case "$lineno" ;;
    'esac')
      [ "$case_depth" -gt 0 ] && bashunit::coverage::_branch_emit_case "$lineno"
      ;;
    'while' | 'until' | 'for' | 'select')
      bashunit::coverage::_branch_push_loop "$lineno"
      ;;
    'done')
      [ "$loop_depth" -gt 0 ] && bashunit::coverage::_branch_emit_loop "$lineno"
      ;;
    *)
      [ "$case_depth" -eq 0 ] && continue
      case "$trimmed" in
      ';;&'* | ';;'* | ';&'*)
        bashunit::coverage::_branch_close_case_arm "$lineno"
        ;;
      *)
        if bashunit::coverage::_is_case_pattern_line "$trimmed"; then
          bashunit::coverage::_branch_open_case_pattern "$lineno"
        fi
        ;;
      esac
      ;;
    esac
  done
}

_BASHUNIT_ARM_TAKEN_OUT=0

function bashunit::coverage::_arm_taken() {
  local arm_start="$1" arm_end="$2" ln
  for ((ln = arm_start; ln <= arm_end; ln++)); do
    bashunit::coverage::is_executable_line \
      "${src_lines[$((ln - 1))]:-}" "$ln" || continue
    _BASHUNIT_ARM_TAKEN_OUT="${_BASHUNIT_COVERAGE_HITS_BY_LINE[$ln]:-0}"
    return
  done
  _BASHUNIT_ARM_TAKEN_OUT=0
}

function bashunit::coverage::compute_branch_hits() {
  local file="$1"

  bashunit::coverage::load_hits_by_line "$file"

  local -a src_lines=()
  local _sli=0 _sl
  while IFS= read -r _sl || [ -n "$_sl" ]; do
    src_lines[_sli]="$_sl"
    ((++_sli))
  done <"$file"

  local block=0 decision_line _kind arms branch_entry
  local -a arm_specs=()
  local arm arm_index
  while IFS= read -r branch_entry; do
    [ -z "$branch_entry" ] && continue
    IFS='|' read -r decision_line _kind arms <<<"$branch_entry"

    arm_index=0
    IFS=',' read -ra arm_specs <<<"$arms"
    for arm in "${arm_specs[@]}"; do
      bashunit::coverage::_arm_taken "${arm%%:*}" "${arm##*:}"
      echo "${decision_line}|${block}|${arm_index}|${_BASHUNIT_ARM_TAKEN_OUT}"
      arm_index=$((arm_index + 1))
    done

    block=$((block + 1))
  done < <(bashunit::coverage::extract_branches "$file")
}

# src/coverage/report_text.sh

function bashunit::coverage::print_engine_notice() {
  if bashunit::coverage::engine_was_downgraded; then
    printf "%sWarning: coverage engine 'xtrace' needs Bash 4.1+ (running %s.%s); using 'trap'.%s\n" \
      "$_BASHUNIT_COLOR_INCOMPLETE" "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" \
      "$_BASHUNIT_COLOR_DEFAULT"
  fi

  if bashunit::env::is_verbose_enabled; then
    printf "Coverage engine: %s\n" "$(bashunit::coverage::engine_in_use)"

    if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
      printf "%sNote: Bash %s.%s does not report lines run inside a subshell; \
coverage may read lower than on Bash 4+.%s\n" \
        "$_BASHUNIT_COLOR_INCOMPLETE" "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" \
        "$_BASHUNIT_COLOR_DEFAULT"
    fi
  fi
}

function bashunit::coverage::report_text() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  local total_executable=0
  local total_hit=0
  local has_files=false

  echo ""
  bashunit::coverage::print_engine_notice
  echo "Coverage Report"
  echo "---------------"

  local file
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue
    has_files=true

    local executable hit pct class stats
    stats=$(bashunit::coverage::get_cached_stats "$file")
    bashunit::coverage::split_stats "$stats"
    executable="$_BASHUNIT_COVERAGE_SPLIT_EXEC_OUT"
    hit="$_BASHUNIT_COVERAGE_SPLIT_HIT_OUT"
    pct="$_BASHUNIT_COVERAGE_SPLIT_PCT_OUT"
    class="$_BASHUNIT_COVERAGE_SPLIT_CLASS_OUT"

    total_executable=$((total_executable + executable))
    total_hit=$((total_hit + hit))

    local color reset="$_BASHUNIT_COLOR_DEFAULT"
    bashunit::coverage::color_to_slot "$class"
    color="$_BASHUNIT_COVERAGE_COLOR_OUT"

    local display_file="${file#"$(pwd)"/}"
    printf "%s%-40s %3d/%3d lines (%3d%%)%s\n" \
      "$color" "$display_file" "$hit" "$executable" "$pct" "$reset"
  done < <(bashunit::coverage::get_tracked_files)

  if [ "$has_files" != "true" ]; then
    echo "---------------"
    echo "Total: 0/0 (0%)"
    return 0
  fi

  echo "---------------"

  local total_pct total_class
  total_pct=$(bashunit::coverage::calculate_percentage "$total_hit" "$total_executable")
  bashunit::coverage::class_to_slot "$total_pct"
  total_class="$_BASHUNIT_COVERAGE_CLASS_OUT"

  local color reset="$_BASHUNIT_COLOR_DEFAULT"
  bashunit::coverage::color_to_slot "$total_class"
  color="$_BASHUNIT_COVERAGE_COLOR_OUT"

  printf "%sTotal: %d/%d (%d%%)%s\n" \
    "$color" "$total_hit" "$total_executable" "$total_pct" "$reset"

  if [ "${BASHUNIT_COVERAGE_SHOW_FUNCTIONS:-false}" = "true" ]; then
    bashunit::coverage::report_text_functions
  fi

  if [ "${BASHUNIT_COVERAGE_SHOW_UNCOVERED:-false}" = "true" ]; then
    bashunit::coverage::report_text_uncovered
  fi

  if [ "${BASHUNIT_COVERAGE_SHOW_LINE_HITS:-false}" = "true" ]; then
    bashunit::coverage::report_text_line_hits
  fi

  if [ -n "$BASHUNIT_COVERAGE_REPORT" ]; then
    echo ""
    echo "Coverage report written to: $BASHUNIT_COVERAGE_REPORT"
  fi
}

_BASHUNIT_RANGES_OUT=""

function bashunit::coverage::_compress_ranges() {
  local out="" start="" end="" n
  for n in "$@"; do
    if [ -z "$start" ]; then
      start="$n"
      end="$n"
    elif [ "$n" -eq $((end + 1)) ]; then
      end="$n"
    else
      if [ "$start" = "$end" ]; then
        out="${out}${start},"
      else
        out="${out}${start}-${end},"
      fi
      start="$n"
      end="$n"
    fi
  done
  if [ -n "$start" ]; then
    if [ "$start" = "$end" ]; then
      out="${out}${start}"
    else
      out="${out}${start}-${end}"
    fi
  fi
  _BASHUNIT_RANGES_OUT="${out%,}"
}

function bashunit::coverage::report_text_uncovered() {
  local file
  local printed_header=false
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    bashunit::coverage::load_hits_by_line "$file"

    local -a uncovered_lines=()
    local _ucount=0
    local lineno=0 line
    while IFS= read -r line || [ -n "$line" ]; do
      lineno=$((lineno + 1))
      bashunit::coverage::is_executable_line "$line" "$lineno" || continue
      local lh="${_BASHUNIT_COVERAGE_HITS_BY_LINE[$lineno]:-0}"
      if [ "$lh" -eq 0 ]; then
        uncovered_lines[_ucount]="$lineno"
        _ucount=$((_ucount + 1))
      fi
    done <"$file"

    [ "$_ucount" -eq 0 ] && continue

    if [ "$printed_header" != "true" ]; then
      echo ""
      echo "Uncovered Lines"
      echo "---------------"
      printed_header=true
    fi

    local display_file="${file#"$(pwd)"/}"
    local color="$_BASHUNIT_COLOR_FAILED" reset="$_BASHUNIT_COLOR_DEFAULT"
    local out
    bashunit::coverage::_compress_ranges "${uncovered_lines[@]}"
    out="$_BASHUNIT_RANGES_OUT"

    printf "%s%s:%s%s\n" "$color" "$display_file" "$out" "$reset"
  done < <(bashunit::coverage::get_tracked_files)
}

function bashunit::coverage::report_text_line_hits() {
  local IFS=$' \t\n'
  local file
  local printed_header=false
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    bashunit::coverage::load_hits_by_line "$file"

    local -a hit_specs=()
    local _hc=0
    local lineno=0 line
    while IFS= read -r line || [ -n "$line" ]; do
      lineno=$((lineno + 1))
      bashunit::coverage::is_executable_line "$line" "$lineno" || continue
      local lh="${_BASHUNIT_COVERAGE_HITS_BY_LINE[$lineno]:-0}"
      if [ "$lh" -gt 0 ]; then
        hit_specs[_hc]="${lineno}:${lh}"
        _hc=$((_hc + 1))
      fi
    done <"$file"

    [ "$_hc" -eq 0 ] && continue

    if [ "$printed_header" != "true" ]; then
      echo ""
      echo "Line Hits"
      echo "---------"
      printed_header=true
    fi

    local display_file="${file#"$(pwd)"/}"
    printf "%s: %s\n" "$display_file" "${hit_specs[*]}"
  done < <(bashunit::coverage::get_tracked_files)
}

function bashunit::coverage::report_text_functions() {
  local file
  local printed_header=false
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local functions_data
    functions_data=$(bashunit::coverage::extract_functions "$file")
    [ -z "$functions_data" ] && continue

    bashunit::coverage::load_hits_by_line "$file"

    local -a file_lines=()
    local _fli=0 _fl
    while IFS= read -r _fl || [ -n "$_fl" ]; do
      file_lines[_fli]="$_fl"
      ((++_fli))
    done <"$file"

    local display_file="${file#"$(pwd)"/}"

    if [ "$printed_header" != "true" ]; then
      echo ""
      echo "Functions"
      echo "---------"
      printed_header=true
    fi
    echo "${display_file}"

    local fn_name fn_start fn_end ln fn_executable fn_hit
    local fn_pct fn_class color reset="$_BASHUNIT_COLOR_DEFAULT"
    while IFS='|' read -r fn_name fn_start fn_end; do
      [ -z "$fn_name" ] && continue

      fn_executable=0
      fn_hit=0
      for ((ln = fn_start; ln <= fn_end; ln++)); do
        bashunit::coverage::is_executable_line \
          "${file_lines[$((ln - 1))]:-}" "$ln" || continue
        fn_executable=$((fn_executable + 1))
        [ "${_BASHUNIT_COVERAGE_HITS_BY_LINE[$ln]:-0}" -gt 0 ] && fn_hit=$((fn_hit + 1))
      done

      fn_pct=0
      if [ "$fn_executable" -gt 0 ]; then
        fn_pct=$((fn_hit * 100 / fn_executable))
      fi
      bashunit::coverage::class_to_slot "$fn_pct"
      fn_class="$_BASHUNIT_COVERAGE_CLASS_OUT"
      bashunit::coverage::color_to_slot "$fn_class"
      color="$_BASHUNIT_COVERAGE_COLOR_OUT"

      printf "  %s%-38s %3d/%3d lines (%3d%%)%s\n" \
        "$color" "$fn_name" "$fn_hit" "$fn_executable" "$fn_pct" "$reset"
    done <<<"$functions_data"
  done < <(bashunit::coverage::get_tracked_files)
}

# src/coverage/diff.sh

function bashunit::coverage::is_diff_enabled() {
  [ -n "${BASHUNIT_COVERAGE_DIFF:-}" ]
}

function bashunit::coverage::diff_base() {
  echo "${BASHUNIT_COVERAGE_DIFF:-}"
}

function bashunit::coverage::diff_percentage() {
  local total="$1"
  local hit="$2"
  if [ "$total" -le 0 ]; then
    echo "100"
    return 0
  fi
  echo $((hit * 100 / total))
}

function bashunit::coverage::changed_line_stats() {
  local base="$1"
  local file="$2"

  local changed
  changed="$(bashunit::helper::git_changed_lines "$base" "$file")"
  if [ -z "$changed" ]; then
    echo "0:0"
    return 0
  fi

  local -a src=()
  local _i=0 _l
  while IFS= read -r _l || [ -n "$_l" ]; do
    src[_i]="$_l"
    _i=$((_i + 1))
  done <"$file"

  local total=0 hit=0 lineno content
  for lineno in $changed; do
    content="${src[$((lineno - 1))]:-}"
    if bashunit::coverage::is_executable_line "$content" "$lineno"; then
      total=$((total + 1))
      if [ "${_BASHUNIT_COVERAGE_HITS_BY_LINE[lineno]:-0}" -gt 0 ]; then
        hit=$((hit + 1))
      fi
    fi
  done

  echo "${total}:${hit}"
}

function bashunit::coverage::diff_files() {
  local base=$1
  local changed
  changed="$(bashunit::helper::git_changed_files "$base")"
  [ -n "$changed" ] || return 0

  local tracked
  tracked="$(bashunit::coverage::get_tracked_files)"

  local file normalized
  while IFS= read -r file; do
    [ -n "$file" ] || continue
    [ -f "$file" ] || continue

    normalized="$(bashunit::coverage::normalize_path "$file")"
    case "$file" in
    *.sh) ;;
    *)
      case "
$tracked
" in
      *"
$normalized
"*) ;;
      *) continue ;;
      esac
      ;;
    esac

    if bashunit::coverage::should_track "$file"; then
      printf '%s\n' "$normalized"
    fi
  done <<EOF
$changed
EOF
}

function bashunit::coverage::report_diff() {
  local base
  base="$(bashunit::coverage::diff_base)"

  echo ""
  bashunit::coverage::print_engine_notice
  printf 'Diff Coverage (vs %s)\n' "$base"
  echo "---------------"

  local total_changed=0 total_hit=0 has_files=false
  local file stats changed hit pct color reset="$_BASHUNIT_COLOR_DEFAULT"
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    bashunit::coverage::load_hits_by_line "$file"
    stats="$(bashunit::coverage::changed_line_stats "$base" "$file")"
    changed="${stats%%:*}"
    hit="${stats##*:}"
    [ "$changed" -eq 0 ] && continue

    has_files=true
    total_changed=$((total_changed + changed))
    total_hit=$((total_hit + hit))

    pct=$(bashunit::coverage::diff_percentage "$changed" "$hit")
    bashunit::coverage::class_to_slot "$pct"
    bashunit::coverage::color_to_slot "$_BASHUNIT_COVERAGE_CLASS_OUT"
    color="$_BASHUNIT_COVERAGE_COLOR_OUT"

    local display_file="${file#"$(pwd)"/}"
    printf "%s%-40s %3d/%3d lines (%3d%%)%s\n" \
      "$color" "$display_file" "$hit" "$changed" "$pct" "$reset"
  done < <(bashunit::coverage::diff_files "$base")

  if [ "$has_files" = false ]; then
    echo "No changed executable lines."
  fi

  echo "---------------"
  local total_pct
  total_pct=$(bashunit::coverage::diff_percentage "$total_changed" "$total_hit")
  bashunit::coverage::class_to_slot "$total_pct"
  bashunit::coverage::color_to_slot "$_BASHUNIT_COVERAGE_CLASS_OUT"
  color="$_BASHUNIT_COVERAGE_COLOR_OUT"
  printf "%sTotal: %d/%d (%d%%)%s\n" \
    "$color" "$total_hit" "$total_changed" "$total_pct" "$reset"

  _BASHUNIT_COVERAGE_DIFF_PCT_OUT="$total_pct"
}

_BASHUNIT_COVERAGE_DIFF_PCT_OUT=""

# src/coverage/report_lcov.sh

function bashunit::coverage::report_lcov() {
  local output_file="${1:-$BASHUNIT_COVERAGE_REPORT}"

  if [ -z "$output_file" ]; then
    return 0
  fi

  mkdir -p "$(dirname "$output_file")"

  if bashunit::coverage::write_batch_manifest "lcov-manifest"; then
    if [ -z "$_BASHUNIT_COVERAGE_MANIFEST_OUT" ]; then
      echo "TN:" >"$output_file"
      return 0
    fi
    if bashunit::coverage::awk_lcov_report "$_BASHUNIT_COVERAGE_MANIFEST_OUT" \
      >"$output_file" 2>/dev/null && [ -s "$output_file" ]; then
      return 0
    fi
  fi

  {
    echo "TN:"

    while IFS= read -r file; do
      { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

      echo "SF:$file"

      bashunit::coverage::load_hits_by_line "$file"

      local fn_total=0 fn_hit=0 fn_name fn_start fn_end fln any_hit
      local -a fn_dn_records=()
      local _fdi=0
      while IFS='|' read -r fn_name fn_start fn_end; do
        [ -z "$fn_name" ] && continue

        case "$fn_start$fn_end" in '' | *[!0-9]*) continue ;; esac
        echo "FN:${fn_start},${fn_name}"
        fn_total=$((fn_total + 1))

        any_hit=0
        for ((fln = fn_start; fln <= fn_end; fln++)); do
          if [ "${_BASHUNIT_COVERAGE_HITS_BY_LINE[$fln]:-0}" -gt 0 ]; then
            any_hit=1
            break
          fi
        done
        fn_dn_records[_fdi]="FNDA:${any_hit},${fn_name}"
        _fdi=$((_fdi + 1))
        [ "$any_hit" -eq 1 ] && fn_hit=$((fn_hit + 1))
      done < <(bashunit::coverage::extract_functions "$file")

      local fda
      for fda in ${fn_dn_records[@]+"${fn_dn_records[@]}"}; do
        echo "$fda"
      done
      echo "FNF:$fn_total"
      echo "FNH:$fn_hit"

      local br_total=0 br_hit=0 br_line br_block br_idx br_taken
      while IFS='|' read -r br_line br_block br_idx br_taken; do
        [ -z "$br_line" ] && continue
        echo "BRDA:${br_line},${br_block},${br_idx},${br_taken}"
        br_total=$((br_total + 1))
        [ "$br_taken" -gt 0 ] && br_hit=$((br_hit + 1))
      done < <(bashunit::coverage::compute_branch_hits "$file")
      echo "BRF:$br_total"
      echo "BRH:$br_hit"

      bashunit::coverage::awk_lcov_lines "$file"
      echo "end_of_record"
    done < <(bashunit::coverage::get_tracked_files)
  } >"$output_file"
}

# src/coverage/report_cobertura.sh

_BASHUNIT_COBERTURA_RATE_OUT=""
function bashunit::coverage::__cobertura_rate() {
  local hit="$1"
  local total="$2"
  local pct=0
  if [ "$total" -gt 0 ]; then
    pct=$((hit * 100 / total))
  fi
  _BASHUNIT_COBERTURA_RATE_OUT="$((pct / 100)).$(printf '%02d' "$((pct % 100))")"
}

function bashunit::coverage::report_cobertura() {
  local output_file="${1:-$BASHUNIT_COVERAGE_REPORT_COBERTURA}"

  if [ -z "$output_file" ]; then
    return 0
  fi

  mkdir -p "$(dirname "$output_file")"

  local timestamp
  timestamp=$(date +%s)

  local pkg_names pkg_exec pkg_hit pkg_br_total pkg_br_taken pkg_classes
  pkg_names=()
  pkg_exec=()
  pkg_hit=()
  pkg_br_total=()
  pkg_br_taken=()
  pkg_classes=()

  local total_exec=0 total_hit=0 total_br=0 total_br_taken=0

  local file
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local rel="${file#"$PWD"/}"
    local class_name="${rel##*/}"
    local pkg_name
    case "$rel" in
    */*) pkg_name="${rel%/*}" ;;
    *) pkg_name="." ;;
    esac
    pkg_name="${pkg_name//\//.}"

    bashunit::coverage::load_hits_by_line "$file"

    local -a br_arms=()
    local -a br_arms_taken=()
    local br_line br_block br_idx br_taken
    local file_br=0 file_br_taken=0

    while IFS='|' read -r br_line br_block br_idx br_taken; do
      [ -z "$br_line" ] && continue
      br_arms[br_line]=$((${br_arms[br_line]:-0} + 1))
      file_br=$((file_br + 1))
      if [ "$br_taken" -gt 0 ]; then
        br_arms_taken[br_line]=$((${br_arms_taken[br_line]:-0} + 1))
        file_br_taken=$((file_br_taken + 1))
      fi
    done < <(bashunit::coverage::compute_branch_hits "$file")

    local -a src_lines=()
    local _sli=0 _sl
    while IFS= read -r _sl || [ -n "$_sl" ]; do
      src_lines[_sli]="$_sl"
      _sli=$((_sli + 1))
    done <"$file"

    local lines_xml="" lineno=0 file_exec=0 file_hit=0 line
    for line in ${src_lines[@]+"${src_lines[@]}"}; do
      lineno=$((lineno + 1))
      bashunit::coverage::is_executable_line "$line" "$lineno" || continue
      file_exec=$((file_exec + 1))
      local lh="${_BASHUNIT_COVERAGE_HITS_BY_LINE[$lineno]:-0}"
      [ "$lh" -gt 0 ] && file_hit=$((file_hit + 1))

      local arms="${br_arms[$lineno]:-0}"
      if [ "$arms" -gt 0 ]; then
        local taken="${br_arms_taken[$lineno]:-0}"
        local cond_pct=$((taken * 100 / arms))
        lines_xml="$lines_xml          <line number=\"$lineno\" hits=\"$lh\" branch=\"true\" \
condition-coverage=\"${cond_pct}% (${taken}/${arms})\"/>
"
      else
        lines_xml="$lines_xml          <line number=\"$lineno\" hits=\"$lh\" branch=\"false\"/>
"
      fi
    done

    total_exec=$((total_exec + file_exec))
    total_hit=$((total_hit + file_hit))
    total_br=$((total_br + file_br))
    total_br_taken=$((total_br_taken + file_br_taken))

    local p=-1 j
    for j in ${pkg_names[@]+"${!pkg_names[@]}"}; do
      if [ "${pkg_names[$j]}" = "$pkg_name" ]; then
        p=$j
        break
      fi
    done
    if [ "$p" -eq -1 ]; then
      p=${#pkg_names[@]}
      pkg_names[p]="$pkg_name"
      pkg_exec[p]=0
      pkg_hit[p]=0
      pkg_br_total[p]=0
      pkg_br_taken[p]=0
      pkg_classes[p]=""
    fi
    pkg_exec[p]=$((pkg_exec[p] + file_exec))
    pkg_hit[p]=$((pkg_hit[p] + file_hit))
    pkg_br_total[p]=$((pkg_br_total[p] + file_br))
    pkg_br_taken[p]=$((pkg_br_taken[p] + file_br_taken))

    local class_line_rate class_branch_rate
    bashunit::coverage::__cobertura_rate "$file_hit" "$file_exec"
    class_line_rate=$_BASHUNIT_COBERTURA_RATE_OUT
    bashunit::coverage::__cobertura_rate "$file_br_taken" "$file_br"
    class_branch_rate=$_BASHUNIT_COBERTURA_RATE_OUT

    pkg_classes[p]="${pkg_classes[p]}        <class name=\"$class_name\" filename=\"$rel\" \
line-rate=\"$class_line_rate\" branch-rate=\"$class_branch_rate\" complexity=\"0.0\">
          <methods/>
          <lines>
$lines_xml          </lines>
        </class>
"
  done < <(bashunit::coverage::get_tracked_files)

  local line_rate branch_rate
  bashunit::coverage::__cobertura_rate "$total_hit" "$total_exec"
  line_rate=$_BASHUNIT_COBERTURA_RATE_OUT
  bashunit::coverage::__cobertura_rate "$total_br_taken" "$total_br"
  branch_rate=$_BASHUNIT_COBERTURA_RATE_OUT

  {
    echo '<?xml version="1.0"?>'
    echo "<coverage line-rate=\"$line_rate\" branch-rate=\"$branch_rate\"" \
      "lines-covered=\"$total_hit\" lines-valid=\"$total_exec\"" \
      "branches-covered=\"$total_br_taken\" branches-valid=\"$total_br\"" \
      "complexity=\"0.0\" version=\"${BASHUNIT_VERSION:-0}\" timestamp=\"$timestamp\">"
    echo "  <sources>"
    echo "    <source>$PWD</source>"
    echo "  </sources>"
    echo "  <packages>"

    local s
    for s in ${pkg_names[@]+"${!pkg_names[@]}"}; do
      local pkg_line_rate pkg_branch_rate
      bashunit::coverage::__cobertura_rate "${pkg_hit[$s]}" "${pkg_exec[$s]}"
      pkg_line_rate=$_BASHUNIT_COBERTURA_RATE_OUT
      bashunit::coverage::__cobertura_rate "${pkg_br_taken[$s]}" "${pkg_br_total[$s]}"
      pkg_branch_rate=$_BASHUNIT_COBERTURA_RATE_OUT
      echo "    <package name=\"${pkg_names[$s]}\" line-rate=\"$pkg_line_rate\"" \
        "branch-rate=\"$pkg_branch_rate\" complexity=\"0.0\">"
      echo "      <classes>"
      printf '%s' "${pkg_classes[$s]}"
      echo "      </classes>"
      echo "    </package>"
    done

    echo "  </packages>"
    echo "</coverage>"
  } >"$output_file"
}

# src/coverage/report_html.sh

function bashunit::coverage::html_escape() {
  local text="$1"
  printf "%s" "$text" | sed "s/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g"
}

_BASHUNIT_COVERAGE_AWK_HTML_ESCAPE='
{
  gsub(/&/, "\\&amp;")
  gsub(/</, "\\&lt;")
  gsub(/>/, "\\&gt;")
  print
}
'

function bashunit::coverage::emit_block() {
  local _line
  while IFS= read -r _line || [ -n "$_line" ]; do
    printf '%s\n' "$_line"
  done
}

function bashunit::coverage::html_escape_file() {
  env LC_ALL=C "$AWK" "$_BASHUNIT_COVERAGE_AWK_HTML_ESCAPE" "$1"
}

function bashunit::coverage::report_html() {
  local output_dir="${1:-coverage/html}"

  if [ -z "$output_dir" ]; then
    return 0
  fi

  mkdir -p "$output_dir/files"

  local IFS=$' \t\n'
  local total_executable=0
  local total_hit=0
  local -a file_data=()
  local file_data_count=0
  local file=""

  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local executable hit pct
    bashunit::coverage::cached_stats_to_slots "$file"
    executable="$_BASHUNIT_COVERAGE_SPLIT_EXEC_OUT"
    hit="$_BASHUNIT_COVERAGE_SPLIT_HIT_OUT"
    pct="$_BASHUNIT_COVERAGE_SPLIT_PCT_OUT"

    total_executable=$((total_executable + executable))
    total_hit=$((total_hit + hit))

    local display_file="${file#"$PWD"/}"
    bashunit::coverage::path_to_filename_to_slot "$file"
    local safe_filename="$_BASHUNIT_COVERAGE_SAFE_NAME_OUT"

    file_data[file_data_count]="$display_file$(printf '\037')$hit$(printf '\037')$executable"
    file_data[file_data_count]="${file_data[file_data_count]}$(printf '\037')$pct$(printf '\037')$safe_filename"
    file_data_count=$((file_data_count + 1))

    bashunit::coverage::generate_file_html "$file" "$output_dir/files/${safe_filename}.html"
  done < <(bashunit::coverage::get_tracked_files)

  local total_pct
  total_pct=$(bashunit::coverage::calculate_percentage "$total_hit" "$total_executable")

  local tests_passed tests_failed tests_total
  tests_passed=$(bashunit::state::get_tests_passed)
  tests_failed=$(bashunit::state::get_tests_failed)
  tests_total=$((tests_passed + tests_failed))

  bashunit::coverage::generate_index_html \
    "$output_dir/index.html" "$total_hit" "$total_executable" "$total_pct" \
    "$tests_total" "$tests_passed" "$tests_failed" ${file_data[@]+"${file_data[@]}"}

  echo "Coverage HTML report written to: $output_dir/index.html"
}

# src/coverage/html_index.sh

function bashunit::coverage::generate_index_html() {

  local IFS=$' \t\n'
  local output_file="$1"
  local total_hit="$2"
  local total_executable="$3"
  local total_pct="$4"
  local tests_total="$5"
  local tests_passed="$6"
  local tests_failed="$7"
  shift 7

  local -a file_data=()
  local file_count=0
  if [ $# -gt 0 ]; then
    file_data=("$@")
    file_count=$#
  fi

  local total_uncovered=$((total_executable - total_hit))

  local gauge_offset=$((440 - (440 * total_pct / 100)))

  local total_class gauge_color_start gauge_color_end gauge_text_gradient
  bashunit::coverage::class_to_slot "$total_pct"
  total_class="$_BASHUNIT_COVERAGE_CLASS_OUT"
  case "$total_class" in
  high)
    gauge_color_start="#10b981"
    gauge_color_end="#34d399"
    gauge_text_gradient="linear-gradient(135deg, #10b981 0%, #34d399 100%)"
    ;;
  medium)
    gauge_color_start="#f59e0b"
    gauge_color_end="#fbbf24"
    gauge_text_gradient="linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)"
    ;;
  low)
    gauge_color_start="#ef4444"
    gauge_color_end="#f87171"
    gauge_text_gradient="linear-gradient(135deg, #ef4444 0%, #f87171 100%)"
    ;;
  esac

  {
    bashunit::coverage::emit_block <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Coverage Report | bashunit</title>
  <style>
    :root {
      --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
      --success: #10b981; --success-light: #34d399;
      --warning: #f59e0b; --warning-light: #fbbf24;
      --danger: #ef4444; --danger-light: #f87171;
      --bg-light: #ffffff; --bg-card: #f8fafc; --bg-hover: #f1f5f9;
      --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
      --border: #e2e8f0;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--bg-light); color: var(--text-primary); min-height: 100vh; line-height: 1.6; }
    .header { background: var(--bg-card); padding: 0; position: relative; overflow: hidden; border-bottom: 1px solid var(--border); }
    .header-content { position: relative; z-index: 1; max-width: 1400px; margin: 0 auto; padding: 40px 30px; }
    .header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
    .logo { display: flex; align-items: center; gap: 12px; }
    .logo img { width: 40px; height: 40px; }
    .logo-text { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.5px; color: var(--text-primary); }
    .logo-text span { opacity: 0.6; font-weight: 400; }
    .header-badge { background: var(--bg-hover); padding: 8px 16px; border-radius: 20px; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
    .header-title { font-size: 2.5rem; font-weight: 800; margin-bottom: 8px; letter-spacing: -1px; color: var(--text-primary); }
    .header-subtitle { font-size: 1.1rem; opacity: 0.7; color: var(--text-secondary); }
    .main { max-width: 1400px; margin: 0 auto; padding: 40px 30px; }
    .gauge-section { background: var(--bg-card); border-radius: 20px; padding: 40px; margin-bottom: 30px; border: 1px solid var(--border); display: flex; align-items: center; gap: 60px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .gauge-container { position: relative; width: 200px; height: 200px; flex-shrink: 0; }
    .gauge-bg { fill: none; stroke: #e5e7eb; stroke-width: 20; }
    .gauge-fill { fill: none; stroke: url(#gaugeGradient); stroke-width: 20; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; animation: gaugeAnimation 1.5s ease-out forwards; }
    @keyframes gaugeAnimation { from { stroke-dashoffset: 440; } }
    @keyframes fadeInUp { from { opacity: 0; } to { opacity: 1 } }
    .gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; width: 100%; }
EOF
    echo "    .gauge-percent { font-size: 3.5rem; font-weight: 800; background: ${gauge_text_gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; margin: 0; display: block; }"
    bashunit::coverage::emit_block <<'EOF'
    .gauge-label { color: var(--text-secondary); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 2px; margin: 0; display: block; }
    .gauge-info { flex: 1; }
    .gauge-title { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; }
    .gauge-description { color: var(--text-secondary); font-size: 1.05rem; margin-bottom: 24px; line-height: 1.7; }
    .breakdown-item { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
    .breakdown-dot { width: 12px; height: 12px; border-radius: 50%; }
    .breakdown-dot.total { background: #94a3b8; }
    .breakdown-dot.covered { background: var(--success); }
    .breakdown-dot.uncovered { background: var(--danger); }
    .breakdown-dot.files { background: var(--warning); }
    .breakdown-dot.tests { background: #a78bfa; }
    .breakdown-dot.tests-passed { background: var(--success); }
    .breakdown-dot.tests-failed { background: var(--danger); }
    .breakdown-label { color: var(--text-secondary); font-size: 0.9rem; }
    .breakdown-value { font-weight: 600; color: var(--text-primary); }
    .compact-metrics { display: flex; flex-direction: column; gap: 10px; }
    .metrics-group { background: var(--bg-hover); padding: 12px 16px; border-radius: 8px; border-left: 3px solid var(--primary); }
    .metrics-group.coverage-group { border-left-color: var(--success); }
    .metrics-group.test-group { border-left-color: #a78bfa; }
    .metrics-group-title { font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
    .metrics-inline { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; font-size: 0.9rem; }
    .metrics-inline .breakdown-item { margin: 0; }
    .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px; }
    .section-title { font-size: 1.5rem; font-weight: 700; display: flex; align-items: center; gap: 12px; }
    .legend { display: flex; gap: 20px; background: #f1f5f9; padding: 12px 20px; border-radius: 10px; }
    .legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-secondary); pointer-events: none; }
    .legend-color { width: 16px; height: 16px; border-radius: 4px; }
    .legend-color.high { background: var(--success); }
    .legend-color.medium { background: var(--warning); }
    .legend-color.low { background: var(--danger); }
    .files-table { background: var(--bg-card); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .files-table table { width: 100%; border-collapse: collapse; }
    .files-table th { background: #f8fafc; padding: 16px 24px; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid var(--border); }
    .files-table td { padding: 20px 24px; border-bottom: 1px solid var(--border); vertical-align: middle; }
    .files-table tr:last-child td { border-bottom: none; }
    .files-table tbody tr { transition: all 0.2s ease; animation: fadeInUp 0.5s ease-out forwards; opacity: 0; cursor: pointer; }
    .files-table tbody tr:hover { background: var(--bg-hover); }
    .file-info { display: flex; flex-direction: column; gap: 4px; }
    .file-name { font-weight: 600; color: var(--text-primary); text-decoration: none; font-size: 1rem; transition: color 0.2s; }
    .file-name:hover { color: var(--primary-light); }
    .file-path { color: var(--text-muted); font-size: 0.85rem; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .lines-info { text-align: center; }
    .lines-covered { font-weight: 700; font-size: 1.1rem; color: var(--text-primary); }
    .lines-total { color: var(--text-muted); font-size: 0.85rem; }
    .coverage-cell { width: 200px; }
    .coverage-bar-container { display: flex; align-items: center; gap: 16px; }
    .coverage-bar { flex: 1; height: 10px; background: var(--bg-hover); border-radius: 5px; overflow: hidden; }
    .coverage-bar-fill { height: 100%; border-radius: 5px; transition: width 1s ease-out; }
    .coverage-bar-fill.high { background: linear-gradient(90deg, var(--success) 0%, var(--success-light) 100%); }
    .coverage-bar-fill.medium { background: linear-gradient(90deg, var(--warning) 0%, var(--warning-light) 100%); }
    .coverage-bar-fill.low { background: linear-gradient(90deg, var(--danger) 0%, var(--danger-light) 100%); }
    .coverage-percent { font-weight: 700; font-size: 1rem; min-width: 50px; text-align: right; }
    .coverage-percent.high { color: var(--success); }
    .coverage-percent.medium { color: var(--warning); }
    .coverage-percent.low { color: var(--danger); }
    .view-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; background: var(--bg-hover); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: all 0.2s; }
    .view-btn:hover { background: var(--primary); border-color: var(--primary); }
    .footer { max-width: 1400px; margin: 0 auto; padding: 40px 30px; text-align: center; border-top: 1px solid var(--border); }
    .footer-content { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
    .footer-text { color: var(--text-muted); font-size: 0.9rem; }
    .footer-link { color: var(--primary-light); text-decoration: none; font-weight: 500; transition: color 0.2s; }
    .footer-link:hover { color: var(--primary); }
    .footer-divider { width: 4px; height: 4px; background: var(--text-muted); border-radius: 50%; }
    @media (max-width: 768px) {
      .header-content { padding: 30px 20px; } .header-title { font-size: 1.8rem; }
      .main { padding: 30px 20px; }
      .gauge-section { flex-direction: column; padding: 30px; gap: 30px; }
      .gauge-container { width: 160px; height: 160px; }
      .gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; }
      .gauge-percent { font-size: 2.5rem; line-height: 1; margin: 0; }
      .gauge-label { font-size: 0.75rem; letter-spacing: 1.5px; margin: 0; }
      .metrics-inline { flex-direction: column; gap: 10px; align-items: flex-start; }
      .files-table th, .files-table td { padding: 15px; }
      .coverage-cell { width: auto; }
      .coverage-bar-container { flex-direction: column; align-items: flex-start; gap: 8px; }
      .coverage-bar { width: 100%; }
    }
  </style>
</head>
<body>
  <header class="header">
    <div class="header-content">
      <div class="header-top">
        <div class="logo">
          <img src="https://bashunit.com/logo.svg" alt="bashunit">
          <div class="logo-text">bashunit <span>coverage</span></div>
        </div>
EOF
    echo "        <div class=\"header-badge\">v${BASHUNIT_VERSION:-0.0.0}</div>"
    bashunit::coverage::emit_block <<'EOF'
      </div>
      <h1 class="header-title">Code Coverage Report</h1>
      <p class="header-subtitle">Comprehensive line-by-line coverage analysis for your bash scripts</p>
    </div>
  </header>
  <main class="main">
    <section class="gauge-section">
      <div class="gauge-container">
        <svg viewBox="0 0 160 160" width="200" height="200">
          <defs>
            <linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="0%">
EOF
    echo "              <stop offset=\"0%\" style=\"stop-color:${gauge_color_start}\"/>"
    echo "              <stop offset=\"100%\" style=\"stop-color:${gauge_color_end}\"/>"
    bashunit::coverage::emit_block <<'EOF'
            </linearGradient>
          </defs>
          <circle class="gauge-bg" cx="80" cy="80" r="70"/>
EOF
    echo "          <circle class=\"gauge-fill\" cx=\"80\" cy=\"80\" r=\"70\" stroke-dasharray=\"440\" stroke-dashoffset=\"${gauge_offset}\"/>"
    bashunit::coverage::emit_block <<'EOF'
        </svg>
        <div class="gauge-text">
EOF
    echo "          <div class=\"gauge-percent\">${total_pct}%</div>"
    bashunit::coverage::emit_block <<'EOF'
          <div class="gauge-label">Coverage</div>
        </div>
      </div>
      <div class="gauge-info">
        <h2 class="gauge-title">Overall Code Coverage</h2>
EOF
    echo "        <p class=\"gauge-description\"><strong>${total_hit} of ${total_executable}</strong> executable lines covered across <strong>${file_count} files</strong>.</p>"
    bashunit::coverage::emit_block <<'EOF'

        <div class="compact-metrics">
          <div class="metrics-group coverage-group">
            <div class="metrics-group-title">Coverage Metrics</div>
            <div class="metrics-inline">
              <div class="breakdown-item">
                <span class="breakdown-dot total"></span>
                <span class="breakdown-label">Total:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_executable} lines</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot covered"></span>
                <span class="breakdown-label">Covered:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_hit} lines</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot uncovered"></span>
                <span class="breakdown-label">Uncovered:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_uncovered} lines</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
            </div>
          </div>
          <div class="metrics-group test-group">
            <div class="metrics-group-title">Test Results</div>
            <div class="metrics-inline">
              <div class="breakdown-item">
                <span class="breakdown-dot files"></span>
                <span class="breakdown-label">Files:</span>
EOF
    echo "                <span class=\"breakdown-value\">${file_count}</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests"></span>
                <span class="breakdown-label">Tests:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_total} total</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests-passed"></span>
                <span class="breakdown-label">Passed:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_passed}</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests-failed"></span>
                <span class="breakdown-label">Failed:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_failed}</span>"
    bashunit::coverage::emit_block <<'EOF'
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
    <section>
      <div class="section-header">
        <h2 class="section-title">File Coverage Details</h2>
        <div class="legend">
          <div class="legend-item">
            <span class="legend-color high"></span>
EOF
    echo "            <span>≥${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}% High</span>"
    bashunit::coverage::emit_block <<'EOF'
          </div>
          <div class="legend-item">
            <span class="legend-color medium"></span>
EOF
    echo "            <span>${BASHUNIT_COVERAGE_THRESHOLD_LOW:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}-${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}% Medium</span>"
    bashunit::coverage::emit_block <<'EOF'
          </div>
          <div class="legend-item">
            <span class="legend-color low"></span>
EOF
    echo "            <span>&lt;${BASHUNIT_COVERAGE_THRESHOLD_LOW:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}% Low</span>"
    bashunit::coverage::emit_block <<'EOF'
          </div>
        </div>
      </div>
      <div class="files-table">
        <table>
          <thead>
            <tr>
              <th>File</th>
              <th style="text-align: center;">Lines</th>
              <th>Coverage</th>
            </tr>
          </thead>
          <tbody>
EOF

    local data display_file hit executable pct safe_filename
    local _us
    _us=$(printf '\037')
    for data in ${file_data[@]+"${file_data[@]}"}; do
      IFS="$_us" read -r display_file hit executable pct safe_filename <<<"$data"

      local esc_name esc_path
      esc_name=$(bashunit::str::html_escape "${display_file##*/}")
      esc_path=$(bashunit::str::html_escape "$display_file")

      local class
      bashunit::coverage::class_to_slot "$pct"
      class="$_BASHUNIT_COVERAGE_CLASS_OUT"

      echo "            <tr onclick=\"window.location='files/${safe_filename}.html'\">"
      echo "              <td>"
      echo "                <div class=\"file-info\">"
      echo "                  <a href=\"files/${safe_filename}.html\" class=\"file-name\">${esc_name}</a>"
      echo "                  <div class=\"file-path\">./${esc_path}</div>"
      echo "                </div>"
      echo "              </td>"
      echo "              <td>"
      echo "                <div class=\"lines-info\">"
      echo "                  <div class=\"lines-covered\">${hit}</div>"
      echo "                  <div class=\"lines-total\">of ${executable} lines</div>"
      echo "                </div>"
      echo "              </td>"
      echo "              <td class=\"coverage-cell\">"
      echo "                <div class=\"coverage-bar-container\">"
      echo "                  <div class=\"coverage-bar\">"
      echo "                    <div class=\"coverage-bar-fill $class\" style=\"width: ${pct}%;\"></div>"
      echo "                  </div>"
      echo "                  <span class=\"coverage-percent $class\">${pct}%</span>"
      echo "                </div>"
      echo "              </td>"
      echo "            </tr>"
    done

    bashunit::coverage::emit_block <<'EOF'
          </tbody>
        </table>
      </div>
    </section>
  </main>
  <footer class="footer">
    <div class="footer-content">
      <span class="footer-text">Generated by</span>
      <a href="https://bashunit.com" class="footer-link" target="_blank">bashunit</a>
      <span class="footer-divider"></span>
      <span class="footer-text">Documentation at</span>
      <a href="https://bashunit.com/coverage" class="footer-link" target="_blank">bashunit.com/coverage</a>
    </div>
  </footer>
</body>
</html>
EOF
  } >"$output_file"
}

# src/coverage/html_file.sh

_BASHUNIT_COVERAGE_AWK_HTML_ROWS='
FILENAME == hitsfile {
  hits[$1 + 0] = $2 + 0
  next
}

FILENAME == testsfile {
  # "<lineno>|<test_file>:<test_fn>", deduplicated, first-seen order kept.
  p = index($0, "|")
  if (p == 0) { next }
  tln = substr($0, 1, p - 1) + 0
  info = substr($0, p + 1)
  key = tln SUBSEP info
  if (key in seen) { next }
  seen[key] = 1
  tests[tln] = (tln in tests) ? tests[tln] "\n" info : info
  next
}

{
  total++
  sl[total] = $0
}

function escape(t) {
  gsub(/&/, "\\&amp;", t)
  gsub(/</, "\\&lt;", t)
  gsub(/>/, "\\&gt;", t)
  return t
}

END {
  # The DEBUG trap attributes a multi-line statement to its starting line, so
  # the count carries forward across the backslash chain (#722).
  carry = 0
  for (ln = 1; ln <= total; ln++) {
    h = (ln in hits) ? hits[ln] : 0
    if (carry > 0 && h < carry) { h = carry; hits[ln] = h }
    if (h > 0 && bu_ends_with_continuation(sl[ln])) { carry = h } else { carry = 0 }
  }

  for (ln = 1; ln <= total; ln++) {
    row_class = ""
    hits_display = ""

    if (bu_is_executable(sl[ln])) {
      h = (ln in hits) ? hits[ln] : 0
      if (h > 0) {
        row_class = "covered"
        if (ln in tests) {
          tooltip = "<div class=\"hits-tooltip\"><div class=\"hits-tooltip-title\">Tests hitting this line</div><ul class=\"hits-tooltip-list\">"
          n = split(tests[ln], entries, "\n")
          for (e = 1; e <= n; e++) {
            if (entries[e] == "") { continue }
            c = index(entries[e], ":")
            if (c == 0) { tfile = entries[e]; tfn = "" } else { tfile = substr(entries[e], 1, c - 1); tfn = substr(entries[e], c + 1) }
            sub(/^.*\//, "", tfile)
            tooltip = tooltip "<li><span class=\"hits-tooltip-file\">" tfile "</span>:<span class=\"hits-tooltip-fn\">" tfn "</span></li>"
          }
          tooltip = tooltip "</ul></div>"
          hits_display = "<span class=\"hits-badge has-tooltip\">" h times tooltip "</span>"
        } else {
          hits_display = "<span class=\"hits-badge\">" h times "</span>"
        }
      } else {
        row_class = "uncovered"
        hits_display = "<span class=\"hits-badge\">" h times "</span>"
      }
    }

    printf "          <tr id=\"line-%s\" class=\"%s line-anchor\">\n", ln, row_class
    printf "            <td class=\"line-num\">%s</td>\n", ln
    printf "            <td class=\"hits\">%s</td>\n", hits_display
    printf "            <td class=\"code\">%s</td>\n", escape(sl[ln])
    printf "          </tr>\n"
  }
}
'

_BASHUNIT_COVERAGE_AWK_HTML_FUNCTIONS='
FILENAME == hitsfile {
  hits[$1 + 0] = $2 + 0
  next
}

{
  total++
  sl[total] = $0
}

END {
  carry = 0
  for (ln = 1; ln <= total; ln++) {
    h = (ln in hits) ? hits[ln] : 0
    if (carry > 0 && h < carry) { h = carry; hits[ln] = h }
    if (h > 0 && bu_ends_with_continuation(sl[ln])) { carry = h } else { carry = 0 }
  }

  bu_fn_reset()
  for (ln = 1; ln <= total; ln++) { bu_fn_line(sl[ln], ln) }
  bu_fn_finish(total)

  for (i = 1; i <= fn_count; i++) {
    executable = 0
    hit = 0
    for (ln = fns[i]; ln <= fne[i]; ln++) {
      if (!bu_is_executable(sl[ln])) { continue }
      executable++
      if ((ln in hits) && hits[ln] > 0) { hit++ }
    }
    print fnn[i] "|" fns[i] "|" fne[i] "|" executable "|" hit
  }
}
'

function bashunit::coverage::html_function_rows() {
  local file="$1"

  bashunit::coverage::ensure_hits_aggregated
  bashunit::coverage::hits_file_for "$file"
  local hits_file="$_BASHUNIT_COVERAGE_HITS_FILE_OUT"
  if [ -z "$hits_file" ] || [ ! -f "$hits_file" ]; then
    hits_file="/dev/null"
  fi

  env LC_ALL=C "$AWK" -v hitsfile="$hits_file" \
    "${_BASHUNIT_COVERAGE_AWK_RULES}${_BASHUNIT_COVERAGE_AWK_FUNCTIONS}\
${_BASHUNIT_COVERAGE_AWK_HTML_FUNCTIONS}" \
    "$hits_file" "$file"
}

function bashunit::coverage::html_code_rows() {
  local file="$1" tests_file="$2"

  bashunit::coverage::ensure_hits_aggregated
  bashunit::coverage::hits_file_for "$file"
  local hits_file="$_BASHUNIT_COVERAGE_HITS_FILE_OUT"
  if [ -z "$hits_file" ] || [ ! -f "$hits_file" ]; then
    hits_file="/dev/null"
  fi

  env LC_ALL=C "$AWK" -v hitsfile="$hits_file" -v testsfile="$tests_file" -v times="×" \
    "${_BASHUNIT_COVERAGE_AWK_RULES}${_BASHUNIT_COVERAGE_AWK_HTML_ROWS}" \
    "$hits_file" "$tests_file" "$file"
}

function bashunit::coverage::generate_file_html() {
  local file="$1"
  local output_file="$2"

  local display_file="${file#"$PWD"/}"

  local esc_file esc_base
  esc_file=$(bashunit::str::html_escape "$display_file")
  esc_base=$(bashunit::str::html_escape "${display_file##*/}")
  local executable hit pct class stats
  stats=$(bashunit::coverage::get_cached_stats "$file")
  bashunit::coverage::split_stats "$stats"
  executable="$_BASHUNIT_COVERAGE_SPLIT_EXEC_OUT"
  hit="$_BASHUNIT_COVERAGE_SPLIT_HIT_OUT"
  pct="$_BASHUNIT_COVERAGE_SPLIT_PCT_OUT"
  class="$_BASHUNIT_COVERAGE_SPLIT_CLASS_OUT"
  local uncovered=$((executable - hit))

  bashunit::coverage::load_hits_by_line "$file"

  local -a file_lines=()
  local _fli=0 _fl
  while IFS= read -r _fl || [ -n "$_fl" ]; do
    file_lines[_fli]="$_fl"
    ((++_fli))
  done <"$file"

  local tests_file="${_BASHUNIT_COVERAGE_DATA_FILE%/*}/page-tests"
  if ! bashunit::coverage::get_all_line_tests "$file" >"$tests_file" 2>/dev/null; then
    : >"$tests_file"
  fi

  local total_lines="${#file_lines[@]}"
  local non_executable=$((total_lines - executable))

  {
    bashunit::coverage::emit_block <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
EOF
    echo "  <title>${esc_base} | Coverage Report</title>"
    bashunit::coverage::emit_block <<'EOF'
  <style>
    :root {
      --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
      --success: #10b981; --success-bg: rgba(16, 185, 129, 0.1); --success-border: rgba(16, 185, 129, 0.2);
      --warning: #f59e0b;
      --danger: #ef4444; --danger-bg: rgba(239, 68, 68, 0.1); --danger-border: rgba(239, 68, 68, 0.2);
      --bg-light: #ffffff; --bg-card: #f8fafc; --bg-hover: #e1e5ea; --bg-code: #f6f8fa;
      --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
      --border: #e2e8f0; --line-number-bg: #f8fafc;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--bg-light); color: var(--text-primary); min-height: 100vh; line-height: 1.6; }
    .header { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 20px 30px; position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .header-content { max-width: 1600px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 20px; }
    .nav-section { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
    .back-btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; background: #475569; border: 2px solid #475569; border-radius: 8px; color: #ffffff; text-decoration: none; font-size: 1rem; font-weight: 600; transition: all 0.2s; box-shadow: 0 2px 4px rgba(71, 85, 105, 0.2); }
    .back-btn:hover { background: #334155; border-color: #334155; box-shadow: 0 4px 12px rgba(51, 65, 85, 0.3); }
    .file-title { display: flex; align-items: center; gap: 12px; }
    .file-name { font-size: 1.3rem; font-weight: 700; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .stats-section { display: flex; align-items: center; gap: 30px; flex-wrap: wrap; }
    .stat-item { display: flex; align-items: center; gap: 10px; }
    .stat-badge { padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 0.9rem; }
    .stat-badge.coverage.high { background: linear-gradient(135deg, var(--success) 0%, #34d399 100%); color: #000; }
    .stat-badge.coverage.medium { background: linear-gradient(135deg, var(--warning) 0%, #fbbf24 100%); color: #000; }
    .stat-badge.coverage.low { background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%); color: #fff; }
    .stat-badge.lines { background: var(--bg-hover); color: var(--text-primary); }
    .stat-label { color: var(--text-secondary); font-size: 0.85rem; }
    .summary-bar { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 20px 30px; }
    .summary-content { max-width: 1600px; margin: 0 auto; display: flex; align-items: center; gap: 40px; flex-wrap: wrap; }
    .progress-section { flex: 1; min-width: 300px; }
    .progress-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
    .progress-label { color: var(--text-secondary); font-size: 0.9rem; }
    .progress-percent { font-weight: 700; }
    .progress-percent.high { color: var(--success); }
    .progress-percent.medium { color: var(--warning); }
    .progress-percent.low { color: var(--danger); }
    .progress-bar { height: 12px; background: var(--bg-hover); border-radius: 6px; overflow: hidden; }
    .progress-fill { height: 100%; border-radius: 6px; transition: width 1s ease-out; }
    .progress-fill.high { background: linear-gradient(90deg, #059669 0%, #10b981 100%); }
    .progress-fill.medium { background: linear-gradient(90deg, #d97706 0%, #f59e0b 100%); }
    .progress-fill.low { background: linear-gradient(90deg, #dc2626 0%, #ef4444 100%); }
    .legend { display: flex; gap: 24px; flex-wrap: wrap; }
    .legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; color: var(--text-secondary); pointer-events: none; }
    .legend-color { width: 16px; height: 16px; border-radius: 4px; }
    .legend-color.covered { background: var(--success); }
    .legend-color.uncovered { background: var(--danger); }
    .legend-color.neutral { background: var(--text-muted); }
    .code-container { max-width: 1600px; margin: 30px auto; padding: 0 30px; }
    .code-wrapper { background: var(--bg-code); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
    .code-header { background: var(--line-number-bg); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 12px; }
    .code-path { font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; color: var(--text-secondary); }
    .code-stats { display: flex; gap: 16px; font-size: 0.85rem; }
    .code-stats span { padding: 4px 12px; background: #e5e7eb; border-radius: 4px; color: var(--text-secondary); }
    .code-body { overflow-x: auto; }
    .code-table { width: 100%; border-collapse: collapse; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 13px; line-height: 1.6; }
    .code-table tr { transition: background 0.15s; }
    .line-num { width: 60px; padding: 2px 16px; text-align: right; color: #9ca3af; background: var(--line-number-bg); border-right: 1px solid var(--border); user-select: none; vertical-align: top; }
    .hits { width: 60px; padding: 2px 12px; text-align: center; color: #9ca3af; background: var(--line-number-bg); border-right: 1px solid var(--border); font-size: 0.85em; vertical-align: top; }
    .hits-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; font-weight: 600; position: relative; }
    .hits-badge.has-tooltip { cursor: help; }
    .covered .hits-badge { background: var(--success-bg); color: var(--success); }
    .uncovered .hits-badge { background: var(--danger-bg); color: var(--danger); }
    .hits-tooltip { display: none; position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 12px; padding: 10px 14px; background: #1e293b; color: #f1f5f9; border-radius: 8px; font-size: 11px; font-weight: 400; white-space: normal; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 200px; max-width: 500px; width: max-content; }
    .hits-tooltip::after { content: ''; position: absolute; right: 100%; top: 50%; transform: translateY(-50%); border: 6px solid transparent; border-right-color: #1e293b; }
    .hits-badge:hover .hits-tooltip { display: block; }
    .hits-tooltip-title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
    .hits-tooltip-list { margin: 0; padding: 0; list-style: none; }
    .hits-tooltip-list li { padding: 3px 0; border-bottom: 1px solid #334155; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .hits-tooltip-list li:last-child { border-bottom: none; }
    .hits-tooltip-file { color: #60a5fa; }
    .hits-tooltip-fn { color: #a5b4fc; }
    .code { padding: 2px 20px; white-space: pre; vertical-align: top; }
    .covered { background: #d1fae5; }
    .covered .line-num, .covered .hits { background: #a7f3d0; border-color: var(--success-border); }
    .covered:hover { background: #ecfdf5; }
    .covered:hover .line-num, .covered:hover .hits { background: #d1fae5; }
    .uncovered { background: #fee2e2; }
    .uncovered .line-num, .uncovered .hits { background: #fecaca; border-color: var(--danger-border); }
    .uncovered:hover { background: #fef2f2; }
    .uncovered:hover .line-num, .uncovered:hover .hits { background: #fee2e2; }
    .function-summary { max-width: 1600px; margin: 30px auto; padding: 0 30px; }
    .function-table { background: var(--bg-card); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); width: 100%; border-collapse: collapse; }
    .function-table th { background: #f1f5f9; padding: 14px 20px; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid var(--border); }
    .function-table th:first-child { width: 40%; }
    .function-table th:nth-child(2), .function-table th:nth-child(3) { text-align: center; }
    .function-table td { padding: 12px 20px; border-bottom: 1px solid var(--border); vertical-align: middle; }
    .function-table tr:last-child td { border-bottom: none; }
    .function-table tbody tr { transition: background 0.15s; }
    .function-table tbody tr:hover { background: var(--bg-hover); }
    .function-table tbody tr.fn-covered { background: #f0fdf4; }
    .function-table tbody tr.fn-covered:hover { background: #dcfce7; }
    .function-table tbody tr.fn-partial { background: #fffbeb; }
    .function-table tbody tr.fn-partial:hover { background: #fef3c7; }
    .function-table tbody tr.fn-uncovered { background: #fef2f2; }
    .function-table tbody tr.fn-uncovered:hover { background: #fee2e2; }
    .fn-name { font-weight: 600; color: var(--primary); cursor: pointer; text-decoration: none; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 0.95rem; }
    .fn-name:hover { color: var(--primary-dark); text-decoration: underline; }
    .fn-lines { text-align: center; color: var(--text-secondary); font-size: 0.9rem; }
    .fn-coverage-cell { text-align: center; }
    .fn-coverage-bar { display: flex; align-items: center; gap: 12px; justify-content: center; }
    .fn-progress { width: 100px; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
    .fn-progress-fill { height: 100%; border-radius: 4px; }
    .fn-progress-fill.high { background: var(--success); }
    .fn-progress-fill.medium { background: var(--warning); }
    .fn-progress-fill.low { background: var(--danger); }
    .fn-pct { font-weight: 600; font-size: 0.9rem; min-width: 50px; text-align: right; }
    .fn-pct.high { color: var(--success); }
    .fn-pct.medium { color: var(--warning); }
    .fn-pct.low { color: var(--danger); }
    .line-anchor { scroll-margin-top: 200px; }
    .line-anchor:target { animation: highlightFade 4s ease-out forwards; }
    .line-anchor:target .line-num, .line-anchor:target .hits { animation: highlightFade 4s ease-out forwards; }
    @keyframes highlightFade {
      0% { background: #93c5fd; }
      70% { background: #dbeafe; }
      100% { background: transparent; }
    }
    .footer { max-width: 1600px; margin: 0 auto; padding: 40px 30px; text-align: center; }
    .footer-text { color: var(--text-muted); font-size: 0.9rem; }
    .footer-link { color: var(--primary-light); text-decoration: none; font-weight: 500; }
    .footer-link:hover { color: var(--primary); }
    @media (max-width: 768px) {
      .header { padding: 15px 20px; } .header-content { gap: 15px; }
      .stats-section { gap: 15px; } .summary-bar { padding: 15px 20px; }
      .summary-content { gap: 20px; } .code-container { padding: 0 15px; margin: 20px auto; }
      .code-header { padding: 12px 16px; } .line-num, .hits { padding: 2px 8px; }
      .code { padding: 2px 12px; }
    }
  </style>
</head>
<body>
  <header class="header">
    <div class="header-content">
      <div class="nav-section">
        <a href="../index.html" class="back-btn">← Back to Overview</a>
        <div class="file-title">
EOF
    echo "          <span class=\"file-name\">${esc_base}</span>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
      </div>
      <div class="stats-section">
        <div class="stat-item">
EOF
    echo "          <span class=\"stat-badge coverage $class\">${pct}%</span>"
    bashunit::coverage::emit_block <<'EOF'
          <span class="stat-label">Coverage</span>
        </div>
        <div class="stat-item">
EOF
    echo "          <span class=\"stat-badge lines\">${hit}/${executable}</span>"
    bashunit::coverage::emit_block <<'EOF'
          <span class="stat-label">Lines</span>
        </div>
      </div>
    </div>
  </header>
  <div class="summary-bar">
    <div class="summary-content">
      <div class="progress-section">
        <div class="progress-header">
          <span class="progress-label">Line Coverage Progress</span>
EOF
    echo "          <span class=\"progress-percent $class\">${pct}%</span>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
        <div class="progress-bar">
EOF
    echo "          <div class=\"progress-fill $class\" style=\"width: ${pct}%;\"></div>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
      </div>
      <div class="legend">
        <div class="legend-item">
          <span class="legend-color covered"></span>
EOF
    echo "          <span>${hit} lines covered</span>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
        <div class="legend-item">
          <span class="legend-color uncovered"></span>
EOF
    echo "          <span>${uncovered} lines uncovered</span>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
        <div class="legend-item">
          <span class="legend-color neutral"></span>
EOF
    echo "          <span>${non_executable} non-executable</span>"
    bashunit::coverage::emit_block <<'EOF'
        </div>
      </div>
    </div>
  </div>
EOF

    local functions_data
    functions_data=$(bashunit::coverage::html_function_rows "$file")

    if [ -n "$functions_data" ]; then
      bashunit::coverage::emit_block <<'EOF'
  <div class="function-summary">
    <table class="function-table">
      <thead>
        <tr>
          <th>Function</th>
          <th>Lines</th>
          <th>Coverage</th>
        </tr>
      </thead>
      <tbody>
EOF
      local fn_entry
      while IFS= read -r fn_entry; do
        [ -z "$fn_entry" ] && continue

        local fn_name fn_start fn_executable fn_hit rest
        fn_name="${fn_entry%%|*}"
        rest="${fn_entry#*|}"
        fn_start="${rest%%|*}"

        rest="${rest#*|}"
        rest="${rest#*|}"
        fn_executable="${rest%%|*}"
        fn_hit="${rest#*|}"

        local fn_pct fn_class row_class
        fn_pct=0
        if [ "$fn_executable" -gt 0 ]; then
          fn_pct=$((fn_hit * 100 / fn_executable))
        fi
        bashunit::coverage::class_to_slot "$fn_pct"
        fn_class="$_BASHUNIT_COVERAGE_CLASS_OUT"
        case "$fn_class" in
        high) row_class="fn-covered" ;;
        medium) row_class="fn-partial" ;;
        low) row_class="fn-uncovered" ;;
        esac

        echo "        <tr class=\"$row_class\">"
        echo "          <td><a href=\"#line-${fn_start}\" class=\"fn-name\">${fn_name}</a></td>"
        echo "          <td class=\"fn-lines\">${fn_hit} / ${fn_executable}</td>"
        echo "          <td class=\"fn-coverage-cell\">"
        echo "            <div class=\"fn-coverage-bar\">"
        echo "              <div class=\"fn-progress\"><div class=\"fn-progress-fill ${fn_class}\" style=\"width: ${fn_pct}%;\"></div></div>"
        echo "              <span class=\"fn-pct ${fn_class}\">${fn_pct}%</span>"
        echo "            </div>"
        echo "          </td>"
        echo "        </tr>"
      done <<<"$functions_data"

      bashunit::coverage::emit_block <<'EOF'
      </tbody>
    </table>
  </div>
EOF
    fi

    bashunit::coverage::emit_block <<'EOF'
  <div class="code-container">
    <div class="code-wrapper">
      <div class="code-header">
EOF
    echo "        <span class=\"code-path\">./${esc_file}</span>"
    echo "        <div class=\"code-stats\">"
    echo "          <span>${total_lines} total lines</span>"
    echo "        </div>"
    bashunit::coverage::emit_block <<'EOF'
      </div>
      <div class="code-body">
        <table class="code-table">
EOF

    bashunit::coverage::html_code_rows "$file" "$tests_file"

    bashunit::coverage::emit_block <<'EOF'
        </table>
      </div>
    </div>
  </div>
  <footer class="footer">
    <p class="footer-text">
      Generated by <a href="https://bashunit.com" class="footer-link" target="_blank">bashunit</a>
    </p>
  </footer>
</body>
</html>
EOF
  } >"$output_file"
}

# src/state/index.sh

# src/state/counters.sh

_BASHUNIT_TESTS_PASSED=0
_BASHUNIT_TESTS_FAILED=0
_BASHUNIT_TESTS_SKIPPED=0
_BASHUNIT_TESTS_INCOMPLETE=0
_BASHUNIT_TESTS_SNAPSHOT=0
_BASHUNIT_TESTS_RISKY=0

_BASHUNIT_TESTS_FLAKY=0
_BASHUNIT_ASSERTIONS_PASSED=0
_BASHUNIT_ASSERTIONS_FAILED=0
_BASHUNIT_ASSERTIONS_SKIPPED=0
_BASHUNIT_ASSERTIONS_INCOMPLETE=0
_BASHUNIT_ASSERTIONS_SNAPSHOT=0

function bashunit::state::get_tests_passed() {
  echo "$_BASHUNIT_TESTS_PASSED"
}

function bashunit::state::add_tests_passed() {
  ((_BASHUNIT_TESTS_PASSED++)) || true
}

function bashunit::state::get_tests_failed() {
  echo "$_BASHUNIT_TESTS_FAILED"
}

function bashunit::state::add_tests_failed() {
  ((_BASHUNIT_TESTS_FAILED++)) || true
}

function bashunit::state::get_tests_skipped() {
  echo "$_BASHUNIT_TESTS_SKIPPED"
}

function bashunit::state::add_tests_skipped() {
  ((_BASHUNIT_TESTS_SKIPPED++)) || true
}

function bashunit::state::get_tests_incomplete() {
  echo "$_BASHUNIT_TESTS_INCOMPLETE"
}

function bashunit::state::add_tests_incomplete() {
  ((_BASHUNIT_TESTS_INCOMPLETE++)) || true
}

function bashunit::state::get_tests_snapshot() {
  echo "$_BASHUNIT_TESTS_SNAPSHOT"
}

function bashunit::state::add_tests_snapshot() {
  ((_BASHUNIT_TESTS_SNAPSHOT++)) || true
}

function bashunit::state::get_tests_risky() {
  echo "$_BASHUNIT_TESTS_RISKY"
}

function bashunit::state::add_tests_risky() {
  ((_BASHUNIT_TESTS_RISKY++)) || true
}

function bashunit::state::get_tests_flaky() {
  echo "$_BASHUNIT_TESTS_FLAKY"
}

function bashunit::state::add_tests_flaky() {
  ((_BASHUNIT_TESTS_FLAKY++)) || true
}

function bashunit::state::get_assertions_passed() {
  echo "$_BASHUNIT_ASSERTIONS_PASSED"
}

function bashunit::state::add_assertions_passed() {

  if [ "${_BASHUNIT_ASSERT_ONCE_ACTIVE:-0}" -eq 1 ]; then
    bashunit::assert::once_is_absorbing && return 0
  fi
  ((_BASHUNIT_ASSERTIONS_PASSED++)) || true
}

function bashunit::state::get_assertions_failed() {
  echo "$_BASHUNIT_ASSERTIONS_FAILED"
}

function bashunit::state::add_assertions_failed() {
  ((_BASHUNIT_ASSERTIONS_FAILED++)) || true
}

function bashunit::state::get_assertions_skipped() {
  echo "$_BASHUNIT_ASSERTIONS_SKIPPED"
}

function bashunit::state::add_assertions_skipped() {
  ((_BASHUNIT_ASSERTIONS_SKIPPED++)) || true
}

function bashunit::state::get_assertions_incomplete() {
  echo "$_BASHUNIT_ASSERTIONS_INCOMPLETE"
}

function bashunit::state::add_assertions_incomplete() {
  ((_BASHUNIT_ASSERTIONS_INCOMPLETE++)) || true
}

function bashunit::state::get_assertions_snapshot() {
  echo "$_BASHUNIT_ASSERTIONS_SNAPSHOT"
}

function bashunit::state::add_assertions_snapshot() {
  ((_BASHUNIT_ASSERTIONS_SNAPSHOT++)) || true
}

# src/state/duplicates.sh

_BASHUNIT_DUPLICATED_FUNCTION_NAMES=""
_BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES=""
_BASHUNIT_DUPLICATED_FUNCTION_DETAILS=""
_BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND=false

function bashunit::state::is_duplicated_test_functions_found() {
  echo "$_BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND"
}

function bashunit::state::set_duplicated_test_functions_found() {
  _BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND=true
}

function bashunit::state::get_duplicated_function_names() {
  echo "$_BASHUNIT_DUPLICATED_FUNCTION_NAMES"
}

function bashunit::state::set_duplicated_function_names() {
  _BASHUNIT_DUPLICATED_FUNCTION_NAMES="$1"
}

function bashunit::state::get_file_with_duplicated_function_names() {
  echo "$_BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES"
}

function bashunit::state::set_file_with_duplicated_function_names() {
  _BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES="$1"
}

function bashunit::state::get_duplicated_function_details() {
  echo "$_BASHUNIT_DUPLICATED_FUNCTION_DETAILS"
}

function bashunit::state::set_duplicated_function_details() {
  _BASHUNIT_DUPLICATED_FUNCTION_DETAILS="$1"
}

function bashunit::state::set_duplicated_functions_merged() {
  bashunit::state::set_duplicated_test_functions_found
  bashunit::state::set_file_with_duplicated_function_names "$1"
  bashunit::state::set_duplicated_function_names "$2"
  bashunit::state::set_duplicated_function_details "${3:-}"
}

# src/state/context.sh

_BASHUNIT_TEST_OUTPUT=""
_BASHUNIT_TEST_TITLE=""
_BASHUNIT_TEST_EXIT_CODE=0
_BASHUNIT_TEST_HOOK_FAILURE=""
_BASHUNIT_TEST_HOOK_MESSAGE=""
_BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME=""
_BASHUNIT_ASSERTION_FAILED_IN_TEST=0

function bashunit::state::add_test_output() {
  _BASHUNIT_TEST_OUTPUT="$_BASHUNIT_TEST_OUTPUT$1"
}

function bashunit::state::set_test_exit_code() {
  _BASHUNIT_TEST_EXIT_CODE="$1"
}

function bashunit::state::set_test_title() {
  _BASHUNIT_TEST_TITLE="$1"
}

function bashunit::state::reset_test_title() {
  _BASHUNIT_TEST_TITLE=""
}

function bashunit::state::set_current_test_interpolated_function_name() {
  _BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME="$1"
}

function bashunit::state::reset_current_test_interpolated_function_name() {
  _BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME=""
}

function bashunit::state::set_test_hook_failure() {
  _BASHUNIT_TEST_HOOK_FAILURE="$1"
}

function bashunit::state::set_test_hook_message() {
  _BASHUNIT_TEST_HOOK_MESSAGE="$1"
}

function bashunit::state::mark_assertion_failed_in_test() {
  _BASHUNIT_ASSERTION_FAILED_IN_TEST=1
}

function bashunit::state::initialize_assertions_count() {
  _BASHUNIT_ASSERTIONS_PASSED=0
  _BASHUNIT_ASSERTIONS_FAILED=0
  _BASHUNIT_ASSERTIONS_SKIPPED=0
  _BASHUNIT_ASSERTIONS_INCOMPLETE=0
  _BASHUNIT_ASSERTIONS_SNAPSHOT=0
  _BASHUNIT_TEST_OUTPUT=""
  _BASHUNIT_TEST_TITLE=""
  _BASHUNIT_TEST_HOOK_FAILURE=""
  _BASHUNIT_TEST_HOOK_MESSAGE=""
  _BASHUNIT_ASSERTION_FAILED_IN_TEST=0
  bashunit::assert::once_reset
}

_BASHUNIT_STATE_ENCODED_OUT=""

# src/state/payload.sh

_bashunit_base64_help="$(base64 --help 2>&1 || true)"
case "$_bashunit_base64_help" in
*-w*) _BASHUNIT_BASE64_WRAP_FLAG=true ;;
*) _BASHUNIT_BASE64_WRAP_FLAG=false ;;
esac
unset _bashunit_base64_help

_BASHUNIT_BASE64_EMPTY_SENTINEL="_BASHUNIT_EMPTY_"

function bashunit::state::encode_field() {
  local value=$1
  if [ -z "$value" ]; then
    _BASHUNIT_STATE_ENCODED_OUT=""
    return
  fi
  if [ "$_BASHUNIT_BASE64_WRAP_FLAG" = true ]; then

    _BASHUNIT_STATE_ENCODED_OUT=$(echo -n "$value" | base64 -w 0)
  else
    _BASHUNIT_STATE_ENCODED_OUT=$(echo -n "$value" | base64)
  fi
}

function bashunit::state::export_subshell_context() {
  local encoded_test_output
  local encoded_test_title
  local encoded_test_hook_message

  bashunit::state::encode_field "$_BASHUNIT_TEST_OUTPUT"
  encoded_test_output=$_BASHUNIT_STATE_ENCODED_OUT
  bashunit::state::encode_field "$_BASHUNIT_TEST_TITLE"
  encoded_test_title=$_BASHUNIT_STATE_ENCODED_OUT
  bashunit::state::encode_field "$_BASHUNIT_TEST_HOOK_MESSAGE"
  encoded_test_hook_message=$_BASHUNIT_STATE_ENCODED_OUT

  local payload="\
##ASSERTIONS_FAILED=$_BASHUNIT_ASSERTIONS_FAILED##ASSERTIONS_PASSED=$_BASHUNIT_ASSERTIONS_PASSED##ASSERTIONS_SKIPPED=$_BASHUNIT_ASSERTIONS_SKIPPED##ASSERTIONS_INCOMPLETE=$_BASHUNIT_ASSERTIONS_INCOMPLETE##ASSERTIONS_SNAPSHOT=$_BASHUNIT_ASSERTIONS_SNAPSHOT##TEST_EXIT_CODE=$_BASHUNIT_TEST_EXIT_CODE##TEST_HOOK_FAILURE=$_BASHUNIT_TEST_HOOK_FAILURE##TEST_HOOK_MESSAGE=$encoded_test_hook_message##TEST_TITLE=$encoded_test_title##TEST_OUTPUT=$encoded_test_output##"
  printf '%s\n' "$payload"
}

# src/state/parallel.sh

function bashunit::state::aggregate_parallel_results() {
  local temp_dir_parallel_test_suite=$1
  local IFS=$' \t\n'

  bashunit::internal_log "aggregate_parallel_results" "dir:$temp_dir_parallel_test_suite"

  local total_failed=0
  local total_passed=0
  local total_skipped=0
  local total_incomplete=0
  local total_snapshot=0

  local script_dir=""
  for script_dir in "$temp_dir_parallel_test_suite"/*; do
    shopt -s nullglob

    local result_files
    result_files=("$script_dir"/*.result)
    shopt -u nullglob

    if [ ${#result_files[@]} -eq 0 ]; then

      printf "%sNo tests found%s\n" "$_BASHUNIT_COLOR_SKIPPED" "$_BASHUNIT_COLOR_DEFAULT"
      continue
    fi

    local result_file=""
    for result_file in "${result_files[@]+"${result_files[@]}"}"; do
      local result_line
      result_line=$(<"$result_file")
      result_line="${result_line##*$'\n'}"

      local failed="${result_line##*##ASSERTIONS_FAILED=}"
      failed="${failed%%##*}"
      failed=${failed:-0}

      local passed="${result_line##*##ASSERTIONS_PASSED=}"
      passed="${passed%%##*}"
      passed=${passed:-0}

      local skipped="${result_line##*##ASSERTIONS_SKIPPED=}"
      skipped="${skipped%%##*}"
      skipped=${skipped:-0}

      local incomplete="${result_line##*##ASSERTIONS_INCOMPLETE=}"
      incomplete="${incomplete%%##*}"
      incomplete=${incomplete:-0}

      local snapshot="${result_line##*##ASSERTIONS_SNAPSHOT=}"
      snapshot="${snapshot%%##*}"
      snapshot=${snapshot:-0}

      local exit_code="${result_line##*##TEST_EXIT_CODE=}"
      exit_code="${exit_code%%##*}"
      exit_code=${exit_code:-0}

      local retries=0
      case "$result_line" in
      *"##TEST_RETRIES="*)
        retries="${result_line##*##TEST_RETRIES=}"
        retries="${retries%%##*}"
        case "$retries" in '' | *[!0-9]*) retries=0 ;; esac
        ;;
      esac

      case "$failed$passed$skipped$incomplete$snapshot$exit_code" in
      *[!0-9]*)
        failed=0 passed=0 skipped=0 incomplete=0 snapshot=0

        exit_code=1
        bashunit::internal_log "aggregate_parallel_results" "unparseable result file:$result_file"
        ;;
      esac

      total_failed=$((total_failed + failed))
      total_passed=$((total_passed + passed))
      total_skipped=$((total_skipped + skipped))
      total_incomplete=$((total_incomplete + incomplete))
      total_snapshot=$((total_snapshot + snapshot))

      if [ "${failed:-0}" -gt 0 ]; then
        bashunit::state::add_tests_failed
        continue
      fi

      if [ "${exit_code:-0}" -ne 0 ]; then
        bashunit::state::add_tests_failed
        continue
      fi

      if [ "${snapshot:-0}" -gt 0 ]; then
        bashunit::state::add_tests_snapshot
        continue
      fi

      if [ "${incomplete:-0}" -gt 0 ]; then
        bashunit::state::add_tests_incomplete
        continue
      fi

      if [ "${skipped:-0}" -gt 0 ]; then
        bashunit::state::add_tests_skipped
        continue
      fi

      local total_for_test=$((failed + passed + skipped + incomplete + snapshot))
      if [ "$total_for_test" -eq 0 ] && [ "${exit_code:-0}" -eq 0 ]; then
        if bashunit::env::is_fail_on_risky_enabled; then
          bashunit::state::add_tests_failed
        else
          bashunit::state::add_tests_risky
        fi
        continue
      fi

      if [ "$retries" -gt 0 ]; then
        bashunit::state::add_tests_flaky
      fi
      bashunit::state::add_tests_passed
    done
  done

  export _BASHUNIT_ASSERTIONS_FAILED=$total_failed
  export _BASHUNIT_ASSERTIONS_PASSED=$total_passed
  export _BASHUNIT_ASSERTIONS_SKIPPED=$total_skipped
  export _BASHUNIT_ASSERTIONS_INCOMPLETE=$total_incomplete
  export _BASHUNIT_ASSERTIONS_SNAPSHOT=$total_snapshot

  bashunit::internal_log "aggregate_totals" \
    "failed:$total_failed" \
    "passed:$total_passed" \
    "skipped:$total_skipped" \
    "incomplete:$total_incomplete" \
    "snapshot:$total_snapshot"
}

# src/console/index.sh

# src/console/colors.sh

bashunit::sgr() {
  local codes=${1:-0}
  shift

  local c
  for c in "$@"; do
    codes="$codes;$c"
  done

  echo $'\e'"[${codes}m"
}

if bashunit::env::is_no_color_enabled; then
  _BASHUNIT_COLOR_BOLD=""
  _BASHUNIT_COLOR_FAINT=""
  _BASHUNIT_COLOR_BLACK=""
  _BASHUNIT_COLOR_FAILED=""
  _BASHUNIT_COLOR_PASSED=""
  _BASHUNIT_COLOR_SKIPPED=""
  _BASHUNIT_COLOR_INCOMPLETE=""
  _BASHUNIT_COLOR_SNAPSHOT=""
  _BASHUNIT_COLOR_RISKY=""
  _BASHUNIT_COLOR_RETURN_ERROR=""
  _BASHUNIT_COLOR_RETURN_SUCCESS=""
  _BASHUNIT_COLOR_RETURN_SKIPPED=""
  _BASHUNIT_COLOR_RETURN_INCOMPLETE=""
  _BASHUNIT_COLOR_RETURN_SNAPSHOT=""
  _BASHUNIT_COLOR_RETURN_RISKY=""
  _BASHUNIT_COLOR_DEFAULT=""
else
  _BASHUNIT_COLOR_BOLD="$(bashunit::sgr 1)"

  _BASHUNIT_COLOR_FAINT="$(bashunit::sgr 90)"
  _BASHUNIT_COLOR_BLACK="$(bashunit::sgr 30)"
  _BASHUNIT_COLOR_FAILED="$(bashunit::sgr 31)"
  _BASHUNIT_COLOR_PASSED="$(bashunit::sgr 32)"
  _BASHUNIT_COLOR_SKIPPED="$(bashunit::sgr 33)"
  _BASHUNIT_COLOR_INCOMPLETE="$(bashunit::sgr 36)"
  _BASHUNIT_COLOR_SNAPSHOT="$(bashunit::sgr 34)"
  _BASHUNIT_COLOR_RISKY="$(bashunit::sgr 35)"
  _BASHUNIT_COLOR_RETURN_ERROR="$(bashunit::sgr 41)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SUCCESS="$(bashunit::sgr 42)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SKIPPED="$(bashunit::sgr 43)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_INCOMPLETE="$(bashunit::sgr 46)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SNAPSHOT="$(bashunit::sgr 44)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_RISKY="$(bashunit::sgr 45)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_DEFAULT="$(bashunit::sgr 0)"
fi

# src/console/header.sh

function bashunit::console_header::print_version_with_env() {
  local filter=${1:-}
  shift || true

  if ! bashunit::env::is_show_header_enabled; then
    return
  fi

  bashunit::console_header::print_version "$filter" "$@"

  if bashunit::env::is_dev_mode_enabled; then
    printf "%sDev log:%s %s\n" \
      "${_BASHUNIT_COLOR_INCOMPLETE}" "${_BASHUNIT_COLOR_DEFAULT}" "$BASHUNIT_DEV_LOG"
  fi
}

function bashunit::console_header::print_random_order_seed() {
  local seed=$1
  printf "%sRandomized with seed:%s %s\n" \
    "${_BASHUNIT_COLOR_INCOMPLETE}" "${_BASHUNIT_COLOR_DEFAULT}" "$seed"
}

function bashunit::console_header::print_version() {
  local filter=${1:-}
  shift || true

  local files_count=$#
  local total_tests
  if [ "$files_count" -eq 0 ]; then
    total_tests=0
  elif bashunit::parallel::is_enabled && bashunit::env::is_simple_output_enabled; then

    total_tests=0
  else

    bashunit::helper::find_total_tests "$filter" "$@" >/dev/null
    total_tests=$_BASHUNIT_HELPER_TOTAL_TESTS_OUT
  fi

  if bashunit::env::is_header_ascii_art_enabled; then
    cat <<EOF
_               _                   _
| |__   __ _ ___| |__  __ __ ____ (_) |_
| '_ \ / _' / __| '_ \| | | | '_ \| | __|
| |_) | (_| \__ \ | | | |_| | | | | | |_
|_.__/ \__,_|___/_| |_|\___/|_| |_|_|\__|
EOF
    if [ "$total_tests" -eq 0 ]; then
      printf "%s\n" "$BASHUNIT_VERSION"
    else
      printf "%s | Tests: %s\n" "$BASHUNIT_VERSION" "$total_tests"
    fi
    return
  fi

  if [ "$total_tests" -eq 0 ]; then
    printf "%s%sbashunit%s - %s\n" \
      "$_BASHUNIT_COLOR_BOLD" "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_DEFAULT" "$BASHUNIT_VERSION"
  else
    printf "%s%sbashunit%s - %s | Tests: %s\n" \
      "${_BASHUNIT_COLOR_BOLD}" "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" \
      "$BASHUNIT_VERSION" "$total_tests"
  fi
}

function bashunit::console_header::print_help() {
  cat <<EOF
Usage: bashunit <command> [arguments] [options]

Commands:
  test [path]         Run tests (default command)
  bench [path]        Run benchmarks
  assert <fn> <args>  Run standalone assertion
  doc [filter]        Display assertion documentation
  init [dir]          Initialize a new test directory
  learn               Start interactive tutorial
  watch [path]        Watch files and re-run tests on change
  upgrade             Upgrade bashunit to latest version

Global Options:
  -h, --help        Show this help message
  -v, --version     Display the current version

Run 'bashunit <command> --help' for command-specific options.

Examples:
  bashunit test tests/                Run all tests in directory
  bashunit tests/                     Run all tests (shorthand)
  bashunit bench                      Run all benchmarks
  bashunit assert equals "foo" "foo"  Run standalone assertion
  bashunit doc contains               Show docs for 'contains' assertions
  bashunit init                       Initialize test directory

More info: https://bashunit.com/command-line
EOF
}

function bashunit::console_header::print_test_help() {
  cat <<EOF
Usage: bashunit test [path] [options]
  bashunit [path] [options]

Run test files. If no path is provided, searches for tests in BASHUNIT_DEFAULT_PATH.

Arguments:
  path                        File or directory containing tests
    - Directories: runs all '*test.sh' files
    - Wildcards: supported to match multiple files

Options:
  -a, --assert <fn> <args>    Run a standalone assert function (deprecated: use 'bashunit assert')
  -e, --env, --boot <file>    Load a custom env/bootstrap file  (supports args)
  -f, --filter <name>         Only run tests matching the name
  --exclude-filter <name>     Skip tests whose name matches (repeatable)
  --suite <name>              Run a [suite:<name>] from .bashunitrc (repeatable)
  --list-suites               Print the suites defined in .bashunitrc and exit
  --tag <expr>                Only run tests with matching @tag (repeatable, OR logic).
                              Supports 'a&&b' (AND) and '!a' (NOT)
  --exclude-tag <name>        Skip tests with matching @tag (repeatable, exclude wins)
  --sandbox                   Fail a test that runs an external command it did not mock
  --sandbox-allow <cmd,...>   Commands the sandbox still allows (repeatable)
  --log-junit, --report-junit <file>  Write JUnit XML report
  --log-gha <file>            Write GitHub Actions annotations to a file
  --gha-annotations <mode>    Annotations on stdout: auto (in GitHub Actions), always or never
  -j, --jobs <N|auto>         Run tests in parallel with max N concurrent jobs ("auto" = CPU cores)
  -p, --parallel              Run tests in parallel (unlimited concurrency)
  --no-parallel               Run tests sequentially
  -r, --report-html <file>    Write HTML report
  --report-tap <file>         Write TAP version 13 report
  --report-json <file>        Write machine-readable JSON report
  --report-md <file>          Write a Markdown summary (auto-appended to \$GITHUB_STEP_SUMMARY)
  -s, --simple                Simple output (dots)
  --detailed                  Detailed output (default)
  --output <format>           Report on stdout: text (default), tap, json or junit
  -R, --run-all               Run all assertions (don't stop on first failure)
  -S, --stop-on-failure       Stop on first failure
  --test-timeout <seconds>    Fail a test if it runs longer than N seconds (0 = off)
  --retry <n>                 Re-run a failed test up to N extra times (0 = off)
  --repeat <n>                Run each selected test N times; it fails if any iteration fails
  --random-order              Randomize test execution order
  --order-by <mode>           Execution order: defined (default), defects (last run's failures first) or random
  --seed <n>                  Seed for --random-order (reproducible shuffle)
  --shard <i>/<n>             Run shard i of n (split the suite across runners)
  --rerun-failed              Replay only the tests that failed on the last run (.bashunit/last-failed)
  --changed [<ref>]           Run only the test files changed since <ref> (default: origin/HEAD, then HEAD)
  --list, --dry-run           Print the tests that would run, then exit without running them
  --list-format <fmt>         Rendering for --list: text (default) or json
  --snapshot-update           Rewrite existing snapshots from the actual value (combine with --filter)
  --no-snapshot-create        Fail on a missing snapshot instead of recording it (for CI)
  --snapshot-report-unused    List snapshot files no test resolved (full runs only, deletes nothing)
  --snapshot-prune            Delete the snapshot files no test resolved (full runs only)
  -vvv, --verbose             Show execution details
  --debug [file]              Enable shell debug mode
  --no-output                 Suppress all output
  --failures-only             Only show failures (suppress passed/skipped/incomplete)
  --show-skipped              Show the skipped tests summary at the end
  --show-incomplete           Show the incomplete tests summary at the end
  --fail-on-risky             Treat risky tests (no assertions) as failures
  --fail-on-flaky             Treat flaky tests (passed only after a retry) as failures
  --profile                   Report the slowest tests (count: BASHUNIT_PROFILE_COUNT, default 10)
  --no-progress               Suppress real-time progress, show only final results
  --show-output               Show test output on failure (default: enabled)
  --no-output-on-failure      Hide test output on failure
  --strict                    Enable strict shell mode (set -euo pipefail)
  --skip-env-file             Skip .env loading, use shell environment only
  -l, --login                 Run tests in login shell context
  -w, --watch                 Watch for changes and re-run tests
  --no-color                  Disable colored output (honors NO_COLOR env var)
  -h, --help                  Show this help message

Coverage:
  --coverage                   Enable code coverage tracking
  --coverage-paths <paths>     Source paths to track (default: auto-discover)
  --coverage-exclude <pats>    Patterns to exclude (comma-separated)
  --coverage-report [file]     Output file (default: coverage/lcov.info)
  --coverage-report-html [dir] HTML report (default: coverage/html)
  --coverage-report-cobertura [file] Cobertura XML for GitLab/Azure/Jenkins (default: coverage/cobertura.xml)
  --coverage-min <pct>         Fail if coverage below percentage
  --coverage-diff <ref>        Report coverage only for lines changed since ref
  --no-coverage-report         Disable file output, console only

Examples:
  bashunit test tests/
  bashunit test tests/unit/ --parallel
  bashunit test --filter "user" tests/
  bashunit test -a equals "foo" "foo"
  bashunit test tests/ --coverage
  bashunit test tests/ --coverage --coverage-min 80
  bashunit test tests/ --coverage-report-html
EOF
}

function bashunit::console_header::print_bench_help() {
  cat <<EOF
Usage: bashunit bench [path] [options]

Run benchmark files. Searches for '*bench.sh' files.

Arguments:
  path                        File or directory containing benchmarks

Options:
  -e, --env, --boot <file>    Load a custom env/bootstrap file (supports args)
  -f, --filter <name>         Only run benchmarks matching the name
  --baseline <file>           Compare against a previous --report-json result
  --baseline-tolerance <pct>  Allowed regression before failing (default: 10)
  --baseline-update <file>    Write this run as the new baseline
  --report-json <file>        Write machine-readable benchmark results
  --report-junit <file>       Write JUnit XML benchmark results
  -s, --simple                Simple output
  --detailed                  Detailed output (default)
  -vvv, --verbose             Show execution details
  --skip-env-file             Skip .env loading, use shell environment only
  -l, --login                 Run in login shell context
  --no-color                  Disable colored output (honors NO_COLOR env var)
  -h, --help                  Show this help message

Examples:
  bashunit bench
  bashunit bench benchmarks/
  bashunit bench --filter "parse"
  bashunit bench --report-json bench.json
  bashunit bench --baseline bench.json --baseline-tolerance 5
EOF
}

function bashunit::console_header::print_doc_help() {
  cat <<EOF
Usage: bashunit doc [options] [filter]

Display documentation for assertion functions.

Arguments:
filter                      Optional filter to show only matching assertions

Options:
--custom                    Show only the assertions your project defines
-e, --env, --boot <file>    Load a bootstrap file defining custom assertions

Examples:
bashunit doc                Show all assertions
bashunit doc equals         Show assertions containing 'equals'
bashunit doc file           Show file-related assertions
bashunit doc --custom       Show only your project's own assertions
EOF
}

function bashunit::console_header::print_init_help() {
  cat <<EOF
Usage: bashunit init [directory]

Initialize a new test directory with sample files.

Arguments:
  directory                   Target directory (default: tests)

Creates:
  - bootstrap.sh              Setup file for test configuration
  - example_test.sh           Sample test file to get started
  - .github/workflows/tests.yml  CI workflow using the official action

Examples:
  bashunit init               Create tests/ directory
  bashunit init spec          Create spec/ directory
EOF
}

function bashunit::console_header::print_learn_help() {
  cat <<EOF
Usage: bashunit learn

Start the interactive learning tutorial.

The tutorial includes 10 progressive lessons:
  1. Basics - Your First Test
  2. Assertions - Testing Different Conditions
  3. Setup & Teardown - Managing Test Lifecycle
  4. Testing Functions - Unit Testing Patterns
  5. Testing Scripts - Integration Testing
  6. Mocking - Test Doubles and Mocks
  7. Spies - Verifying Function Calls
  8. Data Providers - Parameterized Tests
  9. Exit Codes - Testing Success and Failure
  10. Complete Challenge - Real World Scenario

Your progress is saved automatically.
EOF
}

function bashunit::console_header::print_upgrade_help() {
  cat <<EOF
Usage: bashunit upgrade

Upgrade bashunit to the latest version.

Downloads and installs the newest release from GitHub.
EOF
}

function bashunit::console_header::print_assert_help() {
  cat <<EOF
Usage: bashunit assert <function> [args...]
  bashunit assert "<command>" <assertion1> <arg1> [<assertion2> <arg2>...]

Run standalone assertion(s) without creating a test file.

Single assertion:
  bashunit assert equals "foo" "foo"
  bashunit assert same "1" "1"
  bashunit assert contains "world" "hello world"
  bashunit assert exit_code 0 "echo 'success'"

Multiple assertions on command output:
  bashunit assert "echo 'error' && exit 1" exit_code "1" contains "error"
  bashunit assert "./my_script.sh" exit_code "0" contains "success" not_contains "error"

Arguments:
  function                    Assertion function name (with or without 'assert_' prefix)
  command                     Command to execute (for multi-assertion mode)
  assertion                   Assertion name (exit_code, contains, equals, etc.)
  arg                         Expected value for the assertion

Note: You can also use 'bashunit test --assert <fn> <args>' (deprecated).
  The 'bashunit assert' subcommand is the recommended approach.

More info: https://bashunit.com/standalone
EOF
}

function bashunit::console_header::print_watch_help() {
  cat <<'ENDOFHELP'
Usage: bashunit watch [path] [test-options]

Watch .sh files for changes and automatically re-run tests.

Arguments:
  [path]          Directory or file to watch and test (default: .)

Options:
  -h, --help      Show this help message
  Any option accepted by 'bashunit test' is also accepted here.

Requirements:
  Linux:  inotifywait  (sudo apt install inotify-tools)
  macOS:  fswatch      (brew install fswatch)

Examples:
  bashunit watch                      Watch current directory
  bashunit watch tests/               Watch the tests/ directory
  bashunit watch tests/ --filter user Watch and filter by name
  bashunit watch tests/ --simple      Watch with simple output
ENDOFHELP
}

# src/console/line.sh

_BASHUNIT_TOTAL_TESTS_COUNT=0

function bashunit::console_results::print_line() {
  local type=$1
  local line=$2

  ((_BASHUNIT_TOTAL_TESTS_COUNT++)) || true

  bashunit::state::add_test_output "[$type]$line"

  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_tap_output_enabled; then
    bashunit::console_results::print_tap_line "$type" "$line"
    return
  fi

  if bashunit::env::is_machine_output_enabled; then
    return
  fi

  if ! bashunit::env::is_simple_output_enabled; then
    printf "%s\n" "$line"
    return
  fi

  local char
  case "$type" in
  successful) char="." ;;
  failure) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  failed) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  failed_snapshot) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  skipped) char="${_BASHUNIT_COLOR_SKIPPED}S${_BASHUNIT_COLOR_DEFAULT}" ;;
  incomplete) char="${_BASHUNIT_COLOR_INCOMPLETE}I${_BASHUNIT_COLOR_DEFAULT}" ;;
  snapshot) char="${_BASHUNIT_COLOR_SNAPSHOT}N${_BASHUNIT_COLOR_DEFAULT}" ;;
  risky) char="${_BASHUNIT_COLOR_RISKY}R${_BASHUNIT_COLOR_DEFAULT}" ;;
  error) char="${_BASHUNIT_COLOR_FAILED}E${_BASHUNIT_COLOR_DEFAULT}" ;;
  *) char="?" && bashunit::log "warning" "unknown test type '$type'" ;;
  esac

  if bashunit::parallel::is_enabled; then
    printf "%s" "$char"
  else
    if ((_BASHUNIT_TOTAL_TESTS_COUNT % 50 == 0)); then
      printf "%s\n" "$char"
    else
      printf "%s" "$char"
    fi
  fi
}

function bashunit::console_results::print_tap_line() {
  local type=$1
  local line=$2

  local clean_line
  clean_line=$(printf "%s" "$line" | sed 's/\x1B\[[0-9;]*[mK]//g')
  local test_name="${clean_line#*: }"
  test_name="${test_name%%$'\n'*}"

  test_name=$(printf "%s" "$test_name" |
    sed 's/[[:space:]]*[0-9][0-9]*m\{0,1\}[[:space:]]*[0-9.]*[ms]*[[:space:]]*$//')

  case "$type" in
  successful)
    printf "ok %d - %s\n" "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  failure | failed | failed_snapshot | error)
    printf "not ok %d - %s\n" "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    local detail_line
    printf "  ---\n"
    while IFS= read -r detail_line; do
      detail_line=$(printf "%s" "$detail_line" | sed 's/\x1B\[[0-9;]*[mK]//g')
      if [ -n "$detail_line" ] &&
        [ "$(echo "$detail_line" | "$GREP" -cF "Failed:" || true)" -eq 0 ] &&
        [ "$(echo "$detail_line" | "$GREP" -cF "Error:" || true)" -eq 0 ]; then
        local trimmed="${detail_line#"${detail_line%%[![:space:]]*}"}"
        printf "  %s\n" "$trimmed"
      fi
    done <<<"$clean_line"
    printf "  ...\n"
    ;;
  skipped)
    local skip_name="${test_name%%   *}"
    local skip_reason="${test_name#"$skip_name"}"
    skip_reason="${skip_reason#"${skip_reason%%[![:space:]]*}"}"
    if [ -n "$skip_reason" ]; then
      printf "ok %d - %s # SKIP %s\n" \
        "$_BASHUNIT_TOTAL_TESTS_COUNT" "$skip_name" "$skip_reason"
    else
      printf "ok %d - %s # SKIP\n" \
        "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    fi
    ;;
  incomplete)
    printf "ok %d - %s # TODO incomplete\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  snapshot)
    printf "ok %d - %s # snapshot\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  risky)
    printf "ok %d - %s # RISKY no assertions\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  *)
    printf "not ok %d - %s\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  esac
}

# src/console/duration.sh

function bashunit::console_results::format_duration_to_slot() {
  local duration_ms="$1"

  if [ "$duration_ms" -ge 60000 ]; then
    local time_in_seconds=$((duration_ms / 1000))
    local minutes=$((time_in_seconds / 60))
    local seconds=$((time_in_seconds % 60))
    _BASHUNIT_CONSOLE_DURATION_OUT="${minutes}m ${seconds}s"
  elif [ "$duration_ms" -ge 1000 ]; then
    local integer_part=$((duration_ms / 1000))
    local decimal_part=$(((duration_ms % 1000) / 10))

    if [ "$decimal_part" -lt 10 ]; then
      decimal_part="0${decimal_part}"
    fi
    _BASHUNIT_CONSOLE_DURATION_OUT="${integer_part}.${decimal_part}s"
  else
    _BASHUNIT_CONSOLE_DURATION_OUT="${duration_ms}ms"
  fi
}

function bashunit::console_results::format_duration() {
  bashunit::console_results::format_duration_to_slot "$1"
  echo "$_BASHUNIT_CONSOLE_DURATION_OUT"
}

# src/console/diff.sh

function bashunit::console_results::render_diff() {
  local expected_file=$1
  local actual_file=$2

  if ! bashunit::dependencies::has_git; then
    return 0
  fi

  local color_flag="--color=always"
  if bashunit::env::is_no_color_enabled; then
    color_flag="--color=never"
  fi

  git diff --no-index --no-ext-diff --word-diff "$color_flag" \
    "$expected_file" "$actual_file" 2>/dev/null |
    tail -n +6 | sed "s/^/    /" || true
}

function bashunit::console_results::first_line_ellipsis() {
  local text=$1
  local first="${text%%$'\n'*}"
  if [ "$first" != "$text" ]; then
    printf '%s…' "$first"
  else
    printf '%s' "$text"
  fi
}

function bashunit::console_results::snapshot_line_diff() {
  local expected=$1
  local actual=$2

  local expected_lines actual_lines
  expected_lines=()
  actual_lines=()
  local _line=""
  local i=0
  while IFS= read -r _line || [ -n "$_line" ]; do
    expected_lines[i]=$_line
    i=$((i + 1))
  done <<EOF
$expected
EOF
  local expected_count=$i

  i=0
  while IFS= read -r _line || [ -n "$_line" ]; do
    actual_lines[i]=$_line
    i=$((i + 1))
  done <<EOF
$actual
EOF
  local actual_count=$i

  local max=$expected_count
  if [ "$actual_count" -gt "$max" ]; then
    max=$actual_count
  fi

  local out=""
  i=0
  while [ "$i" -lt "$max" ]; do
    local e="" a="" has_e=0 has_a=0
    if [ "$i" -lt "$expected_count" ]; then
      e=${expected_lines[i]:-}
      has_e=1
    fi
    if [ "$i" -lt "$actual_count" ]; then
      a=${actual_lines[i]:-}
      has_a=1
    fi

    if [ "$has_e" = 1 ] && [ "$has_a" = 1 ] && [ "$e" = "$a" ]; then
      out="$out$(printf "\n    ${_BASHUNIT_COLOR_FAINT}  %s${_BASHUNIT_COLOR_DEFAULT}" "$e")"
    else
      if [ "$has_e" = 1 ]; then
        out="$out$(printf "\n    ${_BASHUNIT_COLOR_FAILED}- %s${_BASHUNIT_COLOR_DEFAULT}" "$e")"
      fi
      if [ "$has_a" = 1 ]; then
        out="$out$(printf "\n    ${_BASHUNIT_COLOR_PASSED}+ %s${_BASHUNIT_COLOR_DEFAULT}" "$a")"
      fi
    fi
    i=$((i + 1))
  done

  printf "%s" "$out"
}

# src/console/test_line.sh

function bashunit::console_results::print_successful_test() {
  local test_name=$1
  shift
  local duration=${1:-"0"}
  shift

  local line
  if [ -z "$*" ]; then
    line="${_BASHUNIT_COLOR_PASSED}✓ Passed${_BASHUNIT_COLOR_DEFAULT}: ${test_name}"
  else
    local quoted_args=""
    local arg
    for arg in "$@"; do
      if [ -z "$quoted_args" ]; then
        quoted_args="'$arg'"
      else
        quoted_args="$quoted_args, '$arg'"
      fi
    done
    line="${_BASHUNIT_COLOR_PASSED}✓ Passed${_BASHUNIT_COLOR_DEFAULT}: ${test_name} (${quoted_args})"
  fi

  line="${line}${_BASHUNIT_RETRY_NOTE:-}"

  local full_line=$line
  if bashunit::env::is_show_execution_time_enabled; then
    bashunit::console_results::format_duration_to_slot "$duration"
    full_line="$(bashunit::str::rpad "$line" "$_BASHUNIT_CONSOLE_DURATION_OUT")"
  fi

  bashunit::console_results::print_line "successful" "$full_line"
}

function bashunit::console_results::test_location_suffix() {
  local location=${_BASHUNIT_TEST_LOCATION:-}
  if [ -z "$location" ]; then
    return 0
  fi

  printf "\n    ${_BASHUNIT_COLOR_FAINT}at %s${_BASHUNIT_COLOR_DEFAULT}" "$location"
}

function bashunit::console_results::print_failure_message() {
  local test_name=$1
  local failure_message=$2

  if [ "${_BASHUNIT_ASSERT_ONCE_ACTIVE:-0}" -eq 1 ]; then
    if bashunit::assert::once_is_absorbing; then
      bashunit::assert::once_absorb_message "$failure_message" "" ""
      return 0
    fi
  fi

  local line
  line="$(printf "\
${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Message:${_BASHUNIT_COLOR_DEFAULT} \
${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
    "${test_name}" "${failure_message}")"

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::console_results::print_line "failure" "$line"
}

function bashunit::console_results::print_failed_test() {
  local function_name=$1
  local expected=$2
  local failure_condition_message=$3
  local actual=$4
  local extra_key=${5-}
  local extra_value=${6-}

  local details=${7-}

  if [ "${_BASHUNIT_ASSERT_ONCE_ACTIVE:-0}" -eq 1 ]; then
    if bashunit::assert::once_is_absorbing; then
      bashunit::assert::once_absorb_message "$expected" \
        "$failure_condition_message" "$actual"
      return 0
    fi
  fi

  local show_diff=false
  case "$expected$actual" in
  *$'\n'*)
    if bashunit::env::is_diff_enabled && bashunit::dependencies::has_git; then
      show_diff=true
    fi
    ;;
  esac

  local display_expected=$expected
  local display_actual=$actual
  if [ "$show_diff" = true ]; then
    display_expected="$(bashunit::console_results::first_line_ellipsis "$expected")"
    display_actual="$(bashunit::console_results::first_line_ellipsis "$actual")"
  fi

  local line
  line="$(printf "\
${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Expected${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}
    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
    "${function_name}" "${display_expected}" "${failure_condition_message}" "${display_actual}")"

  if [ "$show_diff" = true ]; then
    local _expected_file _actual_file
    _expected_file="$(bashunit::temp_file diff_expected)"
    _actual_file="$(bashunit::temp_file diff_actual)"
    printf '%s\n' "$expected" >"$_expected_file"
    printf '%s\n' "$actual" >"$_actual_file"
    line="$line
$(bashunit::console_results::render_diff "$_expected_file" "$_actual_file")"
    rm -f "$_expected_file" "$_actual_file"
  fi

  if [ -n "$extra_key" ]; then
    line="$line$(printf "\

    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
      "${extra_key}" "${extra_value}")"
  fi

  if [ -n "$details" ]; then
    line="$line
$details"
  fi

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::console_results::print_line "failed" "$line"
}

function bashunit::console_results::print_failed_snapshot_test() {
  local function_name=$1
  local snapshot_file=$2
  local actual_content=${3-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
    ${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
    ${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
    "$function_name" "$snapshot_file")"

  if bashunit::dependencies::has_git; then
    local actual_file="${snapshot_file}.tmp"
    echo "$actual_content" >"$actual_file"

    line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
    rm "$actual_file"
  else
    line="$line
$(bashunit::console_results::snapshot_line_diff \
      "$(cat "$snapshot_file")" "$actual_content")"
  fi

  bashunit::console_results::print_line "failed_snapshot" "$line"
}

function bashunit::console_results::print_skipped_test() {
  local function_name=$1
  local reason=${2-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_SKIPPED}↷ Skipped${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${function_name}")"

  if [ -n "$reason" ]; then
    line="$line$(printf "${_BASHUNIT_COLOR_FAINT}    %s${_BASHUNIT_COLOR_DEFAULT}\n" "${reason}")"
  fi

  bashunit::console_results::print_line "skipped" "$line"
}

function bashunit::console_results::print_incomplete_test() {
  local function_name=$1
  local pending=${2-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_INCOMPLETE}✒ Incomplete${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${function_name}")"

  if [ -n "$pending" ]; then
    line="$line$(printf "${_BASHUNIT_COLOR_FAINT}    %s${_BASHUNIT_COLOR_DEFAULT}\n" "${pending}")"
  fi

  bashunit::console_results::print_line "incomplete" "$line"
}

function bashunit::console_results::print_snapshot_test() {
  local function_name=$1
  local test_name
  test_name=$(bashunit::helper::normalize_test_function_name "$function_name")

  local line
  line="$(printf "${_BASHUNIT_COLOR_SNAPSHOT}✎ Snapshot${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${test_name}")"

  bashunit::console_results::print_line "snapshot" "$line"
}

function bashunit::console_results::print_risky_test() {
  local test_name=$1
  local duration=${2:-"0"}

  local line
  line=$(printf "%s⚠ Risky%s: %s" "$_BASHUNIT_COLOR_RISKY" "$_BASHUNIT_COLOR_DEFAULT" "$test_name")

  local full_line=$line
  if bashunit::env::is_show_execution_time_enabled; then
    local time_display
    time_display=$(bashunit::console_results::format_duration "$duration")
    full_line="$(bashunit::str::rpad "$line" "$time_display")"
  fi

  bashunit::console_results::print_line "risky" "$full_line"
}

function bashunit::console_results::print_error_test() {
  local function_name=$1
  local error="$2"
  local raw_output="${3:-}"

  local test_name
  test_name=$(bashunit::helper::normalize_test_function_name "$function_name")

  local line
  line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Error${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT}\n" "${test_name}" "${error}")"

  if [ -n "$raw_output" ] && bashunit::env::is_show_output_on_failure_enabled; then
    line="$line$(printf "    %sOutput:%s\n" "${_BASHUNIT_COLOR_FAINT}" "${_BASHUNIT_COLOR_DEFAULT}")"
    local output_line
    while IFS= read -r output_line; do
      line="$line$(printf "      %s\n" "$output_line")"
    done <<<"$raw_output"
  fi

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::console_results::print_line "error" "$line"
}

function bashunit::console_results::print_worker_stderr() {
  local test_file="$1"
  local stderr_file="$2"

  printf "\n%sStderr from %s%s\n" \
    "$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
  sed 's/^/|/' "$stderr_file"
}

# src/console/deferred.sh

function bashunit::console_results::print_failing_tests_and_reset() {
  if [ -s "$FAILURES_OUTPUT_PATH" ]; then
    local total_failed
    total_failed=$(bashunit::state::get_tests_failed)

    if bashunit::env::is_simple_output_enabled; then
      printf "\n\n"
    fi

    if [ "$total_failed" -eq 1 ]; then
      echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 failure:${_BASHUNIT_COLOR_DEFAULT}\n"
    else
      echo -e "${_BASHUNIT_COLOR_BOLD}There were $total_failed failures:${_BASHUNIT_COLOR_DEFAULT}\n"
    fi

    sed '${/^$/d;}' "$FAILURES_OUTPUT_PATH" | sed 's/^/|/'
    rm "$FAILURES_OUTPUT_PATH"

    echo ""
  fi
}

function bashunit::console_results::print_profile_and_reset() {
  if [ ! -s "$PROFILE_OUTPUT_PATH" ]; then
    rm -f "$PROFILE_OUTPUT_PATH"
    return
  fi

  local count="${BASHUNIT_PROFILE_COUNT:-10}"

  echo -e "\n${_BASHUNIT_COLOR_BOLD}Slowest tests:${_BASHUNIT_COLOR_DEFAULT}"

  local duration name file formatted

  while IFS=$'\t' read -r duration name file; do
    formatted=$(bashunit::console_results::format_duration "$duration")
    printf "  %s\t%s (%s)\n" "$formatted" "$name" "$file"
  done < <(sort -t"$(printf '\t')" -k1 -rn "$PROFILE_OUTPUT_PATH" | head -n "$count")

  echo ""

  rm -f "$PROFILE_OUTPUT_PATH"
}

function bashunit::console_results::flush_deferred_block() {
  local output_path=$1
  local total=$2
  local singular=$3
  local plural=$4

  if bashunit::env::is_simple_output_enabled; then
    printf "\n"
  fi

  if [ "$total" -eq 1 ]; then
    echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 ${singular}:${_BASHUNIT_COLOR_DEFAULT}\n"
  else
    echo -e "${_BASHUNIT_COLOR_BOLD}There were ${total} ${plural}:${_BASHUNIT_COLOR_DEFAULT}\n"
  fi

  tr -d '\r' <"$output_path" | sed '/^[[:space:]]*$/d' | sed 's/^/|/'
  rm "$output_path"

  echo ""
}

function bashunit::console_results::print_skipped_tests_and_reset() {
  if [ -s "$SKIPPED_OUTPUT_PATH" ] && bashunit::env::is_show_skipped_enabled; then
    bashunit::console_results::flush_deferred_block "$SKIPPED_OUTPUT_PATH" \
      "$(bashunit::state::get_tests_skipped)" "skipped test" "skipped tests"
  fi
}

function bashunit::console_results::print_incomplete_tests_and_reset() {
  if [ -s "$INCOMPLETE_OUTPUT_PATH" ] && bashunit::env::is_show_incomplete_enabled; then
    bashunit::console_results::flush_deferred_block "$INCOMPLETE_OUTPUT_PATH" \
      "$(bashunit::state::get_tests_incomplete)" "incomplete test" "incomplete tests"
  fi
}

function bashunit::console_results::print_risky_tests_and_reset() {
  if [ -s "$RISKY_OUTPUT_PATH" ]; then
    bashunit::console_results::flush_deferred_block "$RISKY_OUTPUT_PATH" \
      "$(bashunit::state::get_tests_risky)" "risky test" "risky tests"
  fi
}

# src/console/summary.sh

function bashunit::console_results::print_filter_hint() {
  local filter="${_BASHUNIT_ACTIVE_FILTER:-}"
  [ -n "$filter" ] || return 0

  local needle="${filter#test_}"
  local suggestion=""
  if [ -n "$needle" ]; then
    local lowered
    lowered="$(printf '%s' "$needle" | tr '[:upper:]' '[:lower:]')"

    local underscored="${lowered// /_}"
    local fn
    for fn in ${_BASHUNIT_CACHED_ALL_FUNCTIONS:-}; do
      case "$fn" in
      test_*"$lowered"* | test_*"$underscored"*)
        suggestion="$fn"
        break
        ;;
      esac
    done
  fi

  if [ -n "$suggestion" ]; then
    printf "%sNo test matches '%s'. Filters match the function name, not the title: did you mean '%s'?%s\n" \
      "${_BASHUNIT_COLOR_FAINT:-}" "$filter" "$suggestion" "${_BASHUNIT_COLOR_DEFAULT:-}"
    return 0
  fi

  printf "%sNo test matches '%s'. Filters match the function name (test_...), not the title in the report.%s\n" \
    "${_BASHUNIT_COLOR_FAINT:-}" "$filter" "${_BASHUNIT_COLOR_DEFAULT:-}"
}

function bashunit::console_results::render_result() {
  if [ "$(bashunit::state::is_duplicated_test_functions_found)" = true ]; then
    bashunit::console_results::print_execution_time
    printf "%s%s%s\n" "${_BASHUNIT_COLOR_RETURN_ERROR}" "Duplicate test functions found" "${_BASHUNIT_COLOR_DEFAULT}"
    printf "File with duplicate functions: %s\n" "$(bashunit::state::get_file_with_duplicated_function_names)"
    local _dup_detail
    _dup_detail="$(bashunit::state::get_duplicated_function_details)"
    if [ -z "$_dup_detail" ]; then
      _dup_detail="$(bashunit::state::get_duplicated_function_names)"
    fi
    printf "Duplicate functions: %s\n" "$_dup_detail"
    return 1
  fi

  if bashunit::env::is_tap_output_enabled; then
    printf "1..%d\n" "$_BASHUNIT_TOTAL_TESTS_COUNT"
    if [ "$_BASHUNIT_TESTS_FAILED" -gt 0 ]; then
      return 1
    fi
    return 0
  fi

  if bashunit::env::is_machine_output_enabled; then
    if [ "$_BASHUNIT_TESTS_FAILED" -gt 0 ]; then
      return 1
    fi
    if [ "$_BASHUNIT_TESTS_FLAKY" -gt 0 ] && bashunit::env::is_fail_on_flaky_enabled; then
      return 1
    fi
    local machine_total=$((_BASHUNIT_TESTS_PASSED + _BASHUNIT_TESTS_SKIPPED + \
      _BASHUNIT_TESTS_INCOMPLETE + _BASHUNIT_TESTS_SNAPSHOT + \
      _BASHUNIT_TESTS_FAILED + _BASHUNIT_TESTS_RISKY))
    if [ "$machine_total" -eq 0 ]; then
      return 1
    fi
    return 0
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n\n"
  fi

  local tests_passed=$_BASHUNIT_TESTS_PASSED
  local tests_skipped=$_BASHUNIT_TESTS_SKIPPED
  local tests_incomplete=$_BASHUNIT_TESTS_INCOMPLETE
  local tests_snapshot=$_BASHUNIT_TESTS_SNAPSHOT
  local tests_failed=$_BASHUNIT_TESTS_FAILED
  local tests_risky=$_BASHUNIT_TESTS_RISKY
  local tests_flaky=$_BASHUNIT_TESTS_FLAKY
  local assertions_passed=$_BASHUNIT_ASSERTIONS_PASSED
  local assertions_skipped=$_BASHUNIT_ASSERTIONS_SKIPPED
  local assertions_incomplete=$_BASHUNIT_ASSERTIONS_INCOMPLETE
  local assertions_snapshot=$_BASHUNIT_ASSERTIONS_SNAPSHOT
  local assertions_failed=$_BASHUNIT_ASSERTIONS_FAILED

  local total_tests=0
  total_tests=$((total_tests + tests_passed))
  total_tests=$((total_tests + tests_skipped))
  total_tests=$((total_tests + tests_incomplete))
  total_tests=$((total_tests + tests_snapshot))
  total_tests=$((total_tests + tests_failed))
  total_tests=$((total_tests + tests_risky))

  local total_assertions=0
  total_assertions=$((total_assertions + assertions_passed))
  total_assertions=$((total_assertions + assertions_skipped))
  total_assertions=$((total_assertions + assertions_incomplete))
  total_assertions=$((total_assertions + assertions_snapshot))
  total_assertions=$((total_assertions + assertions_failed))

  printf "%sTests:     %s" "$_BASHUNIT_COLOR_FAINT" "$_BASHUNIT_COLOR_DEFAULT"
  if [ "$tests_passed" -gt 0 ] || [ "$assertions_passed" -gt 0 ]; then
    printf " %s%s passed%s," "$_BASHUNIT_COLOR_PASSED" "$tests_passed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_skipped" -gt 0 ] || [ "$assertions_skipped" -gt 0 ]; then
    printf " %s%s skipped%s," "$_BASHUNIT_COLOR_SKIPPED" "$tests_skipped" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_incomplete" -gt 0 ] || [ "$assertions_incomplete" -gt 0 ]; then
    printf " %s%s incomplete%s," "$_BASHUNIT_COLOR_INCOMPLETE" "$tests_incomplete" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_snapshot" -gt 0 ] || [ "$assertions_snapshot" -gt 0 ]; then
    printf " %s%s snapshot%s," "$_BASHUNIT_COLOR_SNAPSHOT" "$tests_snapshot" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_failed" -gt 0 ] || [ "$assertions_failed" -gt 0 ]; then
    printf " %s%s failed%s," "$_BASHUNIT_COLOR_FAILED" "$tests_failed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_risky" -gt 0 ]; then
    printf " %s%s risky%s," "$_BASHUNIT_COLOR_RISKY" "$tests_risky" "$_BASHUNIT_COLOR_DEFAULT"
  fi

  if [ "$tests_flaky" -gt 0 ]; then
    printf " %s%s flaky%s," "$_BASHUNIT_COLOR_INCOMPLETE" "$tests_flaky" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  printf " %s total\n" "$total_tests"

  printf "%sAssertions:%s" "$_BASHUNIT_COLOR_FAINT" "$_BASHUNIT_COLOR_DEFAULT"
  if [ "$tests_passed" -gt 0 ] || [ "$assertions_passed" -gt 0 ]; then
    printf " %s%s passed%s," "$_BASHUNIT_COLOR_PASSED" "$assertions_passed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_skipped" -gt 0 ] || [ "$assertions_skipped" -gt 0 ]; then
    printf " %s%s skipped%s," "$_BASHUNIT_COLOR_SKIPPED" "$assertions_skipped" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_incomplete" -gt 0 ] || [ "$assertions_incomplete" -gt 0 ]; then
    printf " %s%s incomplete%s," "$_BASHUNIT_COLOR_INCOMPLETE" "$assertions_incomplete" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_snapshot" -gt 0 ] || [ "$assertions_snapshot" -gt 0 ]; then
    printf " %s%s snapshot%s," "$_BASHUNIT_COLOR_SNAPSHOT" "$assertions_snapshot" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_failed" -gt 0 ] || [ "$assertions_failed" -gt 0 ]; then
    printf " %s%s failed%s," "$_BASHUNIT_COLOR_FAILED" "$assertions_failed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  printf " %s total\n" "$total_assertions"

  if [ "$tests_failed" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " Some tests failed " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 1
  fi

  if [ "$tests_flaky" -gt 0 ] && bashunit::env::is_fail_on_flaky_enabled; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " Some tests flaky " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 1
  fi

  if [ "$tests_risky" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_RISKY" " Some tests risky (no assertions) " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_incomplete" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_INCOMPLETE" " Some tests incomplete " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_skipped" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SKIPPED" " Some tests skipped " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_snapshot" -gt 0 ]; then
    local snapshot_notice=" Some snapshots created "
    if bashunit::env::is_snapshot_update_enabled; then
      snapshot_notice=" Some snapshots updated "
    fi
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SNAPSHOT" "$snapshot_notice" "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$total_tests" -eq 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " No tests found " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_filter_hint
    bashunit::console_results::print_execution_time
    return 1
  fi

  printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SUCCESS" " All tests passed " "$_BASHUNIT_COLOR_DEFAULT"
  bashunit::console_results::print_execution_time
  return 0
}

function bashunit::console_results::print_execution_time() {
  if ! bashunit::env::is_total_execution_time_enabled; then
    return
  fi

  local time
  time=$(bashunit::clock::total_runtime_in_milliseconds)

  time="${time%%.*}"
  time="${time:-0}"

  local formatted
  formatted=$(bashunit::console_results::format_duration "$time")

  printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
    "Time taken: ${formatted}"
}

_BASHUNIT_CONSOLE_DURATION_OUT=""

function bashunit::console_results::print_hook_completed() {
  local hook_name="$1"
  local duration_ms="$2"

  if bashunit::env::is_simple_output_enabled; then
    return
  fi

  if bashunit::env::is_failures_only_enabled; then
    return
  fi

  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_machine_output_enabled; then
    return
  fi

  if bashunit::parallel::is_enabled; then
    return
  fi

  local line
  line=$(printf "%s● %s%s" \
    "$_BASHUNIT_COLOR_PASSED" "$hook_name" "$_BASHUNIT_COLOR_DEFAULT")

  local time_display
  time_display=$(bashunit::console_results::format_duration "$duration_ms")

  printf "%s\n" "$(bashunit::str::rpad "$line" "$time_display")"
}

# src/helper/index.sh

# src/helper/naming.sh

_BASHUNIT_HELPER_TESTFN_OUT=""

function bashunit::helper::find_test_function_name_to_slot() {
  local fallback_depth="${1:-2}"
  local i
  for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do
    local fn="${FUNCNAME[$i]}"
    case "$fn" in
    test_* | test[A-Z]*)
      _BASHUNIT_HELPER_TESTFN_OUT=$fn
      return
      ;;
    esac
  done
  _BASHUNIT_HELPER_TESTFN_OUT=${FUNCNAME[$fallback_depth]:-}
}

function bashunit::helper::find_test_function_name() {
  local fallback_depth="${1:-2}"
  local i
  for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do
    local fn="${FUNCNAME[$i]}"

    case "$fn" in
    test_* | test[A-Z]*)
      echo "$fn"
      return
      ;;
    esac
  done

  echo "${FUNCNAME[$fallback_depth]:-}"
}

_BASHUNIT_HELPER_NORMALIZED_OUT=""

function bashunit::helper::normalize_test_function_name_to_slot() {
  local original_fn_name="${1-}"
  local interpolated_fn_name="${2-}"

  local custom_title="${_BASHUNIT_TEST_TITLE:-}"
  if [ -n "$custom_title" ]; then
    _BASHUNIT_HELPER_NORMALIZED_OUT=$custom_title
    return
  fi

  if [ -z "${interpolated_fn_name-}" ]; then
    case "${original_fn_name}" in
    *"::"*)
      local state_interpolated_fn_name="${_BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME:-}"

      if [ -n "$state_interpolated_fn_name" ]; then
        interpolated_fn_name="$state_interpolated_fn_name"
      fi
      ;;
    esac
  fi

  if [ -n "${interpolated_fn_name-}" ]; then
    original_fn_name="$interpolated_fn_name"
  fi

  local result

  result="${original_fn_name#test_}"

  if [ "$result" = "$original_fn_name" ]; then
    result="${original_fn_name#test}"
  fi

  result="${result//_/ }"

  local first_char="${result:0:1}"
  case "$first_char" in
  a) first_char='A' ;; b) first_char='B' ;; c) first_char='C' ;; d) first_char='D' ;;
  e) first_char='E' ;; f) first_char='F' ;; g) first_char='G' ;; h) first_char='H' ;;
  i) first_char='I' ;; j) first_char='J' ;; k) first_char='K' ;; l) first_char='L' ;;
  m) first_char='M' ;; n) first_char='N' ;; o) first_char='O' ;; p) first_char='P' ;;
  q) first_char='Q' ;; r) first_char='R' ;; s) first_char='S' ;; t) first_char='T' ;;
  u) first_char='U' ;; v) first_char='V' ;; w) first_char='W' ;; x) first_char='X' ;;
  y) first_char='Y' ;; z) first_char='Z' ;;
  esac
  result="${first_char}${result:1}"

  _BASHUNIT_HELPER_NORMALIZED_OUT=$result
}

function bashunit::helper::normalize_test_function_name() {
  bashunit::helper::normalize_test_function_name_to_slot "${1-}" "${2-}"
  echo "$_BASHUNIT_HELPER_NORMALIZED_OUT"
}

function bashunit::helper::escape_single_quotes() {
  local value="$1"

  echo "${value//\'/'\'\\''\'}"
}

function bashunit::helper::interpolate_function_name() {
  local function_name="$1"
  shift

  case "$function_name" in
  *::*) ;;
  *)
    echo "$function_name"
    return
    ;;
  esac

  local -a args
  local args_count=$#
  args=("$@")
  local result="$function_name"

  local i
  for ((i = 0; i < args_count; i++)); do
    local placeholder="::$((i + 1))::"

    local value="$(bashunit::helper::escape_single_quotes "${args[$i]}")"
    value="'$value'"
    result="${result//${placeholder}/${value}}"
  done

  echo "$result"
}

function bashunit::helper::normalize_variable_name_to_slot() {
  local input_string="$1"
  local normalized_string="${input_string//[^a-zA-Z0-9_]/_}"

  case "${normalized_string:0:1}" in
  [a-zA-Z_]) ;;
  *) normalized_string="_$normalized_string" ;;
  esac

  _BASHUNIT_HELPER_VARNAME_OUT=$normalized_string
}

function bashunit::helper::normalize_variable_name() {
  bashunit::helper::normalize_variable_name_to_slot "$1"
  builtin echo "$_BASHUNIT_HELPER_VARNAME_OUT"
}

_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""

_BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false

# src/helper/encoding.sh

function bashunit::helper::encode_base64() {
  local value="$1"

  if [ -z "$value" ]; then
    printf '%s' "$_BASHUNIT_BASE64_EMPTY_SENTINEL"
    return
  fi

  if [ "$_BASHUNIT_BASE64_WRAP_FLAG" = true ]; then
    printf '%s' "$value" | base64 -w 0
  elif command -v base64 >/dev/null; then
    printf '%s' "$value" | base64 | tr -d '\n'
  else
    printf '%s' "$value" | openssl enc -base64 -A
  fi
}

function bashunit::helper::decode_base64() {
  local value="$1"

  if [ -z "$value" ] || [ "$value" = "$_BASHUNIT_BASE64_EMPTY_SENTINEL" ]; then
    printf ''
    return
  fi

  if command -v base64 >/dev/null; then
    printf '%s' "$value" | base64 -d
  else
    printf '%s' "$value" | openssl enc -d -base64
  fi
}

_BASHUNIT_HELPER_ID_OUT=""
function bashunit::helper::generate_id() {
  local basename="$1"

  local sanitized="${basename//[^a-zA-Z0-9_]/_}"
  case "${sanitized:0:1}" in
  [a-zA-Z_]) ;;
  *) sanitized="_$sanitized" ;;
  esac
  if bashunit::env::is_parallel_run_enabled; then
    local _chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local _suffix=''
    local _i
    for ((_i = 0; _i < 6; _i++)); do
      _suffix="$_suffix${_chars:RANDOM%${#_chars}:1}"
    done
    _BASHUNIT_HELPER_ID_OUT="${sanitized}_$$_${_suffix}"
  else
    _BASHUNIT_HELPER_ID_OUT="${sanitized}_$$"
  fi
}

# src/helper/functions.sh

function bashunit::helper::execute_function_if_exists() {
  local fn_name="$1"

  if declare -F "$fn_name" >/dev/null 2>&1; then
    "$fn_name"
    return $?
  fi

  return 0
}

function bashunit::helper::unset_if_exists() {
  unset "$1" 2>/dev/null
}

# src/helper/git.sh

declare -r BASHUNIT_GIT_REPO="https://github.com/TypedDevs/bashunit"

function bashunit::helper::get_latest_tag() {
  if ! bashunit::dependencies::has_git; then
    return 1
  fi

  git ls-remote --tags "$BASHUNIT_GIT_REPO" |
    awk '{print $2}' |
    sed 's|^refs/tags/||' |
    grep -v '\^{}' |
    grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' |
    sort -Vr |
    head -n 1
}

function bashunit::helper::git_is_repo() {
  if ! bashunit::dependencies::has_git; then
    return 1
  fi

  git rev-parse --is-inside-work-tree >/dev/null 2>&1
}

function bashunit::helper::git_ref_exists() {
  git rev-parse --verify --quiet "$1^{commit}" >/dev/null 2>&1
}

function bashunit::helper::git_changed_ref() {
  if [ -n "${BASHUNIT_CHANGED_REF:-}" ]; then
    echo "$BASHUNIT_CHANGED_REF"
  elif bashunit::helper::git_ref_exists "origin/HEAD"; then
    echo "origin/HEAD"
  else
    echo "HEAD"
  fi
}

function bashunit::helper::git_changed_files() {
  local ref=$1
  local prefix
  prefix="$(git rev-parse --show-prefix 2>/dev/null)"

  {
    git -c core.quotePath=false diff -M --name-only --diff-filter=d "$ref...HEAD" 2>/dev/null
    git -c core.quotePath=false diff -M --name-only --diff-filter=d HEAD 2>/dev/null
    git -c core.quotePath=false ls-files --others --exclude-standard 2>/dev/null
  } | awk -v prefix="$prefix" '
    NF == 0 { next }
    prefix != "" {
      if (index($0, prefix) != 1) next
      $0 = substr($0, length(prefix) + 1)
    }
    !seen[$0]++'
}

function bashunit::helper::git_changed_lines() {
  local ref=$1
  local file=$2

  if git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
    {
      git diff --unified=0 -M "$ref...HEAD" -- "$file" 2>/dev/null
      git diff --unified=0 -M HEAD -- "$file" 2>/dev/null
    } | awk '
      /^@@ / {
        # @@ -old,count +new,count @@
        plus = $3
        sub(/^\+/, "", plus)
        n = index(plus, ",")
        if (n == 0) { start = plus + 0; len = 1 }
        else { start = substr(plus, 1, n - 1) + 0; len = substr(plus, n + 1) + 0 }
        for (i = 0; i < len; i++) { seen[start + i] = 1 }
      }
      END { for (l in seen) { print l + 0 } }
    ' | sort -n -u
    return 0
  fi

  if [ -f "$file" ] &&
    [ -n "$(git ls-files --others --exclude-standard -- "$file" 2>/dev/null)" ]; then
    awk 'END { for (i = 1; i <= NR; i++) print i }' "$file"
  fi
}

function bashunit::helper::git_filter_changed() {
  local ref=$1
  shift

  local changed
  changed="$(bashunit::helper::git_changed_files "$ref")"
  [ -n "$changed" ] || return 0

  local file normalized
  for file in "$@"; do
    normalized="${file#./}"
    case "
$changed
" in
    *"
$normalized
"*) printf '%s\n' "$file" ;;
    esac
  done
}

_BASHUNIT_HELPER_TOTAL_TESTS_OUT=0

# src/helper/annotations.sh

_BASHUNIT_ANNOT_MAP_FNS=()
_BASHUNIT_ANNOT_MAP_TIMEOUTS=()
_BASHUNIT_ANNOT_MAP_RETRIES=()
_BASHUNIT_ANNOT_MAP_SKIPS=()
_BASHUNIT_ANNOT_MAP_REASONS=()

_BASHUNIT_ANNOT_TIMEOUT_OUT=""
_BASHUNIT_ANNOT_RETRY_OUT=""
_BASHUNIT_ANNOT_SKIP_OUT="false"
_BASHUNIT_ANNOT_REASON_OUT=""

function bashunit::helper::annotations_reset() {
  _BASHUNIT_ANNOT_MAP_FNS=()
  _BASHUNIT_ANNOT_MAP_TIMEOUTS=()
  _BASHUNIT_ANNOT_MAP_RETRIES=()
  _BASHUNIT_ANNOT_MAP_SKIPS=()
  _BASHUNIT_ANNOT_MAP_REASONS=()
}

function bashunit::helper::annotations_record() {
  local count=${#_BASHUNIT_ANNOT_MAP_FNS[@]}
  _BASHUNIT_ANNOT_MAP_FNS[count]="$1"
  _BASHUNIT_ANNOT_MAP_TIMEOUTS[count]="${2-}"
  _BASHUNIT_ANNOT_MAP_RETRIES[count]="${3-}"
  _BASHUNIT_ANNOT_MAP_SKIPS[count]="${4-}"
  _BASHUNIT_ANNOT_MAP_REASONS[count]="${5-}"
}

function bashunit::helper::annotations_for_function() {
  local function_name=$1
  local i=0
  local total=${#_BASHUNIT_ANNOT_MAP_FNS[@]}

  _BASHUNIT_ANNOT_TIMEOUT_OUT=""
  _BASHUNIT_ANNOT_RETRY_OUT=""
  _BASHUNIT_ANNOT_SKIP_OUT="false"
  _BASHUNIT_ANNOT_REASON_OUT=""

  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_ANNOT_MAP_FNS[i]}" = "$function_name" ]; then
      _BASHUNIT_ANNOT_TIMEOUT_OUT="${_BASHUNIT_ANNOT_MAP_TIMEOUTS[i]}"
      _BASHUNIT_ANNOT_RETRY_OUT="${_BASHUNIT_ANNOT_MAP_RETRIES[i]}"
      if [ "${_BASHUNIT_ANNOT_MAP_SKIPS[i]}" = "1" ]; then
        _BASHUNIT_ANNOT_SKIP_OUT="true"
      fi
      _BASHUNIT_ANNOT_REASON_OUT="${_BASHUNIT_ANNOT_MAP_REASONS[i]}"
      return
    fi
    i=$((i + 1))
  done
}

function bashunit::helper::_annotations_is_count() {
  case "$1" in
  '' | *[!0-9]*) return 1 ;;
  esac
  return 0
}

function bashunit::helper::annotations_validate_or_exit() {
  local script=$1
  local i=0
  local total=${#_BASHUNIT_ANNOT_MAP_FNS[@]}
  local fn value

  while [ "$i" -lt "$total" ]; do
    fn="${_BASHUNIT_ANNOT_MAP_FNS[i]}"

    value="${_BASHUNIT_ANNOT_MAP_TIMEOUTS[i]}"
    if [ -n "$value" ] && ! bashunit::helper::_annotations_is_count "$value"; then
      bashunit::helper::_annotations_reject "$script" "$fn" "timeout" "$value"
    fi

    value="${_BASHUNIT_ANNOT_MAP_RETRIES[i]}"
    if [ -n "$value" ] && ! bashunit::helper::_annotations_is_count "$value"; then
      bashunit::helper::_annotations_reject "$script" "$fn" "retry" "$value"
    fi

    i=$((i + 1))
  done
}

function bashunit::helper::_annotations_reject() {
  printf "%sError: @%s '%s' above %s in %s is not a non-negative integer.%s\n" \
    "${_BASHUNIT_COLOR_FAILED}" "$3" "$4" "$2" "$1" "${_BASHUNIT_COLOR_DEFAULT}" >&2
  exit 1
}

# src/helper/provider.sh

_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
  local script=$1

  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi
  if [ ! -f "$script" ]; then
    _BASHUNIT_PROVIDER_RESOLVED_OUT=""
    return
  fi
  _BASHUNIT_PROVIDER_RESOLVED_OUT=$script
}

function bashunit::helper::build_provider_map() {
  bashunit::helper::_resolve_provider_script "$1"
  local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

  if [ -z "$script" ]; then

    _BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
    _BASHUNIT_PROVIDER_MAP_FNS=()
    _BASHUNIT_PROVIDER_MAP_PROVIDERS=()
    _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false
    bashunit::helper::annotations_reset
    return
  fi

  if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
    return
  fi

  _BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
  _BASHUNIT_PROVIDER_MAP_FNS=()
  _BASHUNIT_PROVIDER_MAP_PROVIDERS=()
  _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false

  bashunit::helper::annotations_reset

  local count=0
  local fn provider annot_timeout annot_retry annot_skip annot_reason

  while IFS=$'\t' read -r fn provider annot_timeout annot_retry annot_skip annot_reason; do
    [ -z "$fn" ] && continue
    if [ "$fn" = "@@no_parallel@@" ]; then
      [ "$provider" = "1" ] && _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=true
      continue
    fi
    if [ "$fn" = "@@annot@@" ]; then
      [ "$annot_timeout" = "@@none@@" ] && annot_timeout=""
      [ "$annot_retry" = "@@none@@" ] && annot_retry=""
      bashunit::helper::annotations_record \
        "$provider" "$annot_timeout" "$annot_retry" "$annot_skip" "$annot_reason"
      continue
    fi
    _BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
    _BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
    count=$((count + 1))
  done < <(awk '
    /^# bashunit: no-parallel-tests/ { no_parallel = 1; next }
    /^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
      p = $0
      sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
      sub(/[[:space:]]+$/, "", p)
      pending = p
      pending_line = NR
      next
    }
    /^[[:space:]]*#[[:space:]]*@timeout([[:space:]]|=)/ {
      v = $0
      sub(/^[[:space:]]*#[[:space:]]*@timeout[[:space:]=]+/, "", v)
      sub(/[[:space:]]+$/, "", v)
      a_timeout = v
      next
    }
    /^[[:space:]]*#[[:space:]]*@retry([[:space:]]|=)/ {
      v = $0
      sub(/^[[:space:]]*#[[:space:]]*@retry[[:space:]=]+/, "", v)
      sub(/[[:space:]]+$/, "", v)
      a_retry = v
      next
    }
    /^[[:space:]]*#[[:space:]]*@skip([[:space:]]|$)/ {
      v = $0
      sub(/^[[:space:]]*#[[:space:]]*@skip[[:space:]]*/, "", v)
      sub(/[[:space:]]+$/, "", v)
      a_skip = 1
      a_reason = v
      next
    }
    # Any other comment keeps the block open, the same rule @tag follows.
    /^[[:space:]]*#/ { next }
    {
      is_fn = match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)
      if (is_fn) {
        fn = $0
        sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
        sub(/[[:space:]]*\(\).*/, "", fn)
      }

      if (pending != "" && NR - pending_line <= 2) {
        if (is_fn) {
          printf "%s\t%s\n", fn, pending
          pending = ""
        }
      } else if (pending != "" && NR - pending_line > 2) {
        pending = ""
      }

      if (is_fn && (a_timeout != "" || a_retry != "" || a_skip != "")) {
        # Tab is an IFS whitespace character, so `read` collapses a run of them
        # and an empty interior field would shift every later one. Absent
        # values therefore travel as a sentinel; the reason is last and may be
        # empty.
        printf "@@annot@@\t%s\t%s\t%s\t%s\t%s\n", fn,
          (a_timeout == "" ? "@@none@@" : a_timeout),
          (a_retry == "" ? "@@none@@" : a_retry),
          (a_skip == "" ? "0" : a_skip), a_reason
      }
      # A blank or code line ends the block for the next definition, whether or
      # not this line was one.
      a_timeout = ""
      a_retry = ""
      a_skip = ""
      a_reason = ""
    }
    END { printf "@@no_parallel@@\t%d\n", no_parallel }
  ' "$script" 2>/dev/null)
}

function bashunit::helper::provider_for_function() {
  local function_name=$1
  local i=0
  local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
      _BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
      return
    fi
    i=$((i + 1))
  done
  _BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
  local function_name="$1"
  local script="$2"

  bashunit::helper::build_provider_map "$script"
  bashunit::helper::provider_for_function "$function_name"

  if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
    bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
  fi
}

# src/helper/tags.sh

function bashunit::helper::build_tags_map() {
  local script=$1

  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi

  if [ ! -f "$script" ]; then

    _BASHUNIT_TAGS_MAP_SCRIPT="$1"
    _BASHUNIT_TAGS_MAP_FNS=()
    _BASHUNIT_TAGS_MAP_TAGS=()
    return
  fi

  if [ "$script" = "$_BASHUNIT_TAGS_MAP_SCRIPT" ]; then
    return
  fi

  _BASHUNIT_TAGS_MAP_SCRIPT="$script"
  _BASHUNIT_TAGS_MAP_FNS=()
  _BASHUNIT_TAGS_MAP_TAGS=()

  local count=0
  local fn tags

  while IFS=$'\t' read -r fn tags; do
    [ -z "$fn" ] && continue
    _BASHUNIT_TAGS_MAP_FNS[count]="$fn"
    _BASHUNIT_TAGS_MAP_TAGS[count]="$tags"
    count=$((count + 1))
  done < <(awk '
    # An uninitialised awk variable used as a subscript is the empty string,
    # not 0, so the first function would land in order[""] and be unreachable
    # from the numeric loop in END.
    BEGIN { n = 0 }
    # File-level tags: `# @tags a b` applies to every test in the file. Checked
    # before the singular rule and before the generic comment rule, and space
    # separated because it is a list rather than one tag per line.
    /^[[:space:]]*#[[:space:]]*@tags[[:space:]]/ {
      t = $0
      sub(/^[[:space:]]*#[[:space:]]*@tags[[:space:]]+/, "", t)
      sub(/[[:space:]]+$/, "", t)
      gsub(/[[:space:]]+/, ",", t)
      if (t != "") { filetags = (filetags == "" ? t : filetags "," t) }
      next
    }
    /^[[:space:]]*#[[:space:]]*@tag[[:space:]]/ {
      t = $0
      sub(/^[[:space:]]*#[[:space:]]*@tag[[:space:]]+/, "", t)
      tags = (tags == "" ? t : t "," tags)
      next
    }
    /^[[:space:]]*#/ { next }
    /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/ {
      fn = $0
      sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
      sub(/[[:space:]]*\(\).*/, "", fn)
      # Buffered rather than printed here so a `# @tags` line placed below the
      # functions still applies to them (single pass, order preserved).
      order[n] = fn
      own[n] = tags
      n++
      tags = ""
      next
    }
    { tags = "" }
    END {
      for (i = 0; i < n; i++) {
        combined = own[i]
        if (filetags != "") {
          combined = (combined == "" ? filetags : combined "," filetags)
        }
        if (combined == "") { continue }
        # Function tags come first (nearest-first, as before); a tag carried at
        # both levels is emitted once.
        count = split(combined, parts, ",")
        out = ""
        delete seen
        for (j = 1; j <= count; j++) {
          if (parts[j] == "" || (parts[j] in seen)) { continue }
          seen[parts[j]] = 1
          out = (out == "" ? parts[j] : out "," parts[j])
        }
        if (out != "") { printf "%s\t%s\n", order[i], out }
      }
    }
  ' "$script" 2>/dev/null)
}

function bashunit::helper::tags_for_function() {
  local function_name=$1
  local i=0
  local total=${#_BASHUNIT_TAGS_MAP_FNS[@]}
  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_TAGS_MAP_FNS[i]}" = "$function_name" ]; then
      _BASHUNIT_TAGS_OUT="${_BASHUNIT_TAGS_MAP_TAGS[i]}"
      return
    fi
    i=$((i + 1))
  done
  _BASHUNIT_TAGS_OUT=""
}

function bashunit::helper::_tags_contain() {
  local fn_tags="$1"
  local needle="$2"
  local IFS=','
  local tag
  for tag in $fn_tags; do
    if [ "$tag" = "$needle" ]; then
      return 0
    fi
  done
  return 1
}

function bashunit::helper::tag_expression_matches() {
  local fn_tags="$1"
  local rest="$2"

  local term negate more=true
  while [ "$more" = true ]; do
    case "$rest" in
    *"&&"*)
      term="${rest%%&&*}"
      rest="${rest#*&&}"
      ;;
    *)
      term="$rest"
      rest=""
      more=false
      ;;
    esac

    term="${term#"${term%%[![:space:]]*}"}"
    term="${term%"${term##*[![:space:]]}"}"

    negate=false
    case "$term" in
    '!'*)
      negate=true
      term="${term#!}"
      term="${term#"${term%%[![:space:]]*}"}"
      ;;
    esac

    if [ -z "$term" ]; then
      return 1
    fi

    if bashunit::helper::_tags_contain "$fn_tags" "$term"; then
      if [ "$negate" = true ]; then
        return 1
      fi
    elif [ "$negate" = false ]; then
      return 1
    fi
  done

  return 0
}

function bashunit::helper::function_matches_tags() {
  local fn_tags="$1"
  local include_tags="$2"
  local exclude_tags="$3"

  if [ -n "$exclude_tags" ]; then
    local IFS=','
    local etag
    for etag in $exclude_tags; do
      local check_tag
      for check_tag in $fn_tags; do
        if [ "$check_tag" = "$etag" ]; then
          return 1
        fi
      done
    done
  fi

  if [ -n "$include_tags" ]; then
    local IFS=','
    local expression
    for expression in $include_tags; do
      if bashunit::helper::tag_expression_matches "$fn_tags" "$expression"; then
        return 0
      fi
    done
    return 1
  fi

  return 0
}

# src/helper/discovery.sh

function bashunit::helper::check_duplicate_functions() {
  local script="$1"

  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi

  local duplicates
  duplicates=$(awk '
    /^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)[[:space:]]*\{/ {
      for (i = 1; i <= NF; i++) {
        if ($i ~ /^test[a-zA-Z_][a-zA-Z0-9_]*\(\)$/) {
          name = $i
          gsub(/\(\)/, "", name)
          if (++seen[name] == 2) {
            dup[name] = 1
          }
          # Recorded for every occurrence, the first included: the report needs
          # both definitions to be worth anything.
          if (lines[name] == "") {
            lines[name] = FNR
          } else {
            lines[name] = lines[name] ", " FNR
          }
          break
        }
      }
    }
    END {
      n = 0
      for (name in dup) {
        names[++n] = name
      }
      for (i = 2; i <= n; i++) {
        v = names[i]
        j = i - 1
        while (j >= 1 && names[j] > v) {
          names[j + 1] = names[j]
          j--
        }
        names[j + 1] = v
      }
      for (i = 1; i <= n; i++) {
        print names[i] "\t" lines[names[i]]
      }
    }
  ' "$script")
  if [ -n "$duplicates" ]; then

    local names=""
    local detail=""
    local dup_name dup_lines
    while IFS="$(printf '\t')" read -r dup_name dup_lines; do
      if [ -z "$dup_name" ]; then
        continue
      fi
      names="$names$dup_name
"
      detail="$detail$dup_name (lines $dup_lines)
"
    done <<EOF
$duplicates
EOF
    bashunit::state::set_duplicated_functions_merged \
      "$script" "${names%
}" "${detail%
}"
    return 1
  fi
  return 0
}

function bashunit::helper::name_matches_exclude_filter() {
  local __bu_prefix=$1
  local __bu_fn=$2

  if [ -z "${BASHUNIT_EXCLUDE_FILTER:-}" ]; then
    return 1
  fi

  local IFS=','
  local __bu_excl
  for __bu_excl in $BASHUNIT_EXCLUDE_FILTER; do
    __bu_excl=${__bu_excl/test_/}
    if [ -n "$__bu_excl" ]; then
      case "$__bu_fn" in ${__bu_prefix}_*${__bu_excl}*) return 0 ;; esac
    fi
  done

  return 1
}

function bashunit::helper::get_functions_to_run() {
  local prefix=$1
  local filter=${2/test_/}
  local function_names=$3

  local filtered_functions=""

  local fn
  for fn in $function_names; do
    local _fn_match=false
    case "$fn" in ${prefix}_*${filter}*) _fn_match=true ;; esac

    if [ "$_fn_match" = true ] && bashunit::helper::name_matches_exclude_filter "$prefix" "$fn"; then
      _fn_match=false
    fi
    if [ "$_fn_match" = true ]; then
      local _dup=false
      case "$filtered_functions" in *" $fn"*) _dup=true ;; esac
      if [ "$_dup" = true ]; then
        return 1
      fi
      filtered_functions="$filtered_functions $fn"
    fi
  done

  echo "${filtered_functions# }"
}

function bashunit::helper::find_files_recursive() {

  local path="${1%%/}"
  local pattern="${2:-*[tT]est.sh}"

  local alt_pattern=""
  case "$pattern" in
  *test.sh | *'[tT]est.sh') alt_pattern="${pattern%.sh}.bash" ;;
  esac

  local _has_glob=false
  case "$path" in *"*"*) _has_glob=true ;; esac
  if [ "$_has_glob" = true ]; then

    local _old_ifs=$IFS
    IFS=''
    local _roots

    _roots=($path)
    IFS=$_old_ifs
    if [ -n "$alt_pattern" ]; then
      find "${_roots[@]}" -type f \( -name "$pattern" -o -name "$alt_pattern" \) | sort -u
    else
      find "${_roots[@]}" -type f -name "$pattern" | sort -u
    fi
  elif [ -d "$path" ]; then
    if [ -n "$alt_pattern" ]; then
      find "$path" -type f \( -name "$pattern" -o -name "$alt_pattern" \) | sort -u
    else
      find "$path" -type f -name "$pattern" | sort -u
    fi
  else
    echo "$path"
  fi
}

_BASHUNIT_HELPER_VARNAME_OUT=""

function bashunit::helper::find_total_tests() {
  local filter=${1:-}
  shift || true

  _BASHUNIT_HELPER_TOTAL_TESTS_OUT=0
  if [ $# -eq 0 ]; then
    echo 0
    return
  fi

  local total_count=0
  local file

  for file in "$@"; do
    if [ ! -f "$file" ]; then
      continue
    fi

    bashunit::helper::build_provider_map "$file"

    local file_count
    file_count=$( (

      source "$file"
      local all_fn_names
      all_fn_names=$(compgen -A function)
      local filtered_functions
      filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$all_fn_names") || true

      local count=0
      local IFS=$' \t\n'
      if [ -n "$filtered_functions" ]; then
        local -a functions_to_run=()

        functions_to_run=($filtered_functions)
        local provider_data_count=0
        local fn_name line

        bashunit::helper::build_provider_map "$file"
        for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
          bashunit::helper::provider_for_function "$fn_name"
          if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
            count=$((count + 1))
            continue
          fi
          provider_data_count=0
          while IFS=" " read -r line; do
            [ -z "$line" ] && continue
            provider_data_count=$((provider_data_count + 1))
          done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

          if [ "$provider_data_count" -eq 0 ]; then
            count=$((count + 1))
          else
            count=$((count + provider_data_count))
          fi
        done
      fi

      echo "$count"
    ))

    total_count=$((total_count + file_count))
  done

  _BASHUNIT_HELPER_TOTAL_TESTS_OUT=$total_count
  echo "$total_count"
}

function bashunit::helper::load_test_files() {
  local filter="${1:-}"
  shift || true

  local has_files=$#

  if [ "$has_files" -eq 0 ]; then
    if [ -n "${BASHUNIT_DEFAULT_PATH:-}" ]; then
      bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH"
    fi
  else
    printf "%s\n" "$@"
  fi
}

function bashunit::helper::load_bench_files() {
  local filter="${1:-}"
  shift || true

  local has_files=$#

  if [ "$has_files" -eq 0 ]; then
    if [ -n "${BASHUNIT_DEFAULT_PATH:-}" ]; then
      bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH" '*[bB]ench.sh'
    fi
  else
    printf "%s\n" "$@"
  fi
}

function bashunit::helper::get_function_line_number() {
  local fn_name=$1

  local declaration
  declaration=$(
    shopt -s extdebug
    declare -F "$fn_name"
  )
  declaration="${declaration#* }"
  echo "${declaration%% *}"
}

function bashunit::helper::parse_file_path_filter() {
  local input="$1"
  local file_path=""
  local filter=""

  case "$input" in *"::"*)
    file_path="${input%%::*}"
    filter="${input#*::}"
    ;;
  *)

    local line_number="${input##*:}"
    local maybe_path="${input%:*}"
    case "$line_number" in
    '' | *[!0-9]*)
      file_path="$input"
      ;;
    *)
      if [ -n "$maybe_path" ] && [ "$maybe_path" != "$input" ]; then

        file_path="$maybe_path"
        filter="__line__:${line_number}"
      else
        file_path="$input"
      fi
      ;;
    esac
    ;;
  esac

  echo "$file_path"
  echo "$filter"
}

function bashunit::helper::find_function_at_line() {
  local file="$1"
  local target_line="$2"

  if [ ! -f "$file" ]; then
    return 1
  fi

  local best_match=""
  local best_line=0

  local line_num content
  while IFS=: read -r line_num content; do

    local fn_name=""
    local fn_pattern='^[[:space:]]*(function[[:space:]]+)?(test[a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\(\).*'
    fn_name=$(echo "$content" | sed -nE "s/$fn_pattern/\2/p")

    if [ -n "$fn_name" ] && [ "$line_num" -le "$target_line" ] && [ "$line_num" -gt "$best_line" ]; then
      best_match="$fn_name"
      best_line="$line_num"
    fi
  done < <(grep -n -E '^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)' "$file")

  echo "$best_match"
}

_BASHUNIT_TAGS_MAP_SCRIPT=""
_BASHUNIT_TAGS_MAP_FNS=()
_BASHUNIT_TAGS_MAP_TAGS=()
_BASHUNIT_TAGS_OUT=""

# src/cli/index.sh

# src/cli/upgrade.sh

function bashunit::upgrade::upgrade() {
  local install_dir="${BASHUNIT_INSTALL_DIR:-}"
  if [ -z "$install_dir" ]; then
    install_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  fi
  local target="$install_dir/bashunit"

  local latest_tag
  latest_tag="$(bashunit::helper::get_latest_tag)"

  if [ -z "$latest_tag" ]; then
    echo "Failed to resolve latest bashunit version. Check your internet connection and that 'git' is installed." >&2
    return 1
  fi

  if [ "$BASHUNIT_VERSION" = "$latest_tag" ]; then
    echo "> You are already on latest version"
    return 0
  fi

  echo "> Upgrading bashunit to latest version"

  local url="https://github.com/TypedDevs/bashunit/releases/download/$latest_tag/bashunit"
  local err_file
  err_file="$(mktemp 2>/dev/null || echo "/tmp/bashunit_upgrade_err.$$")"
  local download_status=0
  bashunit::io::download_to "$url" "$target" 2>"$err_file" || download_status=$?

  if [ "$download_status" -ne 0 ]; then
    echo "Failed to download bashunit $latest_tag from $url" >&2
    if [ -s "$err_file" ]; then
      echo "Reason:" >&2
      sed 's/^/  /' "$err_file" >&2
    fi
    rm -f "$err_file" "$target"
    return 1
  fi
  rm -f "$err_file"

  if [ ! -s "$target" ]; then
    echo "Failed to download bashunit $latest_tag from $url (empty file)" >&2
    rm -f "$target"
    return 1
  fi

  if ! chmod u+x "$target"; then
    echo "Failed to make $target executable" >&2
    return 1
  fi

  echo "> bashunit upgraded successfully to latest version $latest_tag"
}

# src/cli/watch.sh

function bashunit::watch::_command_exists() {
  command -v "$1" &>/dev/null
}

function bashunit::watch::is_available() {
  if bashunit::watch::_command_exists inotifywait; then
    echo "inotifywait"
  elif bashunit::watch::_command_exists fswatch; then
    echo "fswatch"
  else
    echo "polling"
  fi
}

function bashunit::watch::run() {
  local path="${1:-.}"
  shift

  local extra_args
  extra_args=("$@")

  local tool
  tool=$(bashunit::watch::is_available)

  if [ "$tool" = "polling" ]; then
    bashunit::watch::_print_polling_notice "$path"
  else
    printf "%sbashunit --watch%s  watching: %s\n\n" \
      "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" "$path"
  fi

  bashunit::watch::run_tests "$path" "${extra_args[@]+"${extra_args[@]}"}"

  while true; do
    bashunit::watch::wait_for_change "$tool" "$path"
    printf "\n%s[change detected — re-running tests]%s\n\n" \
      "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::watch::run_tests "$path" "${extra_args[@]+"${extra_args[@]}"}"
  done
}

function bashunit::watch::run_tests() {
  local path="$1"
  shift

  "$BASHUNIT_ROOT_DIR/bashunit" test "$path" "$@"
  return $?
}

function bashunit::watch::_print_polling_notice() {
  local path="$1"
  printf "%sbashunit --watch%s  polling: %s (every %ss)\n\n" \
    "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" \
    "$path" "${BASHUNIT_WATCH_INTERVAL:-2}"
  printf "  No 'inotifywait' or 'fswatch' found; using pure-shell polling.\n"
  printf "  Install one for instant triggers:\n"
  printf "    Linux:  sudo apt install inotify-tools\n"
  printf "    macOS:  brew install fswatch\n\n"
}

function bashunit::watch::_poll_changes() {
  local sentinel="$1"
  local path="$2"
  find "$path" -name '*.sh' -newer "$sentinel" -print 2>/dev/null
}

function bashunit::watch::wait_for_change() {
  local tool="$1"
  local path="$2"

  case "$tool" in
  polling)
    local sentinel
    sentinel="$(bashunit::temp_dir watch)/sentinel"
    while true; do
      : >"$sentinel"
      sleep "${BASHUNIT_WATCH_INTERVAL:-2}"
      if [ -n "$(bashunit::watch::_poll_changes "$sentinel" "$path")" ]; then
        return 0
      fi
    done
    ;;
  inotifywait)
    inotifywait \
      --quiet \
      --recursive \
      --event modify,create,delete,move \
      --include '.*\.sh$' \
      "$path" 2>/dev/null
    ;;
  fswatch)

    fswatch \
      --recursive \
      --include='.*\.sh$' \
      --exclude='.*' \
      --one-event \
      "$path" 2>/dev/null
    ;;
  esac
}

# src/cli/doc.sh

function bashunit::doc::get_embedded_docs() {
  cat <<'__BASHUNIT_DOCS_EOF__'
---
description: "Complete reference of bashunit assertions for testing bash scripts: assert equals, contains, matches, exit codes, files, arrays and more, with examples."
---

# Assertions

When creating tests, you'll need to verify your commands and functions.
We provide assertions for these checks.
Below is their documentation.

Assertions are called **unprefixed** — `assert_same`, not `bashunit::assert_same`. Every
other helper does take the `bashunit::` prefix; see [Globals](/globals).

Run `bashunit doc` to print this catalogue in your terminal, or `bashunit doc <filter>`
to narrow it (`bashunit doc json`).

Any assertion here also runs from the shell without a test file:
`bashunit assert contains "world" "hello world"`. See [Standalone](/standalone).

`assert_same`, `assert_equals`, `assert_not_same`, `assert_not_equals` and the numeric
comparisons take an optional last argument used as the failure label:
`assert_same "1" "2" "my custom label"` reports `✗ Failed: my custom label`.

The string assertions whose signature ends in `...` take a variadic value: every argument
after the first is joined with **newlines** and compared as one value. They accept no
trailing label override, so `assert_contains "zzz" "abc" "my label"` searches
`abc\nmy label` instead of relabelling the failure.

## Quick reference

| Group | Assertions |
|-------|------------|
| **Booleans and equality** | [assert_true](#assert-true) · [assert_false](#assert-false) · [assert_same](#assert-same) · [assert_not_same](#assert-not-same) · [assert_equals](#assert-equals) · [assert_not_equals](#assert-not-equals) |
| **Strings** | [assert_contains](#assert-contains) · [assert_not_contains](#assert-not-contains) · [assert_contains_ignore_case](#assert-contains-ignore-case) · [assert_matches](#assert-matches) · [assert_not_matches](#assert-not-matches) · [assert_string_starts_with](#assert-string-starts-with) · [assert_string_not_starts_with](#assert-string-not-starts-with) · [assert_string_ends_with](#assert-string-ends-with) · [assert_string_not_ends_with](#assert-string-not-ends-with) · [assert_string_matches_format](#assert-string-matches-format) · [assert_string_not_matches_format](#assert-string-not-matches-format) · [assert_empty](#assert-empty) · [assert_not_empty](#assert-not-empty) · [assert_line_count](#assert-line-count) |
| **Numbers** | [assert_less_than](#assert-less-than) · [assert_less_or_equal_than](#assert-less-or-equal-than) · [assert_greater_than](#assert-greater-than) · [assert_greater_or_equal_than](#assert-greater-or-equal-than) · [assert_between](#assert-between) · [assert_not_between](#assert-not-between) · [assert_within_delta](#assert-within-delta) |
| **Dates** | [assert_date_equals](#assert-date-equals) · [assert_date_before](#assert-date-before) · [assert_date_after](#assert-date-after) · [assert_date_within_range](#assert-date-within-range) · [assert_date_within_delta](#assert-date-within-delta) |
| **Exit codes and commands** | [assert_exit_code](#assert-exit-code) · [assert_successful_code](#assert-successful-code) · [assert_unsuccessful_code](#assert-unsuccessful-code) · [assert_general_error](#assert-general-error) · [assert_command_available](#assert-command-available) · [assert_command_not_found](#assert-command-not-found) · [assert_exec](#assert-exec) |
| **Files** | [assert_file_exists](#assert-file-exists) · [assert_file_not_exists](#assert-file-not-exists) · [assert_file_contains](#assert-file-contains) · [assert_file_not_contains](#assert-file-not-contains) · [assert_is_file](#assert-is-file) · [assert_is_file_empty](#assert-is-file-empty) · [assert_is_file_not_empty](#assert-is-file-not-empty) · [assert_is_file_readable](#assert-is-file-readable) · [assert_is_file_not_readable](#assert-is-file-not-readable) · [assert_is_file_writable](#assert-is-file-writable) · [assert_is_file_not_writable](#assert-is-file-not-writable) · [assert_is_file_executable](#assert-is-file-executable) · [assert_is_file_not_executable](#assert-is-file-not-executable) · [assert_is_symlink](#assert-is-symlink) · [assert_is_not_symlink](#assert-is-not-symlink) · [assert_symlink_to](#assert-symlink-to) · [assert_file_permissions](#assert-file-permissions) · [assert_files_equals](#assert-files-equals) · [assert_files_not_equals](#assert-files-not-equals) |
| **Directories** | [assert_directory_exists](#assert-directory-exists) · [assert_directory_not_exists](#assert-directory-not-exists) · [assert_is_directory](#assert-is-directory) · [assert_is_directory_empty](#assert-is-directory-empty) · [assert_is_directory_not_empty](#assert-is-directory-not-empty) · [assert_is_directory_readable](#assert-is-directory-readable) · [assert_is_directory_not_readable](#assert-is-directory-not-readable) · [assert_is_directory_writable](#assert-is-directory-writable) · [assert_is_directory_not_writable](#assert-is-directory-not-writable) |
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) · [assert_json_key_not_exists](#assert-json-key-not-exists) · [assert_json_length](#assert-json-length) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |

## assert_true
> `assert_true bool|function|command [args...]`

Pass a command with its arguments as separate arguments:

```bash
assert_true test -d /tmp
assert_true grep -q foo ./file
assert_true my_function
```

Arguments are passed through untouched, so a value containing a space survives.

A **single** argument keeps its older meaning: it is run as one command word, so
`assert_true "test -d /tmp"` looks for a command with that whole name and fails
with `unknown command`. Quote the whole thing only with an `eval` prefix —
`assert_true "eval test -d /tmp"` — or, better, drop the quotes and use the form
above.

A purpose-built assertion is usually clearer still — `assert_directory_exists`
rather than a hand-rolled `test -d`.

Reports an error **unless** the argument results in a truthy value: `true` or `0`, or a
command or function that exits `0`.

- [assert_false](#assert-false) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_true true
  assert_true 0
  assert_true "eval return 0"
  assert_true mock_true
}

function test_failure() {
  assert_true false
  assert_true 1
  assert_true "eval return 1"
  assert_true mock_false
}
```
```bash [globals.sh]
function mock_true() {
  return 0
}
function mock_false() {
  return 1
}
```
:::

## assert_false
> `assert_false bool|function|command [args...]`

Reports an error **unless** the argument results in a falsy value: `false` or `1`, or a
command or function that exits non-zero.

- [assert_true](#assert-true) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_false false
  assert_false 1
  assert_false "eval return 1"
  assert_false mock_false
}

function test_failure() {
  assert_false true
  assert_false 0
  assert_false "eval return 0"
  assert_false mock_true
}
```
```bash [globals.sh]
function mock_true() {
  return 0
}
function mock_false() {
  return 1
}
```
:::

## assert_same
> `assert_same "expected" "actual"`

Reports an error if the `expected` and `actual` are not the same - including special chars.

- [assert_not_same](#assert-not-same) is the inverse of this assertion and takes the same arguments.
- [assert_equals](#assert-equals) is similar but ignoring the special chars.

::: code-group
```bash [Example]
function test_success() {
  assert_same "foo" "foo"
}

function test_failure() {
  assert_same "foo" "bar"
}
```
:::

## assert_equals
> `assert_equals "expected" "actual"`

Reports an error if the two variables `expected` and `actual` are not equal ignoring the special chars like ANSI Escape Sequences (colors) and other special chars like tabs and new lines.

Those are *characters*, not escape sequences. A backslash followed by `t` is
two characters and stays two characters, so comparing it against a real tab
fails; only the tab itself is stripped.

- [assert_same](#assert-same) is similar but including special chars.

::: code-group
```bash [Example]
function test_success() {
  assert_equals "foo" $'\e[31mfoo'
}

function test_failure() {
  assert_equals "foo" $'\e[31mbar'
}
```
:::

## assert_contains
> `assert_contains "needle" "haystack"...`

Reports an error if `needle` is not a substring of `haystack`.

- [assert_not_contains](#assert-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_contains "foo" "foobar"
}

function test_failure() {
  assert_contains "baz" "foobar"
}
```

:::

## assert_contains_ignore_case
> `assert_contains_ignore_case "needle" "haystack"`

Reports an error if `needle` is not a substring of `haystack`.
Differences in casing are ignored when needle is searched for in haystack.

::: code-group
```bash [Example]
function test_success() {
  assert_contains_ignore_case "foo" "FooBar"
}
function test_failure() {
  assert_contains_ignore_case "baz" "FooBar"
}
```
:::

## assert_empty
> `assert_empty "actual"`

Reports an error if `actual` is not empty.

- [assert_not_empty](#assert-not-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_empty ""
}

function test_failure() {
  assert_empty "foo"
}
```
:::

## assert_matches
> `assert_matches "pattern" "value"...`

Reports an error if `value` does not match the regular expression `pattern`.

`pattern` is an ERE evaluated by `grep -E`. If it does not match as written, the value is
retried with every newline replaced by a space, so a single-line pattern can match across
lines: `assert_matches 'one two'` matches the two-line value `one\ntwo`.

- [assert_not_matches](#assert-not-matches) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_matches "^foo" "foobar"
}

function test_failure() {
  assert_matches "^bar" "foobar"
}
```
:::

::: tip Cost in a hot loop
This assertion runs `grep -E` in a subprocess for every call, so it is far more
expensive than a string comparison: measured here, 500 `assert_matches` take
~1.25 s against ~130 ms for 2000 `assert_same` — about **38x per call**.

That subprocess is deliberate. Bash 3.2 changed whether a quoted right-hand side
of `[[ =~ ]]` is a regex or a literal, so at bashunit's Bash 3.0 floor the same
pattern would match differently across supported versions.

It is irrelevant for ordinary suites. If you are asserting inside a large loop
and the pattern is a fixed substring or a glob, prefer
[assert_contains](#assert-contains).
:::

## assert_string_starts_with
> `assert_string_starts_with "needle" "haystack"...`

Reports an error if `haystack` does not starts with `needle`.

- [assert_string_not_starts_with](#assert-string-not-starts-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_starts_with "foo" "foobar"
}

function test_failure() {
  assert_string_starts_with "baz" "foobar"
}
```
:::

## assert_string_ends_with
> `assert_string_ends_with "needle" "haystack"...`

Reports an error if `haystack` does not ends with `needle`.

- [assert_string_not_ends_with](#assert-string-not-ends-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_ends_with "bar" "foobar"
}

function test_failure() {
  assert_string_ends_with "foo" "foobar"
}
```
:::

## assert_string_matches_format
> `assert_string_matches_format "format" "value"`

Reports an error if `value` does not match the `format` string. The format string uses PHPUnit-style placeholders:

| Placeholder | Matches |
|-------------|---------|
| `%d` | One or more digits |
| `%i` | Signed integer (e.g. `+1`, `-42`) |
| `%f` | Floating point number (e.g. `3.14`) |
| `%s` | One or more characters other than a space (tabs and newlines match) |
| `%x` | Hexadecimal (e.g. `ff00ab`) |
| `%e` | Scientific notation (e.g. `1.5e10`) |
| `%%` | Literal `%` character |

- [assert_string_not_matches_format](#assert-string-not-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_matches_format "%d items found" "42 items found"
  assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each"
}

function test_failure() {
  assert_string_matches_format "%d items" "hello world"
}
```
:::

## assert_line_count
> `assert_line_count "count" "haystack"...`

Reports an error if `haystack` does not contain `count` lines.

A literal `\n` (backslash followed by `n`) counts as a line break too, so
`assert_line_count 2 'one\ntwo'` passes even though the value holds no real newline.

::: code-group
```bash [Example]
function test_success() {
  local string="this is line one
this is line two
this is line three"

  assert_line_count 3 "$string"
}

function test_failure() {
  assert_line_count 2 "foobar"
}
```
:::

## assert_less_than
> `assert_less_than "expected" "actual"`

Reports an error if `actual` is not less than `expected`.

- [assert_greater_than](#assert-greater-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_less_than "999" "1"
}

function test_failure() {
  assert_less_than "1" "999"
}
```
:::

## assert_less_or_equal_than
> `assert_less_or_equal_than "expected" "actual"`

Reports an error if `actual` is not less than or equal to `expected`.

- [assert_greater_or_equal_than](#assert-greater-or-equal-than) is the counterpart of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_less_or_equal_than "999" "1"
}

function test_success_with_two_equal_numbers() {
  assert_less_or_equal_than "999" "999"
}

function test_failure() {
  assert_less_or_equal_than "1" "999"
}
```
:::

## assert_greater_than
> `assert_greater_than "expected" "actual"`

Reports an error if `actual` is not greater than `expected`.

- [assert_less_than](#assert-less-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_greater_than "1" "999"
}

function test_failure() {
  assert_greater_than "999" "1"
}
```
:::

## assert_greater_or_equal_than
> `assert_greater_or_equal_than "expected" "actual"`

Reports an error if `actual` is not greater than or equal to `expected`.

- [assert_less_or_equal_than](#assert-less-or-equal-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_greater_or_equal_than "1" "999"
}

function test_success_with_two_equal_numbers() {
  assert_greater_or_equal_than "999" "999"
}

function test_failure() {
  assert_greater_or_equal_than "999" "1"
}
```
:::

## assert_between
> `assert_between "min" "max" "actual"`

Reports an error if `actual` is outside the inclusive numeric range from `min` to `max`.
Integers, decimals, and negative values are supported. `min` must not be greater than `max`.

A non-numeric argument, or `min` greater than `max`, is a **usage error**: the assertion
returns 2 and the test is reported as an Error rather than a failure, with a message such
as `assert_between expects min <= max, got '500' and '100'`.

- [assert_not_between](#assert-not-between) is the exact negation and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_between "100" "500" "275"
  assert_between "0.1" "0.3" "0.2"
}

function test_failure() {
  assert_between "100" "500" "750"
}
```
:::

## assert_not_between
> `assert_not_between "min" "max" "actual"`

Reports an error if `actual` is inside the inclusive numeric range from `min` to `max`.
Integers, decimals, and negative values are supported. `min` must not be greater than `max`.

A non-numeric argument, or `min` greater than `max`, is a **usage error**: the assertion
returns 2 and the test is reported as an Error rather than a failure, with a message such
as `assert_between expects min <= max, got '500' and '100'`.

- [assert_between](#assert-between) is the exact negation and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_between "400" "499" "200"
}

function test_failure() {
  assert_not_between "400" "499" "404"
}
```
:::

## assert_within_delta
> `assert_within_delta "expected" "actual" "delta"`

Reports an error if `actual` is not within `delta` of `expected`
(i.e. `|actual - expected| > delta`). Supports floating-point values.
Useful for timing or measured values where exact equality is too strict.

The bound is inclusive, so `|actual - expected| == delta` passes. An operand that is not a
number, such as `1.2.3` or `5-3`, fails the assertion with `to all be numeric` instead of
being evaluated as an expression. A leading `+` is accepted.

::: code-group
```bash [Example]
function test_success() {
  assert_within_delta "3.14159" "3.14" "0.01"
}

function test_failure() {
  assert_within_delta "100" "105" "3"
}
```
:::

## assert_date_equals
> `assert_date_equals "expected" "actual"`

Reports an error if the two date values `expected` and `actual` are not equal.

Inputs are automatically converted to epoch seconds. Supported formats:
- Epoch seconds (integers): `1700000000`
- ISO 8601 date: `2023-11-14`
- ISO 8601 datetime: `2023-11-14T12:00:00`
- ISO 8601 datetime with UTC Z: `2023-11-14T12:00:00Z`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).

Anything else, including an empty string, fails the assertion with
`Expected '<value>' to be 'a valid date'` rather than being coerced to `0`. This applies to
all five date assertions.

::: code-group
```bash [Example]
function test_success() {
  local now
  now="$(date +%s)"

  assert_date_equals "$now" "$now"
}

function test_failure() {
  assert_date_equals "1700000000" "1600000000"
}
```
:::

## assert_date_before
> `assert_date_before "expected" "actual"`

Reports an error if `actual` is not before `expected` (i.e. `actual` must be less than `expected`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert-date-equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_before "1700000000" "1600000000"
}

function test_failure() {
  assert_date_before "1700000000" "1800000000"
}
```
:::

## assert_date_after
> `assert_date_after "expected" "actual"`

Reports an error if `actual` is not after `expected` (i.e. `actual` must be greater than `expected`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert-date-equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_after "1600000000" "1700000000"
}

function test_failure() {
  assert_date_after "1600000000" "1500000000"
}
```
:::

## assert_date_within_range
> `assert_date_within_range "from" "to" "actual"`

Reports an error if `actual` does not fall between `from` and `to` (inclusive).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert-date-equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_within_range "1600000000" "1800000000" "1700000000"
}

function test_failure() {
  assert_date_within_range "1600000000" "1800000000" "1900000000"
}
```
:::

## assert_date_within_delta
> `assert_date_within_delta "expected" "actual" "delta"`

Reports an error if `actual` is not within `delta` seconds of `expected`.

`delta` is required: omitting it produces a shell error reported as an Error, not an
assertion failure.

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert-date-equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  local now
  now="$(date +%s)"
  local five_seconds_later=$(( now + 5 ))

  assert_date_within_delta "$now" "$five_seconds_later" "10"
}

function test_failure() {
  assert_date_within_delta "1700000000" "1700000020" "5"
}
```
:::

## assert_exit_code
> `assert_exit_code "expected"`

Reports an error if the exit code of the last executed command is not equal to `expected`.

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a second parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 0`
:::

- [assert_successful_code](#assert-successful-code), [assert_unsuccessful_code](#assert-unsuccessful-code), [assert_general_error](#assert-general-error) and [assert_command_not_found](#assert-command-not-found)
are more semantic versions of this assertion, for which you don't need to specify an exit code.

::: code-group
```bash [Example]
function test_success_checking_previous_command() {
  function foo() {
    return 1
  }

  foo

  assert_exit_code "1"
}

function test_success_with_external_command() {
  touch /tmp/myfile

  assert_exit_code "0"
}

function test_failure() {
  function foo() {
    return 1
  }

  foo

  assert_exit_code "0"
}
```
:::

## assert_exec
> `assert_exec "command" [--exit <code>] [--stdout "text"] [--stderr "text"] [--stdout-contains "needle"] [--stdout-not-contains "needle"] [--stderr-contains "needle"] [--stderr-not-contains "needle"] [--stdin "input"]`

Runs `command` capturing its exit status, standard output and standard error and
checks all provided expectations. When `--exit` is omitted the expected exit
status defaults to `0`.

Use `--stdin` to feed input into interactive commands (e.g. commands using
`read`). Multiple answers can be passed by separating them with newlines.

Use `--stdout-contains` / `--stdout-not-contains` (and the `stderr-*` variants)
for substring matching when you don't want to assert against the full output.

Unrecognised arguments are silently dropped, so a mistyped flag such as
`--stdout-contain` checks nothing and the assertion passes on exit status alone. Extra
words after the command are dropped too: put arguments inside the command string,
`assert_exec "echo hello" --stdout "hello"`.

::: code-group
```bash [Example]
function sample() {
  echo "out"
  echo "err" >&2
  return 1
}

function test_success() {
  assert_exec sample --exit 1 --stdout "out" --stderr "err"
}

function test_failure() {
  assert_exec sample --exit 0 --stdout "out" --stderr "err"
}
```

```bash [Interactive]
function question() {
  local name lang
  read -r name
  read -r lang
  echo "Your name is $name and you prefer $lang."
}

function test_interactive_prompt() {
  assert_exec question \
    --stdin "Chemaclass"$'\n'"Phel-Lang"$'\n' \
    --stdout-contains "Your name is Chemaclass and you prefer Phel-Lang." \
    --stdout-not-contains "Delphi" \
    --exit 0
}
```
:::

## assert_arrays_equal
> `assert_arrays_equal "expected..." -- "actual..."`

Reports an error if the arrays have different lengths or any element differs at the same index.

Use `--` to separate the expected array from the actual array.

::: code-group
```bash [Example]
function test_success() {
  local expected=(foo bar baz)
  local actual=(foo bar baz)

  assert_arrays_equal "${expected[@]}" -- "${actual[@]}"
}

function test_failure() {
  local expected=(foo bar baz)
  local actual=(foo baz bar)

  assert_arrays_equal "${expected[@]}" -- "${actual[@]}"
}
```
:::

## assert_array_contains
> `assert_array_contains "needle" "haystack"`

Reports an error if `needle` is not found in `haystack`.

`needle` is matched as a **substring of the array joined with spaces**, not element by
element, so `assert_array_contains "oob" foobar baz` and
`assert_array_contains "foo bar" foo bar baz` both pass. Use
[assert_arrays_equal](#assert-arrays-equal) when you need exact element comparison.

- [assert_array_not_contains](#assert-array-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_contains "bar" "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_contains "foobar" "${haystack[@]}"
}
```
:::

## assert_array_length
> `assert_array_length "expected_length" "array"`

Reports an error if `array` does not have exactly `expected_length` elements.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_length 3 "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_length 2 "${haystack[@]}"
}
```
:::

## assert_successful_code
> `assert_successful_code`

Reports an error if the exit code of the last executed command is not successful (`0`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command"` (defaults to expecting exit code 0)
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 0
  }

  foo

  assert_successful_code
}

function test_success_with_external_command() {
  touch /tmp/myfile

  assert_successful_code
}

function test_failure() {
  function foo() {
    return 1
  }

  foo

  assert_successful_code
}
```
:::

::: warning Under `set -e`, pass the code as the third argument
A non-zero exit aborts the test before the assertion runs, so the code has to
be captured first — and capturing it sets `$?` to zero:

```bash
local code=0
some_command || code=$?

assert_successful_code            # reads $? — the assignment's 0
assert_successful_code "$code"    # the first argument is ignored, same result

assert_successful_code "" "" "$code"   # correct: read from the third argument
```

`assert_exit_code` takes a captured code the same way, but its first argument
is the *expected* code: `assert_exit_code 7 "" "$code"`.
:::


## assert_unsuccessful_code
> `assert_unsuccessful_code`

Reports an error if the exit code of the last executed command is not unsuccessful (non-zero).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 1`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 1
  }

  foo

  assert_unsuccessful_code
}

function test_success_with_failing_command() {
  ls /nonexistent_path 2>/dev/null

  assert_unsuccessful_code
}

function test_failure() {
  function foo() {
    return 0
  }

  foo

  assert_unsuccessful_code
}
```
:::

::: warning Under `set -e`, pass the code as the third argument
A non-zero exit aborts the test before the assertion runs, so the code has to
be captured first — and capturing it sets `$?` to zero:

```bash
local code=0
some_command || code=$?

assert_unsuccessful_code            # reads $? — the assignment's 0
assert_unsuccessful_code "$code"    # the first argument is ignored, same result

assert_unsuccessful_code "" "" "$code"   # correct: read from the third argument
```

`assert_exit_code` takes a captured code the same way, but its first argument
is the *expected* code: `assert_exit_code 7 "" "$code"`.
:::


## assert_general_error
> `assert_general_error`

Reports an error if the exit code of the last executed command is not a general error (`1`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 1`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 1
  }

  foo

  assert_general_error
}

function test_success_with_external_command() {
  grep "nonexistent" /dev/null

  assert_general_error
}

function test_failure() {
  function foo() {
    return 0
  }

  foo

  assert_general_error
}
```
:::

::: warning Under `set -e`, pass the code as the third argument
A non-zero exit aborts the test before the assertion runs, so the code has to
be captured first — and capturing it sets `$?` to zero:

```bash
local code=0
some_command || code=$?

assert_general_error            # reads $? — the assignment's 0
assert_general_error "$code"    # the first argument is ignored, same result

assert_general_error "" "" "$code"   # correct: read from the third argument
```

`assert_exit_code` takes a captured code the same way, but its first argument
is the *expected* code: `assert_exit_code 7 "" "$code"`.
:::


## assert_command_available
> `assert_command_available "command"`

Reports an error if `command` is not available.

Availability uses the same `command -v` check as the
[bashunit::is_command_available](/globals#bashunit-is-command-available) helper, so
external commands, shell builtins and shell functions are supported. The command
is only resolved; it is not executed.

::: code-group
```bash [Example]
function test_dependencies_are_installed() {
  assert_command_available bash
  assert_command_available jq
}

function test_shell_function_is_available() {
  function project_build() {
    make build
  }

  assert_command_available project_build
}
```
:::

## assert_command_not_found
> `assert_command_not_found`

Reports an error if the last executed command did not return a "command not found" exit code (`127`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "nonexistent_command" --exit 127`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_nonexistent_command() {
  nonexistent_command 2>/dev/null

  assert_command_not_found
}

function test_failure_with_existing_command() {
  ls > /dev/null 2>&1

  assert_command_not_found
}
```
:::

::: warning Under `set -e`, pass the code as the third argument
A non-zero exit aborts the test before the assertion runs, so the code has to
be captured first — and capturing it sets `$?` to zero:

```bash
local code=0
some_command || code=$?

assert_command_not_found            # reads $? — the assignment's 0
assert_command_not_found "$code"    # the first argument is ignored, same result

assert_command_not_found "" "" "$code"   # correct: read from the third argument
```

`assert_exit_code` takes a captured code the same way, but its first argument
is the *expected* code: `assert_exit_code 7 "" "$code"`.
:::


## assert_file_exists
> `assert_file_exists "file"`

Reports an error if `file` does not exists, or it is a directory.

- [assert_file_not_exists](#assert-file-not-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_file_exists "$file_path"
  rm "$file_path"
}

function test_failure() {
  local file_path="foo.txt"
  rm -f $file_path

  assert_file_exists "$file_path"
}
```
:::

## assert_file_contains
> `assert_file_contains "file" "search"`

Reports an error if `file` does not contain the search string.

`search` is matched **literally** (`grep -F`); regex metacharacters have no special meaning.

- [assert_file_not_contains](#assert-file-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_contains "$file" "content"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_contains "$file" "non existing"
}
```
:::

## assert_is_symlink
> `assert_is_symlink "path"`

Reports an error if `path` is not a symbolic link.

Every other filesystem assertion follows the link — `assert_is_file` and
`assert_file_exists` report on the *target*, so a link and the file it points at
look identical, and a dangling link reads as "does not exist". This is the
assertion that tells them apart, and it passes for a link whose target is gone.

::: code-group
```bash [Example]
function test_success() {
  ln -s /etc/hosts ./hosts_link

  assert_is_symlink "./hosts_link"
}
```
:::

## assert_is_not_symlink
> `assert_is_not_symlink "path"`

Reports an error if `path` is a symbolic link.

## assert_symlink_to
> `assert_symlink_to "expected_target" "path"`

Reports an error if `path` is not a symbolic link, or if it points somewhere
other than `expected_target`.

The target is compared **as written**, via `readlink`, not fully resolved: that
is what the test author wrote, and `readlink -f` is GNU-only. A relative link
therefore compares as the relative string it is.

::: code-group
```bash [Example]
function test_success() {
  ln -s ./releases/42 ./current

  assert_symlink_to "./releases/42" "./current"
}
```
:::

## assert_file_permissions
> `assert_file_permissions "mode" "file"`

Reports an error if `file` does not have the expected octal permission `mode`
(e.g. `644`, `0755`). A leading zero is optional (`0755` and `755` are equal).
Works on both Linux (GNU `stat`) and macOS (BSD `stat`).

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  touch "$file"
  chmod 600 "$file"

  assert_file_permissions "600" "$file"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  touch "$file"
  chmod 644 "$file"

  assert_file_permissions "600" "$file"
}
```
:::

## assert_is_file
> `assert_is_file "file"`

Reports an error if `file` is not a file.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_is_file "$file_path"
  rm "$file_path"
}

function test_failure() {
  local dir_path="bar"
  mkdir "$dir_path"

  assert_is_file "$dir_path"
  rmdir "$dir_path"
}
```
:::

## assert_is_file_empty
> `assert_is_file_empty "file"`

Reports an error if `file` is not empty.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_is_file_empty "$file_path"
  rm "$file_path"
}

function test_failure() {
  local file_path="foo.txt"
  echo "bar" > "$file_path"

  assert_is_file_empty "$file_path"
  rm "$file_path"
}
```
:::


## assert_is_file_not_empty
> `assert_is_file_not_empty "file"`

Reports an error if `file` is empty. A file holding a single newline counts as
not empty.

::: code-group
```bash [Example]
function test_success() {
  generate_report > report.txt

  assert_is_file_not_empty "report.txt"
}
```
:::

## assert_is_file_readable
> `assert_is_file_readable "file"`

Reports an error if `file` is not readable.

The failure says which of the three things went wrong: the path does not exist,
it exists but is not a file, or it is a file the current user cannot read. The
same applies to every assertion below.

::: code-group
```bash [Example]
function test_success() {
  assert_is_file_readable "/etc/hosts"
}
```
```[Output]
✗ Failed: Success
    Expected '/tmp/nope'
    to be readable
    but does not exist
```
:::

## assert_is_file_not_readable
> `assert_is_file_not_readable "file"`

Reports an error if `file` is readable.

::: code-group
```bash [Example]
function test_success() {
  chmod 000 "$secret"

  assert_is_file_not_readable "$secret"
}
```
:::

::: warning
Running as root bypasses the permission bits, so this assertion passes for
nothing under `sudo` or in a root container. Skip such a test there:
`bashunit::skip_if "[ \"$(id -u)\" -eq 0 ]" "root reads anything"`.
:::

## assert_is_file_writable
> `assert_is_file_writable "file"`

Reports an error if `file` is not writable.

::: code-group
```bash [Example]
function test_success() {
  assert_is_file_writable "$log_file"
}
```
:::

## assert_is_file_not_writable
> `assert_is_file_not_writable "file"`

Reports an error if `file` is writable.

::: code-group
```bash [Example]
function test_success() {
  chmod 444 "$config"

  assert_is_file_not_writable "$config"
}
```
:::

## assert_is_file_executable
> `assert_is_file_executable "file"`

Reports an error if `file` is not executable — the thing a shell project most
often wants to assert about a file it generated.

::: code-group
```bash [Example]
function test_success() {
  ./build.sh

  assert_is_file_executable "bin/tool"
}
```
```[Output]
✗ Failed: Success
    Expected 'bin/tool'
    to be executable
    but is not executable
```
:::

## assert_is_file_not_executable
> `assert_is_file_not_executable "file"`

Reports an error if `file` is executable.

::: code-group
```bash [Example]
function test_success() {
  assert_is_file_not_executable "README.md"
}
```
:::
## assert_directory_exists
> `assert_directory_exists "directory"`

Reports an error if `directory` does not exist.

- [assert_directory_not_exists](#assert-directory-not-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_directory_exists "$directory"
}

function test_failure() {
  local directory="/nonexistent_directory"

  assert_directory_exists "$directory"
}
```
:::

## assert_is_directory
> `assert_is_directory "directory"`

Reports an error if `directory` is not a directory.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_is_directory "$directory"
}

function test_failure() {
  local file="/etc/hosts"

  assert_is_directory "$file"
}
```
:::

## assert_is_directory_empty
> `assert_is_directory_empty "directory"`

Reports an error if `directory` is not an empty directory.

- [assert_is_directory_not_empty](#assert-is-directory-not-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory
  directory="$(bashunit::temp_dir)"

  assert_is_directory_empty "$directory"
}

function test_failure() {
  local directory="/etc"

  assert_is_directory_empty "$directory"
}
```
:::

## assert_is_directory_readable
> `assert_is_directory_readable "directory"`

Reports an error if `directory` is not a readable directory.

- [assert_is_directory_not_readable](#assert-is-directory-not-readable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_is_directory_readable "$directory"
}

function test_failure() {
  local directory
  directory="$(bashunit::temp_dir)"
  chmod -r "$directory"

  assert_is_directory_readable "$directory"
}
```
:::

## assert_is_directory_writable
> `assert_is_directory_writable "directory"`

Reports an error if `directory` is not a writable directory.

- [assert_is_directory_not_writable](#assert-is-directory-not-writable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/tmp"

  assert_is_directory_writable "$directory"
}

function test_failure() {
  local directory
  directory="$(bashunit::temp_dir)"
  chmod -w "$directory"

  assert_is_directory_writable "$directory"
}
```
:::

## assert_files_equals
> `assert_files_equals "expected" "actual"`

Reports an error if `expected` and `actual` are not equals.

- [assert_files_not_equals](#assert-files-not-equals) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "file content" > "$actual"

  assert_files_equals "$expected" "$actual"
}

function test_failure() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "different content" > "$actual"

  assert_files_equals "$expected" "$actual"
}
```
```[Output]
✓ Passed: Success
✗ Failed: Failure
    Expected '/tmp/file1.txt'
    Compared '/tmp/file2.txt'
    Diff '@@ -1 +1 @@
-file content
+different content'
```
:::

## assert_not_equals
> `assert_not_equals "expected" "actual"`

Reports an error if the two variables `expected` and `actual` are equal ignoring the special chars like ANSI Escape Sequences (colors) and other special chars like tabs and new lines.

- [assert_equals](#assert-equals) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_equals "foo" "bar"
}

function test_failure() {
  assert_not_equals "foo" "foo"
}
```
:::

## assert_not_same
> `assert_not_same "expected" "actual"`

Reports an error if the two variables `expected` and `actual` are the same value.

- [assert_same](#assert-same) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_same "foo" "bar"
}

function test_failure() {
  assert_not_same "foo" "foo"
}
```
:::

## assert_not_contains
> `assert_not_contains "needle" "haystack"...`

Reports an error if `needle` is a substring of `haystack`.

- [assert_contains](#assert-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_contains "baz" "foobar"
}

function test_failure() {
  assert_not_contains "foo" "foobar"
}
```
:::

## assert_string_not_starts_with
> `assert_string_not_starts_with "needle" "haystack"...`

Reports an error if `haystack` does starts with `needle`.

- [assert_string_starts_with](#assert-string-starts-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_starts_with "bar" "foobar"
}

function test_failure() {
  assert_string_not_starts_with "foo" "foobar"
}
```
:::

## assert_string_not_ends_with
> `assert_string_not_ends_with "needle" "haystack"...`

Reports an error if `haystack` does ends with `needle`.

- [assert_string_ends_with](#assert-string-ends-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_ends_with "foo" "foobar"
}

function test_failure() {
  assert_string_not_ends_with "bar" "foobar"
}
```
:::

## assert_not_empty
> `assert_not_empty "actual"`

Reports an error if `actual` is empty.

- [assert_empty](#assert-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_empty "foo"
}

function test_failure() {
  assert_not_empty ""
}
```
:::

## assert_not_matches
> `assert_not_matches "pattern" "value"...`

Reports an error if `value` matches the regular expression `pattern`.

- [assert_matches](#assert-matches) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_matches "foo$" "foobar"
}

function test_failure() {
  assert_not_matches "bar$" "foobar"
}
```
:::

## assert_string_not_matches_format
> `assert_string_not_matches_format "format" "value"`

Reports an error if `value` matches the `format` string. See [assert_string_matches_format](#assert-string-matches-format) for supported placeholders.

- [assert_string_matches_format](#assert-string-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_matches_format "%d items" "hello world"
}

function test_failure() {
  assert_string_not_matches_format "%d items" "42 items"
}
```
:::

## assert_array_not_contains
> `assert_array_not_contains "needle" "haystack"`

Reports an error if `needle` is found in `haystack`.

`needle` is matched as a **substring of the array joined with spaces**, not element by
element, so `assert_array_not_contains "foo bar" foo bar` fails even though no single
element is `foo bar`.

- [assert_array_contains](#assert-array-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_not_contains "foobar" "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_not_contains "baz" "${haystack[@]}"
}
```
:::

## assert_file_not_exists
> `assert_file_not_exists "file"`

Reports an error if `file` does exists.

- [assert_file_exists](#assert-file-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"
  rm "$file_path"

  assert_file_not_exists "$file_path"
}

function test_failed() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_file_not_exists "$file_path"
  rm "$file_path"
}
```
:::

## assert_file_not_contains
> `assert_file_not_contains "file" "search"`

Reports an error if `file` contains the search string.

`search` is matched as a **basic regular expression** (`grep`), unlike
[assert_file_contains](#assert-file-contains) which matches literally, so
`assert_file_not_contains file 'a.c'` fails on a file containing `abc`.

- [assert_file_contains](#assert-file-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_not_contains "$file" "non existing"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_not_contains "$file" "content"
}
```
:::

## assert_directory_not_exists
> `assert_directory_not_exists "directory"`

Reports an error if `directory` exists.

- [assert_directory_exists](#assert-directory-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/nonexistent_directory"

  assert_directory_not_exists "$directory"
}

function test_failure() {
  local directory="/var"

  assert_directory_not_exists "$directory"
}
```
:::

## assert_is_directory_not_empty
> `assert_is_directory_not_empty "directory"`

Reports an error if `directory` is empty.

- [assert_is_directory_empty](#assert-is-directory-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/etc"

  assert_is_directory_not_empty "$directory"
}

function test_failure() {
  local directory
  directory="$(bashunit::temp_dir)"

  assert_is_directory_not_empty "$directory"
}
```
:::

## assert_is_directory_not_readable
> `assert_is_directory_not_readable "directory"`

Reports an error if `directory` is readable.

- [assert_is_directory_readable](#assert-is-directory-readable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory
  directory="$(bashunit::temp_dir)"
  chmod -r "$directory"

  assert_is_directory_not_readable "$directory"
}

function test_failure() {
  local directory="/var"

  assert_is_directory_not_readable "$directory"
}
```
:::

## assert_is_directory_not_writable
> `assert_is_directory_not_writable "directory"`

Reports an error if `directory` is writable.

- [assert_is_directory_writable](#assert-is-directory-writable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory
  directory="$(bashunit::temp_dir)"
  chmod -w "$directory"

  assert_is_directory_not_writable "$directory"
}

function test_failure() {
  local directory="/tmp"

  assert_is_directory_not_writable "$directory"
}
```
:::


## assert_files_not_equals
> `assert_files_not_equals "expected" "actual"`

Reports an error if `expected` and `actual` have the same contents.

- [assert_files_equals](#assert-files-equals) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "different content" > "$actual"

  assert_files_not_equals "$expected" "$actual"
}

function test_failure() {

  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "file content" > "$actual"

  assert_files_not_equals "$expected" "$actual"
}
```
```[Output]
✓ Passed: Success
✗ Failed: Failure
    Expected '/tmp/file1.txt'
    Compared '/tmp/file2.txt'
    Diff 'Files are equals'
```
:::

## assert_json_key_exists
> `assert_json_key_exists "key" "json"`

Reports an error if `key` does not exist in the JSON string. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

A key whose value is `null` or `false` is reported as missing, because `jq -e` treats both
as absent. `0` and `""` are fine. To assert a false or null value, use
`assert_json_contains ".flag" "false" "$json"`.

::: code-group
```bash [Example]
function test_success() {
  assert_json_key_exists ".name" '{"name":"bashunit","version":"1.0"}'
  assert_json_key_exists ".data.id" '{"data":{"id":42}}'
}

function test_failure() {
  assert_json_key_exists ".missing" '{"name":"bashunit"}'
}
```
:::

## assert_json_contains
> `assert_json_contains "key" "expected" "json"`

Reports an error if `key` does not exist in the JSON string or its value does not equal `expected`. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

A key whose value is `null` or `false` is reported as missing, the same guard
[assert_json_key_exists](#assert-json-key-exists) uses.

::: code-group
```bash [Example]
function test_success() {
  assert_json_contains ".name" "bashunit" '{"name":"bashunit","version":"1.0"}'
  assert_json_contains ".count" "42" '{"count":42}'
}

function test_failure() {
  assert_json_contains ".name" "other" '{"name":"bashunit"}'
  assert_json_contains ".missing" "value" '{"name":"bashunit"}'
}
```
:::

## assert_json_key_not_exists
> `assert_json_key_not_exists "key" "json"`

Reports an error if `key` exists in the JSON string. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

A key with a `null` value still exists and makes this assertion fail. Invalid JSON also fails the assertion.

::: code-group
```bash [Example]
function test_success() {
  assert_json_key_not_exists ".user.password" '{"user":{"name":"bashunit"}}'
}

function test_failure() {
  assert_json_key_not_exists ".user.password" '{"user":{"password":null}}'
}
```
:::

## assert_json_equals
> `assert_json_equals "expected" "actual"`

Reports an error if the two JSON strings are not structurally equal. Key order is ignored. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
  assert_json_equals '{"b":2,"a":1}' '{"a":1,"b":2}'
}

function test_failure() {
  assert_json_equals '{"a":1}' '{"a":2}'
}
```
:::

## assert_json_length
> `assert_json_length "expected" "key" "json"`

Reports an error if the array, object, or string at `key` does not have the expected length. Arrays count elements, objects count key-value pairs, and strings count Unicode codepoints, following `jq`'s `length` behavior. A missing path, unsupported value type, invalid JSON, or non-numeric expected length fails instead of being compared as empty or zero. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
  assert_json_length 3 ".items" '{"items":[1,2,3]}'
  assert_json_length 2 ".metadata" '{"metadata":{"page":1,"total":3}}'
}

function test_failure() {
  assert_json_length 2 ".items" '{"items":[1,2,3]}'
  assert_json_length 0 ".missing" '{"items":[]}'
}
```
:::

## assert_duration
> `assert_duration "command" threshold_ms`

Reports an error if `command` takes longer than `threshold_ms` milliseconds to execute. Uses the framework's portable clock internally.

Requires `awk`, plus `bc` or `awk` for the arithmetic. Unlike the JSON assertions, the
duration assertions do **not** skip when the tool is missing: the test is reported as an
Error.

::: code-group
```bash [Example]
function test_success() {
  assert_duration "echo hello" 500
}

function test_failure() {
  assert_duration "sleep 2" 1000
}
```
:::

## assert_duration_less_than
> `assert_duration_less_than "command" threshold_ms`

Reports an error if `command` takes `threshold_ms` milliseconds or more to execute. Stricter than [assert_duration](#assert-duration) which allows equal values.

::: code-group
```bash [Example]
function test_success() {
  assert_duration_less_than "echo hello" 500
}

function test_failure() {
  assert_duration_less_than "sleep 2" 1000
}
```
:::

## assert_duration_greater_than
> `assert_duration_greater_than "command" threshold_ms`

Reports an error if `command` completes in `threshold_ms` milliseconds or less. Useful for verifying that a command takes at least a minimum amount of time.

::: code-group
```bash [Example]
function test_success() {
  assert_duration_greater_than "sleep 1" 500
}

function test_failure() {
  assert_duration_greater_than "echo hello" 5000
}
```
:::

## assert_match_snapshot
> `assert_match_snapshot "actual" ["snapshot_file"]`

Reports an error if `actual` differs from the stored snapshot. On the first run no snapshot exists, so one is written from `actual` and the assertion passes — review and commit that file.

Pass `snapshot_file` to share one snapshot between tests; by default each test gets its own, named after the test function.

::: warning A `@data_provider` test shares one snapshot
The filename comes from the test **function**, and a provider runs that function
once per value — so every value compares against the same file. The first value
to run creates it; the rest fail with `Expected to match the snapshot` even
though nothing is wrong with them:

```bash
function provide_values() { echo "alpha"; echo "beta"; }

# @data_provider provide_values
function test_shared() {
  assert_match_snapshot "value is $1"        # one file for both values
}

# @data_provider provide_values
function test_per_value() {
  assert_match_named_snapshot "$1" "value is $1"   # one file per value
}
```

Use [assert_match_named_snapshot](#assert-match-named-snapshot) with the value
as the name to give each data set its own.
:::

See [Snapshots](/snapshots) for the full workflow, including how to update a snapshot after an intentional change.

::: code-group
```bash [Example]
function test_success() {
  assert_match_snapshot "$(./bin/render --help)"
}

function test_failure() {
  assert_match_snapshot "output that no longer matches the stored snapshot"
}
```
:::

## assert_match_snapshot_ignore_colors
> `assert_match_snapshot_ignore_colors "actual" ["snapshot_file"]`

Same as [assert_match_snapshot](#assert-match-snapshot), but strips ANSI escape sequences from `actual` before comparing. Use it for commands whose colouring depends on the terminal.

::: code-group
```bash [Example]
function test_success() {
  assert_match_snapshot_ignore_colors "$(./bin/render --help)"
}

function test_failure() {
  assert_match_snapshot_ignore_colors "output that no longer matches the stored snapshot"
}
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
  assert_match_named_snapshot "compact" "$(./bin/render --compact)"
  assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
  assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
  assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Reports an error if the spied `command` was never called. Requires `bashunit::spy command` first — see [Test doubles](/test-doubles).

Every `assert_have_been_called*` and `assert_not_called` fails with
`was never registered as a spy` when the name was never passed to `bashunit::spy`,
including `assert_not_called`, which does **not** pass for an unspied name. Spies are
cleared between tests, so spy inside the test that asserts on it.

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_user

  assert_have_been_called send_email
}

function test_failure() {
  bashunit::spy send_email

  assert_have_been_called send_email
}
```
:::

## assert_have_been_called_with
> `assert_have_been_called_with "command" "expected_args" [nth]`

Reports an error if the spied `command` was not called with `expected_args`. Checks the **last** call unless a trailing all-digits `nth` selects a specific one; the failure names the call it compared.
Because `nth` is detected as a trailing all-digits argument, an expectation whose last
argument is a number is read as the selector: `assert_have_been_called_with git commit -m 5`
compares `commit -m` against call 5. Pass the expectation as one quoted string,
`assert_have_been_called_with git "commit -m 5"`, or use
[assert_have_been_called_with_args](#assert-have-been-called-with-args), which has no `nth`. To match any call, use [assert_have_been_called_with_any](#assert-have-been-called-with-any).

Note the argument order: the spy comes first here, but *second* in [assert_have_been_called_times](#assert-have-been-called-times).

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_user "a@b.c"

  assert_have_been_called_with send_email "--to a@b.c"
}

function test_failure() {
  bashunit::spy send_email
  notify_user "a@b.c"

  assert_have_been_called_with send_email "--to nobody@example.com"
}
```
:::

## assert_have_been_called_with_any
> `assert_have_been_called_with_any "command" "expected_args"`

Reports an error if no recorded call to the spied `command` received `expected_args`. Where [assert_have_been_called_with](#assert-have-been-called-with) compares a single call — the last one, or the one at `nth` — this one scans them all, so the assertion does not break when an unrelated call is added after it.

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_with_any send_email "--to a@b.c"
}

function test_failure() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_with_any send_email "--to nobody@example.com"
}
```
:::

## assert_have_been_called_with_args
> `assert_have_been_called_with_args "command" "expected_arg"...`

Reports an error if the **last** call to the spied `command` did not receive exactly these arguments. Unlike [assert_have_been_called_with](#assert-have-been-called-with), the arguments are compared one by one, so `cmd "a b"` does not match `cmd a b`. Use it whenever an argument may contain spaces, such as a path.

There is no `nth` parameter: a trailing number would be indistinguishable from a numeric argument.

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy touch
  create_report "/tmp/my reports"

  assert_have_been_called_with_args touch "/tmp/my reports/out.txt"
}

function test_failure() {
  bashunit::spy touch
  create_report "/tmp/my reports"

  assert_have_been_called_with_args touch "/tmp/my" "reports/out.txt"
}
```
:::

## assert_have_been_called_times
> `assert_have_been_called_times "expected_count" "command"`

Reports an error if the spied `command` was not called exactly `expected_count` times. The count comes **first**, the spy second.

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_times 2 send_email
}

function test_failure() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_times 1 send_email
}
```
:::

## assert_have_been_called_nth_with
> `assert_have_been_called_nth_with "nth" "command" "expected_args"`

Reports an error if call number `nth` of the spied `command` did not receive `expected_args`. Calls are numbered from 1.

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_nth_with 1 send_email "--to a@b.c"
}

function test_failure() {
  bashunit::spy send_email
  notify_all "a@b.c" "d@e.f"

  assert_have_been_called_nth_with 1 send_email "--to d@e.f"
}
```
:::

## assert_not_called
> `assert_not_called "command"`

Reports an error if the spied `command` was called at all. The inverse of [assert_have_been_called](#assert-have-been-called).

::: code-group
```bash [Example]
function test_success() {
  bashunit::spy send_email
  notify_user --dry-run

  assert_not_called send_email
}

function test_failure() {
  bashunit::spy send_email
  notify_user

  assert_not_called send_email
}
```
:::

## bashunit::fail
> `bashunit::fail "failure message"`

Unambiguously reports an error message. Useful for reporting specific message
when testing situations not covered by any `assert_*` functions.

::: code-group
```bash [Example]
function test_success() {
  if [ "$(date +%-H)" -gt 25 ]; then
    bashunit::fail "Something is very wrong with your clock"
  fi
}
function test_failure() {
  if [ "$(date +%-H)" -lt 25 ]; then
    bashunit::fail "This test will always fail"
  fi
}
```
:::

## assert_assertion_passes
> `assert_assertion_passes <assertion> [args...]`

Reports an error unless the given assertion reports a success. Use it to test
your own [custom assertions](/custom-asserts).

The inner assertion runs isolated: its verdict is never added to the run totals,
its failure output never reaches the console, and it cannot trip the
stop-on-failure guard for the rest of your test. Exactly one assertion is
counted — this one.

::: code-group
```bash [Example]
function test_success() {
  assert_assertion_passes assert_positive_number 1
}
function test_failure() {
  assert_assertion_passes assert_positive_number 0
}
```
:::

## assert_assertion_fails
> `assert_assertion_fails <assertion> [args...]`

Reports an error unless the given assertion reports a failure. An assertion that
counts nothing at all also fails this check.

The message the inner assertion produced is left in
`$_BASHUNIT_ASSERT_INNER_OUTPUT_OUT`, colour-stripped and flattened to one line,
so you can also assert on what it did *not* say.

::: code-group
```bash [Example]
function test_success() {
  assert_assertion_fails assert_positive_number 0
}
function test_failure() {
  assert_assertion_fails assert_positive_number 1
}
```
:::

## assert_assertion_fails_with
> `assert_assertion_fails_with <expected_message> <assertion> [args...]`

Reports an error unless the given assertion fails **and** its failure message
contains `expected_message`. This is how a custom assertion's output contract
gets tested without rebuilding the expected string from `console_results`.

::: code-group
```bash [Example]
function test_success() {
  assert_assertion_fails_with "positive number" assert_positive_number 0
}
function test_failure() {
  assert_assertion_fails_with "negative number" assert_positive_number 0
}
```
:::

## Related

- [Custom asserts](/custom-asserts) — build your own domain-specific assertions
- [Test doubles](/test-doubles) — mocks and spies for isolated tests
- [Data providers](/data-providers) — run the same assertions over many inputs
- [Globals](/globals) — `bashunit::` helper functions
- [Standalone](/standalone) — run these assertions straight from the command line
__BASHUNIT_DOCS_EOF__
}

function bashunit::doc::print_asserts() {
  local filter="${1:-}"

  bashunit::doc::get_embedded_docs | awk -v filter="$filter" '
    {
      if ($0 ~ /^## /) {
        # Heading word: the leading [A-Za-z0-9_]* run after "## ". Only
        # assert*/bashunit* headings are doc entries; prose headings like
        # "## Related" fall through and are treated as regular content.
        fn = substr($0, 4)
        sub(/[^A-Za-z0-9_].*$/, "", fn)
        if (fn ~ /^(assert|bashunit)/) {
          if (filter == "" || index(fn, filter) > 0) {
            should_print = 1
            print $0
            doc = ""
          } else {
            should_print = 0
          }
          next
        }
      }

      if (should_print) {
        if ($0 ~ /^```/) {
          print "--------------"
          print doc
          should_print = 0
          next
        }
        if ($0 ~ /^::: code-group/) next

        # Remove markdown link brackets and anchor tags. The bracket class
        # uses the POSIX []][ idiom: busybox awk (Alpine) rejects
        # backslash-escaped brackets inside a bracket expression.
        line = $0
        gsub(/[][]/, "", line)
        gsub(/ *\(#[-a-z0-9]+\)/, "", line)
        doc = doc line "\n"
      }
    }
  '
}

_BASHUNIT_DOC_CUSTOM_FNS_OUT=""

function bashunit::doc::custom_fns_to_slot() {
  local known="$1"

  local -a found
  found=()
  local count=0
  local fn

  for fn in $(compgen -A function assert_ 2>/dev/null); do
    case "
$known
" in
    *"
$fn
"*) continue ;;
    esac

    local i=$count
    while [ "$i" -gt 0 ] && [ "${found[$((i - 1))]}" \> "$fn" ]; do
      found[i]=${found[$((i - 1))]}
      i=$((i - 1))
    done
    found[i]=$fn
    count=$((count + 1))
  done

  local out=""
  local j=0
  while [ "$j" -lt "$count" ]; do
    out="$out${found[j]}"$'\n'
    j=$((j + 1))
  done

  _BASHUNIT_DOC_CUSTOM_FNS_OUT="${out%$'\n'}"
}

function bashunit::doc::print_fn_comment() {
  local fn="$1"

  local info
  info="$(
    shopt -s extdebug
    declare -F "$fn"
  )"

  local rest="${info#* }"
  local def_line="${rest%% *}"
  local file="${rest#* }"

  case "$def_line" in
  '' | *[!0-9]*) return 0 ;;
  esac
  [ -n "$file" ] && [ -f "$file" ] || return 0

  local -a lines
  lines=()
  local count=0
  local line
  while IFS= read -r line || [ -n "$line" ]; do
    lines[count]="$line"
    count=$((count + 1))
  done <"$file"

  local first=$((def_line - 1))
  local i=$((first - 1))
  while [ "$i" -ge 0 ]; do
    case "${lines[i]:-}" in
    '#'*) i=$((i - 1)) ;;
    *) break ;;
    esac
  done

  local j=$((i + 1))
  while [ "$j" -lt "$first" ]; do
    line="${lines[j]:-}"

    line="${line#\#}"
    line="${line#\#}"
    line="${line# }"
    printf '%s\n' "$line"
    j=$((j + 1))
  done
}

function bashunit::doc::print_custom_asserts() {
  local filter="${1:-}"
  local printed=1
  local fn

  for fn in $_BASHUNIT_DOC_CUSTOM_FNS_OUT; do
    if [ -n "$filter" ]; then
      case "$fn" in
      *"$filter"*) ;;
      *) continue ;;
      esac
    fi

    printf '## %s\n' "$fn"
    printf -- '--------------\n'
    bashunit::doc::print_fn_comment "$fn"
    printf '\n'
    printed=0
  done

  return $printed
}

# src/cli/init.sh

function bashunit::init::project() {
  local tests_dir="${1:-$BASHUNIT_DEFAULT_PATH}"
  mkdir -p "$tests_dir"

  local bootstrap_file="$tests_dir/bootstrap.sh"
  if [ ! -f "$bootstrap_file" ]; then
    cat >"$bootstrap_file" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
# Place your common test setup here
SH
    chmod +x "$bootstrap_file"
    echo "> Created $bootstrap_file"
  fi

  local example_test="$tests_dir/example_test.sh"
  if [ ! -f "$example_test" ]; then
    cat >"$example_test" <<'SH'
#!/usr/bin/env bash

function test_bashunit_is_installed() {
  assert_same "bashunit is installed" "bashunit is installed"
}
SH
    chmod +x "$example_test"
    echo "> Created $example_test"
  fi

  local workflow_dir=".github/workflows"
  local workflow_file="$workflow_dir/tests.yml"
  if [ ! -f "$workflow_file" ]; then
    mkdir -p "$workflow_dir"
    cat >"$workflow_file" <<SH
name: Tests
on: [pull_request, push]
jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: TypedDevs/bashunit@v0
        with:
          args: $tests_dir
SH
    echo "> Created $workflow_file"
  fi

  local env_file=".env"
  local env_line="BASHUNIT_BOOTSTRAP=$bootstrap_file"
  if [ -f "$env_file" ]; then

    if grep -Fxq "$env_line" "$env_file"; then
      echo "> $env_file already sets BASHUNIT_BOOTSTRAP"
    else
      if grep -q "^BASHUNIT_BOOTSTRAP=" "$env_file"; then
        if bashunit::check_os::is_macos; then
          sed -i '' -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file"
        else
          sed -i -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file"
        fi
      fi
      echo "$env_line" >>"$env_file"
      echo "> Updated $env_file"
    fi
  else
    echo "$env_line" >"$env_file"
    echo "> Created $env_file"
  fi

  echo "> bashunit initialized in $tests_dir"
}

# src/assert/index.sh

# src/assert/core.sh

function bashunit::assert::mark_failed() {

  if [ "${_BASHUNIT_ASSERT_ONCE_ACTIVE:-0}" -eq 1 ]; then
    if bashunit::assert::once_is_absorbing; then
      _BASHUNIT_ASSERT_ONCE_FAILED=1
      return 0
    fi
  fi
  bashunit::state::add_assertions_failed
  bashunit::state::mark_assertion_failed_in_test
}

function bashunit::assert::should_skip() {
  bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

function bashunit::assert::usage_error_detail() {
  local assertion=$1
  local detail=$2

  printf 'bashunit: assertion usage error: %s %s\n' "$assertion" "$detail" >&2
}

function bashunit::assert::usage_error() {
  local assertion=$1
  local required=$2
  local signature=$3
  local supplied=$4

  printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
    "$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

function bashunit::assert::label_to_slot() {
  local custom_label="${1:-}"
  local fallback_depth="${2:-2}"
  if [ -n "$custom_label" ]; then
    _BASHUNIT_ASSERT_LABEL_OUT=$custom_label
    return
  fi
  bashunit::helper::find_test_function_name_to_slot "$fallback_depth"
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  _BASHUNIT_ASSERT_LABEL_OUT=$_BASHUNIT_HELPER_NORMALIZED_OUT
}

function bashunit::assert::fail_with() {
  bashunit::assert::label_to_slot "${1:-}" 3
  bashunit::assert::mark_failed
  bashunit::console_results::print_failed_test \
    "$_BASHUNIT_ASSERT_LABEL_OUT" "${2-}" "${3-}" "${4-}" "${5-}" "${6-}"
}

_BASHUNIT_ASSERT_JOINED_OUT=""

function bashunit::assert::join_to_slot() {
  local IFS=$'\n'
  local joined="$*"
  while [ "$joined" != "${joined%$'\n'}" ]; do
    joined="${joined%$'\n'}"
  done
  _BASHUNIT_ASSERT_JOINED_OUT=$joined
}

function bashunit::assert::label() {
  bashunit::assert::label_to_slot "${1:-}"
  builtin echo "$_BASHUNIT_ASSERT_LABEL_OUT"
}

function bashunit::fail() {
  bashunit::assert::should_skip && return 0

  local message="${1:-${FUNCNAME[1]}}"

  bashunit::helper::find_test_function_name_to_slot
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  bashunit::assert::mark_failed
  bashunit::console_results::print_failure_message "${label}" "$message"
}

_BASHUNIT_ASSERT_BOOL_EXIT_OUT=0

function bashunit::assert::_run_bool_subject() {
  local exit_code=0

  if [ $# -gt 1 ]; then
    "$@" >/dev/null 2>&1 || exit_code=$?
  else
    bashunit::run_command_or_eval "$1" || exit_code=$?
  fi

  _BASHUNIT_ASSERT_BOOL_EXIT_OUT=$exit_code
}

_BASHUNIT_ASSERT_EXIT_DESC_OUT=""

function bashunit::assert::_describe_exit_code() {
  case "$1" in
  127) _BASHUNIT_ASSERT_EXIT_DESC_OUT="unknown command: $2" ;;
  126) _BASHUNIT_ASSERT_EXIT_DESC_OUT="not executable: $2" ;;
  *) _BASHUNIT_ASSERT_EXIT_DESC_OUT="exit code: $1" ;;
  esac
}

function assert_true() {
  bashunit::assert::should_skip && return 0

  local actual="$1"

  if [ $# -eq 1 ]; then
    case "$actual" in
    "")
      bashunit::handle_bool_assertion_failure "true or 0" "$actual"
      return
      ;;
    "true" | "0")
      bashunit::state::add_assertions_passed
      return
      ;;
    "false" | "1")
      bashunit::handle_bool_assertion_failure "true or 0" "$actual"
      return
      ;;
    esac
  fi

  bashunit::assert::_run_bool_subject "$@"
  local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
  actual="$*"

  if [ "$exit_code" -ne 0 ]; then
    bashunit::assert::_describe_exit_code "$exit_code" "$actual"
    bashunit::handle_bool_assertion_failure \
      "command or function with zero exit code" "$_BASHUNIT_ASSERT_EXIT_DESC_OUT"
  else
    bashunit::state::add_assertions_passed
  fi
}

function assert_false() {
  bashunit::assert::should_skip && return 0

  local actual="$1"

  if [ $# -eq 1 ]; then
    case "$actual" in
    "")
      bashunit::handle_bool_assertion_failure "false or 1" "$actual"
      return
      ;;
    "false" | "1")
      bashunit::state::add_assertions_passed
      return
      ;;
    "true" | "0")
      bashunit::handle_bool_assertion_failure "false or 1" "$actual"
      return
      ;;
    esac
  fi

  bashunit::assert::_run_bool_subject "$@"
  local exit_code=$_BASHUNIT_ASSERT_BOOL_EXIT_OUT
  actual="$*"

  case "$exit_code" in
  0 | 126 | 127)
    bashunit::assert::_describe_exit_code "$exit_code" "$actual"
    bashunit::handle_bool_assertion_failure \
      "command or function with non-zero exit code" "$_BASHUNIT_ASSERT_EXIT_DESC_OUT"
    ;;
  *) bashunit::state::add_assertions_passed ;;
  esac
}

function bashunit::run_command_or_eval() {
  local cmd="$1"

  case "$cmd" in
  eval\ * | eval)
    eval "${cmd#eval }" &>/dev/null
    ;;
  *[=[:space:]]* | "")

    "$cmd" &>/dev/null
    ;;
  *)

    if alias -- "$cmd" >/dev/null 2>&1; then
      eval "$cmd" &>/dev/null
    else
      "$cmd" &>/dev/null
    fi
    ;;
  esac
  return $?
}

function bashunit::handle_bool_assertion_failure() {
  local expected="$1"
  local got="$2"
  bashunit::helper::find_test_function_name_to_slot
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  bashunit::assert::mark_failed
  bashunit::console_results::print_failed_test "$label" "$expected" "but got " "$got"
}

function assert_same() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if [ "$expected" != "$actual" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${expected}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_equals() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  bashunit::str::strip_ansi_to_slot "$actual"
  local actual_cleaned=$_BASHUNIT_STR_STRIPPED_OUT
  bashunit::str::strip_ansi_to_slot "$expected"
  local expected_cleaned=$_BASHUNIT_STR_STRIPPED_OUT

  if [ "$expected_cleaned" != "$actual_cleaned" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${expected_cleaned}" "but got " "${actual_cleaned}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_equals() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  bashunit::str::strip_ansi_to_slot "$actual"
  local actual_cleaned=$_BASHUNIT_STR_STRIPPED_OUT
  bashunit::str::strip_ansi_to_slot "$expected"
  local expected_cleaned=$_BASHUNIT_STR_STRIPPED_OUT

  if [ "$expected_cleaned" = "$actual_cleaned" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${expected_cleaned}" "to not be" "${actual_cleaned}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local label_override="${2:-}"

  if [ "$expected" != "" ]; then
    bashunit::assert::fail_with "${label_override:-}" "to be empty" "but got " "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local label_override="${2:-}"

  if [ "$expected" = "" ]; then
    bashunit::assert::fail_with "${label_override:-}" "to not be empty" "but got " "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_same() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if [ "$expected" = "$actual" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${expected}" "to not be" "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_contains() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local label_override=""
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  case "$actual" in
  *"$expected"*) ;;
  *)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function bashunit::assert::_supports_nocasematch() {
  if [ "${BASH_VERSINFO[0]:-0}" -gt 3 ]; then
    return 0
  fi
  [ "${BASH_VERSINFO[0]:-0}" -eq 3 ] && [ "${BASH_VERSINFO[1]:-0}" -ge 1 ]
}

function assert_contains_ignore_case() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if bashunit::assert::_supports_nocasematch; then
    local nocase_was_set=1
    shopt -q nocasematch || nocase_was_set=0
    shopt -s nocasematch

    local matched=1
    case "$actual" in
    *"$expected"*) ;;
    *) matched=0 ;;
    esac

    [ "$nocase_was_set" -eq 1 ] || shopt -u nocasematch

    if [ "$matched" -eq 0 ]; then
      bashunit::assert::fail_with "${label_override:-}" "${actual}" "to contain" "${expected}"
      return
    fi

    bashunit::state::add_assertions_passed
    return
  fi

  local expected_lower
  local actual_lower
  expected_lower=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
  actual_lower=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')

  case "$actual_lower" in
  *"$expected_lower"*) ;;
  *)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_not_contains() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  case "$actual" in
  *"$expected"*)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to not contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_matches() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -eq 0 ]; then

    if [ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -eq 0 ]; then
      bashunit::helper::find_test_function_name_to_slot
      bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
      local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
      bashunit::assert::mark_failed
      bashunit::console_results::print_failed_test "${label}" "${actual}" "to match" "${expected}"
      return
    fi
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_matches() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -gt 0 ] ||
    [ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -gt 0 ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to not match" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_exec() {
  bashunit::assert::should_skip && return 0
  local label_override=""

  local cmd="$1"
  shift

  local expected_exit=0
  local expected_stdout=""
  local expected_stderr=""
  local stdout_needle=""
  local stdout_no_needle=""
  local stderr_needle=""
  local stderr_no_needle=""
  local stdin_input=""
  local check_stdout=false
  local check_stderr=false
  local check_stdout_contains=false
  local check_stdout_not_contains=false
  local check_stderr_contains=false
  local check_stderr_not_contains=false
  local check_stdin=false

  while [ $# -gt 0 ]; do
    case "$1" in
    --exit)
      expected_exit="$2"
      shift 2
      ;;
    --stdout)
      expected_stdout="$2"
      check_stdout=true
      shift 2
      ;;
    --stderr)
      expected_stderr="$2"
      check_stderr=true
      shift 2
      ;;
    --stdout-contains)
      stdout_needle="$2"
      check_stdout_contains=true
      shift 2
      ;;
    --stdout-not-contains)
      stdout_no_needle="$2"
      check_stdout_not_contains=true
      shift 2
      ;;
    --stderr-contains)
      stderr_needle="$2"
      check_stderr_contains=true
      shift 2
      ;;
    --stderr-not-contains)
      stderr_no_needle="$2"
      check_stderr_not_contains=true
      shift 2
      ;;
    --stdin)
      stdin_input="$2"
      check_stdin=true
      shift 2
      ;;
    *)
      shift
      ;;
    esac
  done

  local stdout_file stderr_file
  stdout_file=$("$MKTEMP")
  stderr_file=$("$MKTEMP")

  if $check_stdin; then
    local stdin_file
    stdin_file=$("$MKTEMP")
    printf '%s' "$stdin_input" >"$stdin_file"

    local exit_code=0
    eval "$cmd" <"$stdin_file" >"$stdout_file" 2>"$stderr_file" || exit_code=$?
    rm -f "$stdin_file"
  else
    local exit_code=0
    eval "$cmd" >"$stdout_file" 2>"$stderr_file" || exit_code=$?
  fi

  local stdout
  stdout=$(cat "$stdout_file")
  local stderr
  stderr=$(cat "$stderr_file")

  rm -f "$stdout_file" "$stderr_file"

  local expected_desc="exit: $expected_exit"
  local actual_desc="exit: $exit_code"
  local failed=0

  if [ "$exit_code" -ne "$expected_exit" ]; then
    failed=1
  fi

  if $check_stdout; then
    expected_desc="$expected_desc"$'\n'"stdout: $expected_stdout"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    if [ "$stdout" != "$expected_stdout" ]; then
      failed=1
    fi
  fi

  if $check_stdout_contains; then
    expected_desc="$expected_desc"$'\n'"stdout contains: $stdout_needle"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    case "$stdout" in
    *"$stdout_needle"*) ;;
    *) failed=1 ;;
    esac
  fi

  if $check_stdout_not_contains; then
    expected_desc="$expected_desc"$'\n'"stdout not contains: $stdout_no_needle"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    case "$stdout" in
    *"$stdout_no_needle"*) failed=1 ;;
    esac
  fi

  if $check_stderr; then
    expected_desc="$expected_desc"$'\n'"stderr: $expected_stderr"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    if [ "$stderr" != "$expected_stderr" ]; then
      failed=1
    fi
  fi

  if $check_stderr_contains; then
    expected_desc="$expected_desc"$'\n'"stderr contains: $stderr_needle"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    case "$stderr" in
    *"$stderr_needle"*) ;;
    *) failed=1 ;;
    esac
  fi

  if $check_stderr_not_contains; then
    expected_desc="$expected_desc"$'\n'"stderr not contains: $stderr_no_needle"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    case "$stderr" in
    *"$stderr_no_needle"*) failed=1 ;;
    esac
  fi

  if [ "$failed" -eq 1 ]; then
    bashunit::assert::fail_with "${label_override:-}" "$expected_desc" "but got " "$actual_desc"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_exit_code() {
  local actual_exit_code=${3-"$?"}
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code="$1"

  if ! [ "$actual_exit_code" -eq "$expected_exit_code" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual_exit_code}" "to be" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_successful_code() {
  local actual_exit_code=${3-"$?"}
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=0

  if ! [ "$actual_exit_code" -eq "$expected_exit_code" ]; then
    bashunit::assert::fail_with "${label_override:-}" \
      "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_unsuccessful_code() {
  local actual_exit_code=${3-"$?"}
  local label_override=""
  bashunit::assert::should_skip && return 0

  if ! [ "$actual_exit_code" -ne 0 ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual_exit_code}" "to be non-zero" "but was 0"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_general_error() {
  local actual_exit_code=${3-"$?"}
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=1

  if ! [ "$actual_exit_code" -eq "$expected_exit_code" ]; then
    bashunit::assert::fail_with "${label_override:-}" \
      "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_command_available() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "command" "$#"
    return 2
  fi

  local command="$1"
  local label_override="${2:-}"

  if ! bashunit::is_command_available "$command"; then
    bashunit::assert::fail_with "${label_override:-}" \
      "${command}" "to be available but was" "not found"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_command_not_found() {
  local actual_exit_code=${3-"$?"}
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=127

  if ! [ "$actual_exit_code" -eq "$expected_exit_code" ]; then
    bashunit::assert::fail_with "${label_override:-}" \
      "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_string_starts_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  case "$actual" in
  "$expected"*) ;;
  *)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to start with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_not_starts_with() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  case "$actual" in
  "$expected"*)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to not start with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_ends_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  case "$actual" in
  *"$expected") ;;
  *)
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to end with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_not_ends_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  bashunit::assert::join_to_slot "${actual_arr[@]+"${actual_arr[@]}"}"
  local actual=$_BASHUNIT_ASSERT_JOINED_OUT

  case "$actual" in
  *"$expected")
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to not end with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_less_than() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -lt "$expected" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to be less than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_less_or_equal_than() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -le "$expected" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to be less or equal than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_greater_than() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -gt "$expected" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to be greater than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_greater_or_equal_than() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -ge "$expected" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to be greater or equal than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_between() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 3 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "min, max, actual" "$#"
    return 2
  fi

  local min="$1"
  local max="$2"
  local actual="$3"
  local label_override="${4:-}"

  if ! bashunit::assert::_validate_range_args "${FUNCNAME[0]}" "$min" "$max" "$actual"; then
    return 2
  fi

  if ! bashunit::math::is_le "$min" "$actual"; then
    bashunit::assert::fail_with "$label_override" "$actual" "to be between" "$min and $max" \
      "Violated lower bound" "$min"
    return
  fi

  if ! bashunit::math::is_le "$actual" "$max"; then
    bashunit::assert::fail_with "$label_override" "$actual" "to be between" "$min and $max" \
      "Violated upper bound" "$max"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_between() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 3 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "min, max, actual" "$#"
    return 2
  fi

  local min="$1"
  local max="$2"
  local actual="$3"
  local label_override="${4:-}"

  if ! bashunit::assert::_validate_range_args "${FUNCNAME[0]}" "$min" "$max" "$actual"; then
    return 2
  fi

  if bashunit::math::is_le "$min" "$actual" && bashunit::math::is_le "$actual" "$max"; then
    bashunit::assert::fail_with "$label_override" "$actual" "to not be between" "$min and $max"
    return
  fi

  bashunit::state::add_assertions_passed
}

function bashunit::assert::_is_numeric() {
  local value="$1"
  case "$value" in
  '' | *[!0-9.+-]*) return 1 ;;
  -*) value=${value#-} ;;
  +*) value=${value#+} ;;
  esac

  case "$value" in
  '' | '.' | *[+-]*) return 1 ;;
  *.*)
    local fraction=${value#*.}
    case "$fraction" in *.*) return 1 ;; esac
    ;;
  esac

  case "$value" in
  *[0-9]*) return 0 ;;
  esac
  return 1
}

function bashunit::assert::_validate_range_args() {
  local assertion=$1
  local min=$2
  local max=$3
  local actual=$4

  if ! bashunit::assert::_is_numeric "$min" ||
    ! bashunit::assert::_is_numeric "$max" ||
    ! bashunit::assert::_is_numeric "$actual"; then
    bashunit::assert::usage_error_detail "$assertion" \
      "expects numeric min, max, and actual values, got '$min', '$max', '$actual'"
    return 1
  fi

  if ! bashunit::math::is_le "$min" "$max"; then
    bashunit::assert::usage_error_detail "$assertion" "expects min <= max, got '$min' and '$max'"
    return 1
  fi
}

function assert_within_delta() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 3 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
    return 2
  fi

  local expected="$1"
  local actual="$2"
  local delta="$3"

  if ! bashunit::assert::_is_numeric "$expected" ||
    ! bashunit::assert::_is_numeric "$actual" ||
    ! bashunit::assert::_is_numeric "$delta"; then
    bashunit::assert::fail_with "" "${expected} ${actual} ${delta}" \
      "to all be numeric" "but got a non-numeric value"
    return
  fi

  expected=${expected#+}
  actual=${actual#+}
  delta=${delta#+}

  local scale expected_places actual_places delta_places
  bashunit::math::decimals_to_slot "$expected"
  expected_places=$_BASHUNIT_MATH_DECIMALS_OUT
  bashunit::math::decimals_to_slot "$actual"
  actual_places=$_BASHUNIT_MATH_DECIMALS_OUT
  bashunit::math::decimals_to_slot "$delta"
  delta_places=$_BASHUNIT_MATH_DECIMALS_OUT
  scale=$expected_places
  if [ "$actual_places" -gt "$scale" ]; then
    scale=$actual_places
  fi
  if [ "$delta_places" -gt "$scale" ]; then
    scale=$delta_places
  fi

  local padded_expected padded_actual padded_delta
  bashunit::math::pad_to_slot "$expected" "$scale"
  padded_expected=$_BASHUNIT_MATH_PADDED_OUT
  bashunit::math::pad_to_slot "$actual" "$scale"
  padded_actual=$_BASHUNIT_MATH_PADDED_OUT
  bashunit::math::pad_to_slot "$delta" "$scale"
  padded_delta=$_BASHUNIT_MATH_PADDED_OUT

  if bashunit::math::scale_pair_to_slots "$padded_expected" "$padded_actual"; then
    local scaled_diff=$((_BASHUNIT_MATH_SCALED_L_OUT - _BASHUNIT_MATH_SCALED_R_OUT))
    if [ "$scaled_diff" -lt 0 ]; then
      scaled_diff=$((-scaled_diff))
    fi
    if bashunit::math::scale_pair_to_slots "$padded_delta" "$padded_expected"; then
      if [ "$scaled_diff" -gt "$_BASHUNIT_MATH_SCALED_L_OUT" ]; then
        bashunit::assert::fail_with "" "${actual}" "to be within ${delta} of" "${expected}"
        return
      fi

      bashunit::state::add_assertions_passed
      return
    fi
  fi

  local diff
  diff="$(bashunit::math::calculate "$expected - $actual")"
  case "$diff" in
  -*) diff="${diff#-}" ;;
  esac

  if [ "$(bashunit::math::calculate "$diff <= $delta")" != "1" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be within ${delta} of" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_line_count() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
    return 2
  fi
  local IFS=$' \t\n'

  local expected="$1"
  local -a input_arr
  input_arr=("${@:2}")
  local label_override=""
  local input_str
  input_str=$(printf '%s\n' ${input_arr+"${input_arr[@]}"})

  if [ -z "$input_str" ]; then
    local actual=0
  else

    local actual=1
    local _rest="$input_str"
    while [ "$_rest" != "${_rest#*$'\n'}" ]; do
      _rest="${_rest#*$'\n'}"
      actual=$((actual + 1))
    done
    _rest="$input_str"
    while [ "$_rest" != "${_rest#*\\n}" ]; do
      _rest="${_rest#*\\n}"
      actual=$((actual + 1))
    done
  fi

  if [ "$expected" != "$actual" ]; then
    bashunit::assert::fail_with "${label_override:-}" "${input_str}" \
      "to contain number of lines equal to" "${expected}" \
      "but found" "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function bashunit::format_to_regex() {
  local format="$1"
  local regex=""
  local i=0
  local len=${#format}

  while [ $i -lt "$len" ]; do
    local char="${format:$i:1}"
    if [ "$char" = "%" ] && [ $((i + 1)) -lt "$len" ]; then
      local next="${format:$((i + 1)):1}"
      case "$next" in
      d) regex="${regex}[0-9]+" ;;
      i) regex="${regex}[+-]?[0-9]+" ;;
      f) regex="${regex}[+-]?[0-9]*\\.?[0-9]+" ;;
      s) regex="${regex}[^ ]+" ;;
      x) regex="${regex}[0-9a-fA-F]+" ;;
      e) regex="${regex}[+-]?[0-9]*\\.?[0-9]+[eE][+-]?[0-9]+" ;;
      %) regex="${regex}%" ;;
      *)
        regex="${regex}%${next}"
        ;;
      esac
      i=$((i + 2))
    else
      case "$char" in
      . | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$')
        regex="${regex}\\${char}"
        ;;
      \\)
        regex="${regex}\\\\"
        ;;
      *)
        regex="${regex}${char}"
        ;;
      esac
      i=$((i + 1))
    fi
  done

  printf '%s' "^${regex}$"
}

function assert_string_matches_format() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
    return 2
  fi

  local format="$1"
  local actual="$2"
  local label_override="${3:-}"

  local regex
  regex="$(bashunit::format_to_regex "$format")"

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -eq 0 ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to match format" "${format}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_string_not_matches_format() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
    return 2
  fi

  local format="$1"
  local actual="$2"
  local label_override="${3:-}"

  local regex
  regex="$(bashunit::format_to_regex "$format")"

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -gt 0 ]; then
    bashunit::assert::fail_with "${label_override:-}" "${actual}" "to not match format" "${format}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/arrays.sh

function assert_arrays_equal() {
  bashunit::assert::should_skip && return 0

  local -a expected_values=()
  local -a actual_values=()
  local found_separator=false
  local argument

  for argument in "$@"; do
    if [ "$found_separator" = false ] && [ "$argument" = "--" ]; then
      found_separator=true
      continue
    fi

    if [ "$found_separator" = true ]; then
      actual_values[${#actual_values[@]}]="$argument"
    else
      expected_values[${#expected_values[@]}]="$argument"
    fi
  done

  if [ "$found_separator" = false ]; then
    bashunit::assert::fail_with "" "--" "but got " "missing array separator"
    return
  fi

  if [ "${#expected_values[@]}" -ne "${#actual_values[@]}" ]; then
    bashunit::assert::fail_with "" "${expected_values[*]}" "but got " "${actual_values[*]}" \
      "Expected length" "${#expected_values[@]}, actual length ${#actual_values[@]}"
    return
  fi

  local index
  for ((index = 0; index < ${#expected_values[@]}; index++)); do
    if [ "${expected_values[$index]}" != "${actual_values[$index]}" ]; then
      bashunit::assert::fail_with "" "${expected_values[*]}" "but got " "${actual_values[*]}" \
        "Different index" "$index"
      return
    fi
  done

  bashunit::state::add_assertions_passed
}

function assert_array_contains() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  shift

  local -a actual
  actual=("$@")

  case "${actual[*]:-}" in
  *"$expected"*) ;;
  *)
    bashunit::assert::fail_with "" "${actual[*]}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_array_length() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  shift

  local actual_length="$#"

  if [ "$expected" != "$actual_length" ]; then
    bashunit::assert::fail_with "" "$*" "to have length ${expected}" "but got ${actual_length}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_array_not_contains() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  shift
  local -a actual
  actual=("$@")

  case "${actual[*]:-}" in
  *"$expected"*)
    bashunit::assert::fail_with "" "${actual[*]}" "to not contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

# src/assert/assertions.sh

_BASHUNIT_ASSERT_INNER_FAILED_OUT=0
_BASHUNIT_ASSERT_INNER_PASSED_OUT=0

_BASHUNIT_ASSERT_INNER_OUTPUT_OUT=""

function bashunit::assert::_capture() {
  local before_passed=$_BASHUNIT_ASSERTIONS_PASSED
  local before_failed=$_BASHUNIT_ASSERTIONS_FAILED
  local before_guard=$_BASHUNIT_ASSERTION_FAILED_IN_TEST
  local before_output=$_BASHUNIT_TEST_OUTPUT
  local before_count=$_BASHUNIT_TOTAL_TESTS_COUNT

  local before_once_active=$_BASHUNIT_ASSERT_ONCE_ACTIVE
  local before_once_frame=$_BASHUNIT_ASSERT_ONCE_FRAME
  local before_once_abs=$_BASHUNIT_ASSERT_ONCE_ABS
  local before_once_label=$_BASHUNIT_ASSERT_ONCE_LABEL
  local before_once_actual=$_BASHUNIT_ASSERT_ONCE_ACTUAL
  local before_once_failed=$_BASHUNIT_ASSERT_ONCE_FAILED
  local before_once_in_expected=$_BASHUNIT_ASSERT_ONCE_IN_EXPECTED
  local before_once_in_condition=$_BASHUNIT_ASSERT_ONCE_IN_CONDITION
  local before_once_in_actual=$_BASHUNIT_ASSERT_ONCE_IN_ACTUAL
  bashunit::assert::once_reset

  _BASHUNIT_ASSERTION_FAILED_IN_TEST=0

  "$@" >/dev/null 2>&1 || true

  bashunit::assert::once_flush >/dev/null 2>&1

  bashunit::str::strip_ansi_to_slot "${_BASHUNIT_TEST_OUTPUT#"$before_output"}"
  local output=$_BASHUNIT_STR_STRIPPED_OUT

  if [ "$_BASHUNIT_ASSERTIONS_FAILED" -gt "$before_failed" ]; then
    _BASHUNIT_ASSERT_INNER_FAILED_OUT=1
  else
    _BASHUNIT_ASSERT_INNER_FAILED_OUT=0
  fi

  if [ "$_BASHUNIT_ASSERTIONS_PASSED" -gt "$before_passed" ]; then
    _BASHUNIT_ASSERT_INNER_PASSED_OUT=1
  else
    _BASHUNIT_ASSERT_INNER_PASSED_OUT=0
  fi

  _BASHUNIT_ASSERT_INNER_OUTPUT_OUT=$output

  _BASHUNIT_ASSERTIONS_PASSED=$before_passed
  _BASHUNIT_ASSERTIONS_FAILED=$before_failed
  _BASHUNIT_ASSERTION_FAILED_IN_TEST=$before_guard
  _BASHUNIT_TEST_OUTPUT=$before_output
  _BASHUNIT_TOTAL_TESTS_COUNT=$before_count

  _BASHUNIT_ASSERT_ONCE_FRAME=$before_once_frame
  _BASHUNIT_ASSERT_ONCE_ABS=$before_once_abs
  _BASHUNIT_ASSERT_ONCE_LABEL=$before_once_label
  _BASHUNIT_ASSERT_ONCE_ACTUAL=$before_once_actual
  _BASHUNIT_ASSERT_ONCE_FAILED=$before_once_failed
  _BASHUNIT_ASSERT_ONCE_IN_EXPECTED=$before_once_in_expected
  _BASHUNIT_ASSERT_ONCE_IN_CONDITION=$before_once_in_condition
  _BASHUNIT_ASSERT_ONCE_IN_ACTUAL=$before_once_in_actual
  _BASHUNIT_ASSERT_ONCE_ACTIVE=$before_once_active
}

_BASHUNIT_ASSERT_INNER_OUTCOME_OUT=""

function bashunit::assert::_inner_outcome_to_slot() {
  if [ "$_BASHUNIT_ASSERT_INNER_FAILED_OUT" -eq 1 ]; then
    _BASHUNIT_ASSERT_INNER_OUTCOME_OUT="a failing assertion"
  elif [ "$_BASHUNIT_ASSERT_INNER_PASSED_OUT" -eq 1 ]; then
    _BASHUNIT_ASSERT_INNER_OUTCOME_OUT="a passing assertion"
  else
    _BASHUNIT_ASSERT_INNER_OUTCOME_OUT="no assertion at all"
  fi
}

function assert_assertion_fails() {
  bashunit::assert::should_skip && return 0

  bashunit::assert::_capture "$@"

  if [ "$_BASHUNIT_ASSERT_INNER_FAILED_OUT" -eq 1 ]; then
    bashunit::state::add_assertions_passed
    return 0
  fi

  bashunit::assert::_inner_outcome_to_slot
  bashunit::assert::fail_with "" "${1-}" \
    "to be a failing assertion, but got " "$_BASHUNIT_ASSERT_INNER_OUTCOME_OUT"
  return 1
}

function assert_assertion_passes() {
  bashunit::assert::should_skip && return 0

  bashunit::assert::_capture "$@"

  if [ "$_BASHUNIT_ASSERT_INNER_PASSED_OUT" -eq 1 ] &&
    [ "$_BASHUNIT_ASSERT_INNER_FAILED_OUT" -eq 0 ]; then
    bashunit::state::add_assertions_passed
    return 0
  fi

  bashunit::assert::_inner_outcome_to_slot
  bashunit::assert::fail_with "" "${1-}" \
    "to be a passing assertion, but got " "$_BASHUNIT_ASSERT_INNER_OUTCOME_OUT"
  return 1
}

function assert_assertion_fails_with() {
  bashunit::assert::should_skip && return 0

  local expected_message=$1
  shift

  bashunit::assert::_capture "$@"

  if [ "$_BASHUNIT_ASSERT_INNER_FAILED_OUT" -ne 1 ]; then
    bashunit::assert::_inner_outcome_to_slot
    bashunit::assert::fail_with "" "${1-}" \
      "to be a failing assertion, but got " "$_BASHUNIT_ASSERT_INNER_OUTCOME_OUT"
    return 1
  fi

  case "$_BASHUNIT_ASSERT_INNER_OUTPUT_OUT" in
  *"$expected_message"*)
    bashunit::state::add_assertions_passed
    return 0
    ;;
  esac

  bashunit::assert::fail_with "" "$_BASHUNIT_ASSERT_INNER_OUTPUT_OUT" \
    "to contain" "$expected_message"
  return 1
}

# src/assert/once.sh

_BASHUNIT_ASSERT_ONCE_ACTIVE=0
_BASHUNIT_ASSERT_ONCE_FRAME=""
_BASHUNIT_ASSERT_ONCE_ABS=0
_BASHUNIT_ASSERT_ONCE_LABEL=""
_BASHUNIT_ASSERT_ONCE_ACTUAL=""
_BASHUNIT_ASSERT_ONCE_FAILED=0
_BASHUNIT_ASSERT_ONCE_IN_EXPECTED=""
_BASHUNIT_ASSERT_ONCE_IN_CONDITION=""
_BASHUNIT_ASSERT_ONCE_IN_ACTUAL=""

_BASHUNIT_ASSERT_ONCE_TEST_LABEL=""

function bashunit::assert::once_is_absorbing() {
  [ "$_BASHUNIT_ASSERT_ONCE_ACTIVE" -eq 1 ] || return 1

  local index=$((${#FUNCNAME[@]} - _BASHUNIT_ASSERT_ONCE_ABS))
  if [ "$index" -ge 0 ] &&
    [ "${FUNCNAME[$index]-}" = "$_BASHUNIT_ASSERT_ONCE_FRAME" ]; then
    return 0
  fi

  bashunit::assert::once_flush
  return 1
}

function bashunit::assert::once_absorb_message() {
  _BASHUNIT_ASSERT_ONCE_FAILED=1

  if [ -n "$_BASHUNIT_ASSERT_ONCE_IN_EXPECTED$_BASHUNIT_ASSERT_ONCE_IN_ACTUAL" ]; then
    return 0
  fi

  _BASHUNIT_ASSERT_ONCE_IN_EXPECTED=${1-}
  _BASHUNIT_ASSERT_ONCE_IN_CONDITION=${2-}
  _BASHUNIT_ASSERT_ONCE_IN_ACTUAL=${3-}
}

function bashunit::assert::once_flush() {
  [ "$_BASHUNIT_ASSERT_ONCE_ACTIVE" -eq 1 ] || return 0

  _BASHUNIT_ASSERT_ONCE_ACTIVE=0

  if [ "$_BASHUNIT_ASSERT_ONCE_FAILED" -eq 0 ]; then
    return 0
  fi

  _BASHUNIT_ASSERTIONS_PASSED=$((_BASHUNIT_ASSERTIONS_PASSED - 1))

  local expected=$_BASHUNIT_ASSERT_ONCE_LABEL
  local condition="but got "
  local actual=$_BASHUNIT_ASSERT_ONCE_ACTUAL

  if [ -z "$expected" ]; then
    expected=$_BASHUNIT_ASSERT_ONCE_IN_EXPECTED
    condition=$_BASHUNIT_ASSERT_ONCE_IN_CONDITION
    actual=$_BASHUNIT_ASSERT_ONCE_IN_ACTUAL
  fi

  bashunit::assert::fail_with \
    "$_BASHUNIT_ASSERT_ONCE_TEST_LABEL" "$expected" "$condition" "$actual"
}

function bashunit::assert::once_reset() {
  _BASHUNIT_ASSERT_ONCE_ACTIVE=0
  _BASHUNIT_ASSERT_ONCE_FRAME=""
  _BASHUNIT_ASSERT_ONCE_ABS=0
  _BASHUNIT_ASSERT_ONCE_LABEL=""
  _BASHUNIT_ASSERT_ONCE_ACTUAL=""
  _BASHUNIT_ASSERT_ONCE_FAILED=0
  _BASHUNIT_ASSERT_ONCE_IN_EXPECTED=""
  _BASHUNIT_ASSERT_ONCE_IN_CONDITION=""
  _BASHUNIT_ASSERT_ONCE_IN_ACTUAL=""
  _BASHUNIT_ASSERT_ONCE_TEST_LABEL=""
}

function bashunit::assert_once() {
  bashunit::assert::should_skip && return 0

  bashunit::assert::once_flush

  bashunit::assert::once_reset

  _BASHUNIT_ASSERT_ONCE_LABEL=${1-}
  _BASHUNIT_ASSERT_ONCE_ACTUAL=${2-}

  bashunit::state::add_assertions_passed

  bashunit::assert::label_to_slot ""
  _BASHUNIT_ASSERT_ONCE_TEST_LABEL=$_BASHUNIT_ASSERT_LABEL_OUT

  _BASHUNIT_ASSERT_ONCE_FRAME=${FUNCNAME[1]-}
  _BASHUNIT_ASSERT_ONCE_ABS=$((${#FUNCNAME[@]} - 1))
  _BASHUNIT_ASSERT_ONCE_ACTIVE=1
}

# src/assert/dates.sh

function bashunit::date::to_epoch() {
  local input="$1"

  if [ -z "$input" ]; then
    echo "$input"
    return 1
  fi

  case "$input" in
  *[!0-9]*) ;;
  *)
    echo "$input"
    return 0
    ;;
  esac

  case "$input" in
  *Z)
    local utc_input="${input%Z}"
    local utc_norm="${utc_input/T/ }"
    local epoch

    epoch=$(TZ=UTC date -d "$utc_input" +%s 2>/dev/null) && {
      echo "$epoch"
      return 0
    }
    epoch=$(TZ=UTC date -d "$utc_norm" +%s 2>/dev/null) && {
      echo "$epoch"
      return 0
    }

    epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "${utc_input}+0000" +%s 2>/dev/null) && {
      echo "$epoch"
      return 0
    }
    echo "$input"
    return 1
    ;;
  esac

  local normalized="$input"
  normalized="${normalized/T/ }"

  case "$normalized" in
  *[+-][0-9][0-9][0-9][0-9])
    normalized="${normalized%[+-][0-9][0-9][0-9][0-9]}"
    ;;
  esac

  local epoch

  epoch=$(date -d "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  case "$input" in
  *[+-][0-9][0-9][0-9][0-9])
    epoch=$(TZ=UTC date -d "$normalized" +%s 2>/dev/null) && {
      local ilen=${#input}
      local ostart=$((ilen - 5))
      local osign="${input:$ostart:1}"
      local ohh="${input:$((ostart + 1)):2}"
      local omm="${input:$((ostart + 3)):2}"
      local osecs=$(((10#$ohh * 3600) + (10#$omm * 60)))
      if [ "$osign" = "+" ]; then
        osecs=$((-osecs))
      fi
      echo $((epoch + osecs))
      return 0
    }
    ;;
  esac

  if [ "$normalized" != "$input" ]; then
    epoch=$(date -d "$normalized" +%s 2>/dev/null) && {
      echo "$epoch"
      return 0
    }
  fi

  epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input 00:00:00" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  echo "$input"
  return 1
}

_BASHUNIT_DATE_EPOCH_OUT=""

function bashunit::date::_epoch_or_fail() {
  local input=$1
  local epoch
  if epoch="$(bashunit::date::to_epoch "$input")"; then
    _BASHUNIT_DATE_EPOCH_OUT=$epoch
    return 0
  fi
  bashunit::assert::fail_with "" "${input}" "to be" "a valid date"
  return 1
}

function assert_date_equals() {
  bashunit::assert::should_skip && return 0

  local expected actual
  bashunit::date::_epoch_or_fail "$1" || return 0
  expected=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$2" || return 0
  actual=$_BASHUNIT_DATE_EPOCH_OUT

  if [ "$actual" -ne "$expected" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be equal to" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_before() {
  bashunit::assert::should_skip && return 0

  local expected actual
  bashunit::date::_epoch_or_fail "$1" || return 0
  expected=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$2" || return 0
  actual=$_BASHUNIT_DATE_EPOCH_OUT

  if [ "$actual" -ge "$expected" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be before" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_after() {
  bashunit::assert::should_skip && return 0

  local expected actual
  bashunit::date::_epoch_or_fail "$1" || return 0
  expected=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$2" || return 0
  actual=$_BASHUNIT_DATE_EPOCH_OUT

  if [ "$actual" -le "$expected" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be after" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_within_range() {
  bashunit::assert::should_skip && return 0

  local from to actual
  bashunit::date::_epoch_or_fail "$1" || return 0
  from=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$2" || return 0
  to=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$3" || return 0
  actual=$_BASHUNIT_DATE_EPOCH_OUT

  if [ "$actual" -lt "$from" ] || [ "$actual" -gt "$to" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be between" "${from} and ${to}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_within_delta() {
  bashunit::assert::should_skip && return 0

  local expected actual
  bashunit::date::_epoch_or_fail "$1" || return 0
  expected=$_BASHUNIT_DATE_EPOCH_OUT
  bashunit::date::_epoch_or_fail "$2" || return 0
  actual=$_BASHUNIT_DATE_EPOCH_OUT
  local delta="$3"

  local diff=$((actual - expected))
  if [ "$diff" -lt 0 ]; then
    diff=$((-diff))
  fi

  if [ "$diff" -gt "$delta" ]; then
    bashunit::assert::fail_with "" "${actual}" "to be within" "${delta} seconds of ${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/duration.sh

function bashunit::duration::measure_ms() {
  local command="$1"

  local start_ns
  start_ns=$(bashunit::clock::now)

  eval "$command" >/dev/null 2>&1

  local end_ns
  end_ns=$(bashunit::clock::now)

  local elapsed_ms
  elapsed_ms=$(bashunit::math::calculate "($end_ns - $start_ns) / 1000000" | awk '{printf "%.0f", $1}')

  echo "$elapsed_ms"
}

function assert_duration() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -gt "$threshold_ms" ]; then
    bashunit::assert::fail_with "" "${threshold_ms}" "to complete within (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_duration_less_than() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -ge "$threshold_ms" ]; then
    bashunit::assert::fail_with "" "${threshold_ms}" "to complete within (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_duration_greater_than() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -le "$threshold_ms" ]; then
    bashunit::assert::fail_with "" "${threshold_ms}" "to take at least (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/files.sh

function assert_file_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -f "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "to exist but" "do not exist"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_not_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ -f "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "to not exist but" "the file exists"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -f "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "to be a file" "but is not a file"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ -s "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "to be empty" "but is not empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function bashunit::assert::file_state() {
  local path=$1
  if [ ! -e "$path" ]; then
    _BASHUNIT_ASSERT_FILE_STATE_OUT="missing"
  elif [ ! -f "$path" ]; then
    _BASHUNIT_ASSERT_FILE_STATE_OUT="not-a-file"
  else
    _BASHUNIT_ASSERT_FILE_STATE_OUT="file"
  fi
}
_BASHUNIT_ASSERT_FILE_STATE_OUT=""

function assert_is_file_readable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be readable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be readable" "but is not a file"
    return
    ;;
  esac

  if [ ! -r "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be readable" "but is not readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_not_readable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be readable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be readable" "but is not a file"
    return
    ;;
  esac

  if [ -r "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be readable" "but is readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_writable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be writable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be writable" "but is not a file"
    return
    ;;
  esac

  if [ ! -w "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be writable" "but is not writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_not_writable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be writable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be writable" "but is not a file"
    return
    ;;
  esac

  if [ -w "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be writable" "but is writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_executable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be executable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be executable" "but is not a file"
    return
    ;;
  esac

  if [ ! -x "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be executable" "but is not executable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_not_executable() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be executable" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be executable" "but is not a file"
    return
    ;;
  esac

  if [ -x "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be executable" "but is executable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_not_empty() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 1 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 1 "path" "$#"
    return 2
  fi

  local expected="$1"
  bashunit::assert::file_state "$expected"
  case "$_BASHUNIT_ASSERT_FILE_STATE_OUT" in
  missing)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be empty" "but does not exist"
    return
    ;;
  not-a-file)
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be empty" "but is not a file"
    return
    ;;
  esac

  if [ ! -s "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be empty" "but is empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_files_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"

  if [ "$(diff -u "$expected" "$actual")" != '' ]; then
    bashunit::assert::fail_with "" "${expected}" "Compared" "${actual}" \
      "Diff" "$(diff -u "$expected" "$actual" | sed '1,2d')"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_files_not_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"

  if [ "$(diff -u "$expected" "$actual")" = '' ]; then
    bashunit::assert::fail_with "" "${expected}" "Compared" "${actual}" \
      "Diff" "Files are equals"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_contains() {
  bashunit::assert::should_skip && return 0

  local file="$1"
  local string="$2"

  if ! grep -F -q -e "$string" -- "$file"; then
    bashunit::assert::fail_with "" "${file}" "to contain" "${string}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_not_contains() {
  bashunit::assert::should_skip && return 0

  local file="$1"
  local string="$2"

  if grep -F -q -e "$string" -- "$file"; then
    bashunit::assert::fail_with "" "${file}" "to not contain" "${string}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function bashunit::assert::_octal_to_decimal() {
  local mode="$1"
  case "$mode" in
  '' | *[!0-7]*) return 1 ;;
  esac
  printf '%d' "$((8#$mode))"
}

function assert_file_permissions() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local file="$2"

  if [ ! -e "$file" ]; then
    bashunit::assert::fail_with "" "${file}" \
      "to have permissions ${expected}" "but the file does not exist"
    return
  fi

  local actual
  actual="$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file" 2>/dev/null)"

  local expected_dec actual_dec
  expected_dec="$(bashunit::assert::_octal_to_decimal "$expected")"
  actual_dec="$(bashunit::assert::_octal_to_decimal "$actual")"

  if [ "$expected_dec" != "$actual_dec" ]; then
    bashunit::assert::fail_with "" "${file}" \
      "to have permissions ${expected}" "but got ${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_symlink() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -L "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "to be a symlink" "but is not a symlink"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_not_symlink() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ -L "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" "not to be a symlink" "but is a symlink"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_symlink_to() {
  bashunit::assert::should_skip && return 0

  if [ $# -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected_target, path" "$#"
    return 2
  fi

  local expected="$1"
  local path="$2"

  if [ ! -L "$path" ]; then
    bashunit::assert::fail_with "${3:-}" "${path}" "to be a symlink" "but is not a symlink"
    return
  fi

  local actual
  actual=$(readlink "$path")

  if [ "$actual" != "$expected" ]; then
    bashunit::assert::fail_with "${3:-}" "${expected}" \
      "to be the target of ${path}, but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/folders.sh

function assert_directory_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to exist but" "do not exist"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_directory_not_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ -d "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not exist but" "the directory exists"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be a directory" "but is not a directory"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || [ -n "$(ls -A "$expected")" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be empty" "but is not empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || [ -z "$(ls -A "$expected")" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to not be empty" "but is empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_readable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || [ ! -r "$expected" ] || [ ! -x "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be readable" "but is not readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_readable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || { [ -r "$expected" ] && [ -x "$expected" ]; }; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be not readable" "but is readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_writable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || [ ! -w "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be writable" "but is not writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_writable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"

  if [ ! -d "$expected" ] || [ -w "$expected" ]; then
    bashunit::assert::fail_with "${2:-}" "${expected}" "to be not writable" "but is writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/json.sh

function bashunit::assert_json::require_jq() {
  if ! command -v jq >/dev/null 2>&1; then
    bashunit::skip::__mark "jq is required for JSON assertions" 3
    return 1
  fi
  return 0
}

function assert_json_key_exists() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local key="$1"
  local json="$2"

  local result
  if ! result=$(printf '%s' "$json" | jq -e "$key" 2>/dev/null) || [ "$result" = "null" ]; then
    bashunit::assert::fail_with "" "${json}" "to have key" "${key}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_key_not_exists() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 2 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "key, json" "$#"
    return 2
  fi
  bashunit::assert_json::require_jq || return 0

  local key="$1"
  local json="$2"

  if ! printf '%s' "$json" | jq -e 'true' >/dev/null 2>&1; then
    bashunit::assert::fail_with "" "${json}" "to be valid JSON" ""
    return
  fi

  local exists
  if ! exists=$(printf '%s' "$json" | jq -r \
    "(path($key)) as \$path | if \$path == [] then true else any(paths; . == \$path) end" \
    2>/dev/null); then
    bashunit::assert::fail_with "" "${json}" "to use a valid key path" "${key}"
    return
  fi

  if [ "$exists" = true ]; then
    bashunit::assert::fail_with "" "${json}" "to not have key" "${key}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_contains() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local key="$1"
  local expected="$2"
  local json="$3"

  local result
  if ! result=$(printf '%s' "$json" | jq -e -r "$key" 2>/dev/null) || [ "$result" = "null" ]; then
    bashunit::assert::fail_with "" "${json}" "to have key" "${key}"
    return
  fi

  if [ "$result" != "$expected" ]; then
    bashunit::assert::fail_with "" "${expected}" "but got " "${result}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_equals() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local expected="$1"
  local actual="$2"

  local expected_sorted actual_valid=true expected_valid=true
  expected_sorted=$(printf '%s' "$expected" | jq -S '.' 2>/dev/null) || expected_valid=false
  local actual_sorted
  actual_sorted=$(printf '%s' "$actual" | jq -S '.' 2>/dev/null) || actual_valid=false

  if [ "$expected_valid" = false ] || [ "$actual_valid" = false ] ||
    [ "$expected_sorted" != "$actual_sorted" ]; then
    bashunit::assert::fail_with "" "${expected}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_length() {
  bashunit::assert::should_skip && return 0
  if [ "$#" -lt 3 ]; then
    bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, key, json" "$#"
    return 2
  fi

  local expected="$1"
  local key="$2"
  local json="$3"

  case "$expected" in
  '' | *[!0-9]*)
    bashunit::assert::usage_error_detail "${FUNCNAME[0]}" \
      "expects a non-negative integer length, got '$expected'"
    return 2
    ;;
  esac

  bashunit::assert_json::require_jq || return 0

  if ! printf '%s' "$json" | jq -e 'true' >/dev/null 2>&1; then
    bashunit::assert::fail_with "" "${json}" "to be valid JSON" ""
    return
  fi

  local exists
  if ! exists=$(printf '%s' "$json" | jq -r \
    "(path($key)) as \$path | if \$path == [] then true else any(paths; . == \$path) end" \
    2>/dev/null); then
    bashunit::assert::fail_with "" "${json}" "to use a valid key path" "${key}"
    return
  fi
  if [ "$exists" != true ]; then
    bashunit::assert::fail_with "" "${json}" "to have path" "${key}"
    return
  fi

  local actual
  local length_filter
  length_filter="$key | if type == \"array\" or type == \"object\" or type == \"string\""
  length_filter="$length_filter then length else error(\"unsupported type\") end"
  if ! actual=$(printf '%s' "$json" | jq -r "$length_filter" 2>/dev/null); then
    bashunit::assert::fail_with "" "${json}" "to have a measurable length at path" "${key}"
    return
  fi

  if [ "$actual" != "$expected" ]; then
    bashunit::assert::fail_with "" "${expected}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# src/assert/snapshot.sh

function bashunit::snapshot::normalize_actual() {
  local normalized="${1//$'\r'/}"
  while [ "${normalized%$'\n'}" != "$normalized" ]; do
    normalized="${normalized%$'\n'}"
  done
  _snapshot_normalized=$normalized
}

function assert_match_snapshot() {
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$1"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "${2:-}" "$test_fn"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
  local snapshot_name=$1
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$2"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {

  local stripped=$1
  case "$stripped" in
  *$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
  esac
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$stripped"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "${2:-}" "$test_fn"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
  local snapshot_name=$1

  local stripped=$2
  case "$stripped" in
  *$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
  esac
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$stripped"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function bashunit::snapshot::normalize_path() {
  local path="$1"
  while [ "${path#./}" != "$path" ]; do
    path="${path#./}"
  done
  while [ "${path%%//*}" != "$path" ]; do
    path="${path%%//*}/${path#*//}"
  done
  builtin echo "$path"
}

_BASHUNIT_SNAPSHOT_UNUSED_OUT=""
_BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT=0

function bashunit::snapshot::collect_unused() {
  local -a search_paths=()
  local path
  for path in "$@"; do
    if [ -d "$path" ]; then
      search_paths[${#search_paths[@]}]="$path"
    elif [ -f "$path" ]; then
      case "$path" in
      */*) search_paths[${#search_paths[@]}]="${path%/*}" ;;
      *) search_paths[${#search_paths[@]}]="." ;;
      esac
    fi
  done
  if [ "${#search_paths[@]}" -eq 0 ]; then
    search_paths[0]="."
  fi

  local owners=""
  for path in "$@"; do
    [ -f "$path" ] || continue
    bashunit::helper::normalize_variable_name_to_slot "${path##*/}"
    owners="$owners$_BASHUNIT_HELPER_VARNAME_OUT
"
  done

  local used=""
  if [ -f "${SNAPSHOT_USED_OUTPUT_PATH:-}" ]; then
    local used_path
    while IFS= read -r used_path; do
      [ -z "$used_path" ] && continue
      used="$used$(bashunit::snapshot::normalize_path "$used_path")
"
    done <"$SNAPSHOT_USED_OUTPUT_PATH"
  fi

  local unused=""
  local total=0
  local dir file normalized
  while IFS= read -r dir; do
    [ -z "$dir" ] && continue
    for file in "$dir"/*.snapshot; do
      [ -f "$file" ] || continue
      normalized="$(bashunit::snapshot::normalize_path "$file")"
      case "$used" in
      *"$normalized"$'\n'*) continue ;;
      esac
      local owner="${file##*/}"
      owner="${owner%%.*}"
      case "$owners" in
      *"$owner"$'\n'*) ;;
      *) continue ;;
      esac
      total=$((total + 1))
      unused="$unused  $normalized
"
    done
  done < <(find "${search_paths[@]}" -type d -name snapshots 2>/dev/null | sort -u)

  _BASHUNIT_SNAPSHOT_UNUSED_OUT="$unused"
  _BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT="$total"
}

function bashunit::snapshot::report_unused() {
  bashunit::snapshot::collect_unused "$@"

  if [ "$_BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT" -eq 0 ]; then
    printf "\n%sNo unused snapshots.%s\n" \
      "${_BASHUNIT_COLOR_FAINT}" "${_BASHUNIT_COLOR_DEFAULT}"
    return
  fi

  printf "\n%sUnused snapshots (%s), no test resolved them:%s\n%s" \
    "${_BASHUNIT_COLOR_SKIPPED}" "$_BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT" \
    "${_BASHUNIT_COLOR_DEFAULT}" "$_BASHUNIT_SNAPSHOT_UNUSED_OUT"
  printf "%sNothing was deleted. Run with --snapshot-prune to remove them.%s\n" \
    "${_BASHUNIT_COLOR_FAINT}" "${_BASHUNIT_COLOR_DEFAULT}"
}

function bashunit::snapshot::prune_unused() {
  if [ "${_BASHUNIT_TESTS_FAILED:-0}" -gt 0 ]; then
    printf "\n%sSnapshots were not pruned: the run has failing tests, so a%s\n" \
      "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    printf "%ssnapshot may be unresolved rather than unused.%s\n" \
      "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    return
  fi

  bashunit::snapshot::collect_unused "$@"

  if [ "$_BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT" -eq 0 ]; then
    printf "\n%sNo unused snapshots.%s\n" \
      "${_BASHUNIT_COLOR_FAINT}" "${_BASHUNIT_COLOR_DEFAULT}"
    return
  fi

  printf "\n%sDeleted %s unused snapshot(s):%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED}" "$_BASHUNIT_SNAPSHOT_UNUSED_COUNT_OUT" \
    "${_BASHUNIT_COLOR_DEFAULT}"

  local entry path
  while IFS= read -r entry; do

    path="${entry#"${entry%%[![:space:]]*}"}"
    [ -n "$path" ] || continue
    case "$path" in
    *.snapshot) ;;
    *) continue ;;
    esac
    [ -f "$path" ] || continue

    printf "  %s\n" "$path"
    rm -f "$path"
  done <<EOF
$_BASHUNIT_SNAPSHOT_UNUSED_OUT
EOF
}

function bashunit::snapshot::assert() {
  local actual="$1"
  local snapshot_file="$2"
  local test_fn="$3"

  if bashunit::env::is_snapshot_report_unused_enabled ||
    bashunit::env::is_snapshot_prune_enabled; then
    printf '%s\n' "$snapshot_file" >>"${SNAPSHOT_USED_OUTPUT_PATH:-/dev/null}" 2>/dev/null || true
  fi

  if [ ! -f "$snapshot_file" ]; then
    if ! bashunit::env::is_snapshot_create_enabled; then
      bashunit::snapshot::fail_missing "$snapshot_file" "$test_fn"
      return
    fi
    bashunit::snapshot::initialize "$snapshot_file" "$actual"
    return
  fi

  if bashunit::snapshot::update "$snapshot_file" "$actual"; then
    return
  fi

  bashunit::snapshot::compare "$actual" "$snapshot_file" "$test_fn"
}

function bashunit::snapshot::fail_missing() {
  local path="$1"
  local func_name="$2"
  local label
  label=$(bashunit::helper::normalize_test_function_name "$func_name")

  bashunit::state::add_assertions_failed
  bashunit::console_results::print_failed_test "$label" "$path" \
    "does not exist; record it with a run without" "--no-snapshot-create"
}

function bashunit::snapshot::match_with_placeholder() {
  local actual="$1"
  local snapshot="$2"
  local placeholder="${BASHUNIT_SNAPSHOT_PLACEHOLDER:-::ignore::}"
  local token="__BASHUNIT_IGNORE__"

  local sanitized="${snapshot//$placeholder/$token}"
  local escaped=$(printf '%s' "$sanitized" | sed -e 's/[.[\\^$*+?{}()|]/\\&/g')
  local regex="^${escaped//$token/(.|\\n)*}$"

  if command -v perl >/dev/null 2>&1; then
    printf '%s' "$actual" | REGEX="$regex" perl -0 -e '
      my $r = $ENV{REGEX};
      my $input = join("", <STDIN>);
      exit($input =~ /$r/s ? 0 : 1);
    ' && return 0 || return 1
  fi

  if bashunit::dependencies::has_awk; then
    printf '%s' "$actual" | REGEX="$regex" awk '
      BEGIN { RS = "\001"; re = ENVIRON["REGEX"] }
      { exit !($0 ~ re) }
    ' && return 0 || return 1
  fi

  printf '%sCannot match a snapshot placeholder: neither perl nor awk is available.%s\n' \
    "${_BASHUNIT_COLOR_FAILED:-}" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
  return 1
}

_BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
  local file_hint="$1"
  local func_name="$2"
  local snapshot_name="${3:-}"

  if [ -n "$file_hint" ]; then
    _BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
    return
  fi

  local src="${4:-${BASH_SOURCE[2]}}"
  local dir_part
  case "$src" in
  */*) dir_part="${src%/*}" ;;
  *) dir_part="." ;;
  esac
  local base_part="${src##*/}"

  bashunit::helper::normalize_variable_name_to_slot "$base_part"
  local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
  bashunit::helper::normalize_variable_name_to_slot "$func_name"
  local name=$_BASHUNIT_HELPER_VARNAME_OUT
  if [ -n "$snapshot_name" ]; then
    bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
    name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
  fi

  case "$dir_part" in
  /*) _BASHUNIT_SNAPSHOT_FILE_OUT="${dir_part}/snapshots/${test_file}.${name}.snapshot" ;;
  *) _BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot" ;;
  esac
}

function bashunit::snapshot::initialize() {
  local path="$1"
  local content="$2"
  mkdir -p "$(dirname "$path")"
  echo "$content" >"$path"
  bashunit::state::add_assertions_snapshot
}

function bashunit::snapshot::update() {
  local path="$1"
  local actual="$2"

  bashunit::env::is_snapshot_update_enabled || return 1

  local placeholder="${BASHUNIT_SNAPSHOT_PLACEHOLDER:-::ignore::}"
  local snapshot
  snapshot=$(<"$path")
  case "$snapshot" in
  *"$placeholder"*)
    printf "%sNot updating %s: it contains the placeholder '%s'.%s\n" \
      "${_BASHUNIT_COLOR_SKIPPED:-}" "$path" "$placeholder" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
    return 1
    ;;
  esac

  echo "$actual" >"$path"
  bashunit::state::add_assertions_snapshot
  return 0
}

function bashunit::snapshot::compare() {
  local actual="$1"
  local snapshot_path="$2"
  local func_name="$3"

  local snapshot
  snapshot=$(<"$snapshot_path")
  snapshot="${snapshot//$'\r'/}"

  if [ "$actual" = "$snapshot" ]; then
    bashunit::state::add_assertions_passed
    return
  fi

  local placeholder="${BASHUNIT_SNAPSHOT_PLACEHOLDER:-::ignore::}"
  case "$snapshot" in
  *"$placeholder"*)
    if bashunit::snapshot::match_with_placeholder "$actual" "$snapshot"; then
      bashunit::state::add_assertions_passed
      return
    fi
    ;;
  esac

  local label=$(bashunit::helper::normalize_test_function_name "$func_name")
  bashunit::state::add_assertions_failed
  bashunit::console_results::print_failed_snapshot_test "$label" "$snapshot_path" "$actual"
  return 1
}

# src/doubles/index.sh

# src/doubles/mock.sh

declare -a _BASHUNIT_MOCKED_FUNCTIONS=()

function bashunit::unmock() {
  local command=$1

  if [ "${#_BASHUNIT_MOCKED_FUNCTIONS[@]}" -eq 0 ]; then
    return
  fi

  local i
  for i in "${!_BASHUNIT_MOCKED_FUNCTIONS[@]}"; do
    if [ "${_BASHUNIT_MOCKED_FUNCTIONS[$i]:-}" = "$command" ]; then
      unset "_BASHUNIT_MOCKED_FUNCTIONS[$i]"
      unset -f "$command"

      bashunit::sandbox::restore_shim "$command"
      local variable
      variable="$(bashunit::helper::normalize_variable_name "$command")"
      local times_file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
      local params_file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
      local sequence_file_var="_BASHUNIT_MOCK_${variable}_SEQUENCE_FILE"
      [ -f "${!sequence_file_var-}" ] && rm -f "${!sequence_file_var}"
      unset "$sequence_file_var"
      [ -f "${!times_file_var-}" ] && rm -f "${!times_file_var}"
      [ -f "${!params_file_var-}" ] && rm -f "${!params_file_var}"
      unset "$times_file_var"
      unset "$params_file_var"
      break
    fi
  done
}

function bashunit::doubles::is_exit_code() {
  case "$1" in
  '' | *[!0-9]*) return 1 ;;
  esac
  return 0
}

function bashunit::doubles::refuse_unusable_name() {
  local fn=$1 command=$2

  case "$command" in
  '')
    bashunit::assert::fail_with "" "$fn" "expects a command name, got" "nothing"
    return 0
    ;;
  *[[:space:]\;\|\&\(\)\{\}\<\>\"\'\`]*)

    bashunit::assert::fail_with "" "$command" \
      "is not a usable command name for $fn; name the command alone, as in" "$fn ls"
    return 0
    ;;
  esac

  return 1
}

function bashunit::mock() {
  local command=$1
  shift

  if bashunit::doubles::refuse_unusable_name "bashunit::mock" "$command"; then
    return 1
  fi

  if [ $# -eq 1 ] && bashunit::doubles::is_exit_code "$1"; then
    eval "function $command() { return $1; }"
  elif [ $# -gt 0 ]; then
    eval "function $command() { $* \"\$@\"; }"
  else
    eval "function $command() { builtin echo \"$($CAT)\" ; }"
  fi

  export -f "${command?}"

  _BASHUNIT_MOCKED_FUNCTIONS[${#_BASHUNIT_MOCKED_FUNCTIONS[@]}]="$command"
}

function bashunit::mock_sequence() {
  local command=$1
  shift

  if bashunit::doubles::refuse_unusable_name "bashunit::mock_sequence" "$command"; then
    return 1
  fi

  if [ $# -eq 0 ]; then
    bashunit::assert::usage_error_detail "bashunit::mock_sequence" \
      "expects at least one answer after the command"
    return 2
  fi

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT

  local test_id="${BASHUNIT_CURRENT_TEST_ID:-global}"
  local step_file
  step_file=$(bashunit::temp_file "${test_id}_${variable}_sequence")
  builtin echo 1 >"$step_file"
  export "_BASHUNIT_MOCK_${variable}_SEQUENCE_FILE"="$step_file"

  local times_file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
  local params_file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local record=""
  if [ -n "${!times_file_var-}" ]; then
    record="bashunit::doubles::record_call '${!params_file_var}' '${!times_file_var}' \"\$@\""
  fi

  local total=$#
  local index=0
  local arms=""
  local entry body
  for entry in "$@"; do
    index=$((index + 1))
    if bashunit::doubles::is_exit_code "$entry"; then
      body="return $entry"
    else
      body="$entry \"\$@\""
    fi

    if [ "$index" -eq "$total" ]; then
      arms="$arms
    *) $body ;;"
    else
      arms="$arms
    $index) $body ;;"
    fi
  done

  eval "function $command() {
    $record
    local _step=1
    read -r _step < '$step_file' 2>/dev/null || _step=1
    case \"\$_step\" in '' | *[!0-9]*) _step=1 ;; esac
    if [ \"\$_step\" -lt $total ]; then
      builtin echo \"\$((_step + 1))\" > '$step_file'
    fi
    case \"\$_step\" in$arms
    esac
  }"

  export -f "${command?}"

  export -f bashunit::doubles::record_call

  _BASHUNIT_MOCKED_FUNCTIONS[${#_BASHUNIT_MOCKED_FUNCTIONS[@]}]="$command"
}

# src/doubles/spy.sh

_BASHUNIT_SPY_TIMES_OUT=0
_BASHUNIT_SPY_REGISTERED_OUT=false

function bashunit::spy::times_to_slot() {
  local command="$1"
  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
  _BASHUNIT_SPY_TIMES_OUT=0
  _BASHUNIT_SPY_REGISTERED_OUT=false
  if [ -n "${!file_var-}" ]; then
    _BASHUNIT_SPY_REGISTERED_OUT=true
  fi
  if [ -f "${!file_var-}" ]; then

    local times_line=""
    read -r times_line <"${!file_var}" 2>/dev/null || times_line=""
    case "$times_line" in
    '' | *[!0-9]*) _BASHUNIT_SPY_TIMES_OUT=0 ;;
    *) _BASHUNIT_SPY_TIMES_OUT=$times_line ;;
    esac
  fi
}

_BASHUNIT_SPY_CALL_OUT=""
_BASHUNIT_SPY_CALL_TOTAL_OUT=0

function bashunit::spy::read_call_to_slots() {
  local file=$1
  local index=${2:-}
  _BASHUNIT_SPY_CALL_OUT=""
  _BASHUNIT_SPY_CALL_TOTAL_OUT=0

  if [ -z "$file" ] || [ ! -f "$file" ]; then
    return
  fi

  local current
  while IFS= read -r current; do
    _BASHUNIT_SPY_CALL_TOTAL_OUT=$((_BASHUNIT_SPY_CALL_TOTAL_OUT + 1))
    if [ -z "$index" ] || [ "$_BASHUNIT_SPY_CALL_TOTAL_OUT" = "$index" ]; then
      _BASHUNIT_SPY_CALL_OUT=$current
    fi
  done <"$file"
}

function bashunit::spy::compared_call() {
  if [ -n "$1" ]; then
    builtin echo "call $1 of $2"
  elif [ "$2" -eq 1 ]; then
    builtin echo "the only call"
  else
    builtin echo "the last of $2 calls"
  fi
}

_BASHUNIT_SPY_CALL_LOG_MAX=10
_BASHUNIT_SPY_CALL_LOG_OUT=""

function bashunit::spy::call_log_to_slot() {
  local command=$1
  local field=${2:-raw}
  _BASHUNIT_SPY_CALL_LOG_OUT=""

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  if [ -z "${!file_var-}" ] || [ ! -f "${!file_var}" ]; then
    return
  fi

  local entries=""
  local total=0
  local shown=0
  local line value
  while IFS= read -r line; do
    total=$((total + 1))
    if [ "$shown" -lt "$_BASHUNIT_SPY_CALL_LOG_MAX" ]; then
      if [ "$field" = args ]; then
        value=${line#*$'\x1e'}
        value=${value//$'\x1f'/ }
      else
        value=${line%%$'\x1e'*}
      fi
      entries="$entries
      ${_BASHUNIT_COLOR_FAINT}${total}:${_BASHUNIT_COLOR_DEFAULT} ${value}"
      shown=$((shown + 1))
    fi
  done <"${!file_var}"

  if [ "$total" -eq 0 ]; then
    return
  fi

  _BASHUNIT_SPY_CALL_LOG_OUT="\
    ${_BASHUNIT_COLOR_FAINT}Recorded calls to '${command}' (${total}):\
${_BASHUNIT_COLOR_DEFAULT}${entries}"

  local remaining=$((total - shown))
  if [ "$remaining" -gt 0 ]; then
    _BASHUNIT_SPY_CALL_LOG_OUT="$_BASHUNIT_SPY_CALL_LOG_OUT
      ${_BASHUNIT_COLOR_FAINT}… and ${remaining} more${_BASHUNIT_COLOR_DEFAULT}"
  fi
}

function bashunit::spy::fail_unregistered() {
  bashunit::state::add_assertions_failed
  bashunit::console_results::print_failed_test "$2" "$1" \
    "was never registered as a spy; call it first with" "bashunit::spy $1"
}

_BASHUNIT_SPY_SERIALIZED_OUT=""

function bashunit::spy::serialize_args_to_slot() {
  local serialized=""
  local arg
  for arg in "$@"; do
    serialized="$serialized$(builtin printf '%q' "$arg")"$'\x1f'
  done
  _BASHUNIT_SPY_SERIALIZED_OUT=${serialized%$'\x1f'}
}

function bashunit::doubles::record_call() {
  local params_file=$1
  local times_file=$2
  shift 2

  local raw="$*"
  local serialized=""
  local arg
  for arg in "$@"; do
    serialized="$serialized$(builtin printf '%q' "$arg")"$'\x1f'
  done
  serialized=${serialized%$'\x1f'}
  builtin printf '%s\x1e%s\n' "$raw" "$serialized" >>"$params_file"

  local count=""
  read -r count <"$times_file" 2>/dev/null || count=""
  case "$count" in '' | *[!0-9]*) count=0 ;; esac
  builtin echo "$((count + 1))" >"$times_file"
}

function bashunit::spy() {
  local command=$1
  local exit_code_or_impl="${2:-}"

  if bashunit::doubles::refuse_unusable_name "bashunit::spy" "$command"; then
    return 1
  fi

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT

  local times_file params_file
  local test_id="${BASHUNIT_CURRENT_TEST_ID:-global}"
  times_file=$(bashunit::temp_file "${test_id}_${variable}_times")
  params_file=$(bashunit::temp_file "${test_id}_${variable}_params")
  echo 0 >"$times_file"
  : >"$params_file"
  export "_BASHUNIT_SPY_${variable}_TIMES_FILE"="$times_file"
  export "_BASHUNIT_SPY_${variable}_PARAMS_FILE"="$params_file"

  local body_suffix=""
  if bashunit::doubles::is_exit_code "$exit_code_or_impl"; then
    body_suffix="return $exit_code_or_impl"
  elif [ -n "$exit_code_or_impl" ]; then
    body_suffix="$exit_code_or_impl \"\$@\""
  fi

  eval "function $command() {
    bashunit::doubles::record_call '$params_file' '$times_file' \"\$@\"
    $body_suffix
  }"

  export -f "${command?}"

  export -f bashunit::doubles::record_call

  _BASHUNIT_MOCKED_FUNCTIONS[${#_BASHUNIT_MOCKED_FUNCTIONS[@]}]="$command"
}

# src/doubles/assertions.sh

function assert_have_been_called() {
  local command=$1
  bashunit::spy::times_to_slot "$command"
  local times=$_BASHUNIT_SPY_TIMES_OUT
  local label="${2:-}"
  if [ -z "$label" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
    label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  fi

  if [ "$_BASHUNIT_SPY_REGISTERED_OUT" = false ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  if [ "$times" -eq 0 ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "${label}" "${command}" "to have been called" "once" \
      "" "" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_with() {
  local command=$1
  shift

  local index=""

  case "${!#}" in
  '' | *[!0-9]*) ;;
  *)
    index=${!#}
    set -- "${@:1:$#-1}"
    ;;
  esac

  local expected="$*"

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local label
  bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
  label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  if [ -z "${!file_var-}" ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  bashunit::spy::read_call_to_slots "${!file_var-}" "$index"
  local raw=${_BASHUNIT_SPY_CALL_OUT%%$'\x1e'*}
  local total=$_BASHUNIT_SPY_CALL_TOTAL_OUT

  if [ "$expected" != "$raw" ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "$label" "$expected" "but got " "$raw" \
      "compared" "$(bashunit::spy::compared_call "$index" "$total")" \
      "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_with_args() {
  local command=$1
  shift

  bashunit::spy::serialize_args_to_slot "$@"
  local expected=$_BASHUNIT_SPY_SERIALIZED_OUT

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local label
  bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
  label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  if [ -z "${!file_var-}" ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  bashunit::spy::read_call_to_slots "${!file_var-}"
  local actual=""
  case "$_BASHUNIT_SPY_CALL_OUT" in
  *$'\x1e'*) actual=${_BASHUNIT_SPY_CALL_OUT#*$'\x1e'} ;;
  esac

  if [ "$expected" != "$actual" ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command" args
    bashunit::console_results::print_failed_test "$label" \
      "${expected//$'\x1f'/ }" "but got " "${actual//$'\x1f'/ }" \
      "compared" "$(bashunit::spy::compared_call "" "$_BASHUNIT_SPY_CALL_TOTAL_OUT")" \
      "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_with_any() {
  local command=$1
  shift
  local expected="$*"

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local label
  bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
  label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  if [ -z "${!file_var-}" ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  local total=0
  local found=false
  local line
  if [ -f "${!file_var}" ]; then
    while IFS= read -r line; do
      total=$((total + 1))
      if [ "${line%%$'\x1e'*}" = "$expected" ]; then
        found=true
        break
      fi
    done <"${!file_var}"
  fi

  if [ "$found" = false ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "$label" "$expected" \
      "not found in any of" "${total} calls" "" "" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_times() {
  local expected_count=$1
  local command=$2

  case "$expected_count" in
  '' | *[!0-9]*)
    bashunit::assert::usage_error_detail "${FUNCNAME[0]}" \
      "expects a numeric count first (expected_count, command), got '$expected_count'"
    return 2
    ;;
  esac
  bashunit::spy::times_to_slot "$command"
  local times=$_BASHUNIT_SPY_TIMES_OUT
  local label="${3:-}"
  if [ -z "$label" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
    label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  fi

  if [ "$_BASHUNIT_SPY_REGISTERED_OUT" = false ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  if [ "$times" -ne "$expected_count" ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "${label}" "${command}" \
      "to have been called" "${expected_count} times" \
      "actual" "${times} times" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_nth_with() {
  local nth=$1

  case "$nth" in
  '' | *[!0-9]*)
    bashunit::assert::usage_error_detail "${FUNCNAME[0]}" \
      "expects a numeric call index first (nth, command, expected_args), got '$nth'"
    return 2
    ;;
  esac
  local command=$2
  shift 2
  local expected="$*"

  local variable
  bashunit::helper::normalize_variable_name_to_slot "$command"
  variable=$_BASHUNIT_HELPER_VARNAME_OUT
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local label
  bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
  label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  bashunit::spy::times_to_slot "$command"
  local times=$_BASHUNIT_SPY_TIMES_OUT

  if [ "$_BASHUNIT_SPY_REGISTERED_OUT" = false ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  if [ "$nth" -gt "$times" ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "${label}" \
      "expected call" "at index ${nth} but" "only called ${times} times" \
      "" "" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  local line=""
  if [ -f "${!file_var-}" ]; then
    line=$(sed -n "${nth}p" "${!file_var}" 2>/dev/null || true)
  fi

  local raw
  IFS=$'\x1e' read -r raw _ <<<"$line" || true

  if [ "$expected" != "$raw" ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    bashunit::console_results::print_failed_test "${label}" \
      "$expected" "but got " "$raw" "" "" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_never_been_called() {
  local command=$1
  bashunit::spy::times_to_slot "$command"
  local times=$_BASHUNIT_SPY_TIMES_OUT
  local label="${2:-}"
  if [ -z "$label" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
    label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  fi

  if [ "$_BASHUNIT_SPY_REGISTERED_OUT" = false ]; then
    bashunit::spy::fail_unregistered "$command" "$label"
    return
  fi

  if [ "$times" -ne 0 ]; then
    bashunit::state::add_assertions_failed
    bashunit::spy::call_log_to_slot "$command"
    local unit="times"
    if [ "$times" -eq 1 ]; then
      unit="time"
    fi
    bashunit::console_results::print_failed_test "${label}" "${command}" \
      "to have never been called" "" \
      "actual" "${times} ${unit}" "$_BASHUNIT_SPY_CALL_LOG_OUT"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_called() {
  local command=$1
  local label="${2:-}"
  if [ -z "$label" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "${FUNCNAME[1]}"
    label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  fi
  assert_have_been_called_times 0 "$command" "$label"
}

# src/reports/index.sh

# src/reports/collect.sh

function bashunit::reports::__strip_ansi() {
  printf '%s' "$1" | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g'
}

_BASHUNIT_REPORTS_TEST_FILES=()
_BASHUNIT_REPORTS_TEST_NAMES=()
_BASHUNIT_REPORTS_TEST_STATUSES=()
_BASHUNIT_REPORTS_TEST_DURATIONS=()
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
_BASHUNIT_REPORTS_TEST_FAILURES=()
_BASHUNIT_REPORTS_TEST_LINES=()
_BASHUNIT_REPORTS_TEST_RETRIES=()
_BASHUNIT_REPORTS_TEST_OUTPUTS=()

_BASHUNIT_REPORTS_CURRENT_OUTPUT=""

function bashunit::reports::set_current_test_output() {
  _BASHUNIT_REPORTS_CURRENT_OUTPUT="$1"
}

function bashunit::reports::add_test_snapshot() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "snapshot"
}

function bashunit::reports::add_test_incomplete() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "incomplete"
}

function bashunit::reports::add_test_skipped() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "skipped"
}

function bashunit::reports::add_test_passed() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "passed"
}

function bashunit::reports::add_test_risky() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "risky"
}

function bashunit::reports::add_test_failed() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "failed" "$5"
}

function bashunit::reports::add_test_flaky() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "flaky" "$5" "$6"
}

function bashunit::reports::is_enabled() {
  [ -n "${BASHUNIT_LOG_JUNIT:-}" ] ||
    [ -n "${BASHUNIT_REPORT_HTML:-}" ] ||
    [ -n "${BASHUNIT_LOG_GHA:-}" ] ||
    [ -n "${BASHUNIT_REPORT_TAP:-}" ] ||
    [ -n "${BASHUNIT_REPORT_JSON:-}" ] ||
    [ -n "${BASHUNIT_REPORT_MD:-}" ] ||
    bashunit::env::is_json_output_enabled ||
    bashunit::env::is_junit_output_enabled ||
    bashunit::env::should_append_step_summary ||
    bashunit::env::should_print_gha_annotations
}

function bashunit::reports::add_test() {

  bashunit::reports::is_enabled || return 0

  local file="$1"
  local test_name="$2"
  local duration="$3"
  local assertions="$4"
  local status="$5"
  local failure_message="${6:-}"
  local retries="${7:-0}"
  local test_output="$_BASHUNIT_REPORTS_CURRENT_OUTPUT"
  _BASHUNIT_REPORTS_CURRENT_OUTPUT=""

  local line=""
  case "${_BASHUNIT_TEST_LOCATION:-}" in
  "$file":*) line="${_BASHUNIT_TEST_LOCATION##*:}" ;;
  esac

  if bashunit::parallel::is_enabled; then
    printf '%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
      "$(bashunit::helper::encode_base64 "$file")" \
      "$(bashunit::helper::encode_base64 "$test_name")" \
      "$(bashunit::helper::encode_base64 "$status")" \
      "$(bashunit::helper::encode_base64 "$duration")" \
      "$(bashunit::helper::encode_base64 "$assertions")" \
      "$(bashunit::helper::encode_base64 "$failure_message")" \
      "$(bashunit::helper::encode_base64 "$line")" \
      "$(bashunit::helper::encode_base64 "$retries")" \
      "$(bashunit::helper::encode_base64 "$test_output")" \
      >>"${REPORTS_OUTPUT_PATH:-/dev/null}" 2>/dev/null || true
  fi

  _BASHUNIT_REPORTS_TEST_FILES[${#_BASHUNIT_REPORTS_TEST_FILES[@]}]="$file"
  _BASHUNIT_REPORTS_TEST_NAMES[${#_BASHUNIT_REPORTS_TEST_NAMES[@]}]="$test_name"
  _BASHUNIT_REPORTS_TEST_STATUSES[${#_BASHUNIT_REPORTS_TEST_STATUSES[@]}]="$status"
  _BASHUNIT_REPORTS_TEST_ASSERTIONS[${#_BASHUNIT_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
  _BASHUNIT_REPORTS_TEST_DURATIONS[${#_BASHUNIT_REPORTS_TEST_DURATIONS[@]}]="$duration"
  _BASHUNIT_REPORTS_TEST_FAILURES[${#_BASHUNIT_REPORTS_TEST_FAILURES[@]}]="$failure_message"
  _BASHUNIT_REPORTS_TEST_LINES[${#_BASHUNIT_REPORTS_TEST_LINES[@]}]="$line"
  _BASHUNIT_REPORTS_TEST_RETRIES[${#_BASHUNIT_REPORTS_TEST_RETRIES[@]}]="$retries"
  _BASHUNIT_REPORTS_TEST_OUTPUTS[${#_BASHUNIT_REPORTS_TEST_OUTPUTS[@]}]="$test_output"
}

function bashunit::reports::load_spooled() {
  bashunit::reports::is_enabled || return 0
  [ -f "${REPORTS_OUTPUT_PATH:-}" ] || return 0

  local file test_name status duration assertions failure_message line retries test_output n
  while IFS='|' read -r file test_name status duration assertions failure_message line retries test_output; do
    [ -n "$file" ] || continue
    local n=${#_BASHUNIT_REPORTS_TEST_FILES[@]}
    _BASHUNIT_REPORTS_TEST_FILES[n]=$(bashunit::helper::decode_base64 "$file")
    _BASHUNIT_REPORTS_TEST_NAMES[n]=$(bashunit::helper::decode_base64 "$test_name")
    _BASHUNIT_REPORTS_TEST_STATUSES[n]=$(bashunit::helper::decode_base64 "$status")
    _BASHUNIT_REPORTS_TEST_DURATIONS[n]=$(bashunit::helper::decode_base64 "$duration")
    _BASHUNIT_REPORTS_TEST_ASSERTIONS[n]=$(bashunit::helper::decode_base64 "$assertions")
    _BASHUNIT_REPORTS_TEST_FAILURES[n]=$(bashunit::helper::decode_base64 "$failure_message")
    _BASHUNIT_REPORTS_TEST_LINES[n]=$(bashunit::helper::decode_base64 "$line")
    _BASHUNIT_REPORTS_TEST_RETRIES[n]=$(bashunit::helper::decode_base64 "$retries")
    _BASHUNIT_REPORTS_TEST_OUTPUTS[n]=$(bashunit::helper::decode_base64 "$test_output")
  done <"$REPORTS_OUTPUT_PATH"
}

# src/reports/junit.sh

function bashunit::reports::__xml_escape() {
  local text="$1"

  bashunit::reports::__strip_ansi "$text" |
    tr -d '\000-\010\013\014\016-\037' |
    sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
}

_BASHUNIT_REPORTS_MS_TO_S_OUT=""
function bashunit::reports::__ms_to_s() {
  local ms="${1:-0}"
  case "$ms" in
  '' | *[!0-9]*) ms=0 ;;
  esac
  _BASHUNIT_REPORTS_MS_TO_S_OUT="$((ms / 1000)).$(printf '%03d' "$((ms % 1000))")"
}

_BASHUNIT_REPORTS_CLASSNAME_OUT=""
function bashunit::reports::__junit_classname() {
  local path="$1"
  path="${path#./}"
  path="${path%.sh}"
  _BASHUNIT_REPORTS_CLASSNAME_OUT="${path//\//.}"
}

function bashunit::reports::generate_junit_xml() {
  bashunit::reports::print_junit_xml >"$1"
}

function bashunit::reports::print_junit_xml() {
  local timestamp
  timestamp=$(date '+%Y-%m-%dT%H:%M:%S')

  local suite_files suite_tests suite_failures suite_skipped suite_time cases
  suite_files=()
  suite_tests=()
  suite_failures=()
  suite_skipped=()
  suite_time=()
  cases=()

  local total_tests="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}"
  local total_failures=0
  local total_skipped=0
  local total_time_ms=0

  local i j s
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
    local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
    local duration_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-0}"
    local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
    local test_output="${_BASHUNIT_REPORTS_TEST_OUTPUTS[$i]:-}"
    case "$duration_ms" in '' | *[!0-9]*) duration_ms=0 ;; esac

    s=-1
    for j in ${suite_files[@]+"${!suite_files[@]}"}; do
      if [ "${suite_files[$j]}" = "$file" ]; then
        s=$j
        break
      fi
    done
    if [ "$s" -eq -1 ]; then
      s=${#suite_files[@]}
      suite_files[s]="$file"
      suite_tests[s]=0
      suite_failures[s]=0
      suite_skipped[s]=0
      suite_time[s]=0
      cases[s]=""
    fi

    suite_tests[s]=$((suite_tests[s] + 1))
    suite_time[s]=$((suite_time[s] + duration_ms))
    total_time_ms=$((total_time_ms + duration_ms))

    local test_time escaped_name classname
    bashunit::reports::__ms_to_s "$duration_ms"
    test_time=$_BASHUNIT_REPORTS_MS_TO_S_OUT
    escaped_name=$(bashunit::reports::__xml_escape "$name")
    bashunit::reports::__junit_classname "$file"
    classname=$_BASHUNIT_REPORTS_CLASSNAME_OUT

    local case_xml="    <testcase classname=\"$classname\" name=\"$escaped_name\"
        file=\"$file\" time=\"$test_time\">
"

    if [ "$status" = "failed" ]; then
      suite_failures[s]=$((suite_failures[s] + 1))
      total_failures=$((total_failures + 1))
      local escaped_message plain_message msg_head
      escaped_message=$(bashunit::reports::__xml_escape "$failure_message")

      plain_message=$(bashunit::reports::__strip_ansi "$failure_message")
      msg_head="${plain_message%%$'\n'*}"
      case "$msg_head" in
      '✗ Failed:'* | '✗ Error:'*)
        local msg_rest="${plain_message#*$'\n'}"
        if [ "$msg_rest" != "$plain_message" ]; then
          msg_head="${msg_rest%%$'\n'*}"
          msg_head="${msg_head#"${msg_head%%[![:space:]]*}"}"
        fi
        ;;
      esac
      local first_line
      first_line=$(bashunit::reports::__xml_escape "$msg_head")
      case_xml="$case_xml      <failure message=\"$first_line\" type=\"AssertionFailed\">$escaped_message</failure>
"
    elif [ "$status" = "flaky" ]; then

      local escaped_flaky
      escaped_flaky=$(bashunit::reports::__xml_escape "$failure_message")
      case_xml="$case_xml      <flakyFailure message=\"Test passed after ${_BASHUNIT_REPORTS_TEST_RETRIES[$i]:-0} \
retries\">$escaped_flaky</flakyFailure>
"
    elif [ "$status" = "risky" ]; then
      suite_skipped[s]=$((suite_skipped[s] + 1))
      total_skipped=$((total_skipped + 1))
      case_xml="$case_xml      <skipped message=\"Test has no assertions (risky)\"/>
"
    elif [ "$status" = "skipped" ]; then
      suite_skipped[s]=$((suite_skipped[s] + 1))
      total_skipped=$((total_skipped + 1))
      case_xml="$case_xml      <skipped/>
"
    elif [ "$status" = "incomplete" ]; then
      suite_skipped[s]=$((suite_skipped[s] + 1))
      total_skipped=$((total_skipped + 1))
      case_xml="$case_xml      <skipped message=\"Test incomplete\"/>
"
    fi

    if [ -n "$test_output" ]; then
      local escaped_output
      escaped_output=$(bashunit::reports::__xml_escape "$test_output")
      case_xml="$case_xml      <system-out>$escaped_output</system-out>
"
    fi

    cases[s]="${cases[s]}$case_xml    </testcase>
"
  done

  local total_time
  bashunit::reports::__ms_to_s "$total_time_ms"
  total_time=$_BASHUNIT_REPORTS_MS_TO_S_OUT

  {
    echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
    echo "<testsuites name=\"bashunit\" tests=\"$total_tests\" failures=\"$total_failures\"" \
      "skipped=\"$total_skipped\" errors=\"0\" time=\"$total_time\">"

    for s in ${suite_files[@]+"${!suite_files[@]}"}; do
      local suite_time_s
      bashunit::reports::__ms_to_s "${suite_time[$s]}"
      suite_time_s=$_BASHUNIT_REPORTS_MS_TO_S_OUT
      echo "  <testsuite name=\"${suite_files[$s]}\" tests=\"${suite_tests[$s]}\"" \
        "failures=\"${suite_failures[$s]}\" skipped=\"${suite_skipped[$s]}\" errors=\"0\"" \
        "time=\"$suite_time_s\" timestamp=\"$timestamp\">"
      printf '%s' "${cases[$s]}"
      echo "  </testsuite>"
    done

    echo "</testsuites>"
  }
}

# src/reports/tap.sh

function bashunit::reports::__tap_message() {
  bashunit::reports::__strip_ansi "$1" |
    tr '\n' ' ' |
    sed -e "s/'/''/g"
}

function bashunit::reports::__tap_description() {
  local text="$1"
  text="${text//\\/\\\\}"

  printf '%s' "${text//[#]/\\#}"
}

function bashunit::reports::generate_report_tap() {
  local output_file="$1"
  local total="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}"

  {
    echo "TAP version 13"
    echo "1..$total"

    local i seq=0
    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      seq=$((seq + 1))
      local name
      name="$(bashunit::reports::__tap_description "${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}")"
      local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
      local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"

      case "$status" in
      failed)
        echo "not ok $seq - $name"
        echo "  ---"
        echo "  message: '$(bashunit::reports::__tap_message "$failure_message")'"
        echo "  ..."
        ;;
      skipped)
        echo "ok $seq - $name # SKIP"
        ;;
      risky)
        echo "ok $seq - $name # SKIP risky (no assertions)"
        ;;
      incomplete)
        echo "ok $seq - $name # TODO"
        ;;
      flaky)

        echo "ok $seq - $name # TODO flaky (retried ${_BASHUNIT_REPORTS_TEST_RETRIES[$i]:-0}/${BASHUNIT_RETRY:-0})"
        ;;
      *)
        echo "ok $seq - $name"
        ;;
      esac
    done
  } >"$output_file"
}

# src/reports/json.sh

function bashunit::reports::__json_escape() {
  local text="$1"
  text=$(bashunit::reports::__strip_ansi "$text" | tr -d '\000-\010\013\014\016-\037')

  text="${text//\\/\\\\}"
  text="${text//\"/\\\"}"
  text="${text//$'\t'/\\t}"
  text="${text//$'\r'/\\r}"
  text="${text//$'\n'/\\n}"
  printf '%s' "$text"
}

function bashunit::reports::generate_report_json() {
  bashunit::reports::print_report_json >"$1"
}

function bashunit::reports::print_report_json() {
  local total="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}"

  local passed=0 failed=0 skipped=0 incomplete=0 flaky=0 duration_total=0
  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    duration_total=$((duration_total + ${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-0}))
    case "${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}" in
    failed) failed=$((failed + 1)) ;;
    skipped) skipped=$((skipped + 1)) ;;
    incomplete) incomplete=$((incomplete + 1)) ;;

    flaky)
      flaky=$((flaky + 1))
      passed=$((passed + 1))
      ;;

    *) passed=$((passed + 1)) ;;
    esac
  done

  {
    printf '{\n'
    printf '  "summary": { "total": %d, "passed": %d, "failed": %d,' \
      "$total" "$passed" "$failed"
    printf ' "skipped": %d, "incomplete": %d, "flaky": %d, "duration_ms": %d },\n' \
      "$skipped" "$incomplete" "$flaky" "$duration_total"
    printf '  "tests": [\n'
    local seq=0
    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      local file name status duration message sep
      file=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}")
      name=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}")
      status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
      duration="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-0}"
      message=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}")
      sep=","
      [ "$seq" -eq "$((total - 1))" ] && sep=""
      printf '    { "file": "%s", "name": "%s", "status": "%s", "duration_ms": %d,' \
        "$file" "$name" "$status" "$duration"
      printf ' "retries": %d, "message": "%s" }%s\n' \
        "${_BASHUNIT_REPORTS_TEST_RETRIES[$i]:-0}" "$message" "$sep"
      seq=$((seq + 1))
    done
    printf '  ]\n'
    printf '}\n'
  }
}

# src/reports/gha.sh

function bashunit::reports::__gha_encode() {
  local text="$1"
  text=$(bashunit::reports::__strip_ansi "$text")

  text="${text//[%]/%25}"
  text="${text//$'\r'/%0D}"
  text="${text//$'\n'/%0A}"
  printf '%s' "$text"
}

function bashunit::reports::print_gha_annotations() {
  local only="${1:-all}"

  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
    local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
    local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
    local line="${_BASHUNIT_REPORTS_TEST_LINES[$i]:-}"
    local level="" message=""

    case "$status" in
    failed)
      level="error"
      message="$failure_message"
      ;;
    risky)
      level="warning"
      message="Test has no assertions (risky)"
      ;;
    flaky)
      level="warning"
      message="Test passed only after ${_BASHUNIT_REPORTS_TEST_RETRIES[$i]:-0} retries: $failure_message"
      ;;
    incomplete)
      level="notice"
      message="Test incomplete"
      ;;
    *)
      continue
      ;;
    esac

    if [ "$only" = "failed-only" ] && [ "$status" != "failed" ]; then
      continue
    fi

    local location="file=${file}"
    if [ -n "$line" ]; then
      location="${location},line=${line}"
    fi

    local encoded_message
    encoded_message=$(bashunit::reports::__gha_encode "$message")
    echo "::${level} ${location},title=${name}::${encoded_message}"
  done
}

function bashunit::reports::generate_gha_log() {
  local output_file="$1"

  bashunit::reports::print_gha_annotations all >"$output_file"
}

# src/reports/html.sh

function bashunit::reports::generate_report_html() {
  local output_file="$1"

  local test_passed=$(bashunit::state::get_tests_passed)
  local tests_skipped=$(bashunit::state::get_tests_skipped)
  local tests_incomplete=$(bashunit::state::get_tests_incomplete)
  local tests_snapshot=$(bashunit::state::get_tests_snapshot)
  local tests_failed=$(bashunit::state::get_tests_failed)

  local tests_risky=$(bashunit::state::get_tests_risky)
  local tests_flaky=$(bashunit::state::get_tests_flaky)
  local time=$(bashunit::clock::total_runtime_in_milliseconds)

  local temp_file
  temp_file=$(mktemp "${TMPDIR:-/tmp}/bashunit-report.XXXXXX")

  local _us
  _us=$(printf '\037')

  : >"$temp_file"
  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
    local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
    local test_time="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
    local test_case="$file$_us$name$_us$status$_us$test_time"

    echo "$test_case" >>"$temp_file"
  done

  local escaped_file
  escaped_file=$(mktemp "${TMPDIR:-/tmp}/bashunit-report-esc.XXXXXX")
  awk -v FS="$_us" -v OFS="$_us" '{
    for (i = 1; i <= NF; i++) {
      gsub(/&/, "\\&amp;", $i)
      gsub(/</, "\\&lt;", $i)
      gsub(/>/, "\\&gt;", $i)
      gsub(/"/, "\\&quot;", $i)
    }
    print
  }' "$temp_file" >"$escaped_file" && mv "$escaped_file" "$temp_file"

  {
    echo "<!DOCTYPE html>"
    echo "<html lang=\"en\">"
    echo "<head>"
    echo "  <meta charset=\"UTF-8\">"
    echo "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
    echo "  <title>Test Report</title>"
    echo "  <style>"
    echo "    body { font-family: Arial, sans-serif; }"
    echo "    table { width: 100%; border-collapse: collapse; }"
    echo "    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }"
    echo "    th { background-color: #f2f2f2; }"
    echo "    .passed { background-color: #dff0d8; }"
    echo "    .failed { background-color: #f2dede; }"
    echo "    .skipped { background-color: #fcf8e3; }"
    echo "    .incomplete { background-color: #d9edf7; }"
    echo "    .snapshot { background-color: #dfe6e9; }"
    echo "    .risky { background-color: #f5e6f5; }"
    echo "    .flaky { background-color: #ffe8cc; }"
    echo "  </style>"
    echo "</head>"
    echo "<body>"
    echo "  <h1>Test Report</h1>"
    echo "  <table>"
    echo "    <thead>"
    echo "      <tr>"
    echo "        <th>Total Tests</th>"
    echo "        <th>Passed</th>"
    echo "        <th>Failed</th>"
    echo "        <th>Incomplete</th>"
    echo "        <th>Skipped</th>"
    echo "        <th>Snapshot</th>"
    echo "        <th>Risky</th>"
    echo "        <th>Flaky</th>"
    echo "        <th>Time (ms)</th>"
    echo "      </tr>"
    echo "    </thead>"
    echo "    <tbody>"
    echo "      <tr>"
    echo "        <td>${#_BASHUNIT_REPORTS_TEST_NAMES[@]}</td>"
    echo "        <td>$test_passed</td>"
    echo "        <td>$tests_failed</td>"
    echo "        <td>$tests_incomplete</td>"
    echo "        <td>$tests_skipped</td>"
    echo "        <td>$tests_snapshot</td>"
    echo "        <td>$tests_risky</td>"
    echo "        <td>$tests_flaky</td>"
    echo "        <td>$time</td>"
    echo "      </tr>"
    echo "    </tbody>"
    echo "  </table>"
    echo "  <p>Time: $time ms</p>"

    local current_file=""
    local file name status test_time
    while IFS="$_us" read -r file name status test_time; do
      if [ "$file" != "$current_file" ]; then
        if [ -n "$current_file" ]; then
          echo "    </tbody>"
          echo "  </table>"
        fi
        echo "  <h2>File: $file</h2>"
        echo "  <table>"
        echo "    <thead>"
        echo "      <tr>"
        echo "        <th>Test Name</th>"
        echo "        <th>Status</th>"
        echo "        <th>Time (ms)</th>"
        echo "      </tr>"
        echo "    </thead>"
        echo "    <tbody>"
        current_file="$file"
      fi
      echo "      <tr class=\"$status\">"
      echo "        <td>$name</td>"
      echo "        <td>$status</td>"
      echo "        <td>$test_time</td>"
      echo "      </tr>"
    done <"$temp_file"

    if [ -n "$current_file" ]; then
      echo "    </tbody>"
      echo "  </table>"
    fi

    local any_failure=false
    local j
    for j in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do

      case "${_BASHUNIT_REPORTS_TEST_STATUSES[$j]:-}" in
      failed) ;;
      *) continue ;;
      esac

      if [ "$any_failure" = false ]; then
        echo "  <h2>Failures</h2>"
        any_failure=true
      fi

      local f_name f_file f_line f_message
      f_name=$(bashunit::str::html_escape "${_BASHUNIT_REPORTS_TEST_NAMES[$j]:-}")
      f_file=$(bashunit::str::html_escape "${_BASHUNIT_REPORTS_TEST_FILES[$j]:-}")
      f_line="${_BASHUNIT_REPORTS_TEST_LINES[$j]:-}"
      f_message=$(bashunit::str::html_escape \
        "$(bashunit::reports::__strip_ansi "${_BASHUNIT_REPORTS_TEST_FAILURES[$j]:-}")")

      echo "  <h3>$f_name</h3>"
      if [ -n "$f_line" ]; then
        echo "  <p><code>$f_file:$f_line</code></p>"
      else
        echo "  <p><code>$f_file</code></p>"
      fi
      echo "  <pre>$f_message</pre>"
    done

    echo "</body>"
    echo "</html>"
  } >"$output_file"

  rm -f "$temp_file"
}

# src/reports/markdown.sh

function bashunit::reports::__md_escape() {
  local text="$1"
  text=$(bashunit::reports::__strip_ansi "$text")
  text="${text//\\/\\\\}"
  text="${text//|/\\|}"
  text="${text//\`/\\\`}"
  text="${text//\*/\\*}"
  text="${text//_/\\_}"
  printf '%s' "$text"
}

function bashunit::reports::__md_count_row() {
  local label=$1
  local count=$2
  if [ "${count:-0}" -gt 0 ]; then
    printf '| %s | %s |\n' "$label" "$count"
  fi
}

function bashunit::reports::print_report_md() {
  local passed failed skipped incomplete snapshot risky flaky duration_ms
  passed=$(bashunit::state::get_tests_passed)
  failed=$(bashunit::state::get_tests_failed)
  skipped=$(bashunit::state::get_tests_skipped)
  incomplete=$(bashunit::state::get_tests_incomplete)
  snapshot=$(bashunit::state::get_tests_snapshot)
  risky=$(bashunit::state::get_tests_risky)
  flaky=$(bashunit::state::get_tests_flaky)
  duration_ms=$(bashunit::clock::total_runtime_in_milliseconds)

  local duration
  duration=$(bashunit::console_results::format_duration "$duration_ms")

  echo "## bashunit"
  echo ""
  if [ "${failed:-0}" -gt 0 ]; then
    printf '❌ **%s failed**, %s passed in %s\n' "$failed" "$passed" "$duration"
  else
    printf '✅ **%s passed** in %s\n' "$passed" "$duration"
  fi
  echo ""

  echo "| Result | Count |"
  echo "|--------|-------|"
  printf '| Passed | %s |\n' "$passed"
  printf '| Failed | %s |\n' "$failed"
  bashunit::reports::__md_count_row "Skipped" "$skipped"
  bashunit::reports::__md_count_row "Incomplete" "$incomplete"
  bashunit::reports::__md_count_row "Snapshot" "$snapshot"
  bashunit::reports::__md_count_row "Risky" "$risky"
  bashunit::reports::__md_count_row "Flaky" "$flaky"
  echo ""

  bashunit::reports::__md_failures
  bashunit::reports::__md_coverage
  bashunit::reports::__md_profile
}

function bashunit::reports::__md_failures() {
  local i any=false
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    case "${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}" in
    failed) ;;
    *) continue ;;
    esac

    if [ "$any" = false ]; then
      echo "## Failures"
      echo ""
      any=true
    fi

    local name file line message
    name=$(bashunit::reports::__md_escape "${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}")
    file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    line="${_BASHUNIT_REPORTS_TEST_LINES[$i]:-}"
    message=$(bashunit::reports::__strip_ansi "${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}")

    printf '### %s\n\n' "$name"
    if [ -n "$line" ]; then
      printf '`%s:%s`\n\n' "$file" "$line"
    else
      printf '`%s`\n\n' "$file"
    fi

    echo '```'
    printf '%s\n' "$message"
    echo '```'
    echo ""
  done
}

function bashunit::reports::__md_coverage() {
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" != 1 ]; then
    return 0
  fi

  local pct
  pct=$(bashunit::coverage::get_percentage 2>/dev/null) || return 0
  [ -n "$pct" ] || return 0

  echo "## Coverage"
  echo ""
  printf '%s%% of tracked lines\n' "$pct"
  echo ""
}

function bashunit::reports::__md_profile() {
  if ! bashunit::env::is_profile_enabled; then
    return 0
  fi
  [ -s "${PROFILE_OUTPUT_PATH:-}" ] || return 0

  echo "## Slowest tests"
  echo ""
  echo "| Duration | Test | File |"
  echo "|----------|------|------|"

  local duration name file formatted
  while IFS=$'\t' read -r duration name file; do
    formatted=$(bashunit::console_results::format_duration "$duration")
    printf '| %s | %s | %s |\n' \
      "$formatted" \
      "$(bashunit::reports::__md_escape "$name")" \
      "$(bashunit::reports::__md_escape "$file")"
  done < <(sort -t"$(printf '\t')" -k1 -rn "$PROFILE_OUTPUT_PATH" | head -n "${BASHUNIT_PROFILE_COUNT:-10}")
  echo ""
}

function bashunit::reports::generate_report_md() {
  local output_file="$1"

  bashunit::reports::print_report_md >"$output_file"
}

function bashunit::reports::append_step_summary() {
  [ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0

  bashunit::reports::print_report_md >>"$GITHUB_STEP_SUMMARY"
}

# src/runner/index.sh

# src/runner/context.sh

function bashunit::runner::restore_workdir() {
  local target="${1:-${BASHUNIT_WORKING_DIR:-}}"
  if cd "$target" 2>/dev/null; then
    return 0
  fi

  printf "%sError: cannot restore the working directory '%s'. Aborting run.%s\n" \
    "${_BASHUNIT_COLOR_FAILED:-}" "$target" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
  exit 1
}

function bashunit::runner::_supports_reliable_pipefail() {
  if [ "${BASH_VERSINFO[0]:-0}" -gt 3 ]; then
    return 0
  fi
  [ "${BASH_VERSINFO[0]:-0}" -eq 3 ] && [ "${BASH_VERSINFO[1]:-0}" -ge 1 ]
}

function bashunit::runner::sync_coverage_flag() {
  if [ "${BASHUNIT_COVERAGE-}" = "true" ]; then
    _BASHUNIT_COVERAGE_ON=1
  else
    _BASHUNIT_COVERAGE_ON=0
  fi
}

function bashunit::runner::source_login_shell_profiles() {

  [ -f /etc/profile ] && source /etc/profile 2>/dev/null || true

  [ -f ~/.bash_profile ] && source ~/.bash_profile 2>/dev/null || true

  [ -f ~/.bash_login ] && source ~/.bash_login 2>/dev/null || true

  [ -f ~/.profile ] && source ~/.profile 2>/dev/null || true
}

function bashunit::runner::export_test_identity() {
  local test_file=$1
  local fn_name=$2
  bashunit::helper::generate_id "$fn_name"
  export BASHUNIT_CURRENT_TEST_ID="$_BASHUNIT_HELPER_ID_OUT"
  bashunit::runner::resolve_test_location "$test_file" "$fn_name"
  export _BASHUNIT_TEST_LOCATION
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    export _BASHUNIT_COVERAGE_CURRENT_TEST_FILE="$test_file"
    export _BASHUNIT_COVERAGE_CURRENT_TEST_FN="$fn_name"
  fi
}

function bashunit::runner::resolve_test_location() {
  local test_file=$1
  local fn_name=$2

  local def line=""
  def="$(
    shopt -s extdebug
    declare -F "$fn_name" 2>/dev/null
  )" || true

  if [ -n "$def" ]; then
    line=${def#* }
    line=${line%% *}
  fi

  if [ -n "$line" ]; then
    _BASHUNIT_TEST_LOCATION="${test_file}:${line}"
  else
    _BASHUNIT_TEST_LOCATION="$test_file"
  fi
}

function bashunit::runner::apply_interpolated_title() {
  local fn_name=$1
  shift

  case "$fn_name" in
  *::*) ;;
  *)
    bashunit::state::reset_current_test_interpolated_function_name
    _BASHUNIT_RUNNER_INTERP_OUT=$fn_name
    return
    ;;
  esac

  local interpolated
  interpolated="$(bashunit::helper::interpolate_function_name "$fn_name" "$@")"
  if [ "$interpolated" != "$fn_name" ]; then
    bashunit::state::set_current_test_interpolated_function_name "$interpolated"
  else
    bashunit::state::reset_current_test_interpolated_function_name
  fi
  _BASHUNIT_RUNNER_INTERP_OUT=$interpolated
}

function bashunit::runner::needs_test_duration() {
  bashunit::env::is_profile_enabled && return 0
  bashunit::env::is_verbose_enabled && return 0
  bashunit::reports::is_enabled && return 0
  bashunit::env::is_show_execution_time_enabled && return 0
  return 1
}

# src/runner/sandbox.sh

_BASHUNIT_SANDBOX_BASELINE="awk sed grep cat cut tr sort uniq head tail wc
date mktemp mkdir rm rmdir mv cp ln touch chmod stat find dirname basename
base64 cksum od diff sleep env printf test true false expr id getconf
bc perl python3 uname tput git kill ps sh bash"

_BASHUNIT_SANDBOX_DIR=""

_BASHUNIT_SANDBOX_VIOLATION_FILE=""
_BASHUNIT_SANDBOX_VIOLATION_SEQ=0
_BASHUNIT_SANDBOX_ALLOWED=""

_BASHUNIT_SANDBOX_BUILTINS=""

function bashunit::sandbox::_allowed_list() {
  local allowed="$_BASHUNIT_SANDBOX_BASELINE"
  if [ -n "${BASHUNIT_SANDBOX_ALLOW:-}" ]; then

    allowed="$allowed $(printf '%s' "$BASHUNIT_SANDBOX_ALLOW" | tr ',' ' ')"
  fi

  printf ' %s ' "$(printf '%s' "$allowed" | tr '\n' ' ')"
}

function bashunit::sandbox::is_allowed() {
  case "$_BASHUNIT_SANDBOX_ALLOWED" in
  *" $1 "*) return 0 ;;
  esac
  return 1
}

function bashunit::sandbox::shim() {
  local name=$1

  case "$name" in
  '' | *[!A-Za-z0-9_.+-]*) return 0 ;;
  esac
  case "$_BASHUNIT_SANDBOX_BUILTINS" in
  *" $name "*) return 0 ;;
  esac
  bashunit::sandbox::is_allowed "$name" && return 0

  eval "function $name() { bashunit::sandbox::blocked '$name'; }"
}

function bashunit::sandbox::restore_shim() {
  bashunit::env::is_sandbox_enabled || return 0
  bashunit::sandbox::shim "$1"
}

function bashunit::sandbox::blocked() {
  local name=$1

  if [ -n "$_BASHUNIT_SANDBOX_VIOLATION_FILE" ]; then
    printf '%s\n' "$name" >>"$_BASHUNIT_SANDBOX_VIOLATION_FILE" 2>/dev/null || true
  fi

  bashunit::sandbox::violation_message "$name" >&2
  return 127
}

function bashunit::sandbox::violation_message() {
  printf "Sandbox: '%s' is not mocked and not allowed. %s\n" \
    "$1" "Mock it with bashunit::mock, or run with --sandbox-allow $1."
}

function bashunit::sandbox::prepare() {
  bashunit::env::is_sandbox_enabled || return 0

  _BASHUNIT_SANDBOX_ALLOWED=$(bashunit::sandbox::_allowed_list)

  _BASHUNIT_SANDBOX_BUILTINS=" $(compgen -b | tr '\n' ' ') "

  local dir="${_BASHUNIT_RUN_OUTPUT_DIR:-${BASHUNIT_TEMP_DIR:-${TMPDIR:-/tmp}}}/sandbox"
  mkdir -p "$dir" 2>/dev/null || return 0
  _BASHUNIT_SANDBOX_DIR="$dir"
  export _BASHUNIT_SANDBOX_DIR

  local link_allowed=true
  if bashunit::check_os::is_windows; then
    link_allowed=false
  fi

  local entry name target
  local path_dir
  local IFS=':'
  for path_dir in $PATH; do
    [ -n "$path_dir" ] || continue
    [ -d "$path_dir" ] || continue
    for entry in "$path_dir"/*; do

      if [ "$link_allowed" = true ]; then
        [ -f "$entry" ] || continue
        [ -x "$entry" ] || continue
      fi
      name=${entry##*/}
      if bashunit::sandbox::is_allowed "$name"; then
        if [ "$link_allowed" = true ] && [ ! -e "$dir/$name" ]; then
          ln -s "$entry" "$dir/$name" 2>/dev/null || true
        fi
        continue
      fi
      bashunit::sandbox::shim "$name"
    done
  done

  [ "$link_allowed" = true ] || return 0

  unset IFS
  for name in $_BASHUNIT_SANDBOX_ALLOWED; do
    [ -e "$dir/$name" ] && continue
    target=$(command -v "$name" 2>/dev/null) || continue
    case "$target" in
    /*) ln -s "$target" "$dir/$name" 2>/dev/null || true ;;
    esac
  done
}

function bashunit::sandbox::activate() {
  bashunit::env::is_sandbox_enabled || return 0
  [ -n "$_BASHUNIT_SANDBOX_DIR" ] || return 0

  if bashunit::check_os::is_windows; then
    return 0
  fi

  PATH="$_BASHUNIT_SANDBOX_DIR"
  export PATH
}

function bashunit::sandbox::begin_test() {
  bashunit::env::is_sandbox_enabled || return 0
  [ -n "$_BASHUNIT_SANDBOX_DIR" ] || return 0

  _BASHUNIT_SANDBOX_VIOLATION_SEQ=$((_BASHUNIT_SANDBOX_VIOLATION_SEQ + 1))
  _BASHUNIT_SANDBOX_VIOLATION_FILE="$_BASHUNIT_SANDBOX_DIR/violation-$$-$_BASHUNIT_SANDBOX_VIOLATION_SEQ"
  export _BASHUNIT_SANDBOX_VIOLATION_FILE
  rm -f "$_BASHUNIT_SANDBOX_VIOLATION_FILE" 2>/dev/null || true
}

function bashunit::sandbox::peek_violation() {
  bashunit::env::is_sandbox_enabled || return 1
  [ -n "$_BASHUNIT_SANDBOX_VIOLATION_FILE" ] || return 1
  [ -s "$_BASHUNIT_SANDBOX_VIOLATION_FILE" ]
}

_BASHUNIT_SANDBOX_COMMAND_OUT=""
function bashunit::sandbox::violation_of_test() {
  _BASHUNIT_SANDBOX_COMMAND_OUT=""

  bashunit::env::is_sandbox_enabled || return 1
  [ -n "$_BASHUNIT_SANDBOX_VIOLATION_FILE" ] || return 1
  [ -s "$_BASHUNIT_SANDBOX_VIOLATION_FILE" ] || return 1

  local first=""
  while IFS= read -r first; do
    [ -n "$first" ] && break
  done <"$_BASHUNIT_SANDBOX_VIOLATION_FILE"
  rm -f "$_BASHUNIT_SANDBOX_VIOLATION_FILE" 2>/dev/null || true

  [ -n "$first" ] || return 1
  _BASHUNIT_SANDBOX_COMMAND_OUT=$first
  return 0
}

# src/runner/payload.sh

_BASHUNIT_RUNNER_FIELD_OUT=""
_BASHUNIT_RUNNER_TOTAL_OUT=""
_BASHUNIT_RUNNER_TYPE_OUT=""
_BASHUNIT_RUNNER_OUTPUT_OUT=""
_BASHUNIT_RUNNER_INTERP_OUT=""
_BASHUNIT_RUNNER_COUNTS_FAILED_OUT=0
_BASHUNIT_RUNNER_COUNTS_PASSED_OUT=0
_BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT=0
_BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT=0
_BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT=0
_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT=0
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT=""

_BASHUNIT_RUNNER_RESULT_ORDINAL=0

_BASHUNIT_RETRY_NOTE=""

function bashunit::runner::extract_encoded_field() {
  local test_execution_result=$1
  local key=$2
  local marker="##${key}="
  case "$test_execution_result" in
  *"$marker"*)
    local rest="${test_execution_result#*"$marker"}"
    _BASHUNIT_RUNNER_FIELD_OUT="${rest%%##*}"
    ;;
  *) _BASHUNIT_RUNNER_FIELD_OUT="" ;;
  esac
}

function bashunit::runner::compute_total_assertions() {
  local test_execution_result=$1
  local failed passed skipped incomplete snapshot
  failed="${test_execution_result##*##ASSERTIONS_FAILED=}"
  failed="${failed%%##*}"
  passed="${test_execution_result##*##ASSERTIONS_PASSED=}"
  passed="${passed%%##*}"
  skipped="${test_execution_result##*##ASSERTIONS_SKIPPED=}"
  skipped="${skipped%%##*}"
  incomplete="${test_execution_result##*##ASSERTIONS_INCOMPLETE=}"
  incomplete="${incomplete%%##*}"
  snapshot="${test_execution_result##*##ASSERTIONS_SNAPSHOT=}"
  snapshot="${snapshot%%##*}"

  case "$failed$passed$skipped$incomplete$snapshot" in
  *[!0-9]*) failed=0 passed=0 skipped=0 incomplete=0 snapshot=0 ;;
  esac
  local total
  total=$((failed + passed + skipped))
  total=$((total + incomplete + snapshot))
  _BASHUNIT_RUNNER_TOTAL_OUT=$total
}

function bashunit::runner::extract_subshell_type() {
  local subshell_output=$1
  local type="${subshell_output%%]*}"
  _BASHUNIT_RUNNER_TYPE_OUT="${type#[}"
}

function bashunit::runner::format_subshell_output() {
  local subshell_output=$1
  local line="${subshell_output#*]}"
  line=${line//\[failed\]/$'\n'}
  line=${line//\[skipped\]/$'\n'}
  line=${line//\[incomplete\]/$'\n'}
  _BASHUNIT_RUNNER_OUTPUT_OUT=$line
}

function bashunit::runner::decode_subshell_output() {
  local test_execution_result="$1"

  local test_output_base64="${test_execution_result##*##TEST_OUTPUT=}"
  test_output_base64="${test_output_base64%%##*}"
  if [ -z "$test_output_base64" ] || [ "$test_output_base64" = "$_BASHUNIT_BASE64_EMPTY_SENTINEL" ]; then
    _BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT=""
    return
  fi
  _BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT="$(bashunit::helper::decode_base64 "$test_output_base64")"
}

function bashunit::runner::is_simple_progress_output() {
  local output="$1"

  [ -n "$output" ] || return 1

  local color
  for color in \
    "$_BASHUNIT_COLOR_DEFAULT" \
    "$_BASHUNIT_COLOR_PASSED" \
    "$_BASHUNIT_COLOR_FAILED" \
    "$_BASHUNIT_COLOR_SKIPPED" \
    "$_BASHUNIT_COLOR_INCOMPLETE" \
    "$_BASHUNIT_COLOR_SNAPSHOT" \
    "$_BASHUNIT_COLOR_RISKY"; do
    [ -n "$color" ] && output="${output//"$color"/}"
  done

  local i
  local char
  for ((i = 0; i < ${#output}; i++)); do
    char="${output:$i:1}"
    case "$char" in
    "." | "F" | "S" | "I" | "N" | "R" | "E" | "?") ;;
    *) return 1 ;;
    esac
  done

  return 0
}

function bashunit::runner::line_exists_in_output() {
  local needle="$1"
  local haystack="$2"
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    [ "$line" = "$needle" ] && return 0
  done <<<"$haystack"

  return 1
}

function bashunit::runner::extract_assertion_runtime_output() {
  local runtime_output="$1"
  local rendered_assertion_output="$2"
  local filtered_output=""
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    if bashunit::runner::line_exists_in_output "$line" "$rendered_assertion_output"; then
      continue
    fi
    if bashunit::runner::is_simple_progress_output "$line"; then
      continue
    fi

    [ -n "$filtered_output" ] && filtered_output="$filtered_output"$'\n'
    filtered_output="$filtered_output$line"
  done <<<"$runtime_output"

  runtime_output="$filtered_output"

  while [ -n "$runtime_output" ]; do
    case "$runtime_output" in
    *$'\n') runtime_output="${runtime_output%$'\n'}" ;;
    *) break ;;
    esac
  done

  echo "$runtime_output"
}

function bashunit::runner::extract_result_counts() {
  local execution_result=$1

  local result_line
  result_line="${execution_result##*$'\n'}"

  local assertions_failed=0
  local assertions_passed=0
  local assertions_skipped=0
  local assertions_incomplete=0
  local assertions_snapshot=0
  local test_exit_code=0

  case "$result_line" in
  *"ASSERTIONS_FAILED="*"##ASSERTIONS_PASSED="*)
    local _tail
    _tail="${result_line##*ASSERTIONS_FAILED=}"
    assertions_failed="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_PASSED=}"
    assertions_passed="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_SKIPPED=}"
    assertions_skipped="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_INCOMPLETE=}"
    assertions_incomplete="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_SNAPSHOT=}"
    assertions_snapshot="${_tail%%##*}"
    _tail="${result_line##*TEST_EXIT_CODE=}"
    test_exit_code="${_tail%%##*}"

    test_exit_code="${test_exit_code%%[!0-9]*}"
    : "${assertions_failed:=0}"
    : "${assertions_passed:=0}"
    : "${assertions_skipped:=0}"
    : "${assertions_incomplete:=0}"
    : "${assertions_snapshot:=0}"
    : "${test_exit_code:=0}"
    ;;
  esac

  _BASHUNIT_RUNNER_COUNTS_FAILED_OUT=$assertions_failed
  _BASHUNIT_RUNNER_COUNTS_PASSED_OUT=$assertions_passed
  _BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT=$assertions_skipped
  _BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT=$assertions_incomplete
  _BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT=$assertions_snapshot
  _BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT=$test_exit_code
}

# src/runner/diagnostics.sh

function bashunit::runner::record_profile() {
  local duration=$1
  local test_name=$2
  local test_file=$3
  printf '%s\t%s\t%s\n' "$duration" "$test_name" "$test_file" >>"$PROFILE_OUTPUT_PATH"
}

function bashunit::runner::_scan_diagnostic_lines() {
  local runtime_output=$1

  local line
  while IFS= read -r line; do
    case "$line" in
    *": line "[0-9]*": "*) ;;
    *) continue ;;
    esac

    case "$line" in
    *"command not found"* | *"unbound variable"* | *"permission denied"* | \
      *"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
      *"division by 0"* | *"bad file descriptor"* | \
      *"illegal option"* | *"argument list too long"* | \
      *"readonly variable"* | *"missing keyword"* | \
      *"cannot execute binary file"* | *"invalid arithmetic operator"* | \
      *"ambiguous redirect"* | *"integer expression expected"* | \
      *"too many arguments"* | *"value too great"* | \
      *"not a valid identifier"* | *"unexpected EOF"*)

      local runtime_error="${runtime_output#*: }"
      _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="${runtime_error//$'\n'/}"
      return
      ;;
    esac
  done <<EOF
$runtime_output
EOF
}

function bashunit::runner::halt_if_stop_on_failure() {
  bashunit::env::is_stop_on_failure_enabled || return 0

  if bashunit::parallel::is_enabled; then
    bashunit::parallel::mark_stop_on_failure
  else
    exit "$EXIT_CODE_STOP_ON_FAILURE"
  fi
}

function bashunit::runner::detect_runtime_error() {
  local runtime_output=$1
  local exit_code=${2:-0}
  _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
  _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

  local usage_prefix="bashunit: assertion usage error: "
  local usage_marker=$'\n'"$usage_prefix"
  local usage_before=""
  local usage_rest=""
  local usage_found=false
  case "$runtime_output" in
  "$usage_prefix"*)
    usage_rest=${runtime_output#"$usage_prefix"}
    usage_found=true
    ;;
  *"$usage_marker"*)
    usage_before=${runtime_output%%"$usage_marker"*}
    usage_rest=${runtime_output#*"$usage_marker"}
    usage_found=true
    ;;
  esac

  if [ "$usage_found" = true ]; then
    local usage_error=${usage_rest%%$'\n'*}
    local usage_after=""
    if [ "$usage_rest" != "$usage_error" ]; then
      usage_after=${usage_rest#*$'\n'}
    fi
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
    if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
      _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
    elif [ -n "$usage_before" ]; then
      _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
    else
      _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
    fi
    return
  fi

  case "$runtime_output" in
  *"killed"* | *"segmentation fault"* | *"cannot allocate memory"*)
    local runtime_error="${runtime_output#*: }"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="${runtime_error//$'\n'/}"
    return
    ;;
  esac

  case "$runtime_output" in
  *"command not found"* | *"unbound variable"* | *"permission denied"* | \
    *"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
    *"division by 0"* | *"bad file descriptor"* | \
    *"illegal option"* | *"argument list too long"* | \
    *"readonly variable"* | *"missing keyword"* | \
    *"cannot execute binary file"* | *"invalid arithmetic operator"* | \
    *"ambiguous redirect"* | *"integer expression expected"* | \
    *"too many arguments"* | *"value too great"* | \
    *"not a valid identifier"* | *"unexpected EOF"*)
    bashunit::runner::_scan_diagnostic_lines "$runtime_output"
    if [ -n "$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT" ]; then
      return
    fi
    ;;
  esac

  case "$exit_code" in
  127) _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="command not found (exit code 127)" ;;
  126) _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="not executable (exit code 126)" ;;
  esac

  if bashunit::sandbox::violation_of_test; then
    local blocked=$_BASHUNIT_SANDBOX_COMMAND_OUT
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="Sandbox: '$blocked' is not mocked and"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT not"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT allowed."
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT Mock it with"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT bashunit::mock,"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT or run with"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT --sandbox-allow $blocked."
  fi
}

function bashunit::runner::classify_kill_signal() {
  local code=$1

  case "$code" in
  124) printf 'Timed out (killed by `timeout`)' ;;
  130) printf 'Interrupted (SIGINT)' ;;
  137) printf 'Killed (SIGKILL — out of memory or forced termination)' ;;
  143) printf 'Terminated (SIGTERM — e.g. a timeout)' ;;
  *)

    case "$code" in
    '' | *[!0-9]*) return 0 ;;
    esac
    if [ "$code" -gt 128 ] && [ "$code" -le 192 ]; then
      printf 'Killed by signal %s' "$((code - 128))"
    fi
    ;;
  esac
}

function bashunit::runner::print_verbose_test_summary() {
  local test_file=$1
  local fn_name=$2
  local duration=$3
  local test_execution_result=$4

  if bashunit::env::is_simple_output_enabled; then
    echo ""
  fi

  printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '='
  printf "%s\n" "File:     $test_file"
  printf "%s\n" "Function: $fn_name"
  printf "%s\n" "Duration: $duration ms"
  local raw_text=${test_execution_result%%##ASSERTIONS_*}
  [ -n "$raw_text" ] && printf "%s" "Raw text: $raw_text"
  printf "%s\n" "##ASSERTIONS_${test_execution_result#*##ASSERTIONS_}"
  printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '-'
}

function bashunit::runner::render_running_file_header() {
  local script="$1"
  local force="${2:-false}"

  bashunit::internal_log "Running file" "$script"

  if [ "$force" != true ] && bashunit::parallel::is_enabled; then
    return
  fi

  if bashunit::env::is_failures_only_enabled; then
    return
  fi

  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_tap_output_enabled; then
    printf "# %s\n" "$script"
  elif bashunit::env::is_machine_output_enabled; then
    return
  elif ! bashunit::env::is_simple_output_enabled; then
    if bashunit::env::is_verbose_enabled; then
      printf "\n${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" "Running $script"
    else
      printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" "Running $script"
    fi
  elif bashunit::env::is_verbose_enabled; then
    printf "\n\n${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}" "Running $script"
  fi
}

# src/runner/parallel.sh

function bashunit::runner::_supports_wait_n() {
  local major="${BASH_VERSINFO[0]:-0}"
  local minor="${BASH_VERSINFO[1]:-0}"
  if [ "$major" -gt 4 ]; then
    return 0
  fi
  if [ "$major" -eq 4 ] && [ "$minor" -ge 3 ]; then
    return 0
  fi
  return 1
}

_BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0

function bashunit::runner::_count_running_jobs() {
  local running
  running=$(jobs -pr)
  if [ -z "$running" ]; then
    _BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0
    return
  fi
  local newlines="${running//[!$'\n']/}"
  _BASHUNIT_RUNNER_RUNNING_JOBS_OUT=$((${#newlines} + 1))
}

function bashunit::runner::wait_for_job_slot() {
  local max_jobs="${BASHUNIT_PARALLEL_JOBS:-0}"
  if [ "$max_jobs" -le 0 ]; then
    return 0
  fi

  if bashunit::runner::_supports_wait_n; then

    bashunit::runner::_count_running_jobs
    while [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -ge "$max_jobs" ]; do
      wait -n 2>/dev/null || break
      bashunit::runner::_count_running_jobs
    done
    return 0
  fi

  local delay="0.05"
  local iterations=0
  while true; do
    bashunit::runner::_count_running_jobs
    if [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -lt "$max_jobs" ]; then
      break
    fi
    sleep "$delay"
    iterations=$((iterations + 1))
    if [ "$iterations" -eq 4 ]; then
      delay="0.1"
    elif [ "$iterations" -eq 20 ]; then
      delay="0.2"
    fi
  done
}

function bashunit::runner::spinner() {

  if [ ! -t 1 ]; then

    while true; do sleep 1; done
    return
  fi

  if bashunit::env::is_no_progress_enabled || bashunit::env::is_machine_output_enabled; then
    while true; do sleep 1; done
    return
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n"
  fi

  local delay=0.1
  local spin_chars="|/-\\"
  while true; do
    local i
    for ((i = 0; i < ${#spin_chars}; i++)); do
      printf "\r%s" "${spin_chars:$i:1}"
      sleep "$delay"
    done
  done
}

# src/runner/hooks.sh

function bashunit::runner::cleanup_on_exit() {
  local test_file="$1"
  local exit_code="$2"

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  set +e

  bashunit::assert::once_flush

  if [ "${_BASHUNIT_SETUP_COMPLETED:-true}" != "true" ]; then
    exec 1>&5
    if [ "$exit_code" -eq 0 ]; then
      exit_code=1
    fi
    if [ -z "${_BASHUNIT_TEST_HOOK_FAILURE:-}" ]; then
      bashunit::state::set_test_hook_failure "set_up"
      bashunit::state::set_test_hook_message "Hook 'set_up' failed unexpectedly (e.g., source of non-existent file)"
    fi
  fi

  bashunit::runner::run_tear_down "$test_file"
  local teardown_status=$?
  bashunit::runner::clear_mocks
  bashunit::cleanup_testcase_temp_files

  if [ $teardown_status -ne 0 ]; then
    bashunit::state::set_test_exit_code "$teardown_status"
  else
    bashunit::state::set_test_exit_code "$exit_code"
  fi

  if bashunit::sandbox::peek_violation; then
    bashunit::state::set_test_exit_code 127
  fi

  bashunit::state::export_subshell_context
}

function bashunit::runner::record_file_hook_failure() {
  local hook_name="$1"
  local test_file="$2"
  local hook_output="$3"
  local status="$4"
  local render_header="${5:-false}"

  if [ "$render_header" = true ]; then
    bashunit::runner::render_running_file_header "$test_file" true
  fi

  if [ -z "$hook_output" ]; then
    hook_output="Hook '$hook_name' failed with exit code $status"
  fi

  bashunit::state::add_tests_failed
  bashunit::console_results::print_error_test "$hook_name" "$hook_output"
  local _normalized_hook
  _normalized_hook="$(bashunit::helper::normalize_test_function_name "$hook_name")"
  bashunit::reports::add_test_failed "$test_file" "$_normalized_hook" 0 0 "$hook_output"
  bashunit::runner::write_failure_result_output "$test_file" "$hook_name" "$hook_output"

  return "$status"
}

function bashunit::runner::execute_file_hook() {
  local hook_name="$1"
  local test_file="$2"
  local render_header="${3:-false}"

  declare -F "$hook_name" >/dev/null 2>&1 || return 0

  local hook_output=""
  local status=0
  local hook_output_file
  hook_output_file=$(bashunit::temp_file "${hook_name}_output")

  _BASHUNIT_HOOK_ERR_STATUS=0
  set -E
  if bashunit::env::is_strict_mode_enabled; then
    set -uo pipefail
  fi

  trap '_BASHUNIT_HOOK_ERR_STATUS=$?
    if [ "${FUNCNAME[0]:-}" != "bashunit::runner::execute_file_hook" ]; then
      set +Eu +o pipefail
      trap - ERR
      return $_BASHUNIT_HOOK_ERR_STATUS
    fi' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1

  status=$?
  if [ "$status" -eq 0 ]; then
    status=$_BASHUNIT_HOOK_ERR_STATUS
  fi

  trap - ERR
  set +Eu +o pipefail

  if [ -f "$hook_output_file" ]; then
    hook_output=""
    local line
    while IFS= read -r line; do
      [ -z "$hook_output" ] && hook_output="$line" || hook_output="$hook_output"$'\n'"$line"
    done <"$hook_output_file"
    rm -f "$hook_output_file"
  fi

  if [ $status -ne 0 ]; then
    bashunit::runner::record_file_hook_failure "$hook_name" "$test_file" "$hook_output" "$status" "$render_header"
    return $status
  fi

  if [ -n "$hook_output" ] && bashunit::env::is_verbose_enabled; then
    printf "%s\n" "$hook_output"
  fi

  return 0
}

function bashunit::runner::run_set_up() {
  local _test_file="${1-}"
  bashunit::internal_log "run_set_up"
  bashunit::runner::execute_test_hook 'set_up'
}

function bashunit::runner::run_set_up_before_script() {
  local test_file="$1"
  bashunit::internal_log "run_set_up_before_script"

  if ! declare -F "set_up_before_script" >/dev/null 2>&1; then
    return 0
  fi

  local start_time
  start_time=$(bashunit::clock::now)

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  bashunit::runner::execute_file_hook 'set_up_before_script' "$test_file" false
  local status=$?

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  local end_time
  end_time=$(bashunit::clock::now)
  local duration_ns=$((end_time - start_time))
  local duration_ms=$((duration_ns / 1000000))

  if [ $status -eq 0 ]; then
    bashunit::console_results::print_hook_completed "set_up_before_script" "$duration_ms"
  fi

  return $status
}

function bashunit::runner::run_tear_down() {
  local _test_file="${1-}"
  bashunit::internal_log "run_tear_down"
  bashunit::runner::execute_test_hook 'tear_down'
}

function bashunit::runner::execute_test_hook() {
  local hook_name="$1"

  declare -F "$hook_name" >/dev/null 2>&1 || return 0

  local hook_output=""
  local status=0
  local hook_output_file
  hook_output_file=$(bashunit::temp_file "${hook_name}_output")

  _BASHUNIT_HOOK_ERR_STATUS=0
  set -E
  if bashunit::env::is_strict_mode_enabled; then
    set -uo pipefail
  fi

  trap '_BASHUNIT_HOOK_ERR_STATUS=$?
    if [ "${FUNCNAME[0]:-}" != "bashunit::runner::execute_test_hook" ]; then
      set +Eu +o pipefail
      trap - ERR
      return $_BASHUNIT_HOOK_ERR_STATUS
    fi' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1

  status=$?
  if [ "$status" -eq 0 ]; then
    status=$_BASHUNIT_HOOK_ERR_STATUS
  fi

  trap - ERR
  set +Eu +o pipefail

  if [ -f "$hook_output_file" ]; then
    hook_output=""
    local line
    while IFS= read -r line; do
      [ -z "$hook_output" ] && hook_output="$line" || hook_output="$hook_output"$'\n'"$line"
    done <"$hook_output_file"
    rm -f "$hook_output_file"
  fi

  if [ $status -ne 0 ]; then
    local message="$hook_output"
    if [ -n "$hook_output" ]; then
      printf "%s" "$hook_output"
    else
      message="Hook '$hook_name' failed with exit code $status"
      printf "%s\n" "$message" >&2
    fi
    bashunit::runner::record_test_hook_failure "$hook_name" "$message" "$status"
    return "$status"
  fi

  if [ -n "$hook_output" ]; then
    printf "%s" "$hook_output"
  fi

  return 0
}

function bashunit::runner::record_test_hook_failure() {
  local hook_name="$1"
  local hook_message="$2"
  local status="$3"

  if [ -n "$_BASHUNIT_TEST_HOOK_FAILURE" ]; then
    return "$status"
  fi

  bashunit::state::set_test_hook_failure "$hook_name"
  bashunit::state::set_test_hook_message "$hook_message"

  return "$status"
}

function bashunit::runner::clear_mocks() {
  if [ "${#_BASHUNIT_MOCKED_FUNCTIONS[@]}" -eq 0 ]; then
    return
  fi

  local i
  for i in "${!_BASHUNIT_MOCKED_FUNCTIONS[@]}"; do
    bashunit::unmock "${_BASHUNIT_MOCKED_FUNCTIONS[$i]:-}"
  done
}

function bashunit::runner::run_tear_down_after_script() {
  local test_file="$1"
  bashunit::internal_log "run_tear_down_after_script"

  if ! declare -F "tear_down_after_script" >/dev/null 2>&1; then

    if ! bashunit::env::is_simple_output_enabled &&
      ! bashunit::env::is_failures_only_enabled &&
      ! bashunit::env::is_no_progress_enabled &&
      ! bashunit::env::is_json_output_enabled &&
      ! bashunit::env::is_junit_output_enabled &&
      ! bashunit::parallel::is_enabled; then
      echo ""
    fi
    return 0
  fi

  local start_time
  start_time=$(bashunit::clock::now)

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  bashunit::runner::execute_file_hook 'tear_down_after_script' "$test_file"
  local status=$?

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  local end_time
  end_time=$(bashunit::clock::now)
  local duration_ns=$((end_time - start_time))
  local duration_ms=$((duration_ns / 1000000))

  if [ $status -eq 0 ]; then
    bashunit::console_results::print_hook_completed "tear_down_after_script" "$duration_ms"
  fi

  if ! bashunit::env::is_simple_output_enabled &&
    ! bashunit::env::is_failures_only_enabled &&
    ! bashunit::env::is_no_progress_enabled &&
    ! bashunit::parallel::is_enabled; then
    echo ""
  fi

  return $status
}

function bashunit::runner::clean_script_test_functions() {
  local IFS=$' \t\n'
  local fn
  for fn in $1; do
    unset -f "$fn" 2>/dev/null || true
  done
}

function bashunit::runner::clean_set_up_and_tear_down_after_script() {
  bashunit::internal_log "clean_set_up_and_tear_down_after_script"
  bashunit::helper::unset_if_exists 'set_up'
  bashunit::helper::unset_if_exists 'tear_down'
  bashunit::helper::unset_if_exists 'set_up_before_script'
  bashunit::helper::unset_if_exists 'tear_down_after_script'
}

# src/runner/result.sh

function bashunit::runner::parse_result() {
  local fn_name=$1
  shift
  local execution_result=$1
  shift
  local IFS=$' \t\n'
  local -a args
  args=("$@")

  if bashunit::parallel::is_enabled; then
    bashunit::runner::parse_result_parallel "$fn_name" "$execution_result" ${args+"${args[@]}"}
  else
    bashunit::runner::parse_result_sync "$fn_name" "$execution_result"
  fi
}

_BASHUNIT_RUNNER_SUITE_DIR_OUT=""

function bashunit::runner::parallel_suite_dir_to_slot() {
  local key="${1#./}"
  key="${key%.sh}"
  key="${key//\//_}"
  _BASHUNIT_RUNNER_SUITE_DIR_OUT="${TEMP_DIR_PARALLEL_TEST_SUITE}/${key}"
}

function bashunit::runner::parse_result_parallel() {
  local fn_name=$1
  shift
  local execution_result=$1

  bashunit::runner::parallel_suite_dir_to_slot "$test_file"
  local test_suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
  [ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

  local unique_test_result_file="${test_suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"

  bashunit::internal_log "[PARA]" "fn_name:$fn_name" "execution_result:$execution_result"

  bashunit::runner::parse_result_sync "$fn_name" "$execution_result"

  echo "$execution_result" >"$unique_test_result_file"
}

function bashunit::runner::parse_result_sync() {
  local fn_name=$1
  local execution_result=$2

  bashunit::runner::extract_result_counts "$execution_result"

  bashunit::internal_log "[SYNC]" "fn_name:$fn_name" "execution_result:$execution_result"

  _BASHUNIT_ASSERTIONS_PASSED=$((_BASHUNIT_ASSERTIONS_PASSED + _BASHUNIT_RUNNER_COUNTS_PASSED_OUT))
  _BASHUNIT_ASSERTIONS_FAILED=$((_BASHUNIT_ASSERTIONS_FAILED + _BASHUNIT_RUNNER_COUNTS_FAILED_OUT))
  _BASHUNIT_ASSERTIONS_SKIPPED=$((_BASHUNIT_ASSERTIONS_SKIPPED + _BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT))
  _BASHUNIT_ASSERTIONS_INCOMPLETE=$((_BASHUNIT_ASSERTIONS_INCOMPLETE + _BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT))
  _BASHUNIT_ASSERTIONS_SNAPSHOT=$((_BASHUNIT_ASSERTIONS_SNAPSHOT + _BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT))
  _BASHUNIT_TEST_EXIT_CODE=$((_BASHUNIT_TEST_EXIT_CODE + _BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT))

  bashunit::internal_log "result_summary" \
    "failed:$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" \
    "passed:$_BASHUNIT_RUNNER_COUNTS_PASSED_OUT" \
    "skipped:$_BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT" \
    "incomplete:$_BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT" \
    "snapshot:$_BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT" \
    "exit_code:$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT"
}

function bashunit::runner::write_failure_result_output() {
  local test_file=$1
  local fn_name=$2
  local error_msg=$3
  local raw_output="${4:-}"

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_failed)
  fi

  local output_section=""
  if [ -n "$raw_output" ] && bashunit::env::is_show_output_on_failure_enabled; then
    output_section="\n    Output:\n$raw_output"
  fi

  local source_context=""
  if [ -n "$line_number" ] && [ -f "$test_file" ]; then
    source_context=$(bashunit::runner::get_failure_source_context \
      "$test_file" "$line_number")
  fi

  echo -e "$test_nr) $test_file:$line_number\n$error_msg$output_section$source_context" \
    >>"$FAILURES_OUTPUT_PATH"
}

function bashunit::runner::get_failure_source_context() {
  local file=$1
  local fn_line=$2

  local line_text line_num=0 assert_lines="" stripped trimmed
  while IFS= read -r line_text || [ -n "$line_text" ]; do
    line_num=$((line_num + 1))

    if [ "$line_num" -le "$fn_line" ]; then
      continue
    fi

    stripped="${line_text#"${line_text%%[![:space:]]*}"}"
    stripped="${stripped%"${stripped##*[![:space:]]}"}"
    if [ "$stripped" = "}" ]; then
      break
    fi

    case "$line_text" in
    *assert_* | *assert\ *)
      trimmed="${line_text#"${line_text%%[![:space:]]*}"}"
      assert_lines="${assert_lines}\n    ${_BASHUNIT_COLOR_FAINT}${line_num}:${_BASHUNIT_COLOR_DEFAULT} ${trimmed}"
      ;;
    esac
  done <"$file"

  if [ -n "$assert_lines" ]; then
    echo -e "\n    ${_BASHUNIT_COLOR_FAINT}Source:${_BASHUNIT_COLOR_DEFAULT}${assert_lines}"
  fi
}

function bashunit::runner::write_skipped_result_output() {
  local test_file=$1
  local fn_name=$2
  local output_msg=$3

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_skipped)
  fi

  echo -e "$test_nr) $test_file:$line_number\n$output_msg" >>"$SKIPPED_OUTPUT_PATH"
}

function bashunit::runner::write_incomplete_result_output() {
  local test_file=$1
  local fn_name=$2
  local output_msg=$3

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_incomplete)
  fi

  echo -e "$test_nr) $test_file:$line_number\n$output_msg" >>"$INCOMPLETE_OUTPUT_PATH"
}

function bashunit::runner::write_risky_result_output() {
  local test_file=$1
  local fn_name=$2

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_risky)
  fi

  echo -e "$test_nr) $test_file:$line_number\nTest has no assertions (risky)" >>"$RISKY_OUTPUT_PATH"
}

# src/runner/provider.sh

function bashunit::runner::parse_data_provider_args() {
  local input="$1"
  local current_arg=""
  local in_quotes=false
  local had_quotes=false
  local quote_char=""
  local escaped=false
  local IFS=$' \t\n'
  local i=0
  local arg=""
  local encoded_arg
  local -a args=()
  local args_count=0

  local has_metachar=false
  if [ "$(echo "$input" | "$GREP" -cE '(^|[^\])[|&;*]' || true)" -gt 0 ]; then
    has_metachar=true
  fi

  local trailing="${input##*[!\\]}"
  if [ $((${#trailing} % 2)) -eq 1 ]; then
    has_metachar=true
  fi

  if [ "$has_metachar" = false ] && eval "args=($input)" 2>/dev/null; then

    args_count=0
    local _tmp arg
    for _tmp in ${args+"${args[@]}"}; do args_count=$((args_count + 1)); done
    if [ "$args_count" -gt 0 ]; then

      local last_idx=$((args_count - 1))
      if [ -z "${args[$last_idx]}" ]; then
        unset 'args[$last_idx]'
      fi

      for arg in "${args[@]+"${args[@]}"}"; do
        encoded_arg="$(bashunit::helper::encode_base64 "${arg}")"
        printf '%s\n' "$encoded_arg"
      done
      return
    fi
  fi

  local i
  for ((i = 0; i < ${#input}; i++)); do
    local char="${input:$i:1}"
    if [ "$escaped" = true ]; then
      case "$char" in
      t) current_arg="$current_arg"$'\t' ;;
      n) current_arg="$current_arg"$'\n' ;;
      *) current_arg="$current_arg$char" ;;
      esac
      escaped=false
    elif [ "$char" = "\\" ]; then
      escaped=true
    elif [ "$in_quotes" = false ]; then
      case "$char" in
      "$")

        if [ "${input:$i:2}" = "$'" ]; then
          in_quotes=true
          had_quotes=true
          quote_char="'"

          i=$((i + 1))
        else
          current_arg="$current_arg$char"
        fi
        ;;
      "'" | '"')
        in_quotes=true
        had_quotes=true
        quote_char="$char"
        ;;
      " " | $'\t')

        if [ -n "$current_arg" ] || [ "$had_quotes" = true ]; then
          args[args_count]="$current_arg"
          args_count=$((args_count + 1))
        fi
        current_arg=""
        had_quotes=false
        ;;
      *)
        current_arg="$current_arg$char"
        ;;
      esac
    elif [ "$char" = "$quote_char" ]; then
      in_quotes=false
      quote_char=""
    else
      current_arg="$current_arg$char"
    fi
  done
  args[args_count]="$current_arg"
  args_count=$((args_count + 1))

  while [ "$args_count" -gt 0 ]; do
    local last_idx=$((args_count - 1))
    if [ -z "${args[$last_idx]}" ]; then
      unset 'args[$last_idx]'
      args_count=$((args_count - 1))
    else
      break
    fi
  done

  local arg
  for arg in ${args+"${args[@]}"}; do
    encoded_arg="$(bashunit::helper::encode_base64 "${arg}")"
    printf '%s\n' "$encoded_arg"
  done
}

# src/runner/exec.sh

_BASHUNIT_RUNNER_ORDERED_FNS_OUT=""

function bashunit::runner::order_functions_for_script() {
  local script="$1"
  local fns="${2:-}"
  local IFS=$' \t\n'

  local -a ordered=()
  local fn
  for fn in $fns; do
    [ -z "$fn" ] && continue
    ordered[${#ordered[@]}]="$fn"
  done

  if bashunit::env::is_defects_order_enabled && [ "${#ordered[@]}" -gt 1 ]; then
    local -a _defect_fns=()
    local _defect_fn
    for _defect_fn in $(bashunit::rerun::order_functions "$script" "${ordered[*]+${ordered[*]}}"); do
      _defect_fns[${#_defect_fns[@]}]=$_defect_fn
    done
    ordered=("${_defect_fns[@]+"${_defect_fns[@]}"}")
  fi

  if bashunit::env::is_random_order_enabled && [ "${#ordered[@]}" -gt 1 ]; then
    local _base _crc _fn_seed
    _base=$(bashunit::env::seed)
    _crc=$(printf '%s' "$script" | cksum | cut -d' ' -f1)
    _fn_seed=$(((_base + _crc) & 2147483647))
    local -a _shuffled_fns=()
    local _sfn
    while IFS= read -r _sfn; do
      [ -n "$_sfn" ] && _shuffled_fns[${#_shuffled_fns[@]}]=$_sfn
    done < <(printf '%s\n' "${ordered[@]+"${ordered[@]}"}" | bashunit::math::shuffle "$_fn_seed")
    ordered=("${_shuffled_fns[@]+"${_shuffled_fns[@]}"}")
  fi

  _BASHUNIT_RUNNER_ORDERED_FNS_OUT="${ordered[*]+${ordered[*]}}"
}

function bashunit::runner::report_unusable_provider() {
  local test_file="$1"
  local fn_name="$2"
  local provider="$3"

  local reason
  if declare -F "$provider" >/dev/null 2>&1; then
    reason="data provider '$provider' produced no data, so the test never ran"
  else
    reason="data provider '$provider' is not defined, so the test never ran"
  fi

  bashunit::state::add_tests_failed
  bashunit::console_results::print_error_test "$fn_name" "$reason"
  local _normalized_fn
  _normalized_fn="$(bashunit::helper::normalize_test_function_name "$fn_name")"
  bashunit::reports::add_test_failed "$test_file" "$_normalized_fn" 0 0 "$reason"
  bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$reason"

  if bashunit::parallel::is_enabled; then
    bashunit::runner::parallel_suite_dir_to_slot "$test_file"
    local suite_dir=$_BASHUNIT_RUNNER_SUITE_DIR_OUT
    [ -d "$suite_dir" ] || mkdir -p "$suite_dir"
    local payload="##ASSERTIONS_FAILED=0##ASSERTIONS_PASSED=0##ASSERTIONS_SKIPPED=0"
    payload="$payload##ASSERTIONS_INCOMPLETE=0##ASSERTIONS_SNAPSHOT=0##TEST_EXIT_CODE=1##"
    printf '%s\n' "$payload" \
      >"${suite_dir}/${_BASHUNIT_RUNNER_RESULT_ORDINAL}.result"
  fi
}

function bashunit::runner::call_test_functions() {
  local script="$1"
  local cached_functions="${2:-}"
  local IFS=$' \t\n'
  local -a functions_to_run=()
  local functions_to_run_count=0

  bashunit::runner::order_functions_for_script "$script" "$cached_functions"
  local _ofn
  for _ofn in $_BASHUNIT_RUNNER_ORDERED_FNS_OUT; do
    functions_to_run[functions_to_run_count]="$_ofn"
    functions_to_run_count=$((functions_to_run_count + 1))
  done

  if [ "$functions_to_run_count" -le 0 ]; then
    return
  fi

  local -a provider_data=()
  local provider_data_count=0
  local -a parsed_data=()
  local parsed_data_count=0

  local _test_ordinal=0

  bashunit::helper::build_provider_map "$script"

  bashunit::helper::annotations_validate_or_exit "$script"

  local allow_test_parallel=true
  if [ "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" = true ]; then
    allow_test_parallel=false
  fi

  if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
    bashunit::runner::parallel_suite_dir_to_slot "$script"
    mkdir -p "$_BASHUNIT_RUNNER_SUITE_DIR_OUT" 2>/dev/null || true
  fi

  for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
    if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
      break
    fi

    bashunit::helper::provider_for_function "$fn_name"
    if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
      if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
        bashunit::runner::wait_for_job_slot
        _test_ordinal=$((_test_ordinal + 1))
        _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal
        bashunit::runner::run_test "$script" "$fn_name" &
      else
        bashunit::runner::run_test "$script" "$fn_name"
      fi
      unset -v fn_name
      continue
    fi

    provider_data=()
    provider_data_count=0
    local line
    while IFS=" " read -r line; do
      [ -z "$line" ] && continue
      provider_data[provider_data_count]="$line"
      provider_data_count=$((provider_data_count + 1))
    done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

    if [ "$provider_data_count" -eq 0 ]; then

      _test_ordinal=$((_test_ordinal + 1))
      _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal
      bashunit::runner::report_unusable_provider \
        "$script" "$fn_name" "$_BASHUNIT_PROVIDER_FN_OUT"
      unset -v fn_name
      continue
    fi

    local data
    for data in "${provider_data[@]+"${provider_data[@]}"}"; do
      parsed_data=()
      parsed_data_count=0
      local line
      while IFS= read -r line; do
        [ -z "$line" ] && continue
        parsed_data[parsed_data_count]="$(bashunit::helper::decode_base64 "${line}")"
        parsed_data_count=$((parsed_data_count + 1))
      done <<<"$(bashunit::runner::parse_data_provider_args "$data")"
      if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
        bashunit::runner::wait_for_job_slot
        _test_ordinal=$((_test_ordinal + 1))
        _BASHUNIT_RUNNER_RESULT_ORDINAL=$_test_ordinal
        bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} &
      else
        bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"}
      fi
    done
    unset -v fn_name
  done

  if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
    wait
  fi
}

_BASHUNIT_RUNNER_EXEC_OUT=""
_BASHUNIT_RUNNER_TIMED_OUT="false"

_BASHUNIT_RUNNER_TIMEOUT_SECS=0

function bashunit::runner::execute_test_body() {
  local test_file=$1
  shift
  local fn_name=$1
  shift

  exec 5>&1

  trap "exit_code=\$?; bashunit::runner::cleanup_on_exit \"$test_file\" \"\$exit_code\"" EXIT
  bashunit::state::initialize_assertions_count

  if bashunit::env::is_login_shell_enabled; then
    bashunit::runner::source_login_shell_profiles
  fi

  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  bashunit::helper::annotations_for_function "$fn_name"
  if [ "$_BASHUNIT_ANNOT_SKIP_OUT" = true ]; then
    bashunit::helper::normalize_test_function_name_to_slot "$fn_name"
    bashunit::skip::__mark_with_label \
      "$_BASHUNIT_HELPER_NORMALIZED_OUT" "$_BASHUNIT_ANNOT_REASON_OUT"
    exit 0
  fi

  bashunit::sandbox::activate

  _BASHUNIT_SETUP_COMPLETED=false

  local setup_exit_code=0
  bashunit::runner::run_set_up "$test_file"
  setup_exit_code=$?
  _BASHUNIT_SETUP_COMPLETED=true
  if [ $setup_exit_code -ne 0 ]; then
    exit $setup_exit_code
  fi

  if bashunit::env::is_strict_mode_enabled; then
    set -eu

    if bashunit::runner::_supports_reliable_pipefail; then
      set -o pipefail
    else
      set +o pipefail
    fi
  else
    set +euo pipefail
  fi

  "$fn_name" "$@" 2>&1
}

function bashunit::runner::build_timeout_result() {
  printf '%s' "##ASSERTIONS_FAILED=0##ASSERTIONS_PASSED=0##ASSERTIONS_SKIPPED=0\
##ASSERTIONS_INCOMPLETE=0##ASSERTIONS_SNAPSHOT=0##TEST_EXIT_CODE=124\
##TEST_HOOK_FAILURE=##TEST_HOOK_MESSAGE=##TEST_TITLE=##TEST_OUTPUT=##"
}

function bashunit::runner::run_with_timeout() {
  local test_file=$1
  shift
  local fn_name=$1
  shift

  local secs=$_BASHUNIT_RUNNER_TIMEOUT_SECS

  local tmp_dir="${BASHUNIT_TEMP_DIR:-${TMPDIR:-/tmp}}"
  local out_file marker_file
  out_file="$("$MKTEMP" "$tmp_dir/bashunit_timeout_out.XXXXXXX")"
  marker_file="$("$MKTEMP" "$tmp_dir/bashunit_timeout_marker.XXXXXXX")"
  rm -f "$marker_file"

  set -m
  (bashunit::runner::execute_test_body "$test_file" "$fn_name" "$@") >"$out_file" 2>&1 &
  local test_pid=$!
  (
    sleep "$secs"

    kill -0 "$test_pid" 2>/dev/null || exit 0
    : >"$marker_file"
    kill -TERM -"$test_pid" 2>/dev/null
    sleep 0.3
    kill -KILL -"$test_pid" 2>/dev/null
  ) </dev/null >/dev/null 2>&1 &
  local watchdog_pid=$!
  set +m

  wait "$test_pid" 2>/dev/null

  kill -TERM "$watchdog_pid" 2>/dev/null
  kill -TERM -"$watchdog_pid" 2>/dev/null
  wait "$watchdog_pid" 2>/dev/null

  if [ -f "$marker_file" ]; then
    _BASHUNIT_RUNNER_TIMED_OUT="true"
    _BASHUNIT_RUNNER_EXEC_OUT="$(bashunit::runner::build_timeout_result)"
  else
    _BASHUNIT_RUNNER_TIMED_OUT="false"
    _BASHUNIT_RUNNER_EXEC_OUT="$(cat "$out_file" 2>/dev/null)"
  fi

  rm -f "$out_file" "$marker_file"
}

function bashunit::runner::run_test() {
  local start_time=0

  local test_file="$1"
  shift
  local fn_name="$1"
  shift

  bashunit::internal_log "Running test" "$fn_name" "$*"
  bashunit::runner::export_test_identity "$test_file" "$fn_name"

  bashunit::state::reset_test_title
  bashunit::runner::apply_interpolated_title "$fn_name" "$@"
  local interpolated_fn_name=$_BASHUNIT_RUNNER_INTERP_OUT
  local current_assertions_failed="$_BASHUNIT_ASSERTIONS_FAILED"
  local current_assertions_snapshot="$_BASHUNIT_ASSERTIONS_SNAPSHOT"
  local current_assertions_incomplete="$_BASHUNIT_ASSERTIONS_INCOMPLETE"
  local current_assertions_skipped="$_BASHUNIT_ASSERTIONS_SKIPPED"

  exec 3>&1

  local test_execution_result
  local timed_out="false"

  bashunit::helper::annotations_for_function "$fn_name"
  if bashunit::env::is_test_timeout_enabled; then
    _BASHUNIT_RUNNER_TIMEOUT_SECS=${BASHUNIT_TEST_TIMEOUT:-0}
  else
    _BASHUNIT_RUNNER_TIMEOUT_SECS=0
  fi
  if [ -n "$_BASHUNIT_ANNOT_TIMEOUT_OUT" ]; then
    _BASHUNIT_RUNNER_TIMEOUT_SECS=$_BASHUNIT_ANNOT_TIMEOUT_OUT
  fi

  bashunit::sandbox::begin_test

  bashunit::env::resolve_retry_count
  local retry_max=$_BASHUNIT_RETRY_VALIDATED
  if [ -n "$_BASHUNIT_ANNOT_RETRY_OUT" ]; then
    retry_max=$_BASHUNIT_ANNOT_RETRY_OUT
  fi
  local retries_used=0

  local first_attempt_result=""
  bashunit::env::resolve_repeat_count
  local repeat_max=$_BASHUNIT_REPEAT_VALIDATED
  local iteration=0
  local failed_iteration=0
  local measure_duration=false
  bashunit::runner::needs_test_duration && measure_duration=true

  while [ "$iteration" -lt "$repeat_max" ]; do
    iteration=$((iteration + 1))
    retries_used=0
    first_attempt_result=""
    while :; do
      if [ "$measure_duration" = true ]; then
        bashunit::clock::now_to_slot
        start_time=$_BASHUNIT_CLOCK_NOW_OUT
      fi
      if [ "$_BASHUNIT_RUNNER_TIMEOUT_SECS" -gt 0 ]; then
        bashunit::runner::run_with_timeout "$test_file" "$fn_name" "$@"
        test_execution_result="$_BASHUNIT_RUNNER_EXEC_OUT"
        timed_out="$_BASHUNIT_RUNNER_TIMED_OUT"
      else
        test_execution_result=$(bashunit::runner::execute_test_body "$test_file" "$fn_name" "$@")
      fi

      local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"

      bashunit::runner::extract_result_counts "$test_execution_result"
      bashunit::runner::detect_runtime_error "$attempt_runtime_output" \
        "$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT"
      local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
      local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT

      if [ -z "$attempt_runtime_error" ] &&
        [ "$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT" -eq 0 ] &&
        [ "$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" -eq 0 ]; then
        break
      fi

      if [ -z "$first_attempt_result" ]; then
        first_attempt_result="$test_execution_result"
      fi
      [ "$retries_used" -ge "$retry_max" ] && break
      retries_used=$((retries_used + 1))
    done

    if [ -n "$attempt_runtime_error" ] ||
      [ "$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT" -ne 0 ] ||
      [ "$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" -ne 0 ]; then
      failed_iteration=$iteration
      break
    fi
  done

  test_execution_result="$test_execution_result##TEST_RETRIES=$retries_used##"

  exec 3>&-

  local duration=0
  if [ "$measure_duration" = true ]; then
    bashunit::clock::now_to_slot
    local end_time=$_BASHUNIT_CLOCK_NOW_OUT
    duration=$(((end_time - start_time) / 1000000))
  fi

  if bashunit::env::is_profile_enabled; then
    bashunit::runner::record_profile "$duration" "$interpolated_fn_name" "$test_file"
  fi

  if bashunit::env::is_verbose_enabled; then
    bashunit::runner::print_verbose_test_summary \
      "$test_file" "$fn_name" "$duration" "$test_execution_result"
  fi

  bashunit::runner::decode_subshell_output "$test_execution_result"
  local subshell_output=$_BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT

  if [ -n "$subshell_output" ]; then
    bashunit::runner::extract_subshell_type "$subshell_output"
    local type=$_BASHUNIT_RUNNER_TYPE_OUT
    bashunit::runner::format_subshell_output "$subshell_output"
    subshell_output=$_BASHUNIT_RUNNER_OUTPUT_OUT
    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::console_results::print_line "$type" "$subshell_output"
    fi
  fi

  local runtime_output=$attempt_display_output
  local runtime_error=$attempt_runtime_error

  bashunit::reports::set_current_test_output "$runtime_output"

  _BASHUNIT_TEST_EXIT_CODE=0
  bashunit::runner::parse_result "$fn_name" "$test_execution_result" "$@"

  local test_exit_code="$_BASHUNIT_TEST_EXIT_CODE"

  bashunit::runner::compute_total_assertions "$test_execution_result"
  local total_assertions=$_BASHUNIT_RUNNER_TOTAL_OUT

  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_TITLE"
  local encoded_test_title=$_BASHUNIT_RUNNER_FIELD_OUT
  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_FAILURE"
  local hook_failure=$_BASHUNIT_RUNNER_FIELD_OUT
  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_MESSAGE"
  local encoded_hook_message=$_BASHUNIT_RUNNER_FIELD_OUT

  local test_title=""
  [ -n "$encoded_test_title" ] && test_title="$(bashunit::helper::decode_base64 "$encoded_test_title")"
  local hook_message=""
  [ -n "$encoded_hook_message" ] && hook_message="$(bashunit::helper::decode_base64 "$encoded_hook_message")"

  bashunit::set_test_title "$test_title"
  bashunit::helper::normalize_test_function_name_to_slot "$fn_name" "$interpolated_fn_name"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  bashunit::state::reset_test_title
  bashunit::state::reset_current_test_interpolated_function_name

  local repeat_note=""
  if [ "$repeat_max" -gt 1 ] && [ "$failed_iteration" -gt 0 ]; then
    repeat_note=" (failed on iteration $failed_iteration of $repeat_max)"
  fi

  local failure_label="$label"
  local failure_function="$fn_name"
  if [ -n "$hook_failure" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "$hook_failure"
    failure_label=$_BASHUNIT_HELPER_NORMALIZED_OUT
    failure_function="$hook_failure"
  fi

  local exit_code_is_unexplained=false
  if [ "$test_exit_code" -ne 0 ] &&
    [ "$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" -eq 0 ]; then
    exit_code_is_unexplained=true
  fi

  if [ -n "$runtime_error" ] || [ "$exit_code_is_unexplained" = true ]; then
    bashunit::state::add_tests_failed
    bashunit::rerun::record "$test_file" "$fn_name"
    local error_message="$runtime_error"
    if [ -n "$hook_failure" ] && [ -n "$hook_message" ]; then
      error_message="$hook_message"
    elif [ -z "$error_message" ] && [ -n "$hook_message" ]; then
      error_message="$hook_message"
    fi

    if [ -z "$hook_failure" ]; then
      local kill_message
      kill_message=$(bashunit::runner::classify_kill_signal "$test_exit_code")
      if [ -n "$kill_message" ]; then
        case "$error_message" in
        '' | *[Kk]illed* | *[Tt]erminated*) error_message="$kill_message" ;;
        esac
      fi
    fi

    if [ "$timed_out" = "true" ]; then
      error_message="Test timed out after ${_BASHUNIT_RUNNER_TIMEOUT_SECS}s"
    fi

    error_message="$error_message$repeat_note"
    bashunit::console_results::print_error_test "$failure_function" "$error_message" "$runtime_output"
    bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions" "$error_message"
    bashunit::runner::write_failure_result_output "$test_file" "$failure_function" "$error_message" "$runtime_output"
    bashunit::internal_log "Test error" "$failure_label" "$error_message"

    bashunit::runner::halt_if_stop_on_failure
    return
  fi

  if [ "$current_assertions_failed" != "$_BASHUNIT_ASSERTIONS_FAILED" ]; then
    bashunit::state::add_tests_failed
    bashunit::rerun::record "$test_file" "$fn_name"
    bashunit::reports::add_test_failed \
      "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output$repeat_note"
    if [ -n "$repeat_note" ]; then
      bashunit::console_results::print_line "failed" "${repeat_note# }"
    fi
    local assertion_runtime_output
    assertion_runtime_output="$(
      bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$subshell_output"
    )"
    bashunit::runner::write_failure_result_output \
      "$test_file" "$fn_name" "$subshell_output" "$assertion_runtime_output"

    bashunit::internal_log "Test failed" "$label"

    bashunit::runner::halt_if_stop_on_failure
    return
  fi

  if [ "$current_assertions_snapshot" != "$_BASHUNIT_ASSERTIONS_SNAPSHOT" ]; then
    bashunit::state::add_tests_snapshot

    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::console_results::print_snapshot_test "$label"
    fi
    bashunit::reports::add_test_snapshot "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::internal_log "Test snapshot" "$label"
    return
  fi

  if [ "$current_assertions_incomplete" != "$_BASHUNIT_ASSERTIONS_INCOMPLETE" ]; then
    bashunit::state::add_tests_incomplete
    bashunit::reports::add_test_incomplete "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_incomplete_result_output "$test_file" "$fn_name" "$subshell_output"
    bashunit::internal_log "Test incomplete" "$label"
    return
  fi

  if [ "$current_assertions_skipped" != "$_BASHUNIT_ASSERTIONS_SKIPPED" ]; then
    bashunit::state::add_tests_skipped
    bashunit::reports::add_test_skipped "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_skipped_result_output "$test_file" "$fn_name" "$subshell_output"
    bashunit::internal_log "Test skipped" "$label"
    return
  fi

  if [ "$total_assertions" -eq 0 ]; then
    if bashunit::env::is_fail_on_risky_enabled; then
      local risky_msg="Test has no assertions (risky)"
      bashunit::state::add_tests_failed
      bashunit::rerun::record "$test_file" "$fn_name"
      bashunit::console_results::print_error_test "$fn_name" "$risky_msg"
      bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$risky_msg"
      bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$risky_msg"
      bashunit::internal_log "Test failed (risky)" "$label"
      bashunit::runner::halt_if_stop_on_failure
      return
    fi
    bashunit::state::add_tests_risky
    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::console_results::print_risky_test "${label}" "$duration"
    fi
    bashunit::reports::add_test_risky "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_risky_result_output "$test_file" "$fn_name"
    bashunit::internal_log "Test risky" "$label"
    return
  fi

  _BASHUNIT_RETRY_NOTE=""
  if [ "$retries_used" -gt 0 ]; then
    _BASHUNIT_RETRY_NOTE=" (retry $retries_used/$retry_max)"
  fi

  if ! bashunit::env::is_failures_only_enabled; then
    if [ "$fn_name" = "$interpolated_fn_name" ]; then
      bashunit::console_results::print_successful_test "${label}" "$duration" "$@"
    else
      bashunit::console_results::print_successful_test "${label}" "$duration"
    fi
  fi
  _BASHUNIT_RETRY_NOTE=""
  bashunit::state::add_tests_passed

  if [ "$retries_used" -gt 0 ]; then
    bashunit::state::add_tests_flaky
    bashunit::runner::decode_subshell_output "$first_attempt_result"
    local first_failure=$_BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT
    bashunit::runner::format_subshell_output "$first_failure"
    first_failure=$_BASHUNIT_RUNNER_OUTPUT_OUT
    bashunit::reports::add_test_flaky \
      "$test_file" "$label" "$duration" "$total_assertions" "$first_failure" "$retries_used"
    bashunit::internal_log "Test flaky" "$label" "retries:$retries_used"
    return
  fi
  bashunit::reports::add_test_passed "$test_file" "$label" "$duration" "$total_assertions"
  bashunit::internal_log "Test passed" "$label"
}

# src/runner/list.sh

_BASHUNIT_LIST_JSON_ITEMS=""
_BASHUNIT_LIST_COUNT=0

function bashunit::runner::list_reset() {
  _BASHUNIT_LIST_JSON_ITEMS=""
  _BASHUNIT_LIST_COUNT=0
}

function bashunit::runner::list_functions() {
  local script="$1"
  local fns="${2:-}"
  local IFS=$' \t\n'

  bashunit::runner::order_functions_for_script "$script" "$fns"

  local wants_json=false
  if [ "$BASHUNIT_LIST_FORMAT" = "json" ]; then
    wants_json=true

    bashunit::helper::build_tags_map "$script"
  fi

  local fn
  for fn in $_BASHUNIT_RUNNER_ORDERED_FNS_OUT; do
    [ -z "$fn" ] && continue
    _BASHUNIT_LIST_COUNT=$((_BASHUNIT_LIST_COUNT + 1))

    if [ "$wants_json" = false ]; then
      printf '%s::%s\n' "$script" "$fn"
      continue
    fi

    local name line tags tags_json tag
    name=$(bashunit::helper::normalize_test_function_name "$fn")
    line=$(bashunit::helper::get_function_line_number "$fn")
    bashunit::helper::tags_for_function "$fn"
    tags="$_BASHUNIT_TAGS_OUT"

    tags_json=""

    local old_ifs="$IFS"
    IFS=','
    local -a tag_list=()
    for tag in $tags; do
      tag_list[${#tag_list[@]}]="$tag"
    done
    IFS="$old_ifs"

    for tag in ${tag_list[@]+"${tag_list[@]}"}; do
      [ -z "$tag" ] && continue
      [ -n "$tags_json" ] && tags_json="$tags_json,"
      tags_json="$tags_json\"$(bashunit::reports::__json_escape "$tag")\""
    done

    local item
    item="{\"file\":\"$(bashunit::reports::__json_escape "$script")\""
    item="$item,\"function\":\"$(bashunit::reports::__json_escape "$fn")\""
    item="$item,\"name\":\"$(bashunit::reports::__json_escape "$name")\""
    item="$item,\"line\":${line:-0}"
    item="$item,\"tags\":[$tags_json]}"

    [ -n "$_BASHUNIT_LIST_JSON_ITEMS" ] && _BASHUNIT_LIST_JSON_ITEMS="$_BASHUNIT_LIST_JSON_ITEMS,"
    _BASHUNIT_LIST_JSON_ITEMS="$_BASHUNIT_LIST_JSON_ITEMS$item"
  done
}

function bashunit::runner::list_render_summary() {
  if [ "$BASHUNIT_LIST_FORMAT" = "json" ]; then
    printf '{"count":%s,"tests":[%s]}\n' \
      "$_BASHUNIT_LIST_COUNT" "$_BASHUNIT_LIST_JSON_ITEMS"
    return 0
  fi

  local unit="tests"
  [ "$_BASHUNIT_LIST_COUNT" -eq 1 ] && unit="test"
  printf '%s %s\n' "$_BASHUNIT_LIST_COUNT" "$unit" >&2
}

# src/runner/discovery.sh

function bashunit::runner::load_test_files() {
  local filter=$1
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"

  _BASHUNIT_ACTIVE_FILTER="$filter"
  shift 3
  local IFS=$' \t\n'
  local -a files
  files=("$@")
  local -a scripts_ids=()
  local scripts_ids_count=0
  local -a worker_stderr_paths=()
  local -a worker_stderr_owners=()
  local worker_stderr_count=0

  if bashunit::env::is_defects_order_enabled; then
    bashunit::rerun::load
    local -a _defect_files=()
    local _defect_file
    while IFS= read -r _defect_file; do
      [ -n "$_defect_file" ] && _defect_files[${#_defect_files[@]}]=$_defect_file
    done < <(bashunit::rerun::order_files "${files[@]+"${files[@]}"}")
    files=("${_defect_files[@]+"${_defect_files[@]}"}")
  fi

  if bashunit::env::is_random_order_enabled; then
    local -a _shuffled_files=()
    local _sf
    while IFS= read -r _sf; do
      [ -n "$_sf" ] && _shuffled_files[${#_shuffled_files[@]}]=$_sf
    done < <(printf '%s\n' "${files[@]+"${files[@]}"}" | bashunit::math::shuffle "$(bashunit::env::seed)")
    files=("${_shuffled_files[@]+"${_shuffled_files[@]}"}")
  fi

  bashunit::runner::sync_coverage_flag

  if [ "$_BASHUNIT_COVERAGE_ON" = 1 ]; then

    if [ -z "$BASHUNIT_COVERAGE_PATHS" ]; then
      BASHUNIT_COVERAGE_PATHS=$(bashunit::coverage::auto_discover_paths "${files[@]}")

      if [ -z "$BASHUNIT_COVERAGE_PATHS" ]; then
        BASHUNIT_COVERAGE_PATHS="src/"
      fi
    fi
    bashunit::coverage::init
  fi

  local test_file
  for test_file in "${files[@]+"${files[@]}"}"; do
    if [ ! -f "$test_file" ]; then
      continue
    fi
    unset BASHUNIT_CURRENT_TEST_ID
    bashunit::helper::generate_id "${test_file}"
    export BASHUNIT_CURRENT_SCRIPT_ID="$_BASHUNIT_HELPER_ID_OUT"
    scripts_ids[scripts_ids_count]="${BASHUNIT_CURRENT_SCRIPT_ID}"
    scripts_ids_count=$((scripts_ids_count + 1))
    bashunit::internal_log "Loading file" "$test_file"

    local source_err_file source_err source_status
    source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"

    if [ ! -d "$_BASHUNIT_RUN_OUTPUT_DIR" ]; then
      if [ "${_BASHUNIT_RUN_DIR_VANISHED:-false}" = false ]; then
        _BASHUNIT_RUN_DIR_VANISHED=true
        printf 'bashunit: the run scratch directory disappeared mid-run: %s\n' \
          "$_BASHUNIT_RUN_OUTPUT_DIR" >&2
        printf 'bashunit: recreating it; please report this with the run log (#1137).\n' >&2
      fi
      if ! mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null; then
        source_err_file=/dev/null
      fi
    fi

    source "$test_file" 2>"$source_err_file"
    source_status=$?

    set +euo pipefail
    source_err=""
    if [ -s "$source_err_file" ]; then
      source_err="$(cat "$source_err_file")"
    fi

    local source_failed=false
    if [ "$source_status" -ne 0 ]; then
      source_failed=true
    else
      case "$source_err" in
      *"syntax error"* | *"unexpected EOF"*) source_failed=true ;;
      esac
    fi
    if [ "$source_failed" = true ]; then
      local message="$source_err"
      if [ -z "$message" ]; then

        local source_bytes="unknown"
        if [ -f "$test_file" ]; then
          source_bytes=$(bashunit::io::file_size "$test_file")
        fi
        message="Failed to source '$test_file' (exit $source_status, $source_bytes bytes, no stderr)"
      fi
      bashunit::runner::record_file_hook_failure \
        "source" "$test_file" "$message" 1 true
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::runner::restore_workdir
      continue
    fi

    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(compgen -A function)

    local filtered_functions
    filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
    local functions_for_script
    functions_for_script=$(bashunit::runner::functions_for_script "$test_file" "$filtered_functions")

    local _script_fns_to_clean="$functions_for_script"

    if [ -n "$tag_filter" ] || [ -n "$exclude_tag_filter" ]; then
      bashunit::helper::build_tags_map "$test_file"
      local _early_filtered=""
      local _early_fn
      for _early_fn in $functions_for_script; do
        bashunit::helper::tags_for_function "$_early_fn"
        if bashunit::helper::function_matches_tags "$_BASHUNIT_TAGS_OUT" "$tag_filter" "$exclude_tag_filter"; then
          _early_filtered="$_early_filtered $_early_fn"
        fi
      done
      functions_for_script="${_early_filtered# }"
    fi

    if bashunit::rerun::is_enabled && bashunit::rerun::has_entries; then
      functions_for_script=$(bashunit::rerun::filter_functions "$test_file" "$functions_for_script")
    fi
    if [ -z "$functions_for_script" ]; then
      bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::runner::restore_workdir
      continue
    fi

    if bashunit::env::is_list_enabled; then
      bashunit::runner::list_functions "$test_file" "$functions_for_script"
      bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::runner::restore_workdir
      continue
    fi

    bashunit::runner::render_running_file_header "$test_file"

    bashunit::runner::run_set_up_before_script "$test_file"
    local setup_before_script_status=$?
    if [ $setup_before_script_status -ne 0 ]; then

      if [ -n "$functions_for_script" ]; then

        local functions_to_run

        functions_to_run=($functions_for_script)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        local i
        for ((i = 0; i < additional_failures; i++)); do
          bashunit::state::add_tests_failed
        done
      fi

      bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
      bashunit::runner::clean_set_up_and_tear_down_after_script
      if ! bashunit::parallel::is_enabled; then
        bashunit::cleanup_script_temp_files
      fi
      bashunit::runner::restore_workdir
      continue
    fi
    local _cached_fns="$functions_for_script"

    bashunit::helper::check_duplicate_functions "$test_file" || true
    if bashunit::parallel::is_enabled; then
      bashunit::runner::wait_for_job_slot

      local _worker_stderr="${WORKER_STDERR_OUTPUT_PREFIX}.${worker_stderr_count}"
      worker_stderr_paths[worker_stderr_count]="$_worker_stderr"
      worker_stderr_owners[worker_stderr_count]="$test_file"
      worker_stderr_count=$((worker_stderr_count + 1))
      bashunit::runner::call_test_functions "$test_file" "$_cached_fns" 2>"$_worker_stderr" &
    else
      bashunit::runner::call_test_functions "$test_file" "$_cached_fns"
    fi
    bashunit::runner::run_tear_down_after_script "$test_file"
    bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
    bashunit::runner::clean_set_up_and_tear_down_after_script
    if ! bashunit::parallel::is_enabled; then
      bashunit::cleanup_script_temp_files
    fi
    bashunit::internal_log "Finished file" "$test_file"
    bashunit::runner::restore_workdir
  done

  if bashunit::parallel::is_enabled; then
    wait
    bashunit::runner::spinner &
    local spinner_pid=$!
    bashunit::state::aggregate_parallel_results "$TEMP_DIR_PARALLEL_TEST_SUITE"

    disown "$spinner_pid" 2>/dev/null || true
    kill "$spinner_pid" 2>/dev/null || true

    if [ -t 1 ] &&
      ! bashunit::env::is_no_progress_enabled &&
      ! bashunit::env::is_machine_output_enabled; then
      printf "\r  \r"
    fi

    local _stderr_idx=0
    while [ "$_stderr_idx" -lt "$worker_stderr_count" ]; do
      if [ -s "${worker_stderr_paths[_stderr_idx]:-}" ]; then
        bashunit::console_results::print_worker_stderr \
          "${worker_stderr_owners[_stderr_idx]:-}" "${worker_stderr_paths[_stderr_idx]:-}"
      fi
      _stderr_idx=$((_stderr_idx + 1))
    done

    local script_id
    for script_id in "${scripts_ids[@]+"${scripts_ids[@]}"}"; do
      export BASHUNIT_CURRENT_SCRIPT_ID="${script_id}"
      bashunit::cleanup_script_temp_files
    done
  fi
}

function bashunit::runner::functions_for_script() {
  local script="$1"
  local all_fn_names="$2"

  local declarations

  declarations=$(
    shopt -s extdebug
    declare -F $all_fn_names 2>/dev/null
  )

  local -a fns=()
  local -a fn_lines=()
  local count=0
  local name line file i
  while read -r name line file; do
    [ "$file" = "$script" ] || continue
    i=$count
    while [ "$i" -gt 0 ] && [ "${fn_lines[i - 1]}" -gt "$line" ]; do
      fns[i]=${fns[i - 1]}
      fn_lines[i]=${fn_lines[i - 1]}
      i=$((i - 1))
    done
    fns[i]=$name
    fn_lines[i]=$line
    count=$((count + 1))
  done <<EOF
$declarations
EOF

  i=0
  while [ "$i" -lt "$count" ]; do
    echo "${fns[i]}"
    i=$((i + 1))
  done
}

# src/runner/bench.sh

function bashunit::runner::load_bench_files() {
  local filter=$1
  shift
  local IFS=$' \t\n'
  local -a files
  files=("$@")

  local bench_file
  for bench_file in "${files[@]+"${files[@]}"}"; do
    [ -f "$bench_file" ] || continue
    unset BASHUNIT_CURRENT_TEST_ID
    bashunit::helper::generate_id "${bench_file}"
    export BASHUNIT_CURRENT_SCRIPT_ID="$_BASHUNIT_HELPER_ID_OUT"

    source "$bench_file"

    set +euo pipefail

    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(compgen -A function)

    bashunit::runner::run_set_up_before_script "$bench_file"
    local setup_before_script_status=$?
    if [ $setup_before_script_status -ne 0 ]; then

      local filtered_functions
      filtered_functions=$(bashunit::helper::get_functions_to_run "bench" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
      if [ -n "$filtered_functions" ]; then

        local functions_to_run

        functions_to_run=($filtered_functions)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        local i
        for ((i = 0; i < additional_failures; i++)); do
          bashunit::state::add_tests_failed
        done
      fi
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::cleanup_script_temp_files
      bashunit::runner::restore_workdir
      continue
    fi
    bashunit::runner::call_bench_functions "$bench_file" "$filter"
    bashunit::runner::run_tear_down_after_script "$bench_file"
    bashunit::runner::clean_set_up_and_tear_down_after_script
    bashunit::cleanup_script_temp_files
    bashunit::runner::restore_workdir
  done
}

function bashunit::runner::call_bench_functions() {
  local script="$1"
  local filter="$2"
  local IFS=$' \t\n'
  local prefix="bench"

  local filtered_functions
  filtered_functions=$(bashunit::helper::get_functions_to_run \
    "$prefix" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
  local -a functions_to_run=()
  local functions_to_run_count=0
  local _fn
  while IFS= read -r _fn; do
    [ -z "$_fn" ] && continue
    functions_to_run[functions_to_run_count]="$_fn"
    functions_to_run_count=$((functions_to_run_count + 1))
  done < <(bashunit::runner::functions_for_script "$script" "$filtered_functions")

  if [ "$functions_to_run_count" -le 0 ]; then
    return
  fi

  if bashunit::env::is_bench_mode_enabled; then
    bashunit::runner::render_running_file_header "$script"
  fi

  local fn_name
  for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do

    local parsed_annotations
    parsed_annotations=$(bashunit::benchmark::parse_annotations "$fn_name" "$script") || exit 1
    read -r revs its max_ms <<<"$parsed_annotations"
    bashunit::benchmark::run_function "$fn_name" "$revs" "$its" "$max_ms" "$script"
    unset -v fn_name
  done

  if ! bashunit::env::is_simple_output_enabled; then
    echo ""
  fi
}

# src/benchmark/index.sh

# src/benchmark/results.sh

_BASHUNIT_BENCH_NAMES=()
_BASHUNIT_BENCH_REVS=()
_BASHUNIT_BENCH_ITS=()
_BASHUNIT_BENCH_AVERAGES=()
_BASHUNIT_BENCH_MAX_MILLIS=()

_BASHUNIT_BENCH_FILES=()
_BASHUNIT_BENCH_DURATIONS=()

function bashunit::benchmark::add_result() {
  _BASHUNIT_BENCH_NAMES[${#_BASHUNIT_BENCH_NAMES[@]}]="$1"
  _BASHUNIT_BENCH_REVS[${#_BASHUNIT_BENCH_REVS[@]}]="$2"
  _BASHUNIT_BENCH_ITS[${#_BASHUNIT_BENCH_ITS[@]}]="$3"
  _BASHUNIT_BENCH_AVERAGES[${#_BASHUNIT_BENCH_AVERAGES[@]}]="$4"
  _BASHUNIT_BENCH_MAX_MILLIS[${#_BASHUNIT_BENCH_MAX_MILLIS[@]}]="$5"
  _BASHUNIT_BENCH_FILES[${#_BASHUNIT_BENCH_FILES[@]}]="${6:-}"
  _BASHUNIT_BENCH_DURATIONS[${#_BASHUNIT_BENCH_DURATIONS[@]}]="${7:-}"
}

function bashunit::benchmark::print_results() {
  if ! bashunit::env::is_bench_mode_enabled; then
    return
  fi

  if ((${#_BASHUNIT_BENCH_NAMES[@]} == 0)); then
    return
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n"
  fi

  printf "\nBenchmark Results (avg ms)\n"
  bashunit::print_line 80 "="
  printf "\n"

  local IFS=$' \t\n'
  local has_threshold=false
  local val
  for val in "${_BASHUNIT_BENCH_MAX_MILLIS[@]+"${_BASHUNIT_BENCH_MAX_MILLIS[@]}"}"; do
    if [ -n "$val" ]; then
      has_threshold=true
      break
    fi
  done

  if $has_threshold; then
    printf '%-40s %6s %6s %10s %12s\n' "Name" "Revs" "Its" "Avg(ms)" "Status"
  else
    printf '%-40s %6s %6s %10s\n' "Name" "Revs" "Its" "Avg(ms)"
  fi

  local i
  for i in "${!_BASHUNIT_BENCH_NAMES[@]}"; do
    local name="${_BASHUNIT_BENCH_NAMES[$i]:-}"
    local revs="${_BASHUNIT_BENCH_REVS[$i]:-}"
    local its="${_BASHUNIT_BENCH_ITS[$i]:-}"
    local avg="${_BASHUNIT_BENCH_AVERAGES[$i]:-}"
    local max_ms="${_BASHUNIT_BENCH_MAX_MILLIS[$i]:-}"

    if [ -z "$max_ms" ]; then
      printf '%-40s %6s %6s %10s\n' "$name" "$revs" "$its" "$avg"
      continue
    fi

    if bashunit::math::is_le "$avg" "$max_ms"; then
      local raw="≤ ${max_ms}"
      local padded
      padded=$(printf "%14s" "$raw")
      printf '%-40s %6s %6s %10s %12s\n' "$name" "$revs" "$its" "$avg" "$padded"
      continue
    fi

    local raw="> ${max_ms}"
    local padded
    padded=$(printf "%12s" "$raw")
    printf '%-40s %6s %6s %10s %s%s%s\n' \
      "$name" "$revs" "$its" "$avg" \
      "$_BASHUNIT_COLOR_FAILED" "$padded" "${_BASHUNIT_COLOR_DEFAULT}"
  done

  bashunit::console_results::print_execution_time
}

# src/benchmark/annotations.sh

function bashunit::benchmark::reject_malformed_marker() {
  local annotation=$1
  local marker=$2
  local extracted=$3

  [ -n "$extracted" ] && return 0
  case "$annotation" in
  *"@$marker="*) ;;
  *) return 0 ;;
  esac

  printf "%sError: @%s in '%s' is not a valid value.%s\n" \
    "${_BASHUNIT_COLOR_FAILED}" "$marker" "$annotation" "${_BASHUNIT_COLOR_DEFAULT}" >&2
  return 1
}

function bashunit::benchmark::parse_annotations() {
  local fn_name=$1
  local script=$2
  local revs=1
  local its=1
  local max_ms=""

  local annotation
  annotation=$(awk "/function[[:space:]]+${fn_name}[[:space:]]*\(/ {print prev; exit} {prev=\$0}" "$script")

  local _extracted
  _extracted=$(echo "$annotation" | sed -n 's/.*@revs=\([0-9][0-9]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    revs="$_extracted"
  else
    _extracted=$(echo "$annotation" | sed -n 's/.*@revolutions=\([0-9][0-9]*\).*/\1/p')
    if [ -n "$_extracted" ]; then
      revs="$_extracted"
    fi
  fi
  bashunit::benchmark::reject_malformed_marker "$annotation" "revs" "$_extracted" || return 1
  bashunit::benchmark::reject_malformed_marker "$annotation" "revolutions" "$_extracted" || return 1

  _extracted=$(echo "$annotation" | sed -n 's/.*@its=\([0-9][0-9]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    its="$_extracted"
  else
    _extracted=$(echo "$annotation" | sed -n 's/.*@iterations=\([0-9][0-9]*\).*/\1/p')
    if [ -n "$_extracted" ]; then
      its="$_extracted"
    fi
  fi
  bashunit::benchmark::reject_malformed_marker "$annotation" "its" "$_extracted" || return 1
  bashunit::benchmark::reject_malformed_marker "$annotation" "iterations" "$_extracted" || return 1

  _extracted=$(echo "$annotation" | sed -n 's/.*@max_ms=\([0-9.][0-9.]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    max_ms="$_extracted"
  fi
  bashunit::benchmark::reject_malformed_marker "$annotation" "max_ms" "$max_ms" || return 1

  if [ -n "$max_ms" ]; then
    echo "$revs" "$its" "$max_ms"
  else
    echo "$revs" "$its"
  fi
}

# src/benchmark/run.sh

function bashunit::benchmark::run_function() {
  local fn_name=$1
  local revs=$2
  local its=$3
  local max_ms=$4
  local bench_file=${5:-}
  local IFS=$' \t\n'
  local -a durations=()
  local durations_count=0
  local i r

  for ((i = 1; i <= its; i++)); do
    local start_time=$(bashunit::clock::now)
    (
      for ((r = 1; r <= revs; r++)); do
        "$fn_name" >/dev/null 2>&1
      done
    )
    local end_time=$(bashunit::clock::now)
    local dur_ns=$(bashunit::math::calculate "($end_time - $start_time)")
    local dur_ms=$(bashunit::math::calculate "$dur_ns / 1000000")
    durations[durations_count]="$dur_ms"
    durations_count=$((durations_count + 1))

    if bashunit::env::is_bench_mode_enabled; then
      local label="$(bashunit::helper::normalize_test_function_name "$fn_name")"
      local line="$label [$i/$its] ${dur_ms} ms"
      bashunit::console_results::print_line "successful" "$line"
    fi
  done

  local sum=0
  local d
  for d in "${durations[@]+"${durations[@]}"}"; do
    sum=$(bashunit::math::calculate "$sum + $d")
  done
  local avg=$(bashunit::math::calculate "$sum / ${#durations[@]}")
  local joined="${durations[*]+${durations[*]}}"
  bashunit::benchmark::add_result "$fn_name" "$revs" "$its" "$avg" "$max_ms" \
    "$bench_file" "$joined"
}

# src/benchmark/reports.sh

_BASHUNIT_BENCH_STATS_MIN_OUT=""
_BASHUNIT_BENCH_STATS_MAX_OUT=""
_BASHUNIT_BENCH_STATS_MEDIAN_OUT=""

function bashunit::benchmark::stats_to_slots() {
  local durations=$1
  _BASHUNIT_BENCH_STATS_MIN_OUT=""
  _BASHUNIT_BENCH_STATS_MAX_OUT=""
  _BASHUNIT_BENCH_STATS_MEDIAN_OUT=""

  [ -n "$durations" ] || return 0

  local stats
  stats=$(printf '%s\n' "$durations" | env LC_ALL=C awk '
    {
      n = 0
      for (i = 1; i <= NF; i++) { values[++n] = $i + 0 }
      if (n == 0) { exit }
      for (i = 2; i <= n; i++) {
        v = values[i]
        j = i - 1
        while (j >= 1 && values[j] > v) { values[j + 1] = values[j]; j-- }
        values[j + 1] = v
      }
      if (n % 2) {
        median = values[(n + 1) / 2]
      } else {
        median = (values[n / 2] + values[n / 2 + 1]) / 2
      }
      printf "%.3f %.3f %.3f\n", values[1], values[n], median
    }
  ')

  local min max median
  read -r min max median <<EOF
$stats
EOF
  _BASHUNIT_BENCH_STATS_MIN_OUT=$min
  _BASHUNIT_BENCH_STATS_MAX_OUT=$max
  _BASHUNIT_BENCH_STATS_MEDIAN_OUT=$median
}

function bashunit::benchmark::_verdict() {
  local index=$1
  local threshold="${_BASHUNIT_BENCH_MAX_MILLIS[$index]:-}"
  [ -n "$threshold" ] || return 0

  if bashunit::math::is_le "${_BASHUNIT_BENCH_AVERAGES[$index]:-0}" "$threshold"; then
    printf 'true'
  else
    printf 'false'
  fi
}

function bashunit::benchmark::_iterations_json() {
  local durations=$1
  local out=""
  local value
  local IFS=' '
  for value in $durations; do
    if [ -z "$out" ]; then
      out="$value"
    else
      out="$out, $value"
    fi
  done
  printf '%s' "$out"
}

function bashunit::benchmark::report_json() {
  local output_file=$1

  local timestamp
  timestamp=$(date '+%Y-%m-%dT%H:%M:%S')
  local os
  os=$(uname -s 2>/dev/null || printf 'unknown')

  {
    printf '{\n'
    printf '  "run": {\n'
    printf '    "timestamp": "%s",\n' "$timestamp"
    printf '    "duration_ms": %s,\n' "$(bashunit::benchmark::_run_duration_ms)"
    printf '    "bashunit_version": "%s",\n' "$(bashunit::reports::__json_escape "${BASHUNIT_VERSION:-unknown}")"
    printf '    "bash_version": "%s",\n' "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}"
    printf '    "os": "%s"\n' "$(bashunit::reports::__json_escape "$os")"
    printf '  },\n'
    printf '  "benchmarks": [\n'

    local total=${#_BASHUNIT_BENCH_NAMES[@]}
    local i sep name label file durations threshold verdict
    for i in $(bashunit::benchmark::_indexes); do
      name="${_BASHUNIT_BENCH_NAMES[$i]:-}"
      bashunit::helper::normalize_test_function_name_to_slot "$name"
      label=$_BASHUNIT_HELPER_NORMALIZED_OUT
      file="${_BASHUNIT_BENCH_FILES[$i]:-}"
      durations="${_BASHUNIT_BENCH_DURATIONS[$i]:-}"
      bashunit::benchmark::stats_to_slots "$durations"
      threshold="${_BASHUNIT_BENCH_MAX_MILLIS[$i]:-}"
      verdict=$(bashunit::benchmark::_verdict "$i")
      sep=","
      [ "$i" -eq "$((total - 1))" ] && sep=""

      printf '    {\n'
      printf '      "file": "%s",\n' "$(bashunit::reports::__json_escape "$file")"
      printf '      "function": "%s",\n' "$(bashunit::reports::__json_escape "$name")"
      printf '      "name": "%s",\n' "$(bashunit::reports::__json_escape "$label")"
      printf '      "revs": %s,\n' "${_BASHUNIT_BENCH_REVS[$i]:-0}"
      printf '      "its": %s,\n' "${_BASHUNIT_BENCH_ITS[$i]:-0}"
      printf '      "iterations_ms": [%s],\n' "$(bashunit::benchmark::_iterations_json "$durations")"
      printf '      "average_ms": %s,\n' "${_BASHUNIT_BENCH_AVERAGES[$i]:-0}"
      printf '      "min_ms": %s,\n' "${_BASHUNIT_BENCH_STATS_MIN_OUT:-0}"
      printf '      "max_ms": %s,\n' "${_BASHUNIT_BENCH_STATS_MAX_OUT:-0}"
      printf '      "median_ms": %s,\n' "${_BASHUNIT_BENCH_STATS_MEDIAN_OUT:-0}"

      printf '      "threshold_ms": %s,\n' "${threshold:-null}"
      printf '      "within_threshold": %s\n' "${verdict:-null}"
      printf '    }%s\n' "$sep"
    done

    printf '  ]\n'
    printf '}\n'
  } >"$output_file"
}

function bashunit::benchmark::report_junit() {
  local output_file=$1

  local timestamp
  timestamp=$(date '+%Y-%m-%dT%H:%M:%S')
  local total=${#_BASHUNIT_BENCH_NAMES[@]}
  local failures=0
  local i verdict
  for i in $(bashunit::benchmark::_indexes); do
    verdict=$(bashunit::benchmark::_verdict "$i")
    if [ "$verdict" = "false" ]; then
      failures=$((failures + 1))
    fi
  done

  local run_seconds
  bashunit::reports::__ms_to_s "$(bashunit::benchmark::_run_duration_ms)"
  run_seconds=$_BASHUNIT_REPORTS_MS_TO_S_OUT

  {
    printf '<?xml version="1.0" encoding="UTF-8"?>\n'
    printf '<testsuites name="bashunit-bench" tests="%s" failures="%s" errors="0" time="%s">\n' \
      "$total" "$failures" "$run_seconds"
    printf '  <testsuite name="benchmarks" tests="%s" failures="%s" errors="0" time="%s" timestamp="%s">\n' \
      "$total" "$failures" "$run_seconds" "$timestamp"

    local name label file classname seconds threshold
    for i in $(bashunit::benchmark::_indexes); do
      name="${_BASHUNIT_BENCH_NAMES[$i]:-}"
      bashunit::helper::normalize_test_function_name_to_slot "$name"
      label=$(bashunit::reports::__xml_escape "$_BASHUNIT_HELPER_NORMALIZED_OUT")
      file="${_BASHUNIT_BENCH_FILES[$i]:-}"
      bashunit::reports::__junit_classname "$file"
      classname=$_BASHUNIT_REPORTS_CLASSNAME_OUT
      bashunit::reports::__ms_to_s "${_BASHUNIT_BENCH_AVERAGES[$i]:-0}"
      seconds=$_BASHUNIT_REPORTS_MS_TO_S_OUT
      threshold="${_BASHUNIT_BENCH_MAX_MILLIS[$i]:-}"

      printf '    <testcase classname="%s" name="%s" file="%s" time="%s">\n' \
        "$classname" "$label" "$(bashunit::reports::__xml_escape "$file")" "$seconds"
      if [ "$(bashunit::benchmark::_verdict "$i")" = "false" ]; then
        printf '      <failure message="%s" type="PerformanceRegression">%s</failure>\n' \
          "average ${_BASHUNIT_BENCH_AVERAGES[$i]:-0}ms exceeds @max_ms ${threshold}" \
          "revs=${_BASHUNIT_BENCH_REVS[$i]:-0} its=${_BASHUNIT_BENCH_ITS[$i]:-0}"
      fi
      printf '    </testcase>\n'
    done

    printf '  </testsuite>\n'
    printf '</testsuites>\n'
  } >"$output_file"
}

function bashunit::benchmark::_indexes() {
  local total=${#_BASHUNIT_BENCH_NAMES[@]}
  local i=0
  while [ "$i" -lt "$total" ]; do
    printf '%s\n' "$i"
    i=$((i + 1))
  done
}

function bashunit::benchmark::_run_duration_ms() {
  if [ -z "${_BASHUNIT_START_TIME:-}" ]; then
    printf '0'
    return
  fi
  local elapsed
  elapsed=$(bashunit::clock::total_runtime_in_milliseconds)
  case "$elapsed" in
  '' | *[!0-9.]*) elapsed=0 ;;
  esac
  printf '%s' "$elapsed"
}

# src/benchmark/baseline.sh

_BASHUNIT_BASELINE_NAMES=()
_BASHUNIT_BASELINE_MEDIANS=()

function bashunit::benchmark::baseline_load() {
  local file=$1
  _BASHUNIT_BASELINE_NAMES=()
  _BASHUNIT_BASELINE_MEDIANS=()

  if [ ! -f "$file" ] || [ ! -r "$file" ]; then
    printf "%sError: cannot read the baseline file: '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$file" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi

  local name median
  while IFS=$'\t' read -r name median; do
    [ -n "$name" ] || continue
    _BASHUNIT_BASELINE_NAMES[${#_BASHUNIT_BASELINE_NAMES[@]}]="$name"
    _BASHUNIT_BASELINE_MEDIANS[${#_BASHUNIT_BASELINE_MEDIANS[@]}]="$median"
  done < <(env LC_ALL=C awk '
    # The document is bashunit s own, one field per line, so the pairing is a
    # matter of remembering the last "function" seen before each "median_ms".
    /"function"[[:space:]]*:/ {
      line = $0
      sub(/.*"function"[[:space:]]*:[[:space:]]*"/, "", line)
      sub(/".*/, "", line)
      fn = line
    }
    /"median_ms"[[:space:]]*:/ {
      line = $0
      sub(/.*"median_ms"[[:space:]]*:[[:space:]]*/, "", line)
      sub(/[^0-9.eE+-].*/, "", line)
      if (fn != "" && line != "") { printf "%s\t%s\n", fn, line; fn = "" }
    }
  ' "$file")

  if [ "${#_BASHUNIT_BASELINE_NAMES[@]}" -eq 0 ]; then
    printf "%sError: the baseline file holds no benchmark results: '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$file" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi
}

_BASHUNIT_BASELINE_MEDIAN_OUT=""
function bashunit::benchmark::baseline_median_of() {
  local wanted=$1
  local i=0
  local total=${#_BASHUNIT_BASELINE_NAMES[@]}
  _BASHUNIT_BASELINE_MEDIAN_OUT=""

  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_BASELINE_NAMES[i]}" = "$wanted" ]; then
      _BASHUNIT_BASELINE_MEDIAN_OUT="${_BASHUNIT_BASELINE_MEDIANS[i]}"
      return 0
    fi
    i=$((i + 1))
  done
  return 1
}

function bashunit::benchmark::baseline_delta() {
  local before=$1
  local now=$2
  local tolerance=$3

  env LC_ALL=C awk -v before="$before" -v now="$now" -v tol="$tolerance" '
    BEGIN {
      before = before + 0
      now = now + 0
      if (before <= 0) {
        # No usable previous number: report the change as unknown rather than
        # dividing by zero and calling it an infinite regression.
        printf "n/a\tno\n"
        exit
      }
      delta = ((now - before) / before) * 100
      printf "%+.1f%%\t%s\n", delta, (delta > tol + 0 ? "yes" : "no")
    }
  '
}

function bashunit::benchmark::baseline_compare() {
  local tolerance=$1
  local regressed=0

  printf "\nBaseline comparison (median ms, tolerance %s%%)\n" "$tolerance"
  bashunit::print_line 80 "="
  printf '%-40s %12s %12s %10s\n' "Name" "Baseline" "Current" "Delta"

  local i total name median before delta verdict
  total=${#_BASHUNIT_BENCH_NAMES[@]}
  i=0
  while [ "$i" -lt "$total" ]; do
    name="${_BASHUNIT_BENCH_NAMES[$i]:-}"
    bashunit::benchmark::stats_to_slots "${_BASHUNIT_BENCH_DURATIONS[$i]:-}"
    median=$_BASHUNIT_BENCH_STATS_MEDIAN_OUT

    if ! bashunit::benchmark::baseline_median_of "$name"; then
      printf '%-40s %12s %12s %10s\n' "$name" "-" "$median" "new"
      i=$((i + 1))
      continue
    fi
    before=$_BASHUNIT_BASELINE_MEDIAN_OUT

    IFS=$'\t' read -r delta verdict < <(
      bashunit::benchmark::baseline_delta "$before" "$median" "$tolerance"
    )

    if [ "$verdict" = "yes" ]; then
      regressed=$((regressed + 1))
      printf '%-40s %12s %12s %s%10s%s\n' "$name" "$before" "$median" \
        "$_BASHUNIT_COLOR_FAILED" "$delta" "$_BASHUNIT_COLOR_DEFAULT"
    else
      printf '%-40s %12s %12s %10s\n' "$name" "$before" "$median" "$delta"
    fi
    i=$((i + 1))
  done

  local j baseline_total baseline_name found k
  baseline_total=${#_BASHUNIT_BASELINE_NAMES[@]}
  j=0
  while [ "$j" -lt "$baseline_total" ]; do
    baseline_name="${_BASHUNIT_BASELINE_NAMES[$j]}"
    found=false
    k=0
    while [ "$k" -lt "$total" ]; do
      if [ "${_BASHUNIT_BENCH_NAMES[$k]:-}" = "$baseline_name" ]; then
        found=true
        break
      fi
      k=$((k + 1))
    done
    if [ "$found" = false ]; then
      printf '%-40s %12s %12s %10s\n' "$baseline_name" \
        "${_BASHUNIT_BASELINE_MEDIANS[$j]}" "-" "removed"
    fi
    j=$((j + 1))
  done

  if [ "$regressed" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_FAILED" \
      " Performance regression in $regressed benchmark(s) " "$_BASHUNIT_COLOR_DEFAULT"
    return 1
  fi

  return 0
}

# src/learn/index.sh

# src/learn/progress.sh

declare -r LEARN_PROGRESS_FILE="$HOME/.bashunit_learn_progress"

function bashunit::learn::mark_completed() {
  local lesson=$1
  echo "$lesson" >>"$LEARN_PROGRESS_FILE"
}

function bashunit::learn::is_completed() {
  local lesson=$1
  [ -f "$LEARN_PROGRESS_FILE" ] && [ "$("$GREP" -c "^$lesson$" "$LEARN_PROGRESS_FILE" || true)" -gt 0 ]
}

function bashunit::learn::show_progress() {
  if [ ! -f "$LEARN_PROGRESS_FILE" ]; then
    echo "${_BASHUNIT_COLOR_INCOMPLETE}No progress yet. Start with lesson 1!${_BASHUNIT_COLOR_DEFAULT}"
    return
  fi

  echo "${_BASHUNIT_COLOR_BOLD}Your Progress:${_BASHUNIT_COLOR_DEFAULT}"
  echo ""

  local total_lessons=10
  local completed=0

  local i
  for i in $(seq 1 $total_lessons); do
    if bashunit::learn::is_completed "lesson_$i"; then
      echo "  ${_BASHUNIT_COLOR_PASSED}✓${_BASHUNIT_COLOR_DEFAULT} Lesson $i completed"
      ((++completed)) || true
    else
      echo "  ${_BASHUNIT_COLOR_INCOMPLETE}○${_BASHUNIT_COLOR_DEFAULT} Lesson $i"
    fi
  done

  echo ""
  echo "Progress: $completed/$total_lessons lessons completed"

  if [ $completed -eq $total_lessons ]; then
    echo ""
    printf "%s%s🎉 Congratulations! You've completed all lessons!%s\n" \
      "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_BOLD" "$_BASHUNIT_COLOR_DEFAULT"
  fi

  read -p "Press Enter to continue..." -r
}

function bashunit::learn::reset_progress() {
  rm -f "$LEARN_PROGRESS_FILE"
  echo "${_BASHUNIT_COLOR_PASSED}Progress reset successfully.${_BASHUNIT_COLOR_DEFAULT}"
  read -p "Press Enter to continue..." -r
}

# src/learn/session.sh

LEARN_TEMP_DIR=""

function bashunit::learn::init() {
  LEARN_TEMP_DIR=$("${MKTEMP:-mktemp}" -d "${TMPDIR:-/tmp}/bashunit_learn.XXXXXXXX")
  mkdir -p tests
}

function bashunit::learn::cleanup() {
  if [ -n "${LEARN_TEMP_DIR:-}" ] && [ -d "$LEARN_TEMP_DIR" ]; then
    rm -rf "$LEARN_TEMP_DIR"
  fi
}

function bashunit::learn::create_example_file() {
  local filename=$1
  local content=$2

  echo ""
  echo "Creating example file ${_BASHUNIT_COLOR_BOLD}$filename${_BASHUNIT_COLOR_DEFAULT}..."
  echo "$content" >"$filename"
  chmod +x "$filename"
  echo "${_BASHUNIT_COLOR_PASSED}✓ Created $filename${_BASHUNIT_COLOR_DEFAULT}"
  echo ""
  echo "File created! Edit it to complete the TODO items, then run this lesson again."
  read -p "Press Enter to continue..." -r
  return 0
}

function bashunit::learn::count_in_code() {
  local file=$1
  local pattern=$2

  "$GREP" -v '^[[:space:]]*#' "$file" | "$GREP" -c "$pattern" || true
}

function bashunit::learn::run_lesson_test() {
  local test_file=$1
  local lesson_number=$2

  echo "${_BASHUNIT_COLOR_BOLD}Running your test...${_BASHUNIT_COLOR_DEFAULT}"
  echo ""

  if "$BASHUNIT_ROOT_DIR/bashunit" "$test_file" --simple --fail-on-risky; then
    echo ""
    printf "%s%s✓ Excellent! Lesson %s completed!%s\n" \
      "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_BOLD" "$lesson_number" "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::learn::mark_completed "lesson_$lesson_number"
    read -p "Press Enter to continue..." -r
    return 0
  else
    echo ""
    echo "${_BASHUNIT_COLOR_FAILED}Not quite right. Review the requirements and try again.${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi
}

# src/learn/lessons/basics.sh

function bashunit::learn::lesson_basics() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                    Lesson 1: Your First Test                   ║
╚════════════════════════════════════════════════════════════════╝

Welcome to bashunit! Let's write your first test.

CONCEPT: A test is a function that starts with 'test_' and uses
assertions to verify behavior.

TASK: Create a test file that checks if two values are equal.

File: tests/first_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function test_bashunit_works() {
  # TODO: Use assert_same to check if "hello" equals "hello"
  # Hint: assert_same "expected" "actual"
}
───────────────────────────────────────────────────────────────

TIPS:
  • The assert_same function takes two arguments:
    assert_same "expected" "actual"
  • Test functions must start with "test_" prefix
  • Always quote your strings to avoid word splitting
  • Keep test files in a tests/ directory for better organization
EOF

  local default_file="tests/first_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_bashunit_works() {
  # TODO: Use assert_same to check if "hello" equals "hello"
  # Hint: assert_same "expected" "actual"
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "assert_same")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use assert_same${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 1
}

# src/learn/lessons/assertions.sh

function bashunit::learn::lesson_assertions() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 2: Testing Different Conditions            ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: bashunit provides many assertion functions for different checks:
  • assert_same - exact equality
  • assert_contains - substring check
  • assert_matches - regex pattern
  • assert_not_same - inequality
  • assert_empty - checks if value is empty
  • assert_not_empty - checks if value is not empty

TASK: Write a test file with 3 different assertions.

File: tests/assertions_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function test_multiple_assertions() {
  local message="Hello, bashunit!"

  # TODO: Check that message contains "bashunit"
  # Hint: assert_contains "substring" "$message"

  # TODO: Check that message matches the pattern "Hello.*!"
  # Hint: assert_matches "pattern" "$message"

  # TODO: Check that message is not empty
  # Hint: assert_not_empty "$message"
}
───────────────────────────────────────────────────────────────

TIPS:
  • assert_same checks exact equality (useful for strings/numbers)
  • assert_contains is more flexible for partial matches
  • assert_matches uses regex patterns (e.g., "^[0-9]+$" for numbers)
  • Explore more: assert_empty, assert_true, assert_false
EOF

  local default_file="tests/assertions_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_multiple_assertions() {
  local message="Hello, bashunit!"

  # TODO: Check that message contains "bashunit"
  # Hint: assert_contains "substring" "$message"

  # TODO: Check that message matches the pattern "Hello.*!"
  # Hint: assert_matches "pattern" "$message"

  # TODO: Check that message is not empty
  # Hint: assert_not_empty "$message"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "assert_contains")" -eq 0 ] ||
    [ "$(bashunit::learn::count_in_code "$test_file" "assert_matches")" -eq 0 ] ||
    [ "$(bashunit::learn::count_in_code "$test_file" "assert_not_empty")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use all three assertion types${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 2
}

# src/learn/lessons/lifecycle.sh

function bashunit::learn::lesson_lifecycle() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║           Lesson 3: Setup and Teardown Functions               ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Tests often need preparation and cleanup. bashunit provides:
  • set_up() - runs before EACH test
  • tear_down() - runs after EACH test
  • set_up_before_script() - runs once before ALL tests
  • tear_down_after_script() - runs once after ALL tests

TASK: Create a test that uses setup and teardown to manage files.

File: tests/lifecycle_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  # Create a temp file before each test
  # TODO: export TEST_FILE="/tmp/test_$$"
  # TODO: echo "test content" > "$TEST_FILE"
}

function tear_down() {
  # Clean up after each test
  # TODO: rm -f "$TEST_FILE"
}

function test_file_exists() {
  # TODO: assert_file_exists "$TEST_FILE"
}

function test_file_has_content() {
  # TODO: assert_file_contains "test content" "$TEST_FILE"
}
───────────────────────────────────────────────────────────────

TIPS:
  • set_up() runs before EACH test (good for test isolation)
  • set_up_before_script() runs ONCE before all tests (good for expensive setup)
  • Always clean up in tear_down() to avoid polluting other tests
  • Use $$ for unique temp file names to avoid conflicts
EOF

  local default_file="tests/lifecycle_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  # Create a temp file before each test
  # TODO: export TEST_FILE="/tmp/test_$$"
  # TODO: echo "test content" > "$TEST_FILE"
  :
}

function tear_down() {
  # Clean up after each test
  # TODO: rm -f "$TEST_FILE"
  :
}

function test_file_exists() {
  # TODO: assert_file_exists "$TEST_FILE"
  :
}

function test_file_has_content() {
  # TODO: assert_file_contains "test content" "$TEST_FILE"
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "function set_up()")" -eq 0 ] ||
    [ "$(bashunit::learn::count_in_code "$test_file" "function tear_down()")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should define set_up and tear_down functions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 3
}

# src/learn/lessons/functions.sh

function bashunit::learn::lesson_functions() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 4: Testing Bash Functions                  ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: To test functions, source the file containing them, then
call them in your tests.

TASK: Create a script with a function, then test it.

File: calculator.sh (source code)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function add() {
  echo $(($1 + $2))
}
───────────────────────────────────────────────────────────────

File: tests/calculator_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  # TODO: Source calculator.sh from parent directory
  # Hint: source ../calculator.sh
}

function test_add_positive_numbers() {
  # TODO: Test that add 2 3 returns "5"
  # Hint: result=$(add 2 3)
  # Hint: assert_same "5" "$result"
}

function test_add_negative_numbers() {
  # TODO: Test that add -2 -3 returns "-5"
  # Hint: result=$(add -2 -3)
  # Hint: assert_same "-5" "$result"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Source files in set_up() to reload them fresh for each test
  • Capture function output with: result=$(function_name args)
  • Test edge cases: positive, negative, zero, large numbers
  • Source files from parent directory: source ../file.sh
EOF

  local default_file="tests/calculator_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  # TODO: Source calculator.sh from parent directory
  # Hint: source ../calculator.sh
  :
}

function test_add_positive_numbers() {
  # TODO: Test that add 2 3 returns "5"
  # Hint: result=$(add 2 3)
  # Hint: assert_same "5" "$result"
  :
}

function test_add_negative_numbers() {
  # TODO: Test that add -2 -3 returns "-5"
  # Hint: result=$(add -2 -3)
  # Hint: assert_same "-5" "$result"
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "source")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should source the calculator.sh file${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 4
}

# src/learn/lessons/scripts.sh

function bashunit::learn::lesson_scripts() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                 Lesson 5: Testing Bash Scripts                 ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Scripts that execute commands directly are tested differently.
Run them and capture their output.

TASK: Create a script and test its output.

File: greeter.sh (source code)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash
name=${1:-World}
echo "Hello, $name!"
───────────────────────────────────────────────────────────────

File: tests/greeter_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function test_default_greeting() {
  # TODO: Run greeter.sh from parent directory and capture output
  # Hint: output=$(../greeter.sh)

  # TODO: Assert output contains "Hello, World!"
  # Hint: assert_contains "Hello, World!" "$output"
}

function test_custom_greeting() {
  # TODO: Run greeter.sh with argument "Alice"
  # Hint: output=$(../greeter.sh "Alice")

  # TODO: Assert output contains "Hello, Alice!"
  # Hint: assert_contains "Hello, Alice!" "$output"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Use command substitution: output=$(./script.sh)
  • Make scripts executable: chmod +x script.sh
  • Test both default behavior and with various arguments
  • Scripts run in subshells, so they can't modify parent environment
  • Run scripts from parent directory: ../script.sh
EOF

  local default_file="tests/greeter_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_default_greeting() {
  # TODO: Run greeter.sh from parent directory and capture output
  # Hint: output=$(../greeter.sh)

  # TODO: Assert output contains "Hello, World!"
  # Hint: assert_contains "Hello, World!" "$output"
  :
}

function test_custom_greeting() {
  # TODO: Run greeter.sh with argument "Alice"
  # Hint: output=$(../greeter.sh "Alice")

  # TODO: Assert output contains "Hello, Alice!"
  # Hint: assert_contains "Hello, Alice!" "$output"
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 5
}

# src/learn/lessons/mocking.sh

function bashunit::learn::lesson_mocking() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║               Lesson 6: Mocking External Commands              ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Mocks let you override external commands or functions to
control their behavior in tests.

TASK: Test a function that uses external commands.

File: system_info.sh (source code)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function get_system_info() {
  echo "OS: $(uname -s)"
}
───────────────────────────────────────────────────────────────

File: tests/system_info_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  source ../system_info.sh
}

function test_system_info_on_linux() {
  # TODO: Mock uname to return "Linux"
  # Hint: mock uname echo "Linux"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Linux"
}

function test_system_info_on_macos() {
  # TODO: Mock uname to return "Darwin"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Darwin"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Mocks replace commands/functions with custom behavior
  • Syntax: mock command_name echo "mocked output"
  • Mocks are automatically cleaned up after each test
  • Use mocks to avoid calling expensive external commands
EOF

  local default_file="tests/system_info_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source ../system_info.sh
}

function test_system_info_on_linux() {
  # TODO: Mock uname to return "Linux"
  # Hint: mock uname echo "Linux"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Linux"
}

function test_system_info_on_macos() {
  # TODO: Mock uname to return "Darwin"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Darwin"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "mock")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use mock${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 6
}

# src/learn/lessons/spies.sh

function bashunit::learn::lesson_spies() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 7: Spies - Verifying Calls                 ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Spies let you verify that functions were called with specific
arguments or a certain number of times.

KEY DIFFERENCE: Spies track calls without changing behavior, while
mocks (Lesson 6) replace the function entirely with custom behavior.

TASK: Use spies to verify function calls.

File: deploy.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function deploy_app() {
  git push origin main
  docker build -t myapp .
  docker push myapp
}
───────────────────────────────────────────────────────────────

File: deploy_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  source deploy.sh
}

function test_deploy_calls_git_push() {
  # TODO: Create spies for git and docker
  # Hint: spy git
  # Hint: spy docker

  deploy_app

  # TODO: Assert git was called
  # Hint: assert_have_been_called git

  # TODO: Assert docker was called
}

function test_deploy_calls_docker_twice() {
  # TODO: Spy on docker

  deploy_app

  # TODO: Assert docker was called exactly 2 times
  # Hint: assert_have_been_called_times 2 docker
}
───────────────────────────────────────────────────────────────

TIPS:
  • Spies track calls but don't change behavior (unlike mocks)
  • assert_have_been_called - verifies at least one call
  • assert_have_been_called_times N - verifies exact call count
  • assert_have_been_called_with - verifies specific arguments
  • Spies are cleaned up automatically after each test
EOF

  local default_file="deploy_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source deploy.sh
}

function test_deploy_calls_git_push() {
  # TODO: Create spies for git and docker
  # Hint: spy git
  # Hint: spy docker

  deploy_app

  # TODO: Assert git was called
  # Hint: assert_have_been_called git

  # TODO: Assert docker was called
}

function test_deploy_calls_docker_twice() {
  # TODO: Spy on docker

  deploy_app

  # TODO: Assert docker was called exactly 2 times
  # Hint: assert_have_been_called_times 2 docker
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "spy")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use spy${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 7
}

# src/learn/lessons/data_providers.sh

function bashunit::learn::lesson_data_providers() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║           Lesson 8: Data Providers - Parameterized Tests       ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Data providers let you run the same test with different inputs.
Define a function that echoes test data, one per line.

HOW IT WORKS: Each line from data_provider_* becomes $1 in your test.
The test runs once for each line of data.

TASK: Test multiple email formats using a data provider.

File: validator.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function is_valid_email() {
  local email_pattern='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
  [ "$(echo "$1" | "$GREP" -cE "$email_pattern" || true)" -gt 0 ]
}
───────────────────────────────────────────────────────────────

File: validator_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  source validator.sh
}

function data_provider_valid_emails() {
  # TODO: Echo valid email addresses, one per line
  # Example: echo "user@example.com"
}

function test_valid_emails() {
  # $1 contains the email from data provider
  # TODO: Assert is_valid_email succeeds
  # Hint: assert_successful_code "is_valid_email \"$1\""
}

function data_provider_invalid_emails() {
  # TODO: Echo invalid email addresses, one per line
  # Example: echo "not-an-email"
}

function test_invalid_emails() {
  # TODO: Assert is_valid_email fails
  # Hint: assert_general_error "is_valid_email \"$1\""
}
───────────────────────────────────────────────────────────────

TIPS:
  • Data providers must be named: data_provider_<test_name>
  • Each line of output becomes one test case
  • The test function receives the line as $1
  • Great for testing multiple inputs without duplicating code
  • You can have multiple data provider/test pairs in one file
EOF

  local default_file="validator_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source validator.sh
}

function data_provider_valid_emails() {
  # TODO: Echo valid email addresses, one per line
  # Example: echo "user@example.com"
  :
}

function test_valid_emails() {
  # $1 contains the email from data provider
  # TODO: Assert is_valid_email succeeds
  # Hint: assert_successful_code "is_valid_email \"$1\""
  :
}

function data_provider_invalid_emails() {
  # TODO: Echo invalid email addresses, one per line
  # Example: echo "not-an-email"
  :
}

function test_invalid_emails() {
  # TODO: Assert is_valid_email fails
  # Hint: assert_general_error "is_valid_email \"$1\""
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "function data_provider_")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should define data provider functions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 8
}

# src/learn/lessons/exit_codes.sh

function bashunit::learn::lesson_exit_codes() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║             Lesson 9: Testing Exit Codes                       ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Exit codes indicate success (0) or failure (non-zero).
bashunit provides assertions to test them:
  • assert_successful_code - expects exit code 0
  • assert_general_error - expects exit code 1
  • assert_exit_code N - expects specific exit code N

TASK: Test different exit codes.

File: checker.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function check_file() {
  if [ ! -e "$1" ]; then
    echo "File not found" >&2
    return 127
  fi

  if [ ! -r "$1" ]; then
    echo "Permission denied" >&2
    return 1
  fi

  echo "File OK"
  return 0
}
───────────────────────────────────────────────────────────────

File: checker_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function set_up() {
  source checker.sh
  # Create a test file
  export TEST_FILE="/tmp/test_file_$$"
  touch "$TEST_FILE"
}

function tear_down() {
  rm -f "$TEST_FILE"
}

function test_existing_file_returns_success() {
  # TODO: Assert check_file succeeds with TEST_FILE
  # Hint: assert_successful_code "check_file '$TEST_FILE'"
}

function test_missing_file_returns_127() {
  # TODO: Assert check_file returns exit code 127 for missing file
  # Hint: assert_exit_code 127 "check_file '/nonexistent/file'"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Exit code 0 = success (assert_successful_code)
  • Exit code 1 = general error (assert_general_error)
  • Other codes = specific errors (assert_exit_code N)
  • Bash uses 'return N' in functions, 'exit N' in scripts
  • Common codes: 127=not found, 126=not executable, 2=misuse
EOF

  local default_file="checker_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source checker.sh
  # Create a test file
  export TEST_FILE="/tmp/test_file_$$"
  touch "$TEST_FILE"
}

function tear_down() {
  rm -f "$TEST_FILE"
}

function test_existing_file_returns_success() {
  # TODO: Assert check_file succeeds with TEST_FILE
  # Hint: assert_successful_code "check_file '\''$TEST_FILE'\''"
  :
}

function test_missing_file_returns_127() {
  # TODO: Assert check_file returns exit code 127 for missing file
  # Hint: assert_exit_code 127 "check_file '\''/nonexistent/file'\''"
  :
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  local _exit_assert_pattern="assert_successful_code\|assert_exit_code\|assert_general_error"
  if [ "$(bashunit::learn::count_in_code "$test_file" "$_exit_assert_pattern")" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use exit code assertions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 9
}

# src/learn/lessons/challenge.sh

function bashunit::learn::lesson_challenge() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║          Lesson 10: Complete Challenge - Backup Script         ║
╚════════════════════════════════════════════════════════════════╝

FINAL CHALLENGE: Combine everything you've learned!

CONCEPT: Real-world tests combine multiple concepts: lifecycle
management, assertions, exit codes, and test doubles.

TASK: Create a backup script and comprehensive tests.

File: backup.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function create_backup() {
  local source=$1
  local dest=$2

  if [ ! -d "$source" ]; then
    echo "Source directory not found" >&2
    return 1
  fi

  tar -czf "$dest" -C "$source" .
  echo "Backup created: $dest"
}
───────────────────────────────────────────────────────────────

File: backup_test.sh
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

Your test must include:
  1. set_up and tear_down functions
  2. Test successful backup creation
  3. Test failure when source doesn't exist
  4. Mock or spy on tar command
  5. Verify backup file exists
  6. Check output message

TIP: Combine patterns from all previous lessons!
EOF

  local default_file="backup_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source backup.sh
  # TODO: Create test directories and variables
}

function tear_down() {
  # TODO: Clean up test files
  :
}

function test_successful_backup() {
  # TODO: Test backup creation
  :
}

function test_backup_failure_when_source_missing() {
  # TODO: Test failure case
  :
}

# Add more tests as needed:
# - Mock or spy on tar command
# - Verify backup file exists
# - Check output message
#
# TIPS:
# - Combine lifecycle (set_up/tear_down) with file assertions
# - Use spies to verify tar was called correctly
# - Test both success and failure scenarios
# - Mock external commands to avoid side effects'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  local -a missing_components=()
  local missing_components_count=0

  if [ "$(bashunit::learn::count_in_code "$test_file" "function set_up()")" -eq 0 ]; then
    missing_components[missing_components_count]="set_up function"
    missing_components_count=$((missing_components_count + 1))
  fi

  if [ "$(bashunit::learn::count_in_code "$test_file" "function tear_down()")" -eq 0 ]; then
    missing_components[missing_components_count]="tear_down function"
    missing_components_count=$((missing_components_count + 1))
  fi

  if [ "$missing_components_count" -gt 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Missing required components:${_BASHUNIT_COLOR_DEFAULT}"
    printf "  - %s\n" "${missing_components[@]}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  if bashunit::learn::run_lesson_test "$test_file" 10; then
    echo ""
    echo "${_BASHUNIT_COLOR_PASSED}${_BASHUNIT_COLOR_BOLD}"
    cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                   🎉 CONGRATULATIONS! 🎉                       ║
║                                                                ║
║          You've completed all bashunit lessons!                ║
║                                                                ║
║  You now know how to:                                          ║
║    ✓ Write and run tests                                       ║
║    ✓ Use various assertions                                    ║
║    ✓ Manage test lifecycle                                     ║
║    ✓ Test functions and scripts                                ║
║    ✓ Mock external dependencies                                ║
║    ✓ Spy on function calls                                     ║
║    ✓ Use data providers                                        ║
║    ✓ Test exit codes                                           ║
║                                                                ║
║  Next steps:                                                   ║
║    • Explore https://bashunit.com                              ║
║    • Check out /common-patterns for more examples              ║
║    • Start testing your own bash scripts!                      ║
╚════════════════════════════════════════════════════════════════╝
EOF
    echo "${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
  fi
}

# src/learn/menu.sh

function bashunit::learn::print_menu() {
  cat <<EOF
${_BASHUNIT_COLOR_BOLD}${_BASHUNIT_COLOR_PASSED}bashunit${_BASHUNIT_COLOR_DEFAULT} - Interactive Learning

Choose a lesson:

  ${_BASHUNIT_COLOR_BOLD}1.${_BASHUNIT_COLOR_DEFAULT} Basics - Your First Test
  ${_BASHUNIT_COLOR_BOLD}2.${_BASHUNIT_COLOR_DEFAULT} Assertions - Testing Different Conditions
  ${_BASHUNIT_COLOR_BOLD}3.${_BASHUNIT_COLOR_DEFAULT} Setup & Teardown - Managing Test Lifecycle
  ${_BASHUNIT_COLOR_BOLD}4.${_BASHUNIT_COLOR_DEFAULT} Testing Functions - Unit Testing Patterns
  ${_BASHUNIT_COLOR_BOLD}5.${_BASHUNIT_COLOR_DEFAULT} Testing Scripts - Integration Testing
  ${_BASHUNIT_COLOR_BOLD}6.${_BASHUNIT_COLOR_DEFAULT} Mocking - Test Doubles and Mocks
  ${_BASHUNIT_COLOR_BOLD}7.${_BASHUNIT_COLOR_DEFAULT} Spies - Verifying Function Calls
  ${_BASHUNIT_COLOR_BOLD}8.${_BASHUNIT_COLOR_DEFAULT} Data Providers - Parameterized Tests
  ${_BASHUNIT_COLOR_BOLD}9.${_BASHUNIT_COLOR_DEFAULT} Exit Codes - Testing Success and Failure
  ${_BASHUNIT_COLOR_BOLD}10.${_BASHUNIT_COLOR_DEFAULT} Complete Challenge - Real World Scenario

  ${_BASHUNIT_COLOR_BOLD}p.${_BASHUNIT_COLOR_DEFAULT} Show Progress
  ${_BASHUNIT_COLOR_BOLD}r.${_BASHUNIT_COLOR_DEFAULT} Reset Progress
  ${_BASHUNIT_COLOR_BOLD}q.${_BASHUNIT_COLOR_DEFAULT} Quit

Enter your choice:
EOF
}

function bashunit::learn::start() {
  bashunit::learn::init

  trap 'bashunit::learn::cleanup' EXIT

  while true; do
    echo ""
    bashunit::learn::print_menu
    read -r choice
    echo ""

    case "$choice" in
    1) bashunit::learn::lesson_basics || true ;;
    2) bashunit::learn::lesson_assertions || true ;;
    3) bashunit::learn::lesson_lifecycle || true ;;
    4) bashunit::learn::lesson_functions || true ;;
    5) bashunit::learn::lesson_scripts || true ;;
    6) bashunit::learn::lesson_mocking || true ;;
    7) bashunit::learn::lesson_spies || true ;;
    8) bashunit::learn::lesson_data_providers || true ;;
    9) bashunit::learn::lesson_exit_codes || true ;;
    10) bashunit::learn::lesson_challenge || true ;;
    p) bashunit::learn::show_progress ;;
    r) bashunit::learn::reset_progress ;;
    q)
      echo "${_BASHUNIT_COLOR_PASSED}Happy testing!${_BASHUNIT_COLOR_DEFAULT}"
      break
      ;;
    *)
      echo "${_BASHUNIT_COLOR_FAILED}Invalid choice. Please try again.${_BASHUNIT_COLOR_DEFAULT}"
      ;;
    esac
  done

  bashunit::learn::cleanup
}

# src/main/index.sh

# src/main/validate.sh

function bashunit::main::abort_unknown_option() {
  printf "%sError: unknown option '%s'. Run 'bashunit %s --help' to list the available options.%s\n" \
    "${_BASHUNIT_COLOR_FAILED}" "$1" "$2" "${_BASHUNIT_COLOR_DEFAULT}" >&2
  exit 1
}

function bashunit::main::require_non_negative_int_or_exit() {
  case "$1" in
  '' | *[!0-9]*)
    printf "%sError: %s must be a non-negative integer, got '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$2" "$1" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac
}

function bashunit::main::report_path_is_a_directory() {
  printf "%sError: %s is a directory, not a file: '%s'.%s\n" \
    "$_BASHUNIT_COLOR_FAILED" "${2:-path}" "$1" "$_BASHUNIT_COLOR_DEFAULT" >&2
  exit 1
}

function bashunit::main::report_unreadable_bootstrap() {
  local boot_file=$1
  local raw=${2-}

  if [ ! -e "$boot_file" ]; then
    printf "%sError: the bootstrap file does not exist: '%s'.%s\n" \
      "$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
  elif [ -d "$boot_file" ]; then
    printf "%sError: the bootstrap path is a directory, not a file: '%s'.%s\n" \
      "$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
  elif [ ! -f "$boot_file" ]; then

    printf "%sError: the bootstrap path is not a regular file: '%s'.%s\n" \
      "$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
  else
    printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
      "$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
  fi

  if [ "$raw" != "$boot_file" ] && [ -r "$raw" ]; then
    printf "%s--env splits its value on the first space to pass bootstrap arguments,%s\n" \
      "$_BASHUNIT_COLOR_FAINT" "$_BASHUNIT_COLOR_DEFAULT" >&2
    printf "%sso a path containing one cannot be used. Set BASHUNIT_BOOTSTRAP='%s' instead.%s\n" \
      "$_BASHUNIT_COLOR_FAINT" "$raw" "$_BASHUNIT_COLOR_DEFAULT" >&2
  fi

  exit 1
}

function bashunit::main::require_writable_path_or_exit() {
  local path=$1
  local parent=${1%/*}
  [ "$parent" = "$1" ] && parent="."
  [ -z "$parent" ] && parent="/"

  if [ -d "$path" ]; then
    bashunit::main::report_path_is_a_directory "$path" "${2:-}"
  fi

  if [ -e "$path" ]; then
    [ -w "$path" ] && return 0
  elif [ -d "$parent" ] && [ -w "$parent" ]; then
    return 0
  fi

  printf "%sError: %s cannot be written: '%s'.%s\n" \
    "${_BASHUNIT_COLOR_FAILED}" "$2" "$path" "${_BASHUNIT_COLOR_DEFAULT}" >&2
  exit 1
}

function bashunit::main::require_creatable_path_or_exit() {
  local path=$1
  local ancestor=$1
  while [ -n "$ancestor" ] && [ ! -e "$ancestor" ]; do
    case "$ancestor" in
    */*) ancestor=${ancestor%/*} ;;
    *) ancestor="." ;;
    esac
  done
  [ -z "$ancestor" ] && ancestor="/"

  if [ -d "$path" ]; then
    bashunit::main::report_path_is_a_directory "$path" "${2:-}"
  fi

  if [ -d "$ancestor" ] && [ -w "$ancestor" ] &&
    { [ ! -e "$path" ] || [ -w "$path" ]; }; then
    return 0
  fi

  printf "%sError: %s cannot be written: '%s'.%s\n" \
    "${_BASHUNIT_COLOR_FAILED}" "$2" "$path" "${_BASHUNIT_COLOR_DEFAULT}" >&2
  exit 1
}

function bashunit::main::validate_config_or_exit() {
  if [ "${BASHUNIT_PARALLEL_JOBS:-0}" != "0" ]; then
    bashunit::main::require_non_negative_int_or_exit \
      "${BASHUNIT_PARALLEL_JOBS}" "BASHUNIT_PARALLEL_JOBS (--jobs)"
  fi
  bashunit::main::require_non_negative_int_or_exit \
    "${BASHUNIT_RETRY:-0}" "BASHUNIT_RETRY (--retry)"
  bashunit::main::require_non_negative_int_or_exit \
    "${BASHUNIT_TEST_TIMEOUT:-0}" "BASHUNIT_TEST_TIMEOUT (--test-timeout)"
  bashunit::main::require_non_negative_int_or_exit \
    "${BASHUNIT_REPEAT:-1}" "BASHUNIT_REPEAT (--repeat)"

  if [ "${BASHUNIT_REPEAT:-1}" -lt 1 ]; then
    printf "%sError: BASHUNIT_REPEAT (--repeat) must be at least 1, got '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_REPEAT}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi

  if [ -n "${BASHUNIT_COVERAGE_MIN:-}" ]; then
    bashunit::main::require_non_negative_int_or_exit \
      "${BASHUNIT_COVERAGE_MIN}" "BASHUNIT_COVERAGE_MIN (--coverage-min)"
  fi

  bashunit::main::require_non_negative_int_or_exit \
    "${BASHUNIT_COVERAGE_THRESHOLD_LOW:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}" \
    "BASHUNIT_COVERAGE_THRESHOLD_LOW"
  bashunit::main::require_non_negative_int_or_exit \
    "${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}" \
    "BASHUNIT_COVERAGE_THRESHOLD_HIGH"

  if [ -n "${BASHUNIT_SEED:-}" ]; then
    bashunit::main::require_non_negative_int_or_exit "${BASHUNIT_SEED}" "BASHUNIT_SEED (--seed)"
  fi

  if bashunit::env::is_shard_enabled; then
    bashunit::main::require_non_negative_int_or_exit \
      "${BASHUNIT_SHARD_INDEX}" "BASHUNIT_SHARD_INDEX"
    bashunit::main::require_non_negative_int_or_exit \
      "${BASHUNIT_SHARD_TOTAL}" "BASHUNIT_SHARD_TOTAL"
    if [ "$BASHUNIT_SHARD_TOTAL" -lt 1 ] || [ "$BASHUNIT_SHARD_INDEX" -lt 1 ] ||
      [ "$BASHUNIT_SHARD_INDEX" -gt "$BASHUNIT_SHARD_TOTAL" ]; then
      printf "%sError: BASHUNIT_SHARD_INDEX/BASHUNIT_SHARD_TOTAL must satisfy 1 <= index <= total.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
  fi

  if bashunit::env::is_changed_enabled; then
    if ! bashunit::helper::git_is_repo; then
      printf "%sError: --changed needs a git work tree; '%s' is not inside one.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$PWD" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
    local _changed_ref
    _changed_ref="$(bashunit::helper::git_changed_ref)"
    if ! bashunit::helper::git_ref_exists "$_changed_ref"; then
      printf "%sError: --changed cannot resolve the git ref '%s'.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$_changed_ref" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
  fi

  local _report_var _report_path
  for _report_var in BASHUNIT_LOG_JUNIT BASHUNIT_LOG_GHA BASHUNIT_REPORT_HTML \
    BASHUNIT_REPORT_TAP BASHUNIT_REPORT_JSON BASHUNIT_REPORT_MD; do
    _report_path=${!_report_var:-}
    if [ -n "$_report_path" ]; then
      bashunit::main::require_writable_path_or_exit "$_report_path" "$_report_var"
    fi
  done

  if [ -n "${BASHUNIT_COVERAGE_REPORT_COBERTURA:-}" ]; then
    bashunit::main::require_creatable_path_or_exit \
      "$BASHUNIT_COVERAGE_REPORT_COBERTURA" "BASHUNIT_COVERAGE_REPORT_COBERTURA"
  fi

  case "${BASHUNIT_OUTPUT_FORMAT:-}" in
  '' | text | tap | json | junit) ;;
  *)
    printf "%sError: unsupported output format '%s' for --output. Supported: text, tap, json, junit.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_OUTPUT_FORMAT}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac

  case "${BASHUNIT_SANDBOX_ALLOW:-}" in
  '') ;;
  *[!A-Za-z0-9_.+,-]*)
    printf "%sError: invalid --sandbox-allow value '%s'. Expected a comma-separated list of commands.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_SANDBOX_ALLOW}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac

  case "${BASHUNIT_GHA_ANNOTATIONS:-auto}" in
  auto | always | never) ;;
  *)
    printf "%sError: unsupported mode '%s' for --gha-annotations. Supported: auto, always, never.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_GHA_ANNOTATIONS}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac

  case "${BASHUNIT_ORDER_BY:-defined}" in
  defined | defects | random) ;;
  *)
    printf "%sError: unsupported order '%s' for --order-by. Supported: defined, defects, random.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_ORDER_BY}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac

  if [ -n "${BASHUNIT_COVERAGE_DIFF:-}" ]; then
    if ! bashunit::dependencies::has_git; then
      printf "%sError: --coverage-diff needs git, which was not found.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
    if ! bashunit::helper::git_is_repo; then
      printf "%sError: --coverage-diff needs a git repository; '%s' is not inside one.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$(pwd)" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
    if ! bashunit::helper::git_ref_exists "${BASHUNIT_COVERAGE_DIFF}"; then
      printf "%sError: --coverage-diff base '%s' does not resolve to a commit. \
On a shallow clone, fetch it first (git fetch --depth=... origin %s).%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_COVERAGE_DIFF}" \
        "${BASHUNIT_COVERAGE_DIFF}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
  fi

  case "${BASHUNIT_LIST_FORMAT:-}" in
  text | json) ;;
  *)
    printf "%sError: unsupported list format '%s' for --list-format. Supported: text, json.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${BASHUNIT_LIST_FORMAT}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
    ;;
  esac
}

function bashunit::main::require_valid_tag_expression_or_exit() {
  local value="${1:-}"
  local IFS=','
  local expression
  for expression in $value; do
    local rest="$expression"
    local term more=true

    while [ "$more" = true ]; do
      case "$rest" in
      *"&&"*)
        term="${rest%%&&*}"
        rest="${rest#*&&}"
        ;;
      *)
        term="$rest"
        rest=""
        more=false
        ;;
      esac
      term="${term#"${term%%[![:space:]]*}"}"
      term="${term%"${term##*[![:space:]]}"}"
      case "$term" in
      '!'*)
        term="${term#!}"
        term="${term#"${term%%[![:space:]]*}"}"
        ;;
      esac
      if [ -z "$term" ]; then
        printf "%sError: invalid tag expression '%s' for --tag. \
Use 'a', 'a&&b', '!a' or 'a&&!b'.%s\n" \
          "${_BASHUNIT_COLOR_FAILED}" "$expression" "${_BASHUNIT_COLOR_DEFAULT}" >&2
        exit 1
      fi
    done
  done
}

function bashunit::main::set_shard_or_exit() {
  local spec="${1:-}"
  local index total
  case "$spec" in
  */*)
    index="${spec%%/*}"
    total="${spec##*/}"
    ;;
  *)
    index=""
    total=""
    ;;
  esac
  case "$index" in '' | *[!0-9]*) index="" ;; esac
  case "$total" in '' | *[!0-9]*) total="" ;; esac

  if [ -z "$index" ] || [ -z "$total" ] ||
    [ "$total" -lt 1 ] || [ "$index" -lt 1 ] || [ "$index" -gt "$total" ]; then
    printf "%sError: --shard must be <index>/<total> with 1 <= index <= total (e.g. 1/4).%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi

  BASHUNIT_SHARD_INDEX="$index"
  export -n BASHUNIT_SHARD_INDEX
  BASHUNIT_SHARD_TOTAL="$total"
  export -n BASHUNIT_SHARD_TOTAL
}

# src/main/run.sh

function bashunit::main::exec_tests() {
  local filter=$1
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"
  shift 3

  local test_files
  local test_files_count=0
  local _line
  while IFS= read -r _line; do
    [ -z "$_line" ] && continue
    test_files[test_files_count]="$_line"
    test_files_count=$((test_files_count + 1))
  done < <(bashunit::helper::load_test_files "$filter" "$@")

  bashunit::internal_log "exec_tests" "filter:$filter" "files:${test_files[*]:-}"

  if [ "$test_files_count" -eq 0 ]; then
    printf "%sError: At least one file path is required.%s\n" "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_help
    exit 1
  fi

  if bashunit::env::is_changed_enabled; then
    local _changed_ref
    _changed_ref=$(bashunit::helper::git_changed_ref)
    local -a _changed_files=()
    local _changed_file
    while IFS= read -r _changed_file; do
      [ -z "$_changed_file" ] && continue
      _changed_files[${#_changed_files[@]}]="$_changed_file"
    done < <(bashunit::helper::git_filter_changed "$_changed_ref" "${test_files[@]}")
    test_files=("${_changed_files[@]+"${_changed_files[@]}"}")
    test_files_count=${#test_files[@]}
    bashunit::internal_log "changed" "ref:$_changed_ref" "files:$test_files_count"
  fi

  if bashunit::env::is_shard_enabled; then
    local _shard_index _shard_total
    _shard_index=$(bashunit::env::shard_index)
    _shard_total=$(bashunit::env::shard_total)
    local -a _sharded=()
    local _i=0
    while [ "$_i" -lt "$test_files_count" ]; do
      if [ "$((_i % _shard_total))" -eq "$((_shard_index - 1))" ]; then
        _sharded[${#_sharded[@]}]="${test_files[_i]}"
      fi
      _i=$((_i + 1))
    done
    test_files=("${_sharded[@]+"${_sharded[@]}"}")
    test_files_count=${#test_files[@]}
    bashunit::internal_log "shard" "index:$_shard_index" "total:$_shard_total" "files:$test_files_count"
  fi

  trap 'bashunit::main::cleanup' SIGINT
  trap '[ $? -eq $EXIT_CODE_STOP_ON_FAILURE ] && bashunit::main::handle_stop_on_failure_sync' EXIT

  bashunit::parallel::resolve_enabled

  if bashunit::env::is_parallel_run_enabled && ! bashunit::parallel::is_enabled; then
    printf "%sWarning: Parallel tests are supported on macOS, Ubuntu, Alpine and Windows.\n" \
      "${_BASHUNIT_COLOR_INCOMPLETE}"
    printf "On other systems --parallel is not enabled due to inconsistent results,\n"
    printf "particularly involving race conditions.%s " "${_BASHUNIT_COLOR_DEFAULT}"
    printf "%sFallback using --no-parallel%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  fi

  if bashunit::parallel::is_enabled; then
    bashunit::parallel::init
  fi

  bashunit::sandbox::prepare

  if bashunit::env::is_list_enabled; then
    :
  elif bashunit::env::is_tap_output_enabled; then
    printf "TAP version 13\n"
  elif bashunit::env::is_machine_output_enabled; then

    :
  else
    bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
  fi

  if bashunit::env::is_random_order_enabled; then
    if [ -z "${BASHUNIT_SEED:-}" ]; then
      BASHUNIT_SEED=$RANDOM
      export -n BASHUNIT_SEED
    fi
    if ! bashunit::env::is_machine_output_enabled && ! bashunit::env::is_list_enabled; then
      bashunit::console_header::print_random_order_seed "$BASHUNIT_SEED"
    fi
  fi

  if bashunit::env::is_verbose_enabled; then
    if bashunit::env::is_simple_output_enabled; then
      echo ""
    fi
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '#'
    printf "%s\n" "Filter:      ${filter:-None}"
    printf "%s\n" "Total files: ${#test_files[@]}"
    printf "%s\n" "Test files:"
    printf -- "- %s\n" "${test_files[@]}"
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '.'
    bashunit::env::print_verbose
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '#'
  fi

  bashunit::runner::load_test_files "$filter" "$tag_filter" "$exclude_tag_filter" "${test_files[@]}"

  if bashunit::env::is_list_enabled; then
    bashunit::runner::list_render_summary
    bashunit::env::cleanup_run_output_dir
    exit 0
  fi

  if bashunit::parallel::is_enabled; then
    wait
  fi

  if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure &&
    ! bashunit::env::is_machine_output_enabled; then
    printf "%sStop on failure enabled...%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  fi

  if ! bashunit::env::is_machine_output_enabled; then
    bashunit::console_results::print_failing_tests_and_reset
    bashunit::console_results::print_risky_tests_and_reset
    bashunit::console_results::print_incomplete_tests_and_reset
    bashunit::console_results::print_skipped_tests_and_reset
  fi
  bashunit::console_results::render_result
  exit_code=$?

  bashunit::reports::load_spooled

  if bashunit::env::is_coverage_enabled; then

    bashunit::coverage::finalize

    if bashunit::parallel::is_enabled; then
      bashunit::coverage::aggregate_parallel
    fi

    bashunit::coverage::precompute_file_stats
  fi

  if [ -n "$BASHUNIT_REPORT_MD" ]; then
    bashunit::reports::generate_report_md "$BASHUNIT_REPORT_MD"
  elif bashunit::env::should_append_step_summary; then
    bashunit::reports::append_step_summary
  fi

  if bashunit::env::is_profile_enabled && ! bashunit::env::is_machine_output_enabled; then
    bashunit::console_results::print_profile_and_reset
  fi

  if bashunit::env::is_snapshot_report_unused_enabled; then
    bashunit::snapshot::report_unused ${test_files[@]+"${test_files[@]}"}
  fi

  if bashunit::env::is_snapshot_prune_enabled; then
    bashunit::snapshot::prune_unused ${test_files[@]+"${test_files[@]}"}
  fi

  if bashunit::env::should_print_gha_annotations; then
    bashunit::reports::print_gha_annotations all
  fi

  if [ -n "$BASHUNIT_LOG_JUNIT" ]; then
    bashunit::reports::generate_junit_xml "$BASHUNIT_LOG_JUNIT"
  fi

  if [ -n "$BASHUNIT_LOG_GHA" ]; then
    bashunit::reports::generate_gha_log "$BASHUNIT_LOG_GHA"
  fi

  if [ -n "$BASHUNIT_REPORT_HTML" ]; then
    bashunit::reports::generate_report_html "$BASHUNIT_REPORT_HTML"
  fi

  if [ -n "$BASHUNIT_REPORT_TAP" ]; then
    bashunit::reports::generate_report_tap "$BASHUNIT_REPORT_TAP"
  fi

  if [ -n "$BASHUNIT_REPORT_JSON" ]; then
    bashunit::reports::generate_report_json "$BASHUNIT_REPORT_JSON"
  fi

  if bashunit::env::is_json_output_enabled; then
    bashunit::reports::print_report_json
  elif bashunit::env::is_junit_output_enabled; then
    bashunit::reports::print_junit_xml
  fi

  if bashunit::env::is_coverage_enabled; then
    if bashunit::coverage::is_diff_enabled; then
      if bashunit::env::is_machine_output_enabled; then

        bashunit::coverage::report_diff >/dev/null
      else
        bashunit::coverage::report_diff
      fi
    elif ! bashunit::env::is_machine_output_enabled; then
      bashunit::coverage::report_text
    fi

    if [ -n "$BASHUNIT_COVERAGE_REPORT" ]; then
      bashunit::coverage::report_lcov "$BASHUNIT_COVERAGE_REPORT"
    fi

    if [ -n "$BASHUNIT_COVERAGE_REPORT_COBERTURA" ]; then
      bashunit::coverage::report_cobertura "$BASHUNIT_COVERAGE_REPORT_COBERTURA"
    fi

    if [ -n "$BASHUNIT_COVERAGE_REPORT_HTML" ]; then
      bashunit::coverage::report_html "$BASHUNIT_COVERAGE_REPORT_HTML"
    fi

    if ! bashunit::coverage::check_threshold; then
      exit_code=1
    fi

    bashunit::coverage::cleanup
  fi

  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi

  bashunit::rerun::persist

  bashunit::env::cleanup_run_output_dir

  bashunit::internal_log "Finished tests" "exit_code:$exit_code"
  exit $exit_code
}

function bashunit::main::exec_benchmarks() {
  local filter=$1
  shift

  local bench_files
  local bench_files_count=0
  local _line
  while IFS= read -r _line; do
    [ -z "$_line" ] && continue
    bench_files[bench_files_count]="$_line"
    bench_files_count=$((bench_files_count + 1))
  done < <(bashunit::helper::load_bench_files "$filter" "$@")

  bashunit::internal_log "exec_benchmarks" "filter:$filter" "files:${bench_files[*]:-}"

  if [ "$bench_files_count" -eq 0 ]; then
    printf "%sError: At least one file path is required.%s\n" "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_help
    exit 1
  fi

  bashunit::console_header::print_version_with_env "$filter" "${bench_files[@]}"

  bashunit::runner::load_bench_files "$filter" "${bench_files[@]}"

  if [ "${#_BASHUNIT_BENCH_NAMES[@]}" -eq 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " No benchmarks found " \
      "$_BASHUNIT_COLOR_DEFAULT"
    exit 1
  fi

  bashunit::benchmark::print_results

  if [ -n "${BASHUNIT_BENCH_REPORT_JSON:-}" ]; then
    bashunit::benchmark::report_json "$BASHUNIT_BENCH_REPORT_JSON"
  fi
  if [ -n "${BASHUNIT_BENCH_REPORT_JUNIT:-}" ]; then
    bashunit::benchmark::report_junit "$BASHUNIT_BENCH_REPORT_JUNIT"
  fi

  if [ -n "${BASHUNIT_BENCH_BASELINE_UPDATE:-}" ]; then
    bashunit::benchmark::report_json "$BASHUNIT_BENCH_BASELINE_UPDATE"
  fi

  if [ -n "${BASHUNIT_BENCH_BASELINE:-}" ]; then
    bashunit::benchmark::baseline_load "$BASHUNIT_BENCH_BASELINE"
    if ! bashunit::benchmark::baseline_compare "${BASHUNIT_BENCH_BASELINE_TOLERANCE:-10}"; then
      exit 1
    fi
  fi

  bashunit::internal_log "Finished benchmarks"
}

function bashunit::main::cleanup() {
  printf "%sCaught Ctrl-C, killing all child processes...%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"

  pkill -P $$
  bashunit::cleanup_script_temp_files
  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi
  bashunit::env::cleanup_run_output_dir
  exit 1
}

function bashunit::main::handle_stop_on_failure_sync() {
  printf "\n%sStop on failure enabled...%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  bashunit::console_results::print_failing_tests_and_reset
  bashunit::console_results::print_risky_tests_and_reset
  bashunit::console_results::print_incomplete_tests_and_reset
  bashunit::console_results::print_skipped_tests_and_reset
  bashunit::console_results::render_result
  if bashunit::env::is_profile_enabled; then
    bashunit::console_results::print_profile_and_reset
  fi
  bashunit::cleanup_script_temp_files
  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi
  bashunit::env::cleanup_run_output_dir
  exit 1
}

# src/main/watch.sh

function bashunit::main::watch_get_checksum() {
  local IFS=$' \t\n'
  local -a paths=("$@")

  local file checksum=""
  for file in "${paths[@]+"${paths[@]}"}"; do
    if [ -d "$file" ]; then
      local found
      found=$(find "$file" -name '*.sh' -type f \
        -exec stat -c '%Y %n' {} + 2>/dev/null ||
        find "$file" -name '*.sh' -type f \
          -exec stat -f '%m %N' {} + 2>/dev/null) || true
      checksum="${checksum}${found}"
    elif [ -f "$file" ]; then
      local mtime
      mtime=$(stat -c '%Y' "$file" 2>/dev/null ||
        stat -f '%m' "$file" 2>/dev/null) || true
      checksum="${checksum}${mtime} ${file}"
    fi
  done
  echo "$checksum"
}

function bashunit::main::watch_loop() {
  local filter="$1"
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"
  shift 3

  local IFS=$' \t\n'
  local -a watch_paths=("$@")
  [ -d "src" ] && watch_paths[${#watch_paths[@]}]="src"

  trap 'printf "\n%sWatch mode stopped.%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"; \
    exit 0' INT

  local last_checksum=""
  while true; do
    local current_checksum
    current_checksum=$(bashunit::main::watch_get_checksum \
      "${watch_paths[@]}")

    if [ "$current_checksum" != "$last_checksum" ]; then
      last_checksum="$current_checksum"
      bashunit::io::clear_screen
      printf "%s[watch] Running tests...%s\n\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" \
        "${_BASHUNIT_COLOR_DEFAULT}"

      (
        if [ $# -gt 0 ]; then
          bashunit::main::exec_tests \
            "$filter" "$tag_filter" \
            "$exclude_tag_filter" "$@"
        else
          bashunit::main::exec_tests \
            "$filter" "$tag_filter" \
            "$exclude_tag_filter"
        fi
      ) || true

      printf "\n%s[watch] Waiting for changes...%s\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" \
        "${_BASHUNIT_COLOR_DEFAULT}"
    fi
    sleep 1
  done
}

# src/main/assert.sh

function bashunit::main::is_assertion_function() {
  local name="$1"
  declare -F "assert_$name" &>/dev/null || declare -F "$name" &>/dev/null
}

function bashunit::main::is_exit_code_assertion() {
  local name="$1"
  case "$name" in
  exit_code | successful_code | unsuccessful_code | general_error | command_not_found)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function bashunit::main::cmd_assert() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_assert_help
    exit 0
    ;;
  esac

  local first_arg="${1:-}"
  if [ -z "$first_arg" ]; then
    printf "%sError: Assert function name or command is required.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_assert_help
    exit 1
  fi

  set +euo pipefail

  if bashunit::main::is_assertion_function "$first_arg"; then

    local assert_fn="$first_arg"
    shift
    bashunit::main::exec_assert "$assert_fn" "$@"
  elif [ $# -ge 2 ] && bashunit::main::is_assertion_function "$2"; then

    bashunit::main::exec_multi_assert "$@"
  else

    bashunit::main::exec_assert "$@"
  fi
  exit $?
}

function bashunit::main::exec_assert() {
  local original_assert_fn=$1
  local -a args=()
  local args_count=$(($# - 1))
  [ $# -gt 1 ] && args=("${@:2}")

  local assert_fn=$original_assert_fn

  if ! type "$assert_fn" >/dev/null 2>&1; then
    assert_fn="assert_$assert_fn"
    if ! type "$assert_fn" >/dev/null 2>&1; then
      echo "Function $original_assert_fn does not exist." 1>&2
      exit 127
    fi
  fi

  if [ "$args_count" -lt 1 ]; then
    printf "%sError: assert %s requires at least one argument.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$original_assert_fn" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi

  local last_index=$((args_count - 1))
  local last_arg="${args[$last_index]}"
  local output=""
  local inner_exit_code=0
  local bashunit_exit_code=0

  case "$assert_fn" in
  assert_exit_code)
    output=$(bashunit::main::handle_assert_exit_code "$last_arg")
    inner_exit_code=$?

    args=("${args[@]:0:last_index}")
    args[last_index]="$inner_exit_code"
    ;;
  *)
    # Every other assertion takes its argument as-is; no rewriting needed.
    ;;
  esac

  if [ -n "$output" ]; then
    echo "$output" 1>&1
    assert_fn="assert_same"
  fi

  bashunit::state::set_test_title "assert ${original_assert_fn#assert_}"

  "$assert_fn" "${args[@]}" 1>&2
  bashunit_exit_code=$?

  if [ "$(bashunit::state::get_tests_failed)" -gt 0 ] || [ "$(bashunit::state::get_assertions_failed)" -gt 0 ]; then
    return 1
  fi

  return "$bashunit_exit_code"
}

function bashunit::main::handle_assert_exit_code() {
  local cmd="$1"
  local output
  local inner_exit_code=0

  if command -v "${cmd%% *}" >/dev/null 2>&1; then
    output=$(eval "$cmd" 2>&1 || echo "inner_exit_code:$?")
    local last_line
    last_line=$(echo "$output" | tail -n 1)
    if [ "$(echo "$last_line" | "$GREP" -c 'inner_exit_code:[0-9]*' || true)" -gt 0 ]; then
      inner_exit_code=$(echo "$last_line" | grep -o 'inner_exit_code:[0-9]*' | cut -d':' -f2)
      local _re='^[0-9]+$'
      if [ "$(echo "$inner_exit_code" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then
        inner_exit_code=1
      fi
      output=$(echo "$output" | sed '$d')
    fi
    echo "$output"
    return "$inner_exit_code"
  else
    echo "Command not found: $cmd" 1>&2
    return 127
  fi
}

function bashunit::main::exec_multi_assert() {
  local cmd="$1"
  shift

  if [ $# -lt 1 ]; then
    printf "%sError: Multi-assertion mode requires at least one assertion.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
    printf "Usage: bashunit assert \"<command>\" <assertion1> <arg1> [<assertion2> <arg2>...]\n" 1>&2
    return 1
  fi

  if [ $# -lt 2 ] || [ $(($# % 2)) -ne 0 ]; then
    local assertion_name="${1:-}"
    printf "%sError: Missing argument for assertion '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
    return 1
  fi

  local stdout
  local cmd_exit_code
  stdout=$(eval "$cmd" 2>&1)
  cmd_exit_code=$?

  if [ -n "$stdout" ]; then
    echo "$stdout" 1>&1
  fi

  local overall_result=0
  while [ $# -gt 0 ]; do
    local assertion_name="$1"
    local assertion_arg="${2:-}"

    if [ -z "$assertion_arg" ]; then
      printf "%sError: Missing argument for assertion '%s'.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
      return 1
    fi

    shift 2

    local assert_fn="$assertion_name"
    if ! type "$assert_fn" &>/dev/null; then
      assert_fn="assert_$assertion_name"
      if ! type "$assert_fn" &>/dev/null; then
        printf "%sError: Unknown assertion '%s'.%s\n" \
          "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
        return 1
      fi
    fi

    bashunit::state::set_test_title "assert ${assertion_name#assert_}"

    if bashunit::main::is_exit_code_assertion "$assertion_name"; then

      "$assert_fn" "$assertion_arg" "" "$cmd_exit_code" 1>&2
    else

      "$assert_fn" "$assertion_arg" "$stdout" 1>&2
    fi

    if [ "$(bashunit::state::get_assertions_failed)" -gt 0 ]; then
      overall_result=1
    fi
  done

  return $overall_result
}

# src/main/subcommands.sh

function bashunit::main::cmd_doc() {
  local filter=""
  local custom_only=false
  local boot_file="${BASHUNIT_BOOTSTRAP:-}"
  local boot_provided=false

  while [ $# -gt 0 ]; do
    case "$1" in
    -h | --help)
      bashunit::console_header::print_doc_help
      exit 0
      ;;
    --custom)
      custom_only=true
      shift
      ;;
    -e | --env | --boot)
      boot_file="${2:-}"
      boot_provided=true
      shift 2
      ;;
    *)
      filter="$1"
      shift
      ;;
    esac
  done

  local known
  known="$(compgen -A function assert_ 2>/dev/null)"

  if [ -n "$boot_file" ]; then
    if [ ! -r "$boot_file" ]; then
      if [ "$boot_provided" = true ]; then
        printf "%sError: cannot read the bootstrap file: '%s'.%s\n" \
          "$_BASHUNIT_COLOR_FAILED" "$boot_file" "$_BASHUNIT_COLOR_DEFAULT" >&2
        exit 1
      fi
    else

      _BASHUNIT_LOADING_BOOTSTRAP="$boot_file"

      source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
      _BASHUNIT_LOADING_BOOTSTRAP=""
    fi
  fi

  bashunit::doc::custom_fns_to_slot "$known"

  if [ "$custom_only" = true ]; then
    if ! bashunit::doc::print_custom_asserts "$filter"; then
      printf 'No custom assertions found.\n'
      printf 'Load them with --boot <file> or BASHUNIT_BOOTSTRAP.\n'
    fi
    exit 0
  fi

  local rendered
  rendered=$(bashunit::doc::print_asserts "$filter")
  if [ -n "$rendered" ]; then
    printf '%s\n' "$rendered"
  elif [ -z "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
    printf "No assertion matches '%s'.\n" "$filter"
    printf 'Run `bashunit doc` without a filter to list them all.\n'
  fi

  if [ -n "$_BASHUNIT_DOC_CUSTOM_FNS_OUT" ]; then
    printf '\n## Custom assertions\n\n'
    bashunit::doc::print_custom_asserts "$filter" || true
  fi

  exit 0
}

function bashunit::main::cmd_init() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_init_help
    exit 0
    ;;
  esac

  bashunit::init::project "${1:-}"
  exit 0
}

function bashunit::main::cmd_learn() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_learn_help
    exit 0
    ;;
  esac

  bashunit::learn::start
  exit 0
}

function bashunit::main::cmd_watch() {
  local path=""
  local -a extra_args=()

  while [ $# -gt 0 ]; do
    case "$1" in
    -h | --help)
      bashunit::console_header::print_watch_help
      exit 0
      ;;
    -f | --filter)

      extra_args[${#extra_args[@]}]="$1"
      shift || true
      if [ $# -gt 0 ]; then
        extra_args[${#extra_args[@]}]="$1"
      fi
      ;;
    -*)
      extra_args[${#extra_args[@]}]="$1"
      ;;
    *)
      if [ -z "$path" ]; then
        path="$1"
      else
        extra_args[${#extra_args[@]}]="$1"
      fi
      ;;
    esac
    shift || true
  done

  [ -z "$path" ] && path="."

  bashunit::watch::run "$path" "${extra_args[@]+"${extra_args[@]}"}"
}

function bashunit::main::cmd_upgrade() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_upgrade_help
    exit 0
    ;;
  esac

  bashunit::upgrade::upgrade
  exit 0
}

# src/main/bench.sh

function bashunit::main::cmd_bench() {
  local filter=""
  local IFS=$' \t\n'
  local -a raw_args=()
  local raw_args_count=0
  local -a args=()
  local args_count=0

  BASHUNIT_BENCH_MODE=true
  export -n BASHUNIT_BENCH_MODE

  while [ $# -gt 0 ]; do
    case "$1" in
    -f | --filter)
      filter="$2"
      shift
      ;;
    --baseline)
      BASHUNIT_BENCH_BASELINE="$2"
      export -n BASHUNIT_BENCH_BASELINE
      shift
      ;;
    --baseline-tolerance)
      BASHUNIT_BENCH_BASELINE_TOLERANCE="$2"
      export -n BASHUNIT_BENCH_BASELINE_TOLERANCE
      case "$BASHUNIT_BENCH_BASELINE_TOLERANCE" in
      '' | *[!0-9.]*)
        printf "%sError: --baseline-tolerance expects a percentage, got '%s'.%s\n" \
          "${_BASHUNIT_COLOR_FAILED}" "$2" "${_BASHUNIT_COLOR_DEFAULT}" >&2
        exit 1
        ;;
      esac
      shift
      ;;
    --baseline-update)
      BASHUNIT_BENCH_BASELINE_UPDATE="$2"
      export -n BASHUNIT_BENCH_BASELINE_UPDATE
      bashunit::main::require_writable_path_or_exit \
        "$BASHUNIT_BENCH_BASELINE_UPDATE" "BASHUNIT_BENCH_BASELINE_UPDATE"
      shift
      ;;
    --report-json)
      BASHUNIT_BENCH_REPORT_JSON="$2"
      export -n BASHUNIT_BENCH_REPORT_JSON

      bashunit::main::require_writable_path_or_exit \
        "$BASHUNIT_BENCH_REPORT_JSON" "BASHUNIT_BENCH_REPORT_JSON"
      shift
      ;;
    --report-junit)
      BASHUNIT_BENCH_REPORT_JUNIT="$2"
      export -n BASHUNIT_BENCH_REPORT_JUNIT
      bashunit::main::require_writable_path_or_exit \
        "$BASHUNIT_BENCH_REPORT_JUNIT" "BASHUNIT_BENCH_REPORT_JUNIT"
      shift
      ;;
    -s | --simple)
      BASHUNIT_SIMPLE_OUTPUT=true
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --detailed)
      BASHUNIT_SIMPLE_OUTPUT=false
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    -e | --env | --boot)

      local boot_file="${2%% *}"
      local boot_args="${2#* }"
      if [ "$boot_args" != "$2" ]; then
        BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        export -n BASHUNIT_BOOTSTRAP_ARGS
      fi

      if [ ! -f "$boot_file" ] || [ ! -r "$boot_file" ]; then
        bashunit::main::report_unreadable_bootstrap "$boot_file" "$2"
      fi

      set -o allexport

      _BASHUNIT_LOADING_BOOTSTRAP="$boot_file"

      source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
      _BASHUNIT_LOADING_BOOTSTRAP=""
      set +o allexport
      shift
      ;;
    -vvv | --verbose)
      BASHUNIT_VERBOSE=true
      export -n BASHUNIT_VERBOSE
      ;;
    --skip-env-file)
      BASHUNIT_SKIP_ENV_FILE=true
      export -n BASHUNIT_SKIP_ENV_FILE
      ;;
    -l | --login)
      BASHUNIT_LOGIN_SHELL=true
      export -n BASHUNIT_LOGIN_SHELL
      ;;
    --no-color)

      BASHUNIT_NO_COLOR=true
      ;;
    -h | --help)
      bashunit::console_header::print_bench_help
      exit 0
      ;;
    -*)
      bashunit::main::abort_unknown_option "$1" "bench"
      ;;
    *)
      raw_args[raw_args_count]="$1"
      raw_args_count=$((raw_args_count + 1))
      ;;
    esac
    shift
  done

  if [ "$raw_args_count" -gt 0 ]; then
    local arg file
    for arg in "${raw_args[@]+"${raw_args[@]}"}"; do
      while IFS= read -r file; do
        args[args_count]="$file"
        args_count=$((args_count + 1))
      done < <(bashunit::helper::find_files_recursive "$arg" '*[bB]ench.sh')
    done
  fi

  if [ -f "${BASHUNIT_BOOTSTRAP:-}" ]; then

    _BASHUNIT_LOADING_BOOTSTRAP="$BASHUNIT_BOOTSTRAP"

    source "$BASHUNIT_BOOTSTRAP" ${BASHUNIT_BOOTSTRAP_ARGS:-}
    _BASHUNIT_LOADING_BOOTSTRAP=""
  fi

  set +euo pipefail

  if [ "$args_count" -gt 0 ]; then
    bashunit::main::exec_benchmarks "$filter" "${args[@]}"
  else
    bashunit::main::exec_benchmarks "$filter"
  fi
}

# src/main/test.sh

_BASHUNIT_MAIN_SUITE_ARGV=()

function bashunit::main::apply_suites() {
  local -a suite_names=()
  local -a rest=()
  local -a cli_paths=()
  local has_suite=false

  while [ $# -gt 0 ]; do
    case "$1" in
    --suite)
      if [ -z "${2:-}" ]; then
        printf "%sError: --suite requires a name.%s\n" \
          "${_BASHUNIT_COLOR_FAILED:-}" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2
        exit 1
      fi
      has_suite=true
      suite_names[${#suite_names[@]}]="$2"
      shift
      ;;
    --list-suites)
      bashunit::suites::load ".bashunitrc"
      bashunit::suites::names
      exit 0
      ;;
    -*)
      rest[${#rest[@]}]="$1"
      ;;
    *)
      cli_paths[${#cli_paths[@]}]="$1"
      rest[${#rest[@]}]="$1"
      ;;
    esac
    shift
  done

  if [ "$has_suite" = false ]; then
    _BASHUNIT_MAIN_SUITE_ARGV=(${rest[@]+"${rest[@]}"})
    return 0
  fi

  bashunit::suites::load ".bashunitrc"

  local -a expanded=()
  local -a suite_paths=()
  local name entry path
  for name in ${suite_names[@]+"${suite_names[@]}"}; do
    bashunit::suites::resolve "$name"
    while IFS= read -r entry; do
      [ -z "$entry" ] && continue
      expanded[${#expanded[@]}]="$entry"
    done <<EOF
$_BASHUNIT_SUITE_ARGS_OUT
EOF
    for path in $_BASHUNIT_SUITE_PATHS_OUT; do
      suite_paths[${#suite_paths[@]}]="$path"
    done
  done

  if [ "${#cli_paths[@]}" -eq 0 ]; then
    for path in ${suite_paths[@]+"${suite_paths[@]}"}; do
      expanded[${#expanded[@]}]="$path"
    done
  fi

  _BASHUNIT_MAIN_SUITE_ARGV=(${expanded[@]+"${expanded[@]}"} ${rest[@]+"${rest[@]}"})
}

function bashunit::main::cmd_test() {
  local filter=""
  local tag_filter=""
  local exclude_tag_filter=""
  local IFS=$' \t\n'
  local -a raw_args=()
  local raw_args_count=0
  local -a args=()
  local args_count=0
  local assert_fn=""
  local _bashunit_coverage_opt_set=false

  bashunit::main::apply_suites "$@"
  set -- ${_BASHUNIT_MAIN_SUITE_ARGV[@]+"${_BASHUNIT_MAIN_SUITE_ARGV[@]}"}

  while [ $# -gt 0 ]; do
    case "$1" in
    -a | --assert)

      bashunit::env::warn_deprecated "\`bashunit test --assert\`" "\`bashunit assert\`"
      assert_fn="$2"
      shift
      ;;
    -f | --filter)
      filter="$2"
      shift
      ;;
    --exclude-filter)
      if [ -z "$BASHUNIT_EXCLUDE_FILTER" ]; then
        BASHUNIT_EXCLUDE_FILTER="$2"
      else
        BASHUNIT_EXCLUDE_FILTER="$BASHUNIT_EXCLUDE_FILTER,$2"
      fi

      export -n BASHUNIT_EXCLUDE_FILTER
      shift
      ;;
    --tag)
      bashunit::main::require_valid_tag_expression_or_exit "$2"
      if [ -z "$tag_filter" ]; then
        tag_filter="$2"
      else
        tag_filter="$tag_filter,$2"
      fi
      shift
      ;;
    --exclude-tag)
      if [ -z "$exclude_tag_filter" ]; then
        exclude_tag_filter="$2"
      else
        exclude_tag_filter="$exclude_tag_filter,$2"
      fi
      shift
      ;;
    --sandbox)
      BASHUNIT_SANDBOX=true
      export -n BASHUNIT_SANDBOX
      ;;
    --sandbox-allow)

      if [ -z "$BASHUNIT_SANDBOX_ALLOW" ]; then
        BASHUNIT_SANDBOX_ALLOW="$2"
      else
        BASHUNIT_SANDBOX_ALLOW="$BASHUNIT_SANDBOX_ALLOW,$2"
      fi
      export -n BASHUNIT_SANDBOX_ALLOW
      shift
      ;;
    -s | --simple)
      BASHUNIT_SIMPLE_OUTPUT=true
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --detailed)
      BASHUNIT_SIMPLE_OUTPUT=false
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --output)
      BASHUNIT_OUTPUT_FORMAT="$2"
      export -n BASHUNIT_OUTPUT_FORMAT
      shift
      ;;
    --debug)
      local output_file="${2:-}"
      if [ -n "$output_file" ] && [ "${output_file:0:1}" != "-" ]; then
        exec >"$output_file" 2>&1
        shift
      fi
      set -x
      ;;
    -S | --stop-on-failure)

      BASHUNIT_STOP_ON_FAILURE=true
      export -n BASHUNIT_STOP_ON_FAILURE
      ;;
    -p | --parallel)
      BASHUNIT_PARALLEL_RUN=true
      export -n BASHUNIT_PARALLEL_RUN
      ;;
    -j | --jobs)
      BASHUNIT_PARALLEL_RUN=true
      export -n BASHUNIT_PARALLEL_RUN

      if [ "$2" = "auto" ]; then
        BASHUNIT_PARALLEL_JOBS="$(bashunit::check_os::nproc)"
        export -n BASHUNIT_PARALLEL_JOBS
      else
        BASHUNIT_PARALLEL_JOBS="$2"
        export -n BASHUNIT_PARALLEL_JOBS
      fi
      shift
      ;;
    --no-parallel)
      BASHUNIT_PARALLEL_RUN=false
      export -n BASHUNIT_PARALLEL_RUN
      ;;
    --test-timeout)
      BASHUNIT_TEST_TIMEOUT="$2"
      export -n BASHUNIT_TEST_TIMEOUT
      shift
      ;;
    --retry)
      BASHUNIT_RETRY="$2"
      export -n BASHUNIT_RETRY
      shift
      ;;
    --repeat)
      BASHUNIT_REPEAT="$2"
      export -n BASHUNIT_REPEAT
      shift
      ;;
    --random-order)
      BASHUNIT_RANDOM_ORDER=true
      export -n BASHUNIT_RANDOM_ORDER
      ;;
    --order-by)
      BASHUNIT_ORDER_BY="$2"
      export -n BASHUNIT_ORDER_BY
      shift
      ;;
    --seed)
      BASHUNIT_SEED="$2"
      export -n BASHUNIT_SEED
      shift
      ;;
    --shard)
      bashunit::main::set_shard_or_exit "$2"
      shift
      ;;
    --rerun-failed)
      BASHUNIT_RERUN_FAILED=true
      export -n BASHUNIT_RERUN_FAILED
      ;;
    --changed)
      BASHUNIT_CHANGED=true
      export -n BASHUNIT_CHANGED

      if [ -n "${2:-}" ] && [ "${2#-}" = "${2:-}" ] && [ ! -e "$2" ]; then
        BASHUNIT_CHANGED_REF="$2"
        export -n BASHUNIT_CHANGED_REF
        shift
      fi
      ;;
    --list | --dry-run)
      BASHUNIT_LIST_TESTS=true
      export -n BASHUNIT_LIST_TESTS
      ;;
    --list-format)
      BASHUNIT_LIST_FORMAT="$2"
      export -n BASHUNIT_LIST_FORMAT
      shift
      ;;
    --snapshot-update)
      BASHUNIT_SNAPSHOT_UPDATE=true
      export -n BASHUNIT_SNAPSHOT_UPDATE
      ;;
    --no-snapshot-create)
      BASHUNIT_SNAPSHOT_CREATE=false
      export -n BASHUNIT_SNAPSHOT_CREATE
      ;;
    --snapshot-prune)
      BASHUNIT_SNAPSHOT_PRUNE=true
      export -n BASHUNIT_SNAPSHOT_PRUNE
      ;;
    --snapshot-report-unused)
      BASHUNIT_SNAPSHOT_REPORT_UNUSED=true
      export -n BASHUNIT_SNAPSHOT_REPORT_UNUSED
      ;;
    -w | --watch)
      BASHUNIT_WATCH_MODE=true
      export -n BASHUNIT_WATCH_MODE
      ;;
    -e | --env | --boot)

      local boot_file="${2%% *}"
      local boot_args="${2#* }"
      if [ "$boot_args" != "$2" ]; then
        BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        export -n BASHUNIT_BOOTSTRAP_ARGS
      fi

      if [ ! -f "$boot_file" ] || [ ! -r "$boot_file" ]; then
        bashunit::main::report_unreadable_bootstrap "$boot_file" "$2"
      fi

      set -o allexport

      _BASHUNIT_LOADING_BOOTSTRAP="$boot_file"

      source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
      _BASHUNIT_LOADING_BOOTSTRAP=""
      set +o allexport
      shift
      ;;

    --log-junit | --report-junit)
      BASHUNIT_LOG_JUNIT="$2"
      export -n BASHUNIT_LOG_JUNIT
      shift
      ;;
    --log-gha)
      BASHUNIT_LOG_GHA="$2"
      export -n BASHUNIT_LOG_GHA
      shift
      ;;
    --gha-annotations)
      BASHUNIT_GHA_ANNOTATIONS="$2"
      export -n BASHUNIT_GHA_ANNOTATIONS
      shift
      ;;
    -r | --report-html)
      BASHUNIT_REPORT_HTML="$2"
      export -n BASHUNIT_REPORT_HTML
      shift
      ;;
    --report-tap)
      BASHUNIT_REPORT_TAP="$2"
      export -n BASHUNIT_REPORT_TAP
      shift
      ;;
    --report-md)
      BASHUNIT_REPORT_MD="$2"
      export -n BASHUNIT_REPORT_MD
      shift
      ;;
    --report-json)
      BASHUNIT_REPORT_JSON="$2"
      export -n BASHUNIT_REPORT_JSON
      shift
      ;;
    --no-output)
      BASHUNIT_NO_OUTPUT=true
      export -n BASHUNIT_NO_OUTPUT
      ;;
    -vvv | --verbose)
      BASHUNIT_VERBOSE=true
      export -n BASHUNIT_VERBOSE
      ;;
    -h | --help)
      bashunit::console_header::print_test_help
      exit 0
      ;;
    --show-skipped)
      BASHUNIT_SHOW_SKIPPED=true
      export -n BASHUNIT_SHOW_SKIPPED
      ;;
    --show-incomplete)
      BASHUNIT_SHOW_INCOMPLETE=true
      export -n BASHUNIT_SHOW_INCOMPLETE
      ;;
    --failures-only)
      BASHUNIT_FAILURES_ONLY=true
      export -n BASHUNIT_FAILURES_ONLY
      ;;
    --fail-on-risky)
      BASHUNIT_FAIL_ON_RISKY=true
      export -n BASHUNIT_FAIL_ON_RISKY
      ;;
    --fail-on-flaky)
      BASHUNIT_FAIL_ON_FLAKY=true
      export -n BASHUNIT_FAIL_ON_FLAKY
      ;;
    --profile)
      BASHUNIT_PROFILE=true
      export -n BASHUNIT_PROFILE
      ;;
    --show-output)
      BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true
      export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
      ;;
    --no-output-on-failure)
      BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
      export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
      ;;
    --no-progress)
      BASHUNIT_NO_PROGRESS=true
      export -n BASHUNIT_NO_PROGRESS
      ;;
    --strict)
      BASHUNIT_STRICT_MODE=true
      export -n BASHUNIT_STRICT_MODE
      ;;
    -R | --run-all)
      BASHUNIT_STOP_ON_ASSERTION_FAILURE=false
      export -n BASHUNIT_STOP_ON_ASSERTION_FAILURE
      ;;
    --skip-env-file)
      BASHUNIT_SKIP_ENV_FILE=true
      export -n BASHUNIT_SKIP_ENV_FILE
      ;;
    -l | --login)
      BASHUNIT_LOGIN_SHELL=true
      export -n BASHUNIT_LOGIN_SHELL
      ;;
    --no-color)

      BASHUNIT_NO_COLOR=true
      ;;
    --coverage)

      BASHUNIT_COVERAGE=true
      ;;
    --coverage-paths)

      BASHUNIT_COVERAGE_PATHS="$2"
      shift
      ;;
    --coverage-exclude)

      BASHUNIT_COVERAGE_EXCLUDE="$2"
      shift
      ;;
    --coverage-report)

      case "${2:-}" in
      '' | -*)
        BASHUNIT_COVERAGE_REPORT="$_BASHUNIT_DEFAULT_COVERAGE_REPORT"
        ;;
      *)
        BASHUNIT_COVERAGE_REPORT="$2"
        shift
        ;;
      esac
      _bashunit_coverage_opt_set=true
      ;;
    --coverage-min)

      BASHUNIT_COVERAGE_MIN="$2"
      _bashunit_coverage_opt_set=true
      shift
      ;;
    --coverage-diff)

      BASHUNIT_COVERAGE_DIFF="$2"
      _bashunit_coverage_opt_set=true
      shift
      ;;
    --no-coverage-report)

      BASHUNIT_COVERAGE_REPORT=""
      ;;
    --coverage-report-html)

      if [ -z "${2:-}" ]; then
        BASHUNIT_COVERAGE_REPORT_HTML="coverage/html"
      else
        case "${2:-}" in
        -*)
          BASHUNIT_COVERAGE_REPORT_HTML="coverage/html"
          ;;
        *)
          BASHUNIT_COVERAGE_REPORT_HTML="$2"
          shift
          ;;
        esac
      fi
      _bashunit_coverage_opt_set=true
      ;;
    --coverage-report-cobertura)

      if [ -z "${2:-}" ]; then
        BASHUNIT_COVERAGE_REPORT_COBERTURA="coverage/cobertura.xml"
      else
        case "${2:-}" in
        -*)
          BASHUNIT_COVERAGE_REPORT_COBERTURA="coverage/cobertura.xml"
          ;;
        *)
          BASHUNIT_COVERAGE_REPORT_COBERTURA="$2"
          shift
          ;;
        esac
      fi
      _bashunit_coverage_opt_set=true
      ;;
    -*)

      bashunit::main::abort_unknown_option "$1" "test"
      ;;
    *)
      raw_args[raw_args_count]="$1"
      raw_args_count=$((raw_args_count + 1))
      ;;
    esac
    shift
  done

  bashunit::main::validate_config_or_exit

  if [ "$_bashunit_coverage_opt_set" = true ]; then

    BASHUNIT_COVERAGE=true
  fi

  local inline_filter=""
  local inline_filter_file=""
  if [ "$raw_args_count" -gt 0 ]; then
    if [ -n "$assert_fn" ]; then

      args=("${raw_args[@]}")
      args_count="$raw_args_count"
    else

      local arg
      for arg in "${raw_args[@]+"${raw_args[@]}"}"; do
        local parsed_path parsed_filter
        {
          read -r parsed_path
          read -r parsed_filter
        } < <(bashunit::helper::parse_file_path_filter "$arg")

        if [ -n "$parsed_filter" ]; then
          inline_filter="$parsed_filter"
          inline_filter_file="$parsed_path"
        fi

        local file
        while IFS= read -r file; do
          args[args_count]="$file"
          args_count=$((args_count + 1))
        done < <(bashunit::helper::find_files_recursive "$parsed_path" '*[tT]est.sh')
      done

      case "$inline_filter" in
      "__line__:"*)
        local line_number="${inline_filter#__line__:}"
        local resolved_file="${inline_filter_file}"

        if [ "$args_count" -gt 0 ]; then
          resolved_file="${args[0]}"
        fi

        inline_filter=$(bashunit::helper::find_function_at_line "$resolved_file" "$line_number")
        if [ -z "$inline_filter" ]; then
          printf "%sError: No test function found at line %s in %s%s\n" \
            "${_BASHUNIT_COLOR_FAILED}" "$line_number" "$resolved_file" "${_BASHUNIT_COLOR_DEFAULT}"
          exit 1
        fi
        ;;
      esac

      if [ -z "$filter" ] && [ -n "$inline_filter" ]; then
        filter="$inline_filter"
      fi
    fi
  fi

  if bashunit::env::is_snapshot_report_unused_enabled ||
    bashunit::env::is_snapshot_prune_enabled; then
    local _snapshot_flag="--snapshot-report-unused"
    if bashunit::env::is_snapshot_prune_enabled; then
      _snapshot_flag="--snapshot-prune"
    fi
    local _partial_flag=""
    [ -n "$filter" ] && _partial_flag="--filter"
    [ -n "$tag_filter" ] && _partial_flag="--tag"
    [ -n "$exclude_tag_filter" ] && _partial_flag="--exclude-tag"
    [ -n "${BASHUNIT_SHARD_INDEX:-}" ] && _partial_flag="--shard"
    bashunit::rerun::is_enabled && _partial_flag="--rerun-failed"
    bashunit::env::is_changed_enabled && _partial_flag="--changed"
    if [ -n "$_partial_flag" ]; then
      printf "%sError: %s needs a full run; %s only runs a subset.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$_snapshot_flag" "$_partial_flag" \
        "${_BASHUNIT_COLOR_DEFAULT}" >&2
      exit 1
    fi
  fi

  if [ -z "$assert_fn" ] && bashunit::rerun::is_enabled; then
    bashunit::rerun::load
    if bashunit::rerun::has_entries; then
      local -a _rerun_files=()
      local _rerun_file
      while IFS= read -r _rerun_file; do
        [ -z "$_rerun_file" ] && continue

        [ -f "$_rerun_file" ] || continue
        _rerun_files[${#_rerun_files[@]}]="$_rerun_file"
      done < <(bashunit::rerun::files)
      if [ "${#_rerun_files[@]}" -gt 0 ]; then
        args=("${_rerun_files[@]}")
        args_count=${#args[@]}
      fi
    else
      printf "%sNo previously failing tests recorded; running the full suite.%s\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    fi
  fi

  if [ -f "${BASHUNIT_BOOTSTRAP:-}" ]; then

    _BASHUNIT_LOADING_BOOTSTRAP="$BASHUNIT_BOOTSTRAP"

    source "$BASHUNIT_BOOTSTRAP" ${BASHUNIT_BOOTSTRAP_ARGS:-}
    _BASHUNIT_LOADING_BOOTSTRAP=""
  fi

  if [ "${BASHUNIT_NO_OUTPUT:-false}" = true ]; then
    exec >/dev/null 2>&1
  fi

  set +euo pipefail
  if [ -n "$assert_fn" ]; then

    BASHUNIT_COVERAGE=false
    export -n BASHUNIT_COVERAGE
    bashunit::main::exec_assert "$assert_fn" ${args+"${args[@]}"}
  else
    if [ "${BASHUNIT_WATCH_MODE:-false}" = true ]; then
      bashunit::main::watch_loop \
        "$filter" "$tag_filter" "$exclude_tag_filter" \
        ${args+"${args[@]}"}
    else
      if [ "$args_count" -gt 0 ]; then
        bashunit::main::exec_tests \
          "$filter" "$tag_filter" "$exclude_tag_filter" \
          "${args[@]}"
      else
        bashunit::main::exec_tests \
          "$filter" "$tag_filter" "$exclude_tag_filter"
      fi
    fi
  fi
}

#!/usr/bin/env bash
set -euo pipefail

declare -r BASHUNIT_MIN_BASH_VERSION="3.0"

function _check_bash_version() {
  local current_version
  if [[ -n ${BASHUNIT_TEST_BASH_VERSION:-} ]]; then

    current_version="${BASHUNIT_TEST_BASH_VERSION}"
  elif [[ -n ${BASH_VERSINFO+set} ]]; then

    current_version="${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"
  else

    current_version="$(bash --version | head -n1 | cut -d' ' -f4 | cut -d. -f1,2)"
  fi

  local major minor min_major min_minor
  IFS=. read -r major minor <<<"$current_version"
  IFS=. read -r min_major min_minor <<<"$BASHUNIT_MIN_BASH_VERSION"

  major=${major%%[!0-9]*}
  minor=${minor%%[!0-9]*}

  if ((10#${major:-0} < 10#$min_major)) ||
    { ((10#${major:-0} == 10#$min_major)) && ((10#${minor:-0} < 10#${min_minor:-0})); }; then
    printf 'Bashunit requires Bash >= %s. Current version: %s\n' "$BASHUNIT_MIN_BASH_VERSION" "$current_version" >&2
    exit 1
  fi
}

_check_bash_version

declare -r BASHUNIT_VERSION="0.48.0"

_bashunit_root="${BASH_SOURCE[0]%/*}"

case "$_bashunit_root" in
"${BASH_SOURCE[0]}") _bashunit_root="." ;;
"") _bashunit_root="/" ;;
esac
declare -r BASHUNIT_ROOT_DIR="$_bashunit_root"
unset _bashunit_root
export BASHUNIT_ROOT_DIR

declare -r BASHUNIT_WORKING_DIR="$PWD"
export BASHUNIT_WORKING_DIR

for arg in "$@"; do
  case "$arg" in
  --skip-env-file)

    BASHUNIT_SKIP_ENV_FILE=true
    export -n BASHUNIT_SKIP_ENV_FILE
    ;;
  -l | --login)
    BASHUNIT_LOGIN_SHELL=true
    export -n BASHUNIT_LOGIN_SHELL
    ;;
  --no-color)

    BASHUNIT_NO_COLOR=true
    ;;
  esac
done

bashunit::clock::init

_SUBCOMMAND=""

case "${1:-}" in
test | bench | doc | init | learn | upgrade | assert | watch)
  _SUBCOMMAND="$1"
  shift
  ;;
-v | --version)
  bashunit::console_header::print_version
  exit 0
  ;;
-h | --help)
  bashunit::console_header::print_help
  exit 0
  ;;
-*)

  _SUBCOMMAND="test"
  ;;
"")

  _SUBCOMMAND="test"
  ;;
*)

  _SUBCOMMAND="test"
  ;;
esac

case "$_SUBCOMMAND" in
test) bashunit::main::cmd_test "$@" ;;
bench) bashunit::main::cmd_bench "$@" ;;
doc) bashunit::main::cmd_doc "$@" ;;
init) bashunit::main::cmd_init "$@" ;;
learn) bashunit::main::cmd_learn "$@" ;;
upgrade) bashunit::main::cmd_upgrade "$@" ;;
assert) bashunit::main::cmd_assert "$@" ;;
watch) bashunit::main::cmd_watch "$@" ;;
esac
