shellspec/shellspec

View on GitHub
lib/core/dsl.sh

Summary

Maintainability
Test Coverage
A
99%
#shellcheck shell=sh disable=SC2004

# shellcheck disable=SC2034
SHELLSPEC_PATH_ALIAS='|'
SHELLSPEC_INTERCEPTOR='|'
SHELLSPEC_STDIN_FILE=''
SHELLSPEC_STDOUT_FILE=''
SHELLSPEC_STDERR_FILE=''
SHELLSPEC_LINENO=''
SHELLSPEC_LINENO_BEGIN=''
SHELLSPEC_LINENO_END=''
SHELLSPEC_AUX_LINENO=''
SHELLSPEC_ENABLED=''
SHELLSPEC_FOCUSED=''
SHELLSPEC_SPEC_NO=''
SHELLSPEC_GROUP_ID=''
SHELLSPEC_BLOCK_NO=''
SHELLSPEC_EXAMPLE_ID=''
SHELLSPEC_EXAMPLE_NO=''
SHELLSPEC_EXAMPLE_COUNT=''
SHELLSPEC_DESCRIPTION=''
SHELLSPEC_EXPECTATION=''
SHELLSPEC_EVALUATION=''
SHELLSPEC_HOOK=''
SHELLSPEC_SKIP_ID=''
SHELLSPEC_SKIP_REASON=''
SHELLSPEC_PENDING_REASON=''
SHELLSPEC_SHELL_OPTIONS=''
SHELLSPEC_HOOK_ERROR=''
SHELLSPEC_USE_FDS=''

shellspec_group_id() {
  # shellcheck disable=SC2034
  SHELLSPEC_GROUP_ID=$1 SHELLSPEC_BLOCK_NO=$2
}

shellspec_example_id() {
  # shellcheck disable=SC2034
  SHELLSPEC_EXAMPLE_ID=$1 SHELLSPEC_EXAMPLE_NO=$2 SHELLSPEC_BLOCK_NO=$3
}

shellspec_metadata() {
  shellspec_output METADATA
}

shellspec_finished() {
  shellspec_output FINISHED
}

shellspec_yield() {
  case $# in
    0) "shellspec_yield$SHELLSPEC_BLOCK_NO" ;;
    *) "shellspec_yield$SHELLSPEC_BLOCK_NO" "$@" ;;
  esac
  # shellcheck disable=SC2034
  SHELLSPEC_LINENO=''
}

shellspec_begin() {
  # shellcheck disable=SC2034
  SHELLSPEC_SPECFILE=$1 SHELLSPEC_SPEC_NO=$2 SHELLSPEC_GROUP_ID=''
  SHELLSPEC_WORKDIR="$SHELLSPEC_TMPBASE/$SHELLSPEC_SPEC_NO"
  SHELLSPEC_ERROR_FILE="$SHELLSPEC_WORKDIR/error"
  shellspec_output BEGIN
}

shellspec_execdir() {
  case $1 in
    @project | @project/*) set -- "${1#@project}" ;;
    @basedir | @basedir/*)
      set -- "$1" "/${SHELLSPEC_SPECFILE%/*}"
      while [ "$2" ]; do
        [ -e "${SHELLSPEC_PROJECT_ROOT%/}/$2/.shellspec" ] && break
        [ -e "${SHELLSPEC_PROJECT_ROOT%/}/$2/.shellspec-basedir" ] && break
        set -- "$1" "${2%/*}"
      done
      set -- "${2#/}${1#@basedir}"
      ;;
    @specfile | @specfile/*) set -- "${SHELLSPEC_SPECFILE%/*}${1#@specfile}" ;;
    *) set -- ""
  esac
  case $1 in
    /*) set -- "$1" ;;
    ?*) set -- "/$1" ;;
  esac
  cd "${SHELLSPEC_PROJECT_ROOT%/}$1"
}

shellspec_perform() {
  SHELLSPEC_ENABLED=$1 SHELLSPEC_FILTER=$2
}

shellspec_end() {
  # shellcheck disable=SC2034
  SHELLSPEC_EXAMPLE_COUNT=${1:-}
  shellspec_output END
}

shellspec_include_pack() {
  eval "$1=\$3; shift 3; eval shellspec_pack $2 \${1+'\"\$@\"'}"
}

shellspec_before_first_block() {
  shellspec_mark_group "$SHELLSPEC_BLOCK_NO" ""
  [ "$SHELLSPEC_DRYRUN" ] && return 0
  shellspec_if SKIP && return 0
  if ! shellspec_call_before_hooks ALL; then
    shellspec_output BEFORE_ALL_ERROR
    SHELLSPEC_HOOK_ERROR=1
  fi
  shellspec_mark_group "$SHELLSPEC_BLOCK_NO" 1
}

shellspec_after_last_block() {
  if ! shellspec_call_after_hooks ALL; then
    shellspec_output AFTER_ALL_ERROR
  fi
}

shellspec_after_block() {
  shellspec_call_after_hooks MOCK
}

shellspec_description() {
  if [ "${2:-}" = "@" ]; then
    set -- "$1" "<$1:$SHELLSPEC_LINENO_BEGIN-$SHELLSPEC_LINENO_END>"
  fi
  [ "$1" = "example_group" ] && set -- "$1" "${2:-}${2:+$SHELLSPEC_VT}"
  SHELLSPEC_DESCRIPTION="${SHELLSPEC_DESCRIPTION}$2"
}

shellspec_example_group() {
  shellspec_description "example_group" "${1:-}"
  shellspec_yield
}

shellspec_example_block() {
  SHELLSPEC_STDIO_FILE_BASE="$SHELLSPEC_WORKDIR/$SHELLSPEC_EXAMPLE_ID"
  if [ "${SHELLSPEC_PARAMETER_NO:-}" ]; then
    shellspec_parameters 1
  else
    "shellspec_example$SHELLSPEC_BLOCK_NO"
  fi
}

shellspec_parameters() {
  "shellspec_parameters$1"
  [ "$1" -eq "$SHELLSPEC_PARAMETER_NO" ] && return 0
  shellspec_parameters "$(($1 + 1))"
}

shellspec_parameterized_example() {
  case $SHELLSPEC_STDIO_FILE_BASE in
    *\#*) eval "set -- ${SHELLSPEC_STDIO_FILE_BASE##*\#} ${1+\"\$@\"}" ;;
    *) eval "set -- 0 ${1+\"\$@\"}" ;;
  esac
  SHELLSPEC_STDIO_FILE_BASE="${SHELLSPEC_STDIO_FILE_BASE%\#*}#$(($1 + 1))"
  shift

  ( case $# in
      0) "shellspec_example$SHELLSPEC_BLOCK_NO" ;;
      *) "shellspec_example$SHELLSPEC_BLOCK_NO" "$@" ;;
    esac
  )
  SHELLSPEC_EXAMPLE_NO=$(($SHELLSPEC_EXAMPLE_NO + 1))
}

shellspec_example() {
  shellspec_description "example" "${1:-}"
  if [ $# -gt 0 ]; then
    while shift && [ $# -gt 0 ]; do
      [ "$1" = "--" ] && shift && break
    done
  fi

  # shellcheck disable=SC2034
  SHELLSPEC_STDIN_FILE="$SHELLSPEC_STDIO_FILE_BASE.stdin"
  SHELLSPEC_STDOUT_FILE="$SHELLSPEC_STDIO_FILE_BASE.stdout"
  SHELLSPEC_STDERR_FILE="$SHELLSPEC_STDIO_FILE_BASE.stderr"
  SHELLSPEC_LEAK_FILE="$SHELLSPEC_STDIO_FILE_BASE.leak"
  SHELLSPEC_XTRACE_FILE="$SHELLSPEC_STDIO_FILE_BASE.trace"
  export SHELLSPEC_VARS_FILE="$SHELLSPEC_STDIO_FILE_BASE.vars"

  [ "$SHELLSPEC_ENABLED" ] || return 0
  [ "$SHELLSPEC_FILTER" ] || return 0

  if [ "$SHELLSPEC_DRYRUN" ]; then
    shellspec_output EXAMPLE
    shellspec_output SUCCEEDED
    return 0
  fi

  shellspec_profile_start
  case $- in
    *e*) eval "set -- -e ${1+\"\$@\"}" ;;
    *) eval "set -- +e ${1+\"\$@\"}" ;;
  esac
  shellspec_open_file_descriptors "$SHELLSPEC_USE_FDS"
  set +e
  ( set -e
    shift
    case $# in
      0) shellspec_invoke_example ;;
      *) shellspec_invoke_example "$@" ;;
    esac
  )
  set "$1" -- $? "$SHELLSPEC_LEAK_FILE"
  shellspec_close_file_descriptors "$SHELLSPEC_USE_FDS"
  if [ "$1" -ne 0 ]; then
    [ -s "$2" ] || set -- "$1" "$SHELLSPEC_STDERR_FILE"
    shellspec_output ABORTED "$@"
    shellspec_output FAILED
    [ "$1" -ne "$SHELLSPEC_ERROR_EXIT_CODE" ] || exit "$1"
  fi
  shellspec_profile_end
}

shellspec_invoke_example() {
  shellspec_output EXAMPLE

  shellspec_on NOT_IMPLEMENTED NO_EXPECTATION
  shellspec_off FAILED WARNED LEAK
  shellspec_off UNHANDLED_STATUS UNHANDLED_STDOUT UNHANDLED_STDERR

  # Output SKIP message if skipped in outer group.
  if ! shellspec_output_if SKIP; then
    if [ "$SHELLSPEC_HOOK_ERROR" ]; then
      shellspec_output HOOK_ERROR "$SHELLSPEC_HOOK_ERROR"
      shellspec_output FAILED
      return 0
    fi
    if ! shellspec_dsl_check; then
      shellspec_output ERROR
      return 0
    fi
    if ! shellspec_call_before_hooks EACH; then
      shellspec_output BEFORE_EACH_ERROR
      shellspec_output FAILED
      return 0
    fi
    shellspec_output_if PENDING ||:
    case $# in
      0) shellspec_yield 2>"$SHELLSPEC_LEAK_FILE" ;;
      *) shellspec_yield "$@" 2>"$SHELLSPEC_LEAK_FILE" ;;
    esac
    if ! shellspec_call_after_hooks EACH; then
      shellspec_output AFTER_EACH_ERROR
      shellspec_output FAILED
      return 0
    fi
  fi

  shellspec_if SKIP && shellspec_unless FAILED && {
    shellspec_output SKIPPED && return 0
  }

  shellspec_if NOT_IMPLEMENTED && {
    SHELLSPEC_PENDING_REASON="Not yet implemented"
    shellspec_output NOT_IMPLEMENTED
    shellspec_output TODO
    return 0
  }

  shellspec_output_if NO_EXPECTATION && shellspec_on WARNED
  shellspec_output_if UNHANDLED_STATUS && shellspec_on WARNED
  shellspec_output_if UNHANDLED_STDOUT && shellspec_on WARNED
  shellspec_output_if UNHANDLED_STDERR && shellspec_on WARNED

  shellspec_if PENDING && {
    shellspec_if FAILED && shellspec_output TODO && return 0
    [ "$SHELLSPEC_WARNING_AS_FAILURE" ] && shellspec_if WARNED && {
      shellspec_output TODO && return 0
    }
    shellspec_output FIXED && return 0
  }

  [ -s "$SHELLSPEC_LEAK_FILE" ] && shellspec_on LEAK
  shellspec_output_if LEAK "$SHELLSPEC_LEAK_FILE" && shellspec_on FAILED
  shellspec_output_if FAILED && return 0
  shellspec_output_if WARNED || shellspec_output SUCCEEDED
}

shellspec_dsl_check() {
  shellspec_fds_check
}

shellspec_fds_check() {
  set -- "$SHELLSPEC_USE_FDS:"
  while [ "${1%%:*}" ]; do
    set -- "${1#*:}" "${1%%:*}"
    case $2 in ([0-9]) continue; esac
    if shellspec_is_identifier "$2"; then
      [ "$SHELLSPEC_FDVAR_AVAILABLE" ] && continue
      set -- "Assigning file descriptors to variables is not supported" \
        "in the current shell"
    else
      set -- "Invalid file descriptor:" "$2"
    fi
    shellspec_output DSL_ERROR "UseFD: $1 $2"
    return 1
  done
  return 0
}

shellspec_statement() {
  shellspec_off SYNTAX_ERROR
  shellspec_if SKIP && return 0
  # shellcheck disable=SC2145
  "shellspec_$@"
  shellspec_if SYNTAX_ERROR && shellspec_on FAILED
  return 0
}

shellspec_when() {
  # Allow "When I"
  if [ "$#" -gt "0" ] && [ "$1" = "I" ]; then
    shift
  fi
  eval shellspec_join SHELLSPEC_EVALUATION '" "' When ${1+'"$@"'}
  shellspec_off NOT_IMPLEMENTED

  shellspec_if EVALUATION && {
    set -- "Evaluation has already been executed." \
      "Only one Evaluation is allowed per Example." \
      "(Use 'parameterized example' if you want a loop)"
    shellspec_output SYNTAX_ERROR_EVALUATION "$1 $2${SHELLSPEC_LF}$3"
    shellspec_on FAILED
    return 0
  }
  shellspec_on EVALUATION

  shellspec_if NO_EXPECTATION || {
    set -- "Expectation has already been executed."
    shellspec_output SYNTAX_ERROR_EVALUATION "$1"
    shellspec_on FAILED
    return 0
  }

  if [ $# -eq 0 ]; then
    shellspec_output SYNTAX_ERROR_EVALUATION "Missing evaluation type."
    shellspec_on FAILED
    return 0
  fi

  if [ $# -eq 1 ]; then
    shellspec_output SYNTAX_ERROR_EVALUATION "Missing evaluation."
    shellspec_on FAILED
    return 0
  fi

  shellspec_output EVALUATION
  shellspec_statement_evaluation "$@"
}

shellspec_around_call() {
  shellspec_call_before_evaluation_hooks CALL || return "$?"
  "$@"
  set -- $?
  [ "$1" -ne 0 ] && return "$1"
  shellspec_call_after_evaluation_hooks CALL || return "$?"
}

shellspec_around_run() {
  shellspec_call_before_evaluation_hooks RUN || return "$?"
  "$@"
  set -- $?
  [ "$1" -ne 0 ] && return "$1"
  shellspec_call_after_evaluation_hooks RUN || return "$?"
}

shellspec_the() {
  eval shellspec_join SHELLSPEC_EXPECTATION '" "' The ${1+'"$@"'}
  shellspec_off NOT_IMPLEMENTED NO_EXPECTATION
  [ "$SHELLSPEC_XTRACE_ONLY" ] && return 0

  if [ $# -eq 0 ]; then
    shellspec_output SYNTAX_ERROR_EXPECTATION "Missing expectation"
    shellspec_on FAILED
    return 0
  fi

  shellspec_statement_preposition "$@"
}

shellspec_assert() {
  eval shellspec_join SHELLSPEC_EXPECTATION '" "' Assert ${1+'"$@"'}
  shellspec_off NOT_IMPLEMENTED NO_EXPECTATION
  [ "$SHELLSPEC_XTRACE_ONLY" ] && return 0

  if [ $# -eq 0 ]; then
    shellspec_output SYNTAX_ERROR_EXPECTATION "Missing assertion"
    shellspec_on FAILED
    return 0
  fi

  SHELLSPEC_ASSERT_STDERR_FILE="$SHELLSPEC_WORKDIR/assert.stderr"

  case $- in
    *e*) eval "set -- -e ${1+\"\$@\"}" ;;
    *) eval "set -- +e ${1+\"\$@\"}" ;;
  esac
  set +e
  ( set -e; shift; "$@" </dev/null 2>"$SHELLSPEC_ASSERT_STDERR_FILE" )
  set "$1" -- $?

  if [ "$1" -eq 0 ]; then
    if [ -s "$SHELLSPEC_ASSERT_STDERR_FILE" ]; then
      shellspec_output ASSERT_WARN "$1" "$SHELLSPEC_ASSERT_STDERR_FILE"
      shellspec_on WARNED
      return 0
    fi
    shellspec_output MATCHED
    return 0
  fi

  shellspec_output ASSERT_ERROR "$1" "$SHELLSPEC_ASSERT_STDERR_FILE"
  shellspec_on FAILED
}

shellspec_proxy shellspec_before_all "shellspec_register_before_hook ALL"
shellspec_proxy shellspec_after_all "shellspec_register_after_hook ALL"

shellspec_proxy shellspec_before "shellspec_register_before_hook EACH"
shellspec_proxy shellspec_after "shellspec_register_after_hook EACH"

shellspec_proxy shellspec_before_each "shellspec_register_before_hook EACH"
shellspec_proxy shellspec_after_each "shellspec_register_after_hook EACH"

shellspec_proxy shellspec_before_call "shellspec_register_before_hook CALL"
shellspec_proxy shellspec_after_call "shellspec_register_after_hook CALL"

shellspec_proxy shellspec_before_run "shellspec_register_before_hook RUN"
shellspec_proxy shellspec_after_run "shellspec_register_after_hook RUN"

shellspec_proxy shellspec_after_mock "shellspec_register_after_hook MOCK"

shellspec_path() {
  while [ $# -gt 0 ]; do
    SHELLSPEC_PATH_ALIAS="${SHELLSPEC_PATH_ALIAS}$1|"
    shift
  done
}

shellspec_skip() {
  # Do nothing if already skipped by current example or example group
  shellspec_if SKIP && return 0
  # shellcheck disable=SC2034
  SHELLSPEC_SKIP_ID=$1
  shift

  if [ "${1:-}" = if ]; then
    [ $# -ge 3 ] || return 0
    SHELLSPEC_SKIP_REASON=$2
    shift 2
    ( "$@" ) || return 0
  else
    SHELLSPEC_SKIP_REASON=${1:-}
  fi

  # Output SKIP message if within the example group
  [ "${SHELLSPEC_EXAMPLE_NO:-}" ] && shellspec_output SKIP
  shellspec_on SKIP
}

shellspec_pending() {
  shellspec_if SKIP && return 0
  SHELLSPEC_PENDING_REASON="${1:-}"
  shellspec_off NOT_IMPLEMENTED
  # Output PENDING message if within the example group
  [ "${SHELLSPEC_EXAMPLE_NO:-}" ] && shellspec_output PENDING
  # if already failed, can not pending
  shellspec_if FAILED || shellspec_on PENDING
}

shellspec_logger() {
  shellspec_sleep 0
  if [ "$SHELLSPEC_LOGFILE" ]; then
    shellspec_putsn "$@" >>"$SHELLSPEC_LOGFILE"
  else
    shellspec_putsn "$@"
  fi
}

shellspec_deprecated() {
  set -- "${SHELLSPEC_SPECFILE:-}:${SHELLSPEC_LINENO:-$SHELLSPEC_LINENO_BEGIN}" "$@"
  shellspec_putsn "$@" >>"$SHELLSPEC_DEPRECATION_LOGFILE"
}

shellspec_intercept() {
  while [ $# -gt 0 ]; do
    case $1 in
    *: ) SHELLSPEC_INTERCEPTOR="${SHELLSPEC_INTERCEPTOR}$1${1%:}|" ;;
    *:*) SHELLSPEC_INTERCEPTOR="${SHELLSPEC_INTERCEPTOR}$1|" ;;
    *  ) SHELLSPEC_INTERCEPTOR="${SHELLSPEC_INTERCEPTOR}$1:__$1__|" ;;
    esac
    shift
  done
}

shellspec_set() {
  while [ $# -gt 0 ]; do
    shellspec_append_shell_option SHELLSPEC_SHELL_OPTIONS "$1"
    shift
  done
}

shellspec_marker() {
  shellspec_putsn "${SHELLSPEC_SYN}shellspec_marker:$1 $2" >&2
}

shellspec_abort() {
  shellspec_putsn "${2:-}" >&2
  [ "${3:-}" ] && shellspec_putsn "${3:-}" >&2
  exit "${1:-1}"
}

shellspec_is_temporary_skip() {
  case ${SHELLSPEC_SKIP_REASON:-} in ("" | \# | \#\ *) ;; (*) false; esac
}

shellspec_is_temporary_pending() {
  case ${SHELLSPEC_PENDING_REASON:-} in ("" | \# | \#\ *) ;; (*) false; esac
}

shellspec_cat() {
  # shellcheck disable=SC2154
  ( while IFS= read -r shellspec_cat || [ "$shellspec_cat" ]; do
      shellspec_putsn "$shellspec_cat"
    done
  )
}

# shellcheck disable=SC2034
shellspec_filter() {
  if [ "${1:-}" ]; then SHELLSPEC_ENABLED=$1; fi
  if [ "${2:-}" ]; then SHELLSPEC_FOCUSED=$2; fi
  if [ "${3:-}" ]; then SHELLSPEC_FILTER=$3; fi
}

shellspec_dump() {
  shellspec_putsn
  shellspec_puts "[Dump] $SHELLSPEC_SPECFILE:$SHELLSPEC_AUX_LINENO "
  shellspec_puts "(exit status: ${SHELLSPEC_STATUS-<unset>})"

  shellspec_dump_file stdout SHELLSPEC_STDOUT "$SHELLSPEC_STDOUT_FILE"
  shellspec_dump_file stderr SHELLSPEC_STDERR "$SHELLSPEC_STDERR_FILE"

  shellspec_dump_callback() {
    shellspec_dump_file "fd $1" "SHELLSPEC_FD_$1" "$2"
  }

  shellspec_enum_file_descriptors shellspec_dump_callback "$SHELLSPEC_USE_FDS"
}

shellspec_dump_file() {
  shellspec_readfile_once "$2" "$3"
  shellspec_putsn
  if eval "[ \"\${$2:-}\" ] &&:"; then
    eval "shellspec_puts \"- $1:\${SHELLSPEC_LF}\${$2}\""
  elif eval "[ \${$2+x} ] &&:"; then
    shellspec_puts "- $1: <empty>"
  else
    shellspec_puts "- $1: <unset>"
  fi
}

shellspec_preserve() {
  [ $# -eq 0 ] && return 0
  shellspec_clone "$@" >> "$SHELLSPEC_VARS_FILE"
}

shellspec_mock() {
  set -- "$SHELLSPEC_MOCK_BINDIR/$1" 1
  if [ -e "$1" ]; then
    while [ -e "$1#$2" ]; do
      set -- "$1" $(($2 + 1))
    done
    shellspec_mv "$1" "$1#$2"
  fi
  shellspec_create_mock_file "$1"
  shellspec_chmod +x "$1"
}

shellspec_create_mock_file() {
  shellspec_gen_mock_code "$SHELLSPEC_SHELL" > "$1"
}

shellspec_gen_mock_code() {
  shellspec_putsn "#!$1"
  shellspec_putsn ". \"\$SHELLSPEC_LIB/general.sh\""
  shellspec_putsn ". \"\$SHELLSPEC_LIB/core/clone.sh\""
  shellspec_putsn "if [ -e \"\$SHELLSPEC_VARS_FILE\" ]; then"
  shellspec_putsn "  . \"\$SHELLSPEC_VARS_FILE\""
  shellspec_putsn "fi"
  shellspec_putsn "shellspec_preserve() {"
  shellspec_putsn "  [ \$# -eq 0 ] && return 0"
  shellspec_putsn "  shellspec_clone \"\$@\" >> \"\$SHELLSPEC_VARS_FILE\""
  shellspec_putsn "}"
  shellspec_cat
}

shellspec_unmock() {
  set -- "$SHELLSPEC_MOCK_BINDIR/$1" 0
  [ -e "$1" ] && shellspec_rm "$1"
  while [ -e "$1#$(($2 + 1))" ]; do
    set -- "$1" $(($2 + 1))
  done
  if [ "$2" -gt 0 ]; then
    shellspec_mv "$1#$2" "$1"
  fi
}

shellspec_usefd() {
  SHELLSPEC_USE_FDS="${SHELLSPEC_USE_FDS}${SHELLSPEC_USE_FDS:+:}$1"
}