Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit 1aa1075c53927d23d236e07bebd1b1b995da28a6 @falconindy committed Dec 25, 2012
Showing with 319 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +42 −0 README.md
  3. +188 −0 apron
  4. +70 −0 apron-test
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012 by Dave Reisner <dreisner@archlinux.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
42 README.md
@@ -0,0 +1,42 @@
+# Apron
+
+Apron is a mocking framework for Bash. Why Apron? Because Smock seemed
+way too obvious.
+
+## Installation
+
+You should not try to use Apron from a shared location. Simply include
+it with your project and source it from a controlled location.
+
+## How to Use
+
+Source apron as early as possible, and call `APRON_enable`. This initializes
+a small amount of bookkeeping needed to intercept calls and keep track of
+any mocks which you might register. At this point, any call which cannot
+be resolved by the shell will be caught by Apron as an uninteresting call.
+You can enable Apron with the `-v` to get verbose output as to what Apron
+is doing.
+
+Next, define your mock! Let's call it `badcommand`. Of course, at this point,
+since shell functions take priority over external commands, the function will
+always be called. However, you'll also want to register your mock with Apron,
+by calling `APRON_register badcommand`. This allows Apron to keep track of
+the function in case you want to selectively unregister it, or temporarily
+pause mocking.
+
+Define as many mocks as you want! As mentioned, Apron can temporarily pause
+its mocking behavior with `APRON_pause`. This will cause all further calls
+to previously mocked commands to be executed normally. You can go back to
+your mocked environment with `APRON_unpause`.
+
+Finally, you can tear down your mocks selectively, or all at once. Use
+`APRON_unregister` to destroy a single mock or `APRON_unregister_all` to
+destroy all known mocks. Unregistering mocks will not cause APRON to be
+disabled. To completely uninitialize Apron, call `APRON_disable`. Note that
+calling `APRON_disable` will implicitly unregister all mocks for you.
+
+An example of Apron in use can be found in `apron-test`.
+
+## License
+
+See LICENSE for details.
188 apron
@@ -0,0 +1,188 @@
+#
+# 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.
+#
+
+#
+# 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.
+#
+# Args:
+# -v: Enable verbose output from APRON
+#
+APRON_enable() {
+ (( APRON_enabled )) && return 1
+ APRON_enabled=1
+
+ if [[ $1 = -v ]]; then
+ APRON_verbose=1
+ fi
+
+ # store the old PATH, set the dummy path
+ _APRON_push_PATH
+ _APRON_register_cnf_handler
+ declare -Ag APRON_defined_mocks=()
+}
+
+#
+# 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_enabled )) && return 1
+ (( APRON_paused )) && return 1
+
+ if (( APRON_verbose )); then
+ printf 'APRON: pausing %s mocks\n' "${#APRON_defined_mocks[*]}"
+ fi
+
+ APRON_paused=1
+ _APRON_pop_PATH
+ unset -f "${!APRON_defined_mocks[@]}" 'command_not_found_handle'
+}
+
+#
+# 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_enabled )) && return 1
+ (( ! APRON_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
+ unset APRON_paused
+}
+
+#
+# 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.
+#
+APRON_disable() {
+ (( ! APRON_enabled )) && return 1
+ APRON_enabled=0
+
+ if (( APRON_verbose )); then
+ printf 'APRON: disabling mocking\n'
+ fi
+
+ # restore the PATH
+ _APRON_pop_PATH
+ APRON_unregister_all
+ unset -f command_not_found_handle
+ unset "${!APRON_[@]}"
+}
+
+#
+# APRON_register
+#
+# Registers a mock function. The function should already, but not necessarily
+# already 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_enabled )) || return
+
+ if (( APRON_verbose )); then
+ printf 'APRON: registering mock: %s\n' "$1"
+ fi
+
+ APRON_defined_mocks["$1"]=$(declare -f "$1")
+ export -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_enabled )) || return
+
+ if (( APRON_verbose )); then
+ printf 'APRON: unregistering mock: %s\n' "$1"
+ fi
+
+ local i
+ for fn in "${!APRON_defined_mocks[@]}"; do
+ if [[ $fn = "$1" ]]; then
+ unset "$fn"
+ unset APRON_defined_mocks["$fn"]
+ fi
+ done
+}
+
+#
+# APRON_unregister_all
+#
+# Unregisters and destroys all known mocks. This is called automatically
+# by APRON_disable.
+#
+APRON_unregister_all() {
+ (( APRON_enabled )) || return
+
+ if (( APRON_verbose )); then
+ printf 'APRON: unregistering ALL mocks\n'
+ fi
+
+ unset -f "${!APRON_defined_mocks[@]}"
+ unset APRON_defined_mocks
+}
+
+#
+# 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=\;
+}
+
+_APRON_pop_PATH() {
+ PATH=$APRON_saved_PATH
+ unset APRON_saved_PATH
+}
+
+_APRON_register_cnf_handler() {
+ command_not_found_handle() {
+ printf 'APRON: function call was not mocked: %s\n' "$*" >&2
+ }
+}
+
+# vim: set ft=sh et ts=2 sw=2:
70 apron-test
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+#
+# A short demo of how to use apron.
+#
+
+. './apron'
+
+# Enables mocking, with verbosity
+APRON_enable -v
+
+#
+# define some mocks for external commands and register them
+#
+mock_called() {
+ printf '==> MOCKED CALL: %s with %s args: %s\n' "$1" "$2" "${*:3}"
+}
+ls() { mock_called "$FUNCNAME" "$#" "$@"; }
+df() { mock_called "$FUNCNAME" "$#" "$@"; }
+cp() { mock_called "$FUNCNAME" "$#" "$@"; }
+APRON_register 'cp'
+APRON_register 'ls'
+APRON_register 'df'
+
+# we can even be sneaky and define a mock for a full path to a command
+/bin/ls() { mock_called "$FUNCNAME" "$#" "$@"; }
+APRON_register '/bin/ls'
+
+#
+# call some external commands now, notice what happens to
+# the various calls.
+#
+ls /
+cp moe larry
+mv dont overwriteme
+df | sed 3q
+
+#
+# pause mocking temporarily, try to run ls again
+#
+APRON_pause
+ls /
+df | sed 2q
+
+#
+# resume mocking
+#
+APRON_unpause
+/bin/ls /
+
+#
+# Unregister a single mock, others remain
+#
+APRON_unregister 'ls'
+ls /
+df
+
+#
+# Unregister all mocks, but leave mocking enabled
+#
+APRON_unregister_all
+df
+badcommand
+
+#
+# Disable mocking, return to normal life
+#
+APRON_disable
+df | awk 'NR == 3 { exit; } 1'
+ls /

0 comments on commit 1aa1075

Please sign in to comment.
Something went wrong with that request. Please try again.