#!/bin/sh # This file is part of urchin. It is subject to the license terms in the # COPYING file found in the top-level directory of this distribution or at # https://raw.githubusercontent.com/tlevine/urchin/master/COPYING # No part of urchin, including this file, may be copied, modified, propagated, # or distributed except according to the terms contained in the COPYING file. set -e # Make sure that CDPATH isn't set, as it causes `cd` to behave unpredictably - # notably, it can produce output. unset CDPATH # All temporary files go here tmp=$(mktemp -d) urchin_exit() { rm -Rf "$tmp" exit "$@" } # Source a setup or teardown file urchin_source() { if test -f "$1"; then . ./"$1" > /dev/null fi } # Expand relative paths alias fullpath='readlink -f --' DEFAULT_URCHIN_ROOT="$PWD" urchin_path() { # XXX Change this to be relative the urchin root fullpath "$1" } urchin_root() { } # Urchin version number VERSION=0.1.0-rc1 indent() { level="$1" if test "$level" -gt 0; then printf "%$((2 * ${level}))s" fi } recurse() { set -e potential_test="$1" shell_for_sh_tests="$2" TEST_SHELL="$3" if test $(echo "$potential_test" | wc -l) -ne 1; then echo 'Test file names may not contain newline characters.' >&2 exit 1 fi for ignore in setup_dir teardown_dir setup teardown; do if test "$potential_test" = 'setup_dir'; then return fi done if [ -d "$potential_test" ]; then ( cd -- "$potential_test" > /dev/null urchin_source setup_dir if [ -n "$ZSH_VERSION" ]; then # avoid "no matches found: *" error when directories are empty setopt NULL_GLOB fi for test in *; do urchin_source setup set +e recurse "${test}" "$shell_for_sh_tests" "$TEST_SHELL" exit_code=$? set -e if $exit_on_not_ok && test $exit_code -ne 0; then urchin_source teardown urchin_source teardown_dir urchin_exit 1 fi urchin_source teardown done urchin_source teardown_dir ) else stdout_file="$tmp/stdout$(urchin_path "$potential_test")" time_file="$tmp/time$(urchin_path "$potential_test")" mkdir -p "$(dirname "$stdout_file")" mkdir -p "$(dirname "$time_file")" > $stdout_file if [ -x "$potential_test" ]; then urchin_source setup if [ -n "$shell_for_sh_tests" ] && has_sh_or_no_shebang_line ./"$potential_test"; then cycle_shell=true else cycle_shell=false fi # Run the test start=$(date +%s) set +e if $cycle_shell; 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="$?" set -e finish=$(date +%s) urchin_source teardown if [ $exit_code -eq 0 ]; then result=ok elif [ $exit_code -eq 3 ]; then result=skip else result=not_ok fi else result=skip fi elapsed=$(($finish - $start)) printf "${potential_test}: ${result} ${elapsed} seconds\n" >> "$tmp"/log if $exit_on_not_ok && test 0 -ne $exit_code; then return 1 fi fi } report_outcome() { tap_format="$1" log_file="$2" start="$3" finish="$4" elapsed=$(($finish - $start)) # if "$tap_format"; then # printf \#\ # fi # echo Running tests at $(date +%Y-%m-%dT%H:%M:%S) for number in n oks skips not_oks; do eval "$number=0" done # Use a temporary file rather than a pipe because a pipe starts a sub-shell # and thus makes the above variables local. sorted_log_file=$(mktemp) sort "$log_file" > $sorted_log_file while read line; do regex='^\(.*\): \(ok\|skip\|not_ok\) \([0-9]*\) seconds$' path=$(echo "$line" | sed "s/$regex/\1/") result=$(echo "$line" | sed "s/$regex/\2/") file_elapsed=$(echo "$line" | sed "s/$regex/\3/") # Number of files that have run, including this one n=$(($n + 1)) # Number of files that have been ok, not ok, and skipped eval "old_count=${result}s" eval "${result}s=$(($old_count+1))" # if ! $tap_format; then # indent $indent_level # echo "+ ${path}" # fi if $tap_format; then if [ "$result" == not_ok ]; then not='not ' else not='' fi if [ "$result" == skip ]; then skip='# SKIP' else skip='' fi echo "${not}ok $n - ${path}${skip}" if [ "$result" == not_ok ]; then echo '# ------------ Begin output ------------' # sed 's/^/# /' "$stdout_file" echo '# ------------ End output ------------' fi echo "# Previous test took $file_elapsed seconds." # else # indent $indent_level # case "$result" in # success) # # On success, print a green '✓' # printf '\033[32m✓ \033[0m' # printf '%s\n' "${potential_test}" # ;; # not_ok) # # On not_ok, print a red '✗' # printf '\033[31m✗ \033[0m' # printf '%s\n' "${potential_test}" # # Print output captured from not_oked test in red. # printf '\033[31m' # cat "$stdout_file" # printf '\033[0m' # ;; # skip) # printf ' %s\n' "${potential_test}" # ;; # esac fi # if ! $tap_format; then # echo # fi done < $sorted_log_file rm $sorted_log_file set +e if $tap_format; then echo "# Full test suite took $elapsed $(plural second $elapsed)." echo 1..$n else echo "Done, took $elapsed $(plural second $elapsed)." printf '%s\n' "$oks $(plural test "$oks") oks." printf '%s\n' "$skips $(plural test "$skips") skips." # If any tests are not ok, print the message in red. if [ $not_oks -gt 0 ] ; then printf '\033[31m' fi printf '%s\n' "$not_ok $(plural test "$not_oks") failed." printf '\033[m' fi test "$not_oks" -eq '0' } has_sh_or_no_shebang_line() { # no shebang line at all head -n 1 "$1" | grep -vqE '^#!' && return 0 # shebang line is '#!/bin/sh' or legal variations thereof head -n 1 "$1" | grep -qE '^#![[:blank:]]*/bin/sh($|[[:blank:]])' && return 0 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, --help This help. -v Display the version number. 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_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 urchin_exit 1 } shell_for_sh_tests= force=false exit_on_not_ok=false tap_format=false while [ $# -gt 0 ] do case "$1" in -e) exit_on_not_ok=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 urchin_exit 11 } ;; -t) tap_format=true;; -h|--help) urchin_help exit 0;; -v) echo "$VERSION" urchin_exit;; -*) 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 urchin_exit 11 fi # Run or present the Molly guard. if fullpath "$1" | grep -Fi 'test' > /dev/null || $force then # 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 "$shell_for_sh_tests" ]; then TEST_SHELL="$shell_for_sh_tests" elif [ -n "$TEST_SHELL" ]; then : else TEST_SHELL='/bin/sh' fi start=$(date +%s) set +e # 1 test folder # 2 shell to invoke test scripts with # 3 TEST_SHELL recurse "$1" "$shell_for_sh_tests" "$TEST_SHELL" exit_code=$? set -e finish=$(date +%s) report_outcome true $tmp/log $start $finish urchin_exit $exit_code else urchin_molly_guard fi