urchin/urchin

467 lines
12 KiB
Plaintext
Raw Normal View History

#!/bin/sh
2012-10-04 11:24:03 +00:00
# This file is part of urchin. It is subject to the license terms in the
2016-02-08 16:09:00 +00:00
# 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.
2016-02-28 01:18:55 +00:00
set -e
2016-02-08 16:09:00 +00:00
2016-02-28 14:01:17 +00:00
DEFAULT_SHELLS='sh
bash
dash
mksh
zsh'
2016-02-08 16:05:56 +00:00
# Make sure that CDPATH isn't set, as it causes `cd` to behave unpredictably -
2016-02-28 01:18:55 +00:00
# notably, it can produce output.
Make sure that CDPATH isn't set, as it causes `cd` to behave unpredictably - notably, it can produce output, which breaks fullpath(). Also: Improved CLI help, updated URLs in read-me, cleaned up package.json: I've tried to clarify the intent of `-x` in the CLI help, but I haven't touched the read-me in that respect. I don't see any benefit to `-x`: * Just using `#/bin/sh` as the shebang line in combination with `-s <shell>` gives you the same functionality, * When it comes to invoking scripts from _within_ test scripts, nothing can do the work for you: you consciously have to mark the invocation with _something_ to indicate that it should be controlled from the outside; it won't get any easier than `$TEST_SHELL ...` * Finally, using a shebang line such as `#!/usr/bin/env urchin -x` is problematic for two reasons: * Some platforms can handle only *1* argument in a shebang line. * In a _package-local_ installation, `#!/usr/bin/env` may not find the Urchin executable. I'm also not sure how the following (from `readme.md`) fits in the picture: > It might make sense if you do this. export TEST_SHELL=zsh && urchin -x export TEST_SHELL=bash && urchin -x (As an aside: To achieve the same thing, you don't need `export`; `TEST_SHELL=zsh urchin -x` and `TEST_SHELL=bash urchin -x` is the better choice.) How does this relate to use in a _shebang line_? `urchin_help()` now uses a here-doc: easier to maintain, and should work in all Bourne-like shells. `readmeFilename` removed from `package.json`: > "The readmeFilename does not need to ever be in your actual package.json file" - npm/npm#3573
2014-12-03 14:48:49 +00:00
unset CDPATH
2016-02-28 09:08:20 +00:00
# All temporary files go here
tmp=$(mktemp -d)
2016-02-28 12:02:57 +00:00
> $tmp/log
2016-02-28 09:08:20 +00:00
urchin_exit() {
2016-02-28 10:48:05 +00:00
rm -Rf "$tmp"
2016-02-28 09:08:20 +00:00
exit "$@"
}
2016-02-28 10:00:53 +00:00
# Source a setup or teardown file
urchin_source() {
if test -f "$1"; then
. ./"$1" > /dev/null
fi
}
2016-02-28 13:12:55 +00:00
stdout_file() {
x="$tmp/stdout$(fullpath "$1")"
mkdir -p "$(dirname -- "$x")"
echo "$x"
}
2016-02-28 09:12:57 +00:00
# Expand relative paths
alias fullpath='readlink -f --'
2016-02-28 11:22:51 +00:00
remove_trailing_slash() {
echo "$1" | sed s/\\/$//
}
2016-02-28 11:18:39 +00:00
urchin_root() {
2016-02-28 11:22:51 +00:00
# Call recursively but remember the original argument.
current="$(remove_trailing_slash "$1")"
if test -n "$2"; then
orig="$2"
else
orig="$1"
fi
2016-02-28 11:18:39 +00:00
2016-02-28 11:22:51 +00:00
if test "$(readlink -f -- "$1")" = /; then
# Stop traversing upwards at /
if test -d "$orig"; then
echo "$orig"
else
2016-02-28 12:59:36 +00:00
dirname -- "$orig"
2016-02-28 11:22:51 +00:00
fi
elif ! test -e "$current"; then
echo "$current: No such file or directory">&2
return 1
elif test -f "$current"; then
2016-02-28 12:59:36 +00:00
urchin_root "$(dirname -- "$current")" "$orig"
2016-02-28 11:27:55 +00:00
elif test -f "$current"/.urchin; then
2016-02-28 11:22:51 +00:00
remove_trailing_slash "$current"
else
urchin_root "$current"/.. "$orig"
fi
2016-02-28 11:18:39 +00:00
}
2016-01-29 17:17:31 +00:00
# Urchin version number
2016-02-28 01:18:55 +00:00
VERSION=0.1.0-rc1
2012-10-11 18:50:03 +00:00
2012-10-11 00:43:13 +00:00
indent() {
level="$1"
2016-02-08 15:14:22 +00:00
if test "$level" -gt 0; then
printf "%$((2 * ${level}))s"
fi
2012-10-11 00:43:13 +00:00
}
2012-10-04 11:29:34 +00:00
recurse() {
2016-02-28 10:00:53 +00:00
set -e
2016-02-28 12:14:16 +00:00
requested_path="$1"
potential_test="$(fullpath "$2")"
2016-02-28 14:01:17 +00:00
cycle_shell="$3"
2016-02-28 12:14:16 +00:00
TEST_SHELL="$4"
2016-02-28 10:27:49 +00:00
2016-02-28 10:00:53 +00:00
for ignore in setup_dir teardown_dir setup teardown; do
2016-02-28 12:02:57 +00:00
if test "$potential_test" = $ignore; then
2016-02-28 10:00:53 +00:00
return
fi
done
2012-10-10 18:40:49 +00:00
2016-02-28 12:15:42 +00:00
echo "$requested_path" | grep "^$potential_test" > /dev/null ||
echo "$potential_test" | grep "^$requested_path" > /dev/null ||
2016-02-28 12:14:16 +00:00
return 0
2016-02-28 12:02:57 +00:00
if test $(echo "$potential_test" | wc -l) -ne 1; then
echo 'Test file names may not contain newline characters.' >&2
exit 1
fi
2016-02-28 01:18:55 +00:00
if [ -d "$potential_test" ]; then
(
2016-02-28 09:36:48 +00:00
cd -- "$potential_test" > /dev/null
2016-02-28 10:00:53 +00:00
urchin_source setup_dir
2014-11-05 17:38:22 +00:00
if [ -n "$ZSH_VERSION" ]; then
# avoid "no matches found: *" error when directories are empty
setopt NULL_GLOB
fi
2016-02-28 01:18:55 +00:00
for test in *; do
2016-02-28 12:35:27 +00:00
if test "$test" = '*' && ! test -e $test; then
# The directory is empty.
break
fi
2016-02-28 10:00:53 +00:00
urchin_source setup
2012-10-11 00:43:13 +00:00
2016-02-28 10:00:53 +00:00
set +e
2016-02-28 14:01:17 +00:00
recurse "$requested_path" "$test" "$cycle_shell" "$TEST_SHELL"
2016-02-28 12:14:16 +00:00
2016-02-28 10:00:53 +00:00
exit_code=$?
set -e
2012-10-11 00:43:13 +00:00
2016-02-28 10:57:27 +00:00
if $exit_on_not_ok && test $exit_code -ne 0; then
2016-02-28 10:00:53 +00:00
urchin_source teardown
urchin_source teardown_dir
urchin_exit 1
fi
2016-02-28 10:00:53 +00:00
urchin_source teardown
2012-10-08 12:50:48 +00:00
done
2016-02-28 10:00:53 +00:00
urchin_source teardown_dir
)
else
2016-02-28 01:25:36 +00:00
if [ -x "$potential_test" ]; then
2016-02-28 12:59:36 +00:00
cd -- "$(dirname -- "$potential_test")"
2016-02-28 10:07:55 +00:00
urchin_source setup
# Run the test
2016-02-28 10:07:55 +00:00
start=$(date +%s)
2016-02-28 01:25:36 +00:00
set +e
2016-02-28 14:01:17 +00:00
# Determine the environment variable to define for test scripts
# that reflects the specified or implied shell to use for shell-code tests.
if $cycle_shell && has_sh_or_no_shebang_line "$potential_test"; then
# Set it to the shell specified via -s.
while read the_test_shell; do
TEST_SHELL="$the_test_shell" "$the_test_shell" \
"$potential_test" >> \
"$(stdout_file "$potential_test")" 2>&1
done < $shells_list
else
2016-02-28 14:01:17 +00:00
# Shell cycling is disabled with -n; use the present value of
# TEST_SHELL or default to /bin/sh
if [ -n "$TEST_SHELL" ]; then
the_test_shell="$TEST_SHELL"
else
the_test_shell='/bin/sh'
fi
TEST_SHELL="$the_test_shell" "$the_test_shell" > \
2016-02-28 13:12:55 +00:00
"$(stdout_file "$potential_test")" 2>&1
fi
exit_code="$?"
2016-02-28 01:25:36 +00:00
set -e
2016-02-28 10:39:32 +00:00
finish=$(date +%s)
2012-10-10 18:25:44 +00:00
2016-02-28 10:07:55 +00:00
urchin_source teardown
2016-01-27 01:08:04 +00:00
if [ $exit_code -eq 0 ]; then
2016-02-28 11:06:54 +00:00
result=ok
2016-02-26 17:37:39 +00:00
elif [ $exit_code -eq 3 ]; then
result=skip
2016-01-27 01:08:04 +00:00
else
2016-02-28 10:57:27 +00:00
result=not_ok
2016-01-27 01:08:04 +00:00
fi
2012-10-04 11:29:34 +00:00
else
result=skip
fi
2016-02-28 10:39:32 +00:00
elapsed=$(($finish - $start))
printf "${potential_test}: ${result} ${elapsed} seconds\n" >> "$tmp"/log
2016-02-28 10:57:27 +00:00
if $exit_on_not_ok && test 0 -ne $exit_code; then
2016-02-28 10:00:53 +00:00
return 1
2016-02-28 09:08:20 +00:00
fi
fi
}
2016-02-28 09:08:20 +00:00
report_outcome() {
2016-02-28 12:41:39 +00:00
root="$1"
tap_format="$2"
log_file="$3"
start="$4"
finish="$5"
escaped_root="$(fullpath "$root" | sed 's/\//\\\//g')"
2016-02-28 10:39:32 +00:00
elapsed=$(($finish - $start))
2016-02-28 10:07:55 +00:00
2016-02-28 12:53:26 +00:00
if "$tap_format"; then
printf \#\
fi
echo Running tests at $(date +%Y-%m-%dT%H:%M:%S)
2016-02-28 09:25:21 +00:00
2016-02-28 10:57:27 +00:00
for number in n oks skips not_oks; do
eval "$number=0"
done
2016-02-28 11:14:23 +00:00
# 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
2016-02-28 11:06:54 +00:00
regex='^\(.*\): \(ok\|skip\|not_ok\) \([0-9]*\) seconds$'
2016-02-28 13:12:55 +00:00
abspath=$(echo "$line" | sed "s/$regex/\1/")
path=$(echo "$abspath" | sed "s/$escaped_root\/\?//")
2016-02-28 10:27:49 +00:00
result=$(echo "$line" | sed "s/$regex/\2/")
2016-02-28 10:39:32 +00:00
file_elapsed=$(echo "$line" | sed "s/$regex/\3/")
2016-02-28 11:06:54 +00:00
2016-02-28 12:53:26 +00:00
prevdir=$currentdir
2016-02-28 12:59:36 +00:00
currentdir="$(dirname -- "$path")"
2016-02-28 12:53:26 +00:00
2016-02-28 11:06:54 +00:00
# Number of files that have run, including this one
2016-02-28 10:27:49 +00:00
n=$(($n + 1))
2016-02-28 11:06:54 +00:00
# Number of files that have been ok, not ok, and skipped
eval "old_count=${result}s"
eval "${result}s=$(($old_count+1))"
2016-02-28 09:12:57 +00:00
2016-02-28 10:27:49 +00:00
if $tap_format; then
2016-02-28 10:57:27 +00:00
if [ "$result" == not_ok ]; then
2016-02-28 10:27:49 +00:00
not='not '
else
not=''
fi
if [ "$result" == skip ]; then
2016-02-28 10:49:40 +00:00
skip='# SKIP'
2016-02-28 10:27:49 +00:00
else
skip=''
fi
2016-02-28 11:16:32 +00:00
echo "${not}ok $n - ${path}${skip}"
2016-02-28 10:57:27 +00:00
if [ "$result" == not_ok ]; then
2016-02-28 10:27:49 +00:00
echo '# ------------ Begin output ------------'
2016-02-28 13:12:55 +00:00
sed 's/^/# /' "$(stdout_file "$abspath")"
2016-02-28 10:27:49 +00:00
echo '# ------------ End output ------------'
fi
2016-02-28 10:39:32 +00:00
echo "# Previous test took $file_elapsed seconds."
2016-02-28 12:45:38 +00:00
else
2016-02-28 12:53:26 +00:00
if test "$prevdir" != "$currentdir"; then
echo
fi
2016-02-28 13:24:55 +00:00
pretty_path="%s$(dirname -- "$path")/\n $(basename -- "$path")\n"
2016-02-28 12:45:38 +00:00
case "$result" in
ok)
# On success, print a green '✓'
printf '\033[32m✓ \033[0m'
2016-02-28 13:24:55 +00:00
printf "${pretty_path}"
2016-02-28 12:45:38 +00:00
;;
not_ok)
# On not_ok, print a red '✗'
printf '\033[31m✗ \033[0m'
2016-02-28 13:24:55 +00:00
printf "${pretty_path}"
2016-02-28 12:45:38 +00:00
2016-02-28 13:13:17 +00:00
# Print output captured from not_ok test in red.
2016-02-28 12:45:38 +00:00
printf '\033[31m'
2016-02-28 13:12:55 +00:00
sed 's/^/# /' "$(stdout_file "$abspath")"
2016-02-28 12:45:38 +00:00
printf '\033[0m'
;;
skip)
2016-02-28 13:24:55 +00:00
printf "${pretty_path}"
2016-02-28 12:45:38 +00:00
;;
esac
2016-02-28 10:27:49 +00:00
fi
2016-02-28 09:17:16 +00:00
2016-02-28 11:14:23 +00:00
done < $sorted_log_file
rm $sorted_log_file
2016-02-28 09:17:16 +00:00
set +e
if $tap_format; then
2016-02-28 11:14:23 +00:00
echo "# Full test suite took $elapsed $(plural second $elapsed)."
echo 1..$n
2016-02-28 09:17:16 +00:00
else
2016-02-28 12:53:26 +00:00
echo
2016-02-28 09:17:16 +00:00
echo "Done, took $elapsed $(plural second $elapsed)."
2016-02-28 12:45:38 +00:00
printf '%s\n' "$oks $(plural test "$oks") passed."
2016-02-28 12:59:36 +00:00
printf '%s\n' "$skips $(plural test "$skips") skipped."
2016-02-28 09:17:16 +00:00
2016-02-28 10:57:27 +00:00
# If any tests are not ok, print the message in red.
if [ $not_oks -gt 0 ] ; then
printf '\033[31m'
fi
2016-02-28 12:45:38 +00:00
printf '%s\n' "$not_oks $(plural test "$not_oks") failed."
2016-02-28 09:17:16 +00:00
printf '\033[m'
fi
2016-02-28 10:57:27 +00:00
test "$not_oks" -eq '0'
2012-10-04 11:29:34 +00:00
}
2012-10-04 16:43:49 +00:00
has_sh_or_no_shebang_line() {
2016-02-08 16:05:56 +00:00
# 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 [<options>] <test directory>"
2012-10-11 05:46:02 +00:00
2012-10-11 06:21:05 +00:00
urchin_help() {
cat <<EOF
$USAGE
2016-02-28 14:01:17 +00:00
By default, Urchin checks for the following shells and runs every
particular test file once per shell.
$(echo "$DEFAULT_SHELLS" | sed 's/^/ /')
On each run,
1. The TEST_SHELL environment variable is set to the particular shell.
2. If the test file has a shebang line of "#!/bin/sh" or no shebang
line at all, the test script is also executed in that shell.
2016-02-28 14:12:21 +00:00
The following flags affect how this multiple-shell testing is handled.
-s, --shell <shell> Tell Urchin to use a different list of shells.
(You can pass this flag multiple times.)
-n, --disable-cycling Disable the cycling of shells; Urchin will execute
test files directly, and it will not manipulate the
TEST_SHELL environment variable.
Urchin can run any Unix-style programs for any purpose, not just for testing.
If you are using Urchin for automatally running periodic jobs or for running
lots of configuration files, you may find these options helpful.
-e, --exit-on-fail Stop running if any single test fails.
-f, --force Force running even if the test directory's name
does not contain the word "test".
These options affect how results are formatted.
-t, --tap Format output in Test Anything Protocol (TAP)
-v, --verbose Print stdout from all tests, not just failed tests.
XXX Not yet implemented
The remaining flags provide information about urchin.
-h, --help Display this help.
--version Display the version number.
Go to https://github.com/tlevine/urchin for documentation on writing tests.
EOF
2012-10-11 06:21:05 +00:00
}
2012-10-11 05:46:02 +00:00
2013-06-20 17:56:29 +00:00
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
}
2012-10-11 06:21:05 +00:00
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
2016-02-28 09:08:20 +00:00
urchin_exit 1
2012-10-11 06:21:05 +00:00
}
2016-02-28 14:01:17 +00:00
cycle_shell=true
shell_list=$tmp/shell_list
force=false
2016-02-28 10:57:27 +00:00
exit_on_not_ok=false
2016-01-27 00:32:51 +00:00
tap_format=false
2012-10-11 06:21:05 +00:00
while [ $# -gt 0 ]
do
case "$1" in
2016-02-28 10:57:27 +00:00
-e) exit_on_not_ok=true;;
-f) force=true;;
-s)
shift
shell_for_sh_tests=$1
2016-02-28 14:01:17 +00:00
2016-02-08 16:05:56 +00:00
which "$shell_for_sh_tests" > /dev/null || {
echo "Cannot find specified shell: '$shell_for_sh_tests'" >&2
urchin_help >&2
2016-02-28 09:08:20 +00:00
urchin_exit 11
2016-02-28 14:01:17 +00:00
}
test $(echo "$potential_test" | wc -l) -eq 1 || {
echo 'Test file names may not contain newline characters.' >&2
echo 'If this is really a problem, I will fix it.' >&2
urchin_exit 11
}
echo "$shell_for_sh_tests" > "$shell_list"
;;
-n) cycle_shell=false;;
2016-01-27 00:32:51 +00:00
-t) tap_format=true;;
-h|--help) urchin_help
exit 0;;
2016-02-28 14:12:21 +00:00
--version) echo "$VERSION"
2016-02-28 09:08:20 +00:00
urchin_exit;;
-*) urchin_help >&2
2012-10-11 06:21:05 +00:00
exit 1;;
*) break;;
esac
shift
done
# Verify argument for main stuff
2016-02-28 12:02:57 +00:00
if [ "$#" != '1' ] || [ ! -e "$1" ]; then
if [ -n "$1" ] && [ ! -e "$1" ]; then
echo "No such file or directory: '$1'" >&2
fi
echo "$USAGE" >&2
2016-02-28 09:08:20 +00:00
urchin_exit 11
2012-10-11 19:47:08 +00:00
fi
2012-10-11 06:21:05 +00:00
# Run or present the Molly guard.
2016-02-28 12:02:57 +00:00
root="$(urchin_root "$1")"
if basename "$(fullpath "$root")" |
2016-02-28 11:26:25 +00:00
grep -Fi 'test' > /dev/null || $force; then
2016-02-28 09:25:21 +00:00
2016-02-28 10:39:32 +00:00
start=$(date +%s)
set +e
2016-02-28 12:14:16 +00:00
# 1 test file or folder to run
# 2 urchin root
2016-02-28 14:01:17 +00:00
# 3 Should we cycle shells?
2016-02-28 12:14:16 +00:00
# 4 TEST_SHELL
2016-02-28 14:01:17 +00:00
recurse "$(fullpath "$1")" "$root" "$cycle_shell" "$TEST_SHELL"
2016-02-28 10:00:53 +00:00
exit_code=$?
2016-02-28 10:39:32 +00:00
2016-02-28 10:00:53 +00:00
set -e
2016-02-28 10:39:32 +00:00
finish=$(date +%s)
2016-02-28 10:18:06 +00:00
2016-02-28 12:45:38 +00:00
report_outcome "$root" $tap_format $tmp/log $start $finish
2016-02-28 10:18:06 +00:00
2016-02-28 09:36:48 +00:00
urchin_exit $exit_code
2012-10-11 06:21:05 +00:00
else
urchin_molly_guard
2012-10-08 14:43:14 +00:00
fi