#!/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) echo $tmp urchin_exit() { # rm -f "$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 --' URCHIN_ROOT="$PWD" urchin_path() { # XXX Change this to be relative the urchin root fullpath "$1" } # 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" 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_fail && 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 stop=$(date +%s) urchin_source teardown if [ $exit_code -eq 0 ]; then result=success elif [ $exit_code -eq 3 ]; then result=skip else result=fail fi else result=skip fi printf "${potential_test}\t${result}\n" >> "$tmp"/log if $exit_on_fail && test 0 -ne $exit_code; then return 1 fi fi } report_outcome() { tap_format="$1" log_file="$2" # XXX just copied from elsewhere and thus broken if "$tap_format"; then printf \#\ fi echo Running tests at $(date +%Y-%m-%dT%H:%M:%S) start=$(date +%s) sort "$log_file" | while read line; do while ! echo "$line" | grep '^[^\t\n]\{1,\}\t\(success|fail|skip\)'; do read moreline line="$line$moreline" done path=$(echo "$line" | cut -f 1) result=$(echo "$line" | cut -f 2) done # if $tap_format; then # indent $indent_level | sed 's/ /#/g' # echo "# Begin - ${path}" # else # indent $indent_level # echo "+ ${path}" # fi if $tap_format; then if [ "$result" == fail ]; then not='not ' else not='' fi if [ "$result" == skip ]; then skip='# SKIP ' else skip='' fi echo "${not}ok $n - ${skip}${path}" if [ "$result" == fail ]; then echo '# ------------ Begin output ------------' # sed 's/^/# /' "$stdout_file" echo '# ------------ End output ------------' fi # else # indent $indent_level # case "$result" in # success) # # On success, print a green '✓' # printf '\033[32m✓ \033[0m' # printf '%s\n' "${potential_test}" # ;; # fail) # # On fail, print a red '✗' # printf '\033[31m✗ \033[0m' # printf '%s\n' "${potential_test}" # # Print output captured from failed test in red. # printf '\033[31m' # cat "$stdout_file" # printf '\033[0m' # ;; # skip) # printf ' %s\n' "${potential_test}" # ;; # esac fi # if $tap_format; then # indent $indent_level | sed 's/ /#/g' # echo "# End - ${potential_test}" # else # echo # fi return finish=$(date +%s) elapsed=$(($finish - $start)) set +e passed=$(grep -c '^success' "$logfile") failed=$(grep -c '^fail' "$logfile") skipped=$(grep -c '^skip' "$logfile") if $tap_format; then echo "# Took $elapsed $(plural second $elapsed)." echo 1..$(($passed + $failed + $skipped)) else echo "Done, took $elapsed $(plural second $elapsed)." printf '%s\n' "$passed $(plural test "$passed") passed." printf '%s\n' "$skipped $(plural test "$skipped") skipped." # If tests failed, print the message in red, otherwise in green. [ $failed -gt 0 ] && printf '\033[31m' || printf '\033[32m' printf '%s\n' "$failed $(plural test "$failed") failed." printf '\033[m' fi test -z "$failed" || test "$failed" -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_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 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 # 1 test folder # 2 shell to invoke test scripts with # 3 TEST_SHELL set +e recurse "$1" "$shell_for_sh_tests" "$TEST_SHELL" exit_code=$? set -e report_outcome true $tmp/log urchin_exit $exit_code else urchin_molly_guard fi