#!/bin/sh 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 ( indent $indent_level echo " ${potential_test}" 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" [ -f teardown ] && [ -x teardown ] && ./teardown >> "$stdout_file" done [ -f teardown_dir ] && [ -x teardown_dir ] && ./teardown_dir >> "$stdout_file" echo ) 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" 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. cat "$stdout_file" printf '\033[0m' 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. -f Force running even if the test directory's name does not contain the word "test". -x [Experimental; not meant for direct invocation, but for use in the shebang line of test scripts] Run with "\$TEST_SHELL", falling back on /bin/sh. -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)) 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' return "$2" } 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 while [ $# -gt 0 ] do case "$1" in -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; } ;; -x) # `urchin -x ` is equivalent to `"$TEST_SHELL" ` shift urchinsh=${TEST_SHELL:-/bin/sh} "$urchinsh" "$@" exit $?;; -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