├── Changes ├── LICENSE ├── README.mkdn ├── bash-tap ├── bash-tap-bootstrap └── bash-tap-mock /Changes: -------------------------------------------------------------------------------- 1 | * bash-tap 1.0.2 (2013-05-19-00:59) 2 | * Make bash-tap-mock work with -e. 3 | Contributed by Daniel Nephin (@dnephin). 4 | 5 | * bash-tap 1.0.1 (2012-07-14-20:59) 6 | * Clearer diagnostics for like/unlike. 7 | * Correct syntax for bash 3.2 regexp matching in like/unlike. 8 | 9 | * bash-tap 1.0.0 (2012-06-20-15:31) 10 | * TAP-compliant testing for bash. 11 | * Function and command mocks for bash. 12 | * In-process output capture helpers for testing. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2016 Sam Graham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.mkdn: -------------------------------------------------------------------------------- 1 | Bash-TAP 2 | ======== 3 | 4 | Bash-TAP allows you to perform TAP-compliant tests within bash 5 | using a similar test syntax to Perl's Test::More and Test::Builder, 6 | suitable to run with `prove` or any other TAP-consuming test harness. 7 | 8 | For more information about TAP (the Test Anything Protocol) visit: 9 | http://testanything.org/ 10 | 11 | Installation and Usage 12 | ---------------------- 13 | 14 | 1. Install the bash-tap files somewhere convenient for you. 15 | The default location of `../../bash-tap` relative to your 16 | test files is the easiest zero-conf way, but you can set 17 | the `$BASH_TAP_ROOT` environment variable if you want to 18 | install elsewhere. 19 | 2. If you're writing tests then copy `bash-tap-bootstrap` 20 | into your tests dir and source it inside your tests with: 21 | 22 | ```bash 23 | . $(dirname $0)/bash-tap-bootstrap 24 | ``` 25 | 26 | 3. Run your tests with `prove my_test_dir` or your favourite 27 | TAP-consuming test harness, or run them manually as a 28 | script if you just want to see the raw TAP output. 29 | 30 | Example test file 31 | ----------------- 32 | 33 | Here's example test file `01_read_rows_from_key_value_lines.t` from 34 | https://github.com/illusori/bash-snippets 35 | 36 | ```bash 37 | #!/bin/bash 38 | 39 | . $(dirname $0)/bash-tap-bootstrap 40 | . $(dirname $0)/../read_rows_from_key_value_lines 41 | 42 | columns_per_row=6 43 | max_rows_per_rowset=3 44 | total_rowsets=2 45 | 46 | plan tests $(((columns_per_row * max_rows_per_rowset * total_rowsets) + total_rowsets)) 47 | 48 | # Test data, resultset 1 49 | results1="artist Assemblage 23 50 | track Naked (God Module RMX) 51 | album Addendum 52 | year 2001 53 | rating 80 54 | tracktime 5:22 55 | artist Ayria 56 | track Sapphire 57 | album Debris 58 | year 59 | rating 100 60 | tracktime 6:14 61 | artist Apoptygma Berzerk 62 | track Kathy's Song 63 | album Welcome To Earth \"Extra bit for testing\" 64 | year 65 | rating 100 66 | tracktime 6:35" 67 | 68 | # Test data, resultset 2 69 | results2="artist Colony 5 70 | track The Bottle 71 | album Lifeline 72 | year 73 | rating 80 74 | tracktime 4:34" 75 | 76 | output=$(_read_rows_from_key_value_lines "track" "$results1" 2>&1) 77 | is "$output" "" "Read of rowset 1 should produce no output" 78 | # Since $() runs in a subshell, we need to run it "for real" now 79 | _read_rows_from_key_value_lines "track" "$results1" &>/dev/null 80 | 81 | # Track 1 82 | is "${track_artist[0]}" "Assemblage 23" "rowset 1 track 1 artist" 83 | is "${track_track[0]}" "Naked (God Module RMX)" "rowset 1 track 1 track" 84 | is "${track_album[0]}" "Addendum" "rowset 1 track 1 album" 85 | is "${track_year[0]}" "2001" "rowset 1 track 1 year" 86 | is "${track_rating[0]}" "80" "rowset 1 track 1 rating" 87 | is "${track_tracktime[0]}" "5:22" "rowset 1 track 1 tracktime" 88 | 89 | # Track 2 90 | is "${track_artist[1]}" "Ayria" "rowset 1 track 2 artist" 91 | is "${track_track[1]}" "Sapphire" "rowset 1 track 2 track" 92 | is "${track_album[1]}" "Debris" "rowset 1 track 2 album" 93 | is "${track_year[1]}" "" "rowset 1 track 2 year" 94 | is "${track_rating[1]}" "100" "rowset 1 track 2 rating" 95 | is "${track_tracktime[1]}" "6:14" "rowset 1 track 2 tracktime" 96 | 97 | # Track 3 98 | is "${track_artist[2]}" "Apoptygma Berzerk" "rowset 1 track 3 artist" 99 | is "${track_track[2]}" "Kathy's Song" "rowset 1 track 3 track" 100 | is "${track_album[2]}" "Welcome To Earth \"Extra bit for testing\"" "rowset 1 track 3 album" 101 | is "${track_year[2]}" "" "rowset 1 track 3 year" 102 | is "${track_rating[2]}" "100" "rowset 1 track 3 rating" 103 | is "${track_tracktime[2]}" "6:35" "rowset 1 track 3 tracktime" 104 | 105 | output=$(_read_rows_from_key_value_lines "track" "$results2" 2>&1) 106 | is "$output" "" "Read of rowset 2 should produce no output" 107 | # Since $() runs in a subshell, we need to run it "for real now 108 | _read_rows_from_key_value_lines "track" "$results2" &>/dev/null 109 | 110 | # Track 1 111 | is "${track_artist[0]}" "Colony 5" "rowset 2 track 1 artist" 112 | is "${track_track[0]}" "The Bottle" "rowset 2 track 1 track" 113 | is "${track_album[0]}" "Lifeline" "rowset 2 track 1 album" 114 | is "${track_year[0]}" "" "rowset 2 track 1 year" 115 | is "${track_rating[0]}" "80" "rowset 2 track 1 rating" 116 | is "${track_tracktime[0]}" "4:34" "rowset 2 track 1 tracktime" 117 | 118 | # Track 2 119 | is "${track_artist[1]}" "" "rowset 2 track 2 artist" 120 | is "${track_track[1]}" "" "rowset 2 track 2 track" 121 | is "${track_album[1]}" "" "rowset 2 track 2 album" 122 | is "${track_year[1]}" "" "rowset 2 track 2 year" 123 | is "${track_rating[1]}" "" "rowset 2 track 2 rating" 124 | is "${track_tracktime[1]}" "" "rowset 2 track 2 tracktime" 125 | 126 | # Track 3 127 | is "${track_artist[2]}" "" "rowset 2 track 3 artist" 128 | is "${track_track[2]}" "" "rowset 2 track 3 track" 129 | is "${track_album[2]}" "" "rowset 2 track 3 album" 130 | is "${track_year[2]}" "" "rowset 2 track 3 year" 131 | is "${track_rating[2]}" "" "rowset 2 track 3 rating" 132 | is "${track_tracktime[2]}" "" "rowset 2 track 3 tracktime" 133 | ``` 134 | 135 | Running this gives output: 136 | 137 | ``` 138 | $ prove ~/projects/bash-snippets/t 139 | /Users/illusori/projects/bash-snippets/t/01_read_rows_from_key_value_lines.t .. ok 140 | All tests successful. 141 | Files=1, Tests=38, 0 wallclock secs ( 0.04 usr 0.00 sys + 0.04 cusr 0.02 csys = 0.10 CPU) 142 | Result: PASS 143 | ``` 144 | 145 | Or the verbose output: 146 | 147 | ``` 148 | $ prove -v ~/projects/bash-snippets/t 149 | /Users/illusori/projects/bash-snippets/t/01_read_rows_from_key_value_lines.t .. 150 | 1..38 151 | ok 1 - Read of rowset 1 should produce no output 152 | ok 2 - rowset 1 track 1 artist 153 | ok 3 - rowset 1 track 1 track 154 | ok 4 - rowset 1 track 1 album 155 | ok 5 - rowset 1 track 1 year 156 | ok 6 - rowset 1 track 1 rating 157 | ok 7 - rowset 1 track 1 tracktime 158 | ok 8 - rowset 1 track 2 artist 159 | ok 9 - rowset 1 track 2 track 160 | ok 10 - rowset 1 track 2 album 161 | ok 11 - rowset 1 track 2 year 162 | ok 12 - rowset 1 track 2 rating 163 | ok 13 - rowset 1 track 2 tracktime 164 | ok 14 - rowset 1 track 3 artist 165 | ok 15 - rowset 1 track 3 track 166 | ok 16 - rowset 1 track 3 album 167 | ok 17 - rowset 1 track 3 year 168 | ok 18 - rowset 1 track 3 rating 169 | ok 19 - rowset 1 track 3 tracktime 170 | ok 20 - Read of rowset 2 should produce no output 171 | ok 21 - rowset 2 track 1 artist 172 | ok 22 - rowset 2 track 1 track 173 | ok 23 - rowset 2 track 1 album 174 | ok 24 - rowset 2 track 1 year 175 | ok 25 - rowset 2 track 1 rating 176 | ok 26 - rowset 2 track 1 tracktime 177 | ok 27 - rowset 2 track 2 artist 178 | ok 28 - rowset 2 track 2 track 179 | ok 29 - rowset 2 track 2 album 180 | ok 30 - rowset 2 track 2 year 181 | ok 31 - rowset 2 track 2 rating 182 | ok 32 - rowset 2 track 2 tracktime 183 | ok 33 - rowset 2 track 3 artist 184 | ok 34 - rowset 2 track 3 track 185 | ok 35 - rowset 2 track 3 album 186 | ok 36 - rowset 2 track 3 year 187 | ok 37 - rowset 2 track 3 rating 188 | ok 38 - rowset 2 track 3 tracktime 189 | ok 190 | All tests successful. 191 | Files=1, Tests=38, 0 wallclock secs ( 0.04 usr 0.01 sys + 0.04 cusr 0.02 csys = 0.11 CPU) 192 | Result: PASS 193 | ``` 194 | 195 | Mocking with bash-tap-mock 196 | -------------------------- 197 | 198 | Also included in `bash-tap` is a simple function mocking framework 199 | `bash-tap-mock`, it lets you mock commands and functions with 200 | `mock_command` and `restore_mocked_command`. 201 | 202 | If you particularly care to only mock functions rather than commands 203 | (a good safeguard against typos), use `mock_function` and 204 | `restore_mocked_function`, which have some extended error checking 205 | ensuring the function you're mocking exists in the first place. 206 | 207 | An example from https://github.com/illusori/bash-itunes is clearer: 208 | 209 | ```bash 210 | #!/bin/bash 211 | 212 | . $(dirname $0)/bash-tap-bootstrap 213 | . "$BASH_TAP_ROOT/bash-tap-mock" 214 | . $(dirname $0)/../itunes 215 | 216 | plan tests 4 217 | 218 | sent_command='' 219 | function mock_osascript() { 220 | sent_command="$*" 221 | restore_mocked_function "_osascript" 222 | } 223 | mock_function "_osascript" "mock_osascript" 224 | 225 | start_output_capture 226 | _dispatch "stop" 227 | finish_output_capture stdout stderr 228 | 229 | like "$sent_command" 'stop' "sent command should contain 'stop'" 230 | like "$sent_command" 'tell application "iTunes"' "sent command should contain 'tell application \"iTunes\"'" 231 | 232 | is "$stdout" "Stopping iTunes." "stdout should tell user what happened" 233 | is "$stderr" "" "stderr should be empty" 234 | ``` 235 | -------------------------------------------------------------------------------- /bash-tap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bash_tap_version='1.0.2' 4 | 5 | # Our state. 6 | 7 | _bt_plan='' 8 | _bt_expected_tests=0 9 | _bt_plan_output=0 10 | _bt_current_test=0 11 | _bt_tap_output='' 12 | _bt_has_output_plan=0 13 | _bt_done_testing=0 14 | _bt_output_capture=0 15 | 16 | # Our test results so far 17 | unset _bt_test_ok 18 | unset _bt_test_actual_ok 19 | unset _bt_test_name 20 | unset _bt_test_type 21 | unset _bt_test_reason 22 | 23 | # Cleanup stuff. 24 | declare -a _bt_on_exit_cmds 25 | trap "_bt_on_exit" EXIT 26 | 27 | # Planning functions. 28 | 29 | function _bt_output_plan() { 30 | local num_tests="$1" 31 | local directive="$2" 32 | local reason="$3" 33 | 34 | if [ "$_bt_has_output_plan" = 1 ]; then 35 | _caller_error "The plan was already output" 36 | fi 37 | 38 | _bt_clear_out 39 | _bt_out "1..$num_tests" 40 | if [ -n "$directive" ]; then 41 | _bt_out " # $directive" 42 | fi 43 | if [ -n "$reason" ]; then 44 | _bt_out " $reason" 45 | fi 46 | _bt_print_out 47 | _bt_has_output_plan=1 48 | } 49 | 50 | function plan() { 51 | local plan="$1" 52 | 53 | case "$plan" in 54 | no_plan) no_plan ;; 55 | skip_all) skip_all "$2" ;; 56 | tests) expected_tests "$2" ;; 57 | *) _bt_die "Unknown or missing plan: '$plan'" ;; 58 | esac 59 | } 60 | 61 | function expected_tests() { 62 | local num="$1" 63 | 64 | if [ -z "$num" ]; then 65 | echo $_bt_expected_tests 66 | else 67 | if [ -n "$_bt_plan" ]; then 68 | _bt_caller_error "Plan is already defined" 69 | fi 70 | # TODO: validate 71 | _bt_plan="$num" 72 | _bt_expected_tests="$num" 73 | _bt_output_plan "$_bt_expected_tests" 74 | fi 75 | } 76 | 77 | function no_plan() { 78 | if [ -n "$_bt_plan" ]; then 79 | _bt_caller_error "Plan is already defined" 80 | fi 81 | _bt_plan="no plan" 82 | } 83 | 84 | function done_testing() { 85 | local num_tests="$1" 86 | 87 | if [ -z "$num_tests" ]; then 88 | num_tests="$_bt_current_test" 89 | fi 90 | 91 | if [ "$_bt_done_testing" = 1 ]; then 92 | _bt_caller_error "done_testing was already called" 93 | fi 94 | 95 | if [ "$_bt_expected_tests" != 0 -a "$num_tests" != "$_bt_expected_tests" ]; then 96 | ok 0 "planned to run $_bt_expected_tests but done_testing expects $num_tests" 97 | else 98 | _bt_expected_tests="$num_tests" 99 | fi 100 | 101 | if [ "$_bt_has_output_plan" = 0 ]; then 102 | _bt_plan="done testing" 103 | _bt_output_plan "$num_tests" 104 | fi 105 | } 106 | 107 | function has_plan() { 108 | test -n "$_bt_plan" 109 | } 110 | 111 | function skip_all() { 112 | local reason="${*:?}" 113 | 114 | _bt_output_plan 0 SKIP "$reason" 115 | } 116 | 117 | # Test functions. 118 | 119 | function ok() { 120 | local result="$1" 121 | local name="$2" 122 | 123 | _bt_current_test=$((_bt_current_test + 1)) 124 | 125 | # TODO: validate $name 126 | if [ -z "$name" ]; then 127 | name='unnamed test' 128 | fi 129 | name="${name//#/\\#}" 130 | 131 | _bt_clear_out 132 | if [ "$result" = 0 ]; then 133 | _bt_out "not ok" 134 | if [ -n "$TODO" ]; then 135 | _bt_test_ok[$_bt_current_test]=1 136 | else 137 | _bt_test_ok[$_bt_current_test]=0 138 | fi 139 | _bt_test_actual_ok[$_bt_current_test]=0 140 | else 141 | _bt_out "ok" 142 | _bt_test_ok[$_bt_current_test]=1 143 | _bt_test_actual_ok[$_bt_current_test]="$result" 144 | fi 145 | 146 | _bt_out " $_bt_current_test - $name" 147 | _bt_test_name[$_bt_current_test]="$name" 148 | 149 | if [ -n "$TODO" ]; then 150 | _bt_out " # TODO $TODO" 151 | _bt_test_reason[$_bt_current_test]="$TODO" 152 | _bt_test_type[$_bt_current_test]="todo" 153 | else 154 | _bt_test_reason[$_bt_current_test]='' 155 | _bt_test_type[$_bt_current_test]='' 156 | fi 157 | 158 | _bt_print_out 159 | } 160 | 161 | function _is_diag() { 162 | local result="$1" 163 | local expected="$2" 164 | 165 | diag " got: '$result'" 166 | diag " expected: '$expected'" 167 | } 168 | 169 | function is() { 170 | local result="$1" 171 | local expected="$2" 172 | local name="$3" 173 | 174 | if [ "$result" = "$expected" ]; then 175 | ok 1 "$name" 176 | else 177 | ok 0 "$name" 178 | _is_diag "$result" "$expected" 179 | fi 180 | } 181 | 182 | function _isnt_diag() { 183 | local result="$1" 184 | local expected="$2" 185 | 186 | diag " got: '$result'" 187 | diag " expected: anything else" 188 | } 189 | 190 | function isnt() { 191 | local result="$1" 192 | local expected="$2" 193 | local name="$3" 194 | 195 | if [ "$result" != "$expected" ]; then 196 | ok 1 "$name" 197 | else 198 | ok 0 "$name" 199 | _isnt_diag "$result" "$expected" 200 | fi 201 | } 202 | 203 | function like() { 204 | local result="$1" 205 | local pattern="$2" 206 | local name="$3" 207 | 208 | # NOTE: leave $pattern unquoted, see http://stackoverflow.com/a/218217/870000 209 | if [[ "$result" =~ $pattern ]]; then 210 | ok 1 "$name" 211 | else 212 | ok 0 "$name" 213 | diag " got: '$result'" 214 | diag " expected: match for '$pattern'" 215 | fi 216 | } 217 | 218 | function unlike() { 219 | local result="$1" 220 | local pattern="$2" 221 | local name="$3" 222 | 223 | # NOTE: leave $pattern unquoted, see http://stackoverflow.com/a/218217/870000 224 | if [[ ! "$result" =~ $pattern ]]; then 225 | ok 1 "$name" 226 | else 227 | ok 0 "$name" 228 | diag " got: '$result'" 229 | diag " expected: no match for '$pattern'" 230 | fi 231 | } 232 | 233 | function cmp_ok() { 234 | echo TODO 235 | } 236 | 237 | # Other helper functions 238 | 239 | function BAIL_OUT() { 240 | echo TODO 241 | } 242 | 243 | function skip() { 244 | echo TODO 245 | } 246 | 247 | function todo_skip() { 248 | echo TODO 249 | } 250 | 251 | function todo_start() { 252 | echo TODO 253 | } 254 | 255 | function todo_end() { 256 | echo TODO 257 | } 258 | 259 | # Output 260 | 261 | function diag() { 262 | local message="$1" 263 | 264 | if [ -n "$message" ]; then 265 | _bt_escaped_echo "# $message" 266 | fi 267 | } 268 | 269 | # Util functions for output capture within current shell 270 | 271 | function start_output_capture() { 272 | if [ $_bt_output_capture = 1 ]; then 273 | finish_output_capture 274 | _bt_caller_error "Can't start output capture while already active" 275 | fi 276 | local stdout_tmpfile="/tmp/bash-itunes-test-out.$$" 277 | local stderr_tmpfile="/tmp/bash-itunes-test-err.$$" 278 | _bt_add_on_exit_cmd "rm -f '$stdout_tmpfile' '$stderr_tmpfile'" 279 | _bt_output_capture=1 280 | exec 3>&1 >$stdout_tmpfile 4>&2 2>$stderr_tmpfile 281 | } 282 | 283 | function finish_output_capture() { 284 | local capture_stdout_varname="$1" 285 | local capture_stderr_varname="$2" 286 | if [ $_bt_output_capture != 1 ]; then 287 | _bt_caller_error "Can't finish output capture when it wasn't started" 288 | fi 289 | exec 1>&3 3>&- 2>&4 4>&- 290 | _bt_output_capture=0 291 | if [ -n "$capture_stdout_varname" ]; then 292 | local stdout_tmpfile="/tmp/bash-itunes-test-out.$$" 293 | eval "$capture_stdout_varname=\$(< $stdout_tmpfile)" 294 | fi 295 | if [ -n "$capture_stderr_varname" ]; then 296 | local stderr_tmpfile="/tmp/bash-itunes-test-err.$$" 297 | eval "$capture_stderr_varname=\$(< $stderr_tmpfile)" 298 | fi 299 | } 300 | 301 | # Internals 302 | 303 | function _bt_stdout() { 304 | echo "$@" 305 | } 306 | 307 | function _bt_stderr() { 308 | echo "$@" >&2 309 | } 310 | 311 | function _bt_die() { 312 | _bt_stderr "$@" 313 | exit 255 314 | } 315 | 316 | # Report an error from the POV of the first calling point outside this file 317 | function _bt_caller_error() { 318 | local message="$*" 319 | 320 | local thisfile="${BASH_SOURCE[0]}" 321 | local file="$thisfile" 322 | local frame_num=2 323 | until [ "$file" != "$thisfile" ]; do 324 | frame=$(caller "$frame_num") 325 | IFS=' ' read line func file <<<"$frame" 326 | done 327 | 328 | _bt_die "Error: $message, on line $line of $file" 329 | } 330 | 331 | # Echo the supplied message with lines after the 332 | # first escaped as TAP comments. 333 | function _bt_escaped_echo() { 334 | local message="$*" 335 | 336 | local output='' 337 | while IFS= read -r line; do 338 | output="$output\n# $line" 339 | done <<<"$message" 340 | echo -e "${output:4}" 341 | } 342 | 343 | function _bt_clear_out() { 344 | _bt_tap_output="" 345 | } 346 | 347 | function _bt_out() { 348 | _bt_tap_output="$_bt_tap_output$*" 349 | } 350 | 351 | function _bt_print_out() { 352 | _bt_escaped_echo "$_bt_tap_output" 353 | } 354 | 355 | # Cleanup stuff 356 | function _bt_add_on_exit_cmd() { 357 | _bt_on_exit_cmds[${#_bt_on_exit_cmds[*]}]="$*" 358 | } 359 | 360 | function _bt_on_exit() { 361 | if [ $_bt_output_capture = 1 ]; then 362 | finish_output_capture 363 | fi 364 | for exit_cmd in "${_bt_on_exit_cmds[@]}"; do 365 | diag "cleanup: $exit_cmd" 366 | eval "$exit_cmd" 367 | done 368 | # TODO: check that we've output a plan/results 369 | } 370 | -------------------------------------------------------------------------------- /bash-tap-bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Bash TAP Bootstrap: 4 | # Copy this file into your project tests dir and source it 5 | # from each test file with: 6 | # . $(dirname $0)/bash-tap-bootstrap 7 | # It takes care of finding bash-tap or outputing a usage message. 8 | # 9 | 10 | bash_tap_bootstrap_version='1.0.2' 11 | 12 | if [ "${BASH_SOURCE[0]}" = "$0" ]; then 13 | # Being run directly, probably by test harness running entire dir. 14 | echo "1..0 # SKIP bash-tap-bootstrap isn't a test file" 15 | exit 0 16 | fi 17 | 18 | if [ -z "$BASH_TAP_ROOT" ]; then 19 | # TODO: search likely locations. 20 | BASH_TAP_ROOT="$(dirname ${BASH_SOURCE[0]})/../../bash-tap" 21 | fi 22 | 23 | if [ -f "$BASH_TAP_ROOT/bash-tap" ]; then 24 | . "$BASH_TAP_ROOT/bash-tap" 25 | else 26 | echo "Bail out! Unable to find bash-tap. Install from https://github.com/illusori/bash-tap or set \$BASH_TAP_ROOT if you have it installed somewhere unusual." 27 | exit 255 28 | fi 29 | -------------------------------------------------------------------------------- /bash-tap-mock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # While not directly TAP-specific, being able to mock stuff 4 | # in tests is pretty useful. 5 | # 6 | # If you're using bash-tap-bootstrap, then just source this 7 | # file in your tests from the bash-tap directory found by 8 | # the bootstrap by including this line after you've sourced 9 | # bash-tap-bootstrap: 10 | # 11 | # . "$BASH_TAP_ROOT/bash-tap-mock" 12 | # 13 | # If you're not using bash-tap-bootstrap then copy this file 14 | # to your test directory and source it with: 15 | # 16 | # . $(dirname $0)/bash-tap-mock 17 | # 18 | # It's important to note that if you're capturing the arguments 19 | # passed to your mock function in a variable, and want that 20 | # variable to be accessible to your tests, you must ensure that 21 | # the mocked function is executed in the current shell and not 22 | # a subshell. In particular, this means you cannot use $() or 23 | # `` to capture output of the function at the same time, as these 24 | # invoke a subshell - the mock will happen, but any variables you 25 | # set within your mock will only exist within the subshell. 26 | # If you wish to capture output at the same time, you need to 27 | # make use of the start_output_capture and finish_output_capture 28 | # helper functons in bash-tap, or manually use file-descriptor 29 | # redirects yourself to achieve the same effect. 30 | 31 | bash_tap_mock_version='1.0.2' 32 | 33 | if [ "${BASH_SOURCE[0]}" = "$0" ]; then 34 | # Being run directly, probably by test harness running entire dir. 35 | echo "1..0 # SKIP bash-tap-mock isn't a test file" 36 | exit 0 37 | fi 38 | 39 | function mock_function() { 40 | local original_name="$1" 41 | local mock_name="$2" 42 | local save_original_as="_btm_mocked_${original_name}" 43 | 44 | if [ -z $(declare -F "$save_original_as") ]; then 45 | _btm_copy_function "$original_name" "$save_original_as" 46 | fi 47 | _btm_copy_function "$mock_name" "$original_name" 48 | } 49 | 50 | function restore_mocked_function() { 51 | local original_name="$1" 52 | local save_original_as="_btm_mocked_${original_name}" 53 | 54 | if [ ! -z $(declare -F "$save_original_as") ]; then 55 | _btm_copy_function "$save_original_as" "$original_name" 56 | unset -f "$save_original_as" 57 | else 58 | _btm_caller_error "Can't find saved original function '$original_name' to restore" 59 | fi 60 | } 61 | 62 | function mock_command() { 63 | local command_name="$1" 64 | local mock_name="$2" 65 | 66 | if [ ! -z $(declare -F "$command_name") ]; then 67 | # It's not actually a command, it's a function, mock that 68 | mock_function "$command_name" "$mock_name" 69 | else 70 | _btm_copy_function "$mock_name" "$command_name" 71 | fi 72 | } 73 | 74 | function restore_mocked_command() { 75 | local command_name="$1" 76 | 77 | local save_original_as="_btm_mocked_${command_name}" 78 | if [ ! -z $(declare -F "$save_original_as") ]; then 79 | # Was actually a function mock not a command mock. 80 | restore_mocked_function "$command_name" 81 | else 82 | unset -f "$command_name" >/dev/null 83 | fi 84 | } 85 | 86 | # Copied from http://stackoverflow.com/a/1203628/870000 87 | function _btm_copy_function() { 88 | declare -F $1 >/dev/null || _btm_caller_error "Can't find function '$1' to copy" 89 | eval "$(echo "${2}()"; declare -f ${1} | tail -n +2)" 90 | } 91 | 92 | # Report an error from the POV of the first calling point outside this file 93 | function _btm_caller_error() { 94 | local message="$*" 95 | 96 | local thisfile="${BASH_SOURCE[0]}" 97 | local file="$thisfile" 98 | local frame_num=2 99 | until [ "$file" != "$thisfile" ]; do 100 | frame=$(caller "$frame_num") 101 | IFS=' ' read line func file <<<"$frame" 102 | done 103 | 104 | echo "Error: $message, on line $line of $file" >&2 105 | exit 255 106 | } 107 | --------------------------------------------------------------------------------