#!/bin/sh # Make sure that CDPATH isn't set, as it causes `cd` to behave unpredictably - notably, it can produce output, # which breaks fullpath(). unset CDPATH fullpath() { ( cd -- "$1" pwd ) } indent() { level="$1" printf "%$((2 * ${level}))s" } recurse() { potential_test="$1" indent_level="$2" shell_for_sh_tests="$3" [ "$potential_test" = 'setup_dir' ] && return [ "$potential_test" = 'teardown_dir' ] && return [ "$potential_test" = 'setup' ] && return [ "$potential_test" = 'teardown' ] && return [ $indent_level -eq 0 ] && : > "$stdout_file" if [ -d "$potential_test" ] then ( if $tap_format; then indent $indent_level | sed 's/ /#/g' echo "# ${potential_test}" else indent $indent_level echo " ${potential_test}" fi cd -- "$potential_test" [ -f setup_dir ] && [ -x setup_dir ] && ./setup_dir >> "$stdout_file" if [ -n "$ZSH_VERSION" ]; then # avoid "no matches found: *" error when directories are empty setopt NULL_GLOB fi for test in * do [ -f setup ] && [ -x setup ] && ./setup >> "$stdout_file" # $2 instead of $indent_level so it doesn't clash recurse "${test}" $(( $2 + 1 )) "$shell_for_sh_tests" exit_code=$? if $exit_on_fail && test $exit_code -ne 0; then [ -f teardown ] && [ -x teardown ] && ./teardown >> "$stdout_file" [ -f teardown_dir ] && [ -x teardown_dir ] && ./teardown_dir >> "$stdout_file" return 1 fi [ -f teardown ] && [ -x teardown ] && ./teardown >> "$stdout_file" done [ -f teardown_dir ] && [ -x teardown_dir ] && ./teardown_dir >> "$stdout_file" if ! $tap_format; then echo; fi ) elif [ -x "$potential_test" ] then [ -f setup ] && [ -x setup ] && ./setup >> "$stdout_file" # Run the test if [ -n "$shell_for_sh_tests" ] && has_sh_or_no_shebang_line ./"$potential_test" then TEST_SHELL="$TEST_SHELL" "$shell_for_sh_tests" ./"$potential_test" > "$stdout_file" 2>&1 else TEST_SHELL="$TEST_SHELL" ./"$potential_test" > "$stdout_file" 2>&1 fi exit_code="$?" [ -f teardown ] && [ -x teardown ] && ./teardown >> "$stdout_file" if $tap_format; then if [ $exit_code -eq 0 ]; then result=ok else result=not\ ok fi n=$(grep -ce '^\(?:not \)\?ok' "$logfile") echo "${result} $((n + 1)) - ${potential_test}" | tee --append "$logfile" else indent $indent_level if [ $exit_code -eq 0 ] then # On success, print a green '✓' printf '\033[32m✓ \033[0m' printf '%s\n' "${potential_test}" printf '%s\n' "${potential_test} passed" >> "$logfile" else # On fail, print a red '✗' printf '\033[31m✗ \033[0m' printf '%s\n' "${potential_test}" printf '%s\n' "${potential_test} failed" >> "$logfile" printf '\033[31m' # Print output captured from failed test in red. sed 's/^/# /' "$stdout_file" printf '\033[0m' fi fi if $exit_on_fail && test 0 -ne $exit_code; then return 1 fi fi [ $indent_level -eq 0 ] && rm "$stdout_file" } has_sh_or_no_shebang_line() { head -n 1 "$1" | grep -vqE '^#!' && return 0 # no shebang line at all head -n 1 "$1" | grep -qE '^#![[:blank:]]*/bin/sh($|[[:blank:]])' && return 0 # shebang line is '#!/bin/sh' or legal variations thereof return 1 } USAGE="usage: $0 [] " urchin_help() { cat < Invoke test scripts that either have no shebang line at all or have shebang line "#!/bin/sh" with the specified shell. -e Stop running if any single test fails. This is helpful if you want to use Urchin to run things other than tests, such as a set of configuration scripts. -f Force running even if the test directory's name does not contain the word "test". -t Format output in Test Anything Protocol (TAP) -h This help. Go to https://github.com/tlevine/urchin for documentation on writing tests. EOF } plural () { # Make $1 a plural according to the number $2. # If $3 is supplied, use that instead of "${1}s". # Result is written to stdout. if [ "$2" = 1 ] then printf '%s\n' "$1" else printf '%s\n' "${3-${1}s}" fi } urchin_go() { echo Running tests at $(date +%Y-%m-%dT%H:%M:%S) | tee "$logfile" start=$(date +%s) # Determine the environment variable to define for test scripts # that reflects the specified or implied shell to use for shell-code tests. # - Set it to the shell specified via -s, if any. # - Otherwise, use its present value, if non-empty. # - Otherwise, default to '/bin/sh'. if [ -n "$2" ] then TEST_SHELL="$2" elif [ -z "$TEST_SHELL" ] then TEST_SHELL='/bin/sh' fi recurse "$1" 0 "$2" # test folder -- indentation level -- [shell to invoke test scripts with] finish=$(date +%s) elapsed=$(($finish - $start)) if $tap_format; then echo "# Took $elapsed $(plural second $elapsed)." else echo "Done, took $elapsed $(plural second $elapsed)." set -- $(grep -e 'passed$' "$logfile"|wc -l) $(grep -e 'failed$' "$logfile"|wc -l) printf '%s\n' "$1 $(plural test "$1") passed." [ $2 -gt 0 ] && printf '\033[31m' || printf '\033[32m' # If tests failed, print the message in red, otherwise in green. printf '%s\n' "$2 $(plural test "$2") failed." printf '\033[m' fi test -z "$2" || test "$2" -eq '0' } urchin_molly_guard() { { echo echo 'The name of the directory on which you are running urchin' echo 'does not contain the word "test", so I am not running,' echo 'in case that was an accident. Use the -f flag if you really' echo 'want to run urchin on that directory.' echo } >&2 exit 1 } shell_for_sh_tests= force=false exit_on_fail=false tap_format=false while [ $# -gt 0 ] do case "$1" in -e) exit_on_fail=true;; -f) force=true;; -s) shift shell_for_sh_tests=$1 which "$shell_for_sh_tests" >/dev/null || { echo "Cannot find specified shell: '$shell_for_sh_tests'" >&2; urchin_help >&2; exit 2; } ;; -t) tap_format=true;; -h|--help) urchin_help exit 0;; -*) urchin_help >&2 exit 1;; *) break;; esac shift done # Verify argument for main stuff if [ "$#" != '1' ] || [ ! -d "$1" ] then [ -n "$1" ] && [ ! -d "$1" ] && echo "Not a directory: '$1'" >&2 echo "$USAGE" >&2 exit 2 fi # Constants logfile=$(fullpath "$1")/.urchin.log stdout_file=$(fullpath "$1")/.urchin_stdout # Run or present the Molly guard. if basename "$(fullpath "$1")" | grep -Fi 'test' > /dev/null || $force then urchin_go "$1" "$shell_for_sh_tests" else urchin_molly_guard fi