Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

split apron into modules

Leave 'apron' as sourceable toplevel file.
  • Loading branch information...
commit 79ef31b6810bf7a029e186a7500e5c43a2191f31 1 parent 8826e19
Dave Reisner authored
Showing with 462 additions and 308 deletions.
  1. +11 −308 apron
  2. +70 −0 apron-assert
  3. +260 −0 apron-mock
  4. +121 −0 apron-testcase
319 apron
View
@@ -1,320 +1,23 @@
-#
-# A mocking framework for bash. Facilitates creation of an environment where
-# external commands can be intercepted and redefined for the purposes of
-# testing shell scripts which might involve unwanted side effects.
-#
-# Note that Apron makes no attempts to catch every possible external call. Any
-# call which references a binary directly can easily bypass Apron's "catch-all"
-# net unless those external calls are known beforehand and explicitly caught.
-#
+#!/bin/bash
-# Prevent apron from being sourced more than once
+# prevent apron from being sourced more than once
[[ -v APRON_state ]] && return 0
-#
-# APRON_enable
-#
-# This must be the first function called before mocks can be used. This
-# performs the rudiementary setup needed to establish the environment, saving
-# the PATH variable and defining the command not found handler. If the function
-# APRON_setup is defined by your tests, Apron will run this as well.
-#
-# Args:
-# -v: Enable verbose output from APRON
-#
-APRON_enable() {
- (( APRON_state != APRON_state_disabled )) && return 1
+shopt -s extglob
- if [[ $1 = '-v' ]]; then
- APRON_verbose=1
- fi
+modules=('assert'
+ 'mock'
+ 'testcase')
- # must be run before we destroy the PATH
- if declare -f APRON_setup >/dev/null; then
- APRON_setup
- fi
-
- _APRON_push_PATH
- _APRON_register_cnf_handler
- declare -Ag APRON_defined_mocks=()
- declare -Ag APRON_expectations=()
- APRON_state=$APRON_state_enabled
-}
-
-#
-# APRON_pause
-#
-# Temporarily pauses APRON, allowing external commands to be called by path
-# lookup. This can be reverted by calling APRON_unpause.
-#
-APRON_pause() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: pausing %s mocks\n' "${#APRON_defined_mocks[*]}"
- fi
-
- _APRON_pop_PATH
- unset -f "${!APRON_defined_mocks[@]}" 'command_not_found_handle'
- APRON_state=$APRON_state_paused
-}
-
-#
-# APRON_unpause
-#
-# Reverts the effects of APRON_unpause, restoring all registered mocks. Note
-# that the value of the PATH variable at the time this function is called is
-# what will be saved (and restored), on further calls to APRON_unpause or
-# APRON_disable.
-#
-APRON_unpause() {
- (( APRON_state != APRON_state_paused )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: resuming %s mocks\n' "${#APRON_defined_mocks[*]}"
- fi
-
- for mock in "${!APRON_defined_mocks[@]}"; do
- eval "${APRON_defined_mocks["$mock"]}"
- export -f "$mock"
- done
- _APRON_register_cnf_handler
- _APRON_push_PATH
- APRON_state=$APRON_state_enabled
-}
-
-#
-# APRON_disable
-#
-# Disables all effects of mocking, destroying all mocks, restoring the PATH,
-# and unregstering the command-not-found handler. APRON_enable must be called
-# again if further mocking is needed. If the function APRON_teardown is defined,
-# Apron will run this as well.
-#
-APRON_disable() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: disabling mocking\n'
- fi
-
- _APRON_pop_PATH
- APRON_unregister_all
- unset -f command_not_found_handle
- unset "${!APRON_[@]}"
- APRON_state=$APRON_state_disabled
-
- # must be run after we restore the PATH
- if declare -f APRON_teardown >/dev/null; then
- APRON_teardown
- fi
-}
-
-#
-# APRON_register
-#
-# Registers a mock function. The function must be defined when this function is
-# called. Calling this function again for a mock which is already defined will
-# overwrite the definition of the previously registered mock. Mocks can be
-# individually unregistered with the use of APRON_unregister.
-#
-# Args:
-# $1: The name of the function to register as a mock
-#
-APRON_register() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: registering mock: %s\n' "$1"
- fi
-
- if ! APRON_defined_mocks["mock_$1"]=mock_$(declare -f "$1"); then
- printf '==> FATAL: failed to register mock: %s\n' "$1"
- exit 1
- fi
-
- # swap out the defined function for the mock
- eval "${APRON_defined_mocks["mock_$1"]}"
- export -f "mock_$1"
- unset -f "$1"
-}
-
-#
-# APRON_unregister
-#
-# Unregisters a mock function which has been previously registered by
-# APRON_register. Additionally, this unsets the mock function from the
-# environment.
-#
-# Args:
-# $1: The name of the function to unregister
-#
-APRON_unregister() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: unregistering mock: %s\n' "$1"
- fi
-
- if [[ ${APRON_defined_mocks["mock_$1"]} ]]; then
- unset -f "mock_$1"
- unset APRON_defined_mocks["mock_$1"]
- fi
-}
-
-#
-# APRON_unregister_all
-#
-# Unregisters and destroys all known mocks. This is called automatically
-# by APRON_disable.
-#
-APRON_unregister_all() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: unregistering ALL mocks\n'
- fi
-
- unset -f "${!APRON_defined_mocks[@]}"
- unset APRON_defined_mocks
-}
-
-#
-# APRON_expect_call
-#
-# Set an expectation for a function. This should be called one to many times
-# prior to calling APRON_replay for the given function in order to validate
-# that a function has given behavior for a specified input.
-#
-# Args:
-# $1: Expected binary call
-# $2: Number of times call is expected (optional, default: 1)
-#
-APRON_expect_call() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- # TODO: figure out a way to register expectations not
- # just on the base command, but for arguments as well
- (( APRON_expectations["$1"] += ${2:-1} ))
-}
-
-#
-# APRON_replay
-#
-# Calls a function, recording the side effects, and validating them against
-# expectations set by APRON_expect_call.
-#
-# Args:
-# $@: function with arguments to call
-#
-# Returns:
-# 0 if expectations were met, else 1
-#
-APRON_replay() {
- (( APRON_state != APRON_state_enabled )) && return 1
-
- if (( APRON_verbose )); then
- printf 'APRON: replaying "%s" with expectations set\n' "$1"
- fi
-
- local expectations_file
-
- expectations_file=$(_APRON_run_external mktemp --tmpdir APRON_expect.XXXXXX)
-
- exec {APRON_expect_fd}>>"$expectations_file"
-
- # invoke the user command, collecting expectations
- "$@"
- APRON_function_return=$?
-
- _APRON_verify_expectations "$expectations_file"
- local r=$?
-
- # cleanup/reset
- exec {APRON_expect_fd}>&-
- _APRON_run_external rm "$expectations_file"
- unset APRON_expect_fd
-
- # XXX: wat. why isn't it sufficient to just redeclare?
- unset APRON_expectations
- declare -Ag APRON_expectations=()
-
- return $r
-}
-
-#
-# Internal functions for APRON. These should never be called directly.
-#
-_APRON_push_PATH() {
- # TODO: given the name, maybe this really should be a stack? Not sure
- # having multiple paths is useful.
- APRON_saved_PATH=$PATH
- PATH=__DONT_FORGET_TO_BRING_AN_APRON__
-}
-
-_APRON_pop_PATH() {
- PATH=$APRON_saved_PATH
- unset APRON_saved_PATH
-}
-
-_APRON_register_cnf_handler() {
- # Sadly, this is executed inside the forked child. What happens
- # in command_not_found_handle stays in command_not_found_handle.
- command_not_found_handle() {
- [[ $APRON_expect_fd ]] && echo "$1">&$APRON_expect_fd
-
- if [[ ${APRON_defined_mocks["mock_$1"]} ]]; then
- # mocked function
- "mock_$@"
- return
- fi
-
- if (( APRON_verbose )); then
- printf 'APRON: function call was not mocked: %s\n' "$*" >&2
- fi
- }
-}
-
-_APRON_verify_expectations() {
- local exp r=0
- local -A expectations_actual=()
-
- # collect actual calls
- while read -r line; do
- (( ++expectations_actual["$line"] ))
- done <"$1"
-
- # validate user expectations against reality
- for exp in "${!APRON_expectations[@]}"; do
- if (( APRON_expectations["$exp"] != expectations_actual["$exp"] )); then
- printf "APRON: expectation failed for '%s'\n" "$exp"
- printf ' Expected calls: %s\n' "${APRON_expectations["$exp"]}"
- printf ' Actual calls: %s\n' "${expectations_actual["$exp"]:-0}"
- r=1
- fi
- done
-
- # validate reality against user expectations
- for exp in "${!expectations_actual[@]}"; do
- if [[ -z ${APRON_expectations["$exp"]} ]]; then
- printf "APRON: expectation failed for '%s'\n" "$exp"
- printf ' Expected calls: 0\n'
- printf ' Actual calls: %s\n' "${expectations_actual["$exp"]}"
- r=1
- fi
- done
-
- return $r
-}
-
-# a moment of clarity
-_APRON_run_external() {
- PATH=/usr/bin:/bin:/usr/sbin:/sbin "$@"
-}
+for m in "${modules[@]}"; do
+ . "${0%/*}/apron-$m" "$@"
+done
+unset m modules
# Mark as much as possible readonly. This is pretty dirty, but we don't
# want the calling environment to mess with us.
readonly $(compgen -A function APRON_; compgen -A function _APRON_)
+readonly APRON_rootdir=${0%/*}
readonly APRON_state_disabled=0
readonly APRON_state_enabled=1
readonly APRON_state_paused=2
70 apron-assert
View
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+#
+# TODO: figure out an error collection mechanism?
+#
+
+#
+# Numeric assertions
+#
+APRON_assert_eq() {
+ if ! (( $1 == $2 )); then
+ printf 'Assertion failed: %d == %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_ne() {
+ if ! (( $1 != $2 )); then
+ printf 'Assertion failed: %d != %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_lt() {
+ if ! (( $1 < $2 )); then
+ printf 'Assertion failed: %d < %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_le() {
+ if ! (( $1 <= $2 )); then
+ printf 'Assertion failed: %d <= %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_gt() {
+ if ! (( $1 > $2 )); then
+ printf 'Assertion failed: %d > %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_ge() {
+ if ! (( $1 >= $2 )); then
+ printf 'Assertion failed: %d >= %d\n' "$1" "$2"
+ return 1
+ fi
+}
+
+#
+# String based assertions
+#
+APRON_assert_streq() {
+ if [[ $1 != "$2" ]]; then
+ printf 'Assertion failed: "%s" != "%s"\n' "$1" "$2"
+ return 1
+ fi
+}
+
+APRON_assert_in() {
+ for i in "${@:2}"; do
+ [[ $1 = "$i" ]] && return
+ done
+ printf 'Assertion failed: "%s" not found in: %s\n' "$1" "${*:2}"
+ return 1
+}
+
+# vim: set ft=sh et ts=2 sw=2:
260 apron-mock
View
@@ -0,0 +1,260 @@
+#
+# A mocking framework for bash. Facilitates creation of an environment where
+# external commands can be intercepted and redefined for the purposes of
+# testing shell scripts which might involve unwanted side effects.
+#
+# Note that Apron makes no attempts to catch every possible external call. Any
+# call which references a binary directly can easily bypass Apron's "catch-all"
+# net unless those external calls are known beforehand and explicitly caught.
+#
+
+# Prevent apron from being sourced more than once
+[[ -v APRON_state ]] && return 0
+
+#
+# APRON_pause
+#
+# Temporarily pauses APRON, allowing external commands to be called by path
+# lookup. This can be reverted by calling APRON_unpause.
+#
+APRON_pause() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: pausing %s mocks\n' "${#APRON_defined_mocks[*]}"
+ fi
+
+ _APRON_pop_PATH
+ unset -f "${!APRON_defined_mocks[@]}" 'command_not_found_handle'
+ APRON_state=$APRON_state_paused
+}
+
+#
+# APRON_unpause
+#
+# Reverts the effects of APRON_unpause, restoring all registered mocks. Note
+# that the value of the PATH variable at the time this function is called is
+# what will be saved (and restored), on further calls to APRON_unpause or
+# APRON_disable.
+#
+APRON_unpause() {
+ (( APRON_state != APRON_state_paused )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: resuming %s mocks\n' "${#APRON_defined_mocks[*]}"
+ fi
+
+ for mock in "${!APRON_defined_mocks[@]}"; do
+ eval "${APRON_defined_mocks["$mock"]}"
+ export -f "$mock"
+ done
+ _APRON_register_cnf_handler
+ _APRON_push_PATH
+ APRON_state=$APRON_state_enabled
+}
+
+#
+# APRON_register
+#
+# Registers a mock function. The function must be defined when this function is
+# called. Calling this function again for a mock which is already defined will
+# overwrite the definition of the previously registered mock. Mocks can be
+# individually unregistered with the use of APRON_unregister.
+#
+# Args:
+# $1: The name of the function to register as a mock
+#
+APRON_register() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: registering mock: %s\n' "$1"
+ fi
+
+ if ! APRON_defined_mocks["mock_$1"]=mock_$(declare -f "$1"); then
+ printf '==> FATAL: failed to register mock: %s\n' "$1"
+ exit 1
+ fi
+
+ # swap out the defined function for the mock
+ eval "${APRON_defined_mocks["mock_$1"]}"
+ export -f "mock_$1"
+ unset -f "$1"
+}
+
+#
+# APRON_unregister
+#
+# Unregisters a mock function which has been previously registered by
+# APRON_register. Additionally, this unsets the mock function from the
+# environment.
+#
+# Args:
+# $1: The name of the function to unregister
+#
+APRON_unregister() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: unregistering mock: %s\n' "$1"
+ fi
+
+ if [[ ${APRON_defined_mocks["mock_$1"]} ]]; then
+ unset -f "mock_$1"
+ unset APRON_defined_mocks["mock_$1"]
+ fi
+}
+
+#
+# APRON_unregister_all
+#
+# Unregisters and destroys all known mocks. This is called automatically
+# by APRON_disable.
+#
+APRON_unregister_all() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: unregistering ALL mocks\n'
+ fi
+
+ unset -f "${!APRON_defined_mocks[@]}"
+ unset APRON_defined_mocks
+}
+
+#
+# APRON_expect_call
+#
+# Set an expectation for a function. This should be called one to many times
+# prior to calling APRON_replay for the given function in order to validate
+# that a function has given behavior for a specified input.
+#
+# Args:
+# $1: Expected binary call
+# $2: Number of times call is expected (optional, default: 1)
+#
+APRON_expect_call() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ # TODO: figure out a way to register expectations not
+ # just on the base command, but for arguments as well
+ (( APRON_expectations["$1"] += ${2:-1} ))
+}
+
+#
+# APRON_replay
+#
+# Calls a function, recording the side effects, and validating them against
+# expectations set by APRON_expect_call.
+#
+# Args:
+# $@: function with arguments to call
+#
+# Returns:
+# 0 if expectations were met, else 1
+#
+APRON_replay() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: replaying "%s" with expectations set\n' "$1"
+ fi
+
+ local expectations_file
+
+ expectations_file=$(_APRON_run_external mktemp --tmpdir APRON_expect.XXXXXX)
+
+ exec {APRON_expect_fd}>>"$expectations_file"
+
+ # invoke the user command, collecting expectations
+ "$@"
+ APRON_function_return=$?
+
+ _APRON_verify_expectations "$expectations_file"
+ local r=$?
+
+ # cleanup/reset
+ exec {APRON_expect_fd}>&-
+ _APRON_run_external rm "$expectations_file"
+ unset APRON_expect_fd
+
+ # XXX: wat. why isn't it sufficient to just redeclare? This is
+ # definitely a bug on apron's side.
+ unset APRON_expectations
+ declare -Ag APRON_expectations=()
+
+ return $r
+}
+
+#
+# Internal functions for APRON. These should never be called directly.
+#
+_APRON_push_PATH() {
+ # TODO: given the name, maybe this really should be a stack? Not sure
+ # having multiple paths is useful.
+ APRON_saved_PATH=$PATH
+ PATH=__DONT_FORGET_TO_BRING_AN_APRON__
+}
+
+_APRON_pop_PATH() {
+ PATH=$APRON_saved_PATH
+ unset APRON_saved_PATH
+}
+
+_APRON_register_cnf_handler() {
+ # Sadly, this is executed inside the forked child. What happens
+ # in command_not_found_handle stays in command_not_found_handle.
+ command_not_found_handle() {
+ local FUNCNEST=100
+ [[ $APRON_expect_fd ]] && echo "$1">&$APRON_expect_fd
+
+ if [[ ${APRON_defined_mocks["mock_$1"]} ]]; then
+ # mocked function
+ "mock_$@"
+ return
+ fi
+
+ if (( APRON_verbose )); then
+ printf 'APRON: function call was not mocked: %s\n' "$*" >&2
+ fi
+ }
+}
+
+_APRON_verify_expectations() {
+ local exp r=0
+ local -A expectations_actual=()
+
+ # collect actual calls
+ while read -r line; do
+ (( ++expectations_actual["$line"] ))
+ done <"$1"
+
+ # validate user expectations against reality
+ for exp in "${!APRON_expectations[@]}"; do
+ if (( APRON_expectations["$exp"] != expectations_actual["$exp"] )); then
+ printf "APRON: expectation failed for '%s'\n" "$exp"
+ printf ' Expected calls: %s\n' "${APRON_expectations["$exp"]}"
+ printf ' Actual calls: %s\n' "${expectations_actual["$exp"]:-0}"
+ r=1
+ fi
+ done
+
+ # validate reality against user expectations
+ for exp in "${!expectations_actual[@]}"; do
+ if [[ -z ${APRON_expectations["$exp"]} ]]; then
+ printf "APRON: expectation failed for '%s'\n" "$exp"
+ printf ' Expected calls: 0\n'
+ printf ' Actual calls: %s\n' "${expectations_actual["$exp"]}"
+ r=1
+ fi
+ done
+
+ return $r
+}
+
+# a moment of clarity
+_APRON_run_external() {
+ PATH=/usr/bin:/bin:/usr/sbin:/sbin command "$@"
+}
+
+# vim: set ft=sh et ts=2 sw=2:
121 apron-testcase
View
@@ -0,0 +1,121 @@
+#!/bin/bash
+
+APRON_run_all_tests() {
+ local testcase cases
+
+ mapfile -t cases < <(compgen -A function TEST_)
+
+ for testcase in "${cases[@]}"; do
+ APRON_run_test "$testcase"
+ done
+}
+
+APRON_run_test() {
+ APRON_testname=${1#TEST_}
+
+ if declare -f APRON_setup_testcase >/dev/null; then
+ _APRON_pop_PATH
+ APRON_setup_testcase
+ _APRON_push_PATH
+ fi
+
+ "$1" &>"$APRON_scratchdir/$APRON_testname.OUTPUT"
+
+ APRON_tests_run["$1"]=$?
+}
+
+APRON_load() {
+ (( APRON_state != APRON_state_disabled )) && return 1
+
+ if [[ $1 = '-v' ]]; then
+ APRON_verbose=1
+ shift
+ fi
+
+ APRON_testsuite=$1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: loading\n'
+ fi
+
+ APRON_scratchdir=$(mktemp --tmpdir -d APRON.XXXXXX)
+
+ # must be run before we destroy the PATH
+ if declare -f APRON_setup >/dev/null; then
+ APRON_setup
+ fi
+
+ _APRON_push_PATH
+ _APRON_register_cnf_handler
+ declare -Ag APRON_defined_mocks=()
+ declare -Ag APRON_expectations=()
+ APRON_state=$APRON_state_enabled
+}
+
+APRON_exit() {
+ _APRON_unload
+ _APRON_report
+
+ if (( _APRON_failcount != 0 )); then
+ printf '==> %d tests failed. Logs can be found in: %s\n' \
+ "$_APRON_failcount" "$APRON_scratchdir"
+ else
+ rm -r "$APRON_scratchdir"
+ fi
+
+ exit $(( !! r ))
+}
+
+_APRON_unload() {
+ (( APRON_state != APRON_state_enabled )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: unloading\n'
+ fi
+
+ _APRON_pop_PATH
+ APRON_unregister_all
+ unset -f command_not_found_handle
+ unset -v "${!APRON_[@]}" "${!_APRON[@]}"
+ APRON_state=$APRON_state_disabled
+
+ # must be run after we restore the PATH
+ if declare -f APRON_teardown >/dev/null; then
+ APRON_teardown
+ fi
+}
+
+_APRON_report() {
+ local t
+
+ printf '==> Test results for %s:\n' "$APRON_testsuite"
+
+ # loop through twice so that failures are always
+ # last and stand out.
+
+ for t in "${!APRON_tests_run[@]}"; do
+ if (( APRON_tests_run["$t"] == 0 )); then
+ _APRON_test_pass "${t#TEST_}"
+ fi
+ done
+
+ for t in "${!APRON_tests_run[@]}"; do
+ if (( APRON_tests_run["$t"] != 0 )); then
+ cat "$APRON_scratchdir/${t#TEST_}.OUTPUT"
+ _APRON_test_fail "${t#TEST_}"
+ (( ++_APRON_failcount ))
+ fi
+ done
+}
+
+_APRON_test_pass() {
+ printf ' -> %-69s \e[1;37m[\e[1;32m%4s\e[1;37m]\e[0m\n' "$1" 'PASS'
+}
+
+_APRON_test_fail() {
+ printf ' -> %-69s \e[1;37m[\e[1;31m%4s\e[1;37m]\e[0m\n' "$1" 'FAIL'
+}
+
+declare -A APRON_tests_run
+
+# vim: set ft=sh et ts=2 sw=2:
Please sign in to comment.
Something went wrong with that request. Please try again.