Last active
November 16, 2025 18:19
-
-
Save noel-yap/56402f161682cec0939f8d650ef7e0cf to your computer and use it in GitHub Desktop.
Create a mock executable that returns a successive, caller‑provided behavior on each invocation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # shellcheck shell=bash | |
| # mock_first_with_rest | |
| # --------------------- | |
| # Create a mock executable that returns a successive, caller‑provided behavior | |
| # on each invocation, and persists an index so the next call advances to the | |
| # next behavior. When all behaviors are exhausted, the mock prints an error to | |
| # stderr and exits non‑zero. Re‑invoking this function for the same dependency | |
| # resets the index back to 0. | |
| # | |
| # Why this exists | |
| # - When testing shell scripts that call external executables ("dependencies"), | |
| # it’s useful to simulate different outcomes across repeated calls: success, | |
| # failure, specific outputs, etc. This helper generates a small mock program | |
| # that plays back behaviors one by one. | |
| # | |
| # Runtime model | |
| # - A hidden file named ".<dependency>.index" tracks which behavior to run. | |
| # - Each time the generated mock runs, it: | |
| # 1) Reads the current index | |
| # 2) Executes the indexed behavior | |
| # 3) Increments and persists the index regardless of success/failure | |
| # 4) Exits with the behavior’s exit status | |
| # - If the index is beyond the last behavior, it reports an error and exits 1. | |
| # | |
| # Usage | |
| # mock_first_with_rest <dependency> <behavior1> [behavior2 ...] | |
| # | |
| # Parameters | |
| # <dependency> The file name to create as the mock executable (e.g. "git"). | |
| # The function writes the script to "./<dependency>" and marks | |
| # it executable. The index file used is ".<dependency>.index". | |
| # <behaviorN> A shell snippet (string) or path to an executable to run. | |
| # Behaviors are evaluated via `eval` inside a subshell | |
| # so they can be simple one‑liners like 'echo ok; exit 0'. | |
| # | |
| # Notes | |
| # - Re‑calling mock_first_with_rest with the same <dependency> overwrites the | |
| # mock script and resets ".<dependency>.index" to 0. | |
| # - Place the working directory containing the generated mock at the front of | |
| # PATH when invoking the dependency under test. | |
| # | |
| # Examples | |
| # # Example 1: inline behaviors | |
| # mock_first_with_rest dependency \ | |
| # 'echo first; exit 0' \ | |
| # 'echo second; exit 7' | |
| # PATH="${PWD}:${PATH}" dependency # prints "first", exits 0 | |
| # PATH="${PWD}:${PATH}" dependency # prints "second", exits 7 | |
| # PATH="${PWD}:${PATH}" dependency # prints error, exits 1 (exhausted) | |
| # | |
| # # Example 2: behaviors as helper scripts | |
| # printf '%s\n' '#!/bin/sh' 'echo one' > dep-0 && chmod +x dep-0 | |
| # printf '%s\n' '#!/bin/sh' 'echo two' > dep-1 && chmod +x dep-1 | |
| # mock_first_with_rest dependency dep-0 dep-1 | |
| # PATH="${PWD}:${PATH}" dependency # "one" | |
| # PATH="${PWD}:${PATH}" dependency # "two" | |
| mock_first_with_rest() { | |
| local -r _dependency="${1}" | |
| shift | |
| # Behaviors provided by caller (bash required) | |
| local -a -r _behaviors=("$@") | |
| local -r index_filename=".${_dependency}.index" | |
| # Initialize index to 0 every time we (re)mock | |
| echo '0' >"${index_filename}" | |
| # Generate the mock script (/bin/sh) | |
| # This script: | |
| # - Reads the current index at runtime | |
| # - Evaluates the selected behavior in a subshell to capture its exit code | |
| # - Increments and persists the index regardless of behavior status | |
| # - Exits with the behavior's exit code | |
| # shellcheck disable=SC2016,SC2028 | |
| { | |
| echo "#!/bin/bash" | |
| echo "set -euo pipefail" | |
| echo "readonly dependency='${_dependency}'" | |
| echo "readonly index_filename='${index_filename}'" | |
| echo 'readonly index="$(<"${index_filename}")"' | |
| echo '_cleanup() {' | |
| echo ' echo $((index + 1)) >"${index_filename}"' | |
| echo '}' | |
| # Ensure index increment on exit | |
| echo 'trap _cleanup EXIT' | |
| echo "readonly behaviors=( $(printf '"%s" ' "${_behaviors[@]}") )" | |
| echo 'readonly behavior_count=${#behaviors[@]}' | |
| echo 'if [ "${index}" -ge "${behavior_count}" ]; then' | |
| echo ' printf "%s\n" "ERROR: ${dependency}: no more mocked behaviors (index ${index} >= ${behavior_count})" >&2' | |
| echo ' exit 1' | |
| echo 'else' | |
| echo ' ( eval "${behaviors[${index}]}" )' | |
| echo 'fi' | |
| } | tee >"${_dependency}" | |
| chmod +x "${_dependency}" | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # shellcheck shell=sh | |
| eval "$(shellspec - -c) exit 1" | |
| Describe 'mock_first_with_rest' | |
| It 'initializes index file and creates a mock executable' | |
| # Given | |
| execute_sut_then_output_results() { | |
| mock_first_with_rest dependency 'echo first; exit 0' | |
| printf 'index=%s\n' "$(cat .dependency.index)" | |
| ls dependency | |
| } | |
| When call in_tempdir execute_sut_then_output_results | |
| The status should be success | |
| The line 1 of stdout should equal 'index=0' | |
| The line 2 of stdout should equal 'dependency' | |
| The lines of stdout should equal 2 | |
| End | |
| It 'returns successive behaviors on each invocation and increments index' | |
| # Given | |
| prepare_then_execute_sut() { | |
| # shellcheck disable=SC2016 | |
| { | |
| echo '#!/bin/sh' | |
| echo 'echo INFO: "$(basename "$0")"' | |
| } >dependency-0 | |
| chmod +x dependency-0 | |
| # shellcheck disable=SC2016 | |
| { | |
| echo '#!/bin/sh' | |
| echo 'echo INFO: "$(basename "$0")"' | |
| } >dependency-1 | |
| chmod +x dependency-1 | |
| mock_first_with_rest dependency dependency-0 dependency-1 | |
| PATH="${PWD}:${PATH}" dependency | |
| PATH="${PWD}:${PATH}" dependency | |
| } | |
| When call in_tempdir prepare_then_execute_sut | |
| The status should be success | |
| The line 1 of stdout should equal 'INFO: dependency-0' | |
| The line 2 of stdout should equal 'INFO: dependency-1' | |
| The lines of stdout should equal 2 | |
| End | |
| It 'increments the index even on mock error' | |
| run_calls() { | |
| mock_first_with_rest dependency \ | |
| 'echo first; exit 0' \ | |
| 'echo second; exit 7' | |
| chmod +x dependency | |
| out1="$(bash ./dependency 2>&1)"; rc1=$? | |
| out2="$(bash ./dependency 2>&1)"; rc2=$? | |
| printf '%s\n' "${out1}" | |
| printf '%s\n' "${rc1}" | |
| printf '%s\n' "${out2}" | |
| printf '%s\n' "${rc2}" | |
| printf 'index=%s\n' "$(cat .dependency.index)" | |
| } | |
| When call in_tempdir run_calls | |
| The status should be success | |
| The line 1 of stdout should equal 'first' | |
| The line 2 of stdout should equal '0' | |
| The line 3 of stdout should equal 'second' | |
| The line 4 of stdout should equal '7' | |
| The line 5 of stdout should equal 'index=2' | |
| The lines of stdout should equal 5 | |
| End | |
| It 'fails with an error after behaviors are exhausted (non-zero exit status)' | |
| run_exhausted() { | |
| mock_first_with_rest dependency \ | |
| 'echo only; exit 3' | |
| chmod +x dependency | |
| out1="$(bash ./dependency 2>&1)"; rc1=$? | |
| out2="$(bash ./dependency 2>&1)"; rc2=$? | |
| # Print placeholders for empty output to make assertions simple | |
| [ -n "${out1}" ] && printf '%s\n' "${out1}" || printf '<empty>\n' | |
| printf '%s\n' "${rc1}" | |
| [ -n "${out2}" ] && printf '%s\n' "${out2}" || printf '<empty>\n' | |
| printf '%s\n' "${rc2}" | |
| printf 'index=%s\n' "$(cat .dependency.index)" | |
| } | |
| When call in_tempdir run_exhausted | |
| The status should be success | |
| The line 1 of stdout should equal 'only' | |
| The line 2 of stdout should equal '3' | |
| The line 3 of stdout should equal 'ERROR: dependency: no more mocked behaviors (index 1 >= 1)' | |
| The line 4 of stdout should equal '1' | |
| The line 5 of stdout should equal 'index=2' | |
| The lines of stdout should equal 5 | |
| End | |
| It 'resets the index when re-mocking the same dependency' | |
| run_reset() { | |
| mock_first_with_rest dependency \ | |
| 'echo first-A; exit 0' \ | |
| 'echo second-A; exit 0' | |
| chmod +x dependency | |
| _="$(bash ./dependency)"; _="$(bash ./dependency)" | |
| # Re-run with different behaviors, which should reset index to 0 | |
| mock_first_with_rest dependency \ | |
| 'echo first-B; exit 5' \ | |
| 'echo second-B; exit 0' | |
| out="$(bash ./dependency 2>&1)"; rc=$? | |
| printf '%s\n' "${out}" | |
| printf '%s\n' "${rc}" | |
| printf 'index=%s\n' "$(cat .dependency.index)" | |
| } | |
| When call in_tempdir run_reset | |
| The status should be success | |
| The line 1 of stdout should equal 'first-B' | |
| The line 2 of stdout should equal '5' | |
| The line 3 of stdout should equal 'index=1' | |
| The lines of stdout should equal 3 | |
| End | |
| End |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment