#!/bin/sh # [r5]: roundup.5.html # [r1t]: roundup-1-test.sh.html # [r5t]: roundup-5-test.sh.html # # _(c) 2010 Blake Mizerany - MIT License_ # # Spray **roundup** on your shells to eliminate weeds and bugs. If your shells # survive **roundup**'s deathly toxic properties, they are considered # roundup-ready. # # **roundup** reads shell scripts to form test plans. Each # test plan is sourced into a sandbox where each test is executed. # # See [roundup-1-test.sh.html][r1t] or [roundup-5-test.sh.html][r5t] for example # test plans. # # __Install__ # # git clone http://github.com/bmizerany/roundup.git # cd roundup # make # sudo make install # # Alternatively, copy `roundup` wherever you like. # # __NOTE__: Because test plans are sourced into roundup, roundup prefixes its # variable and function names with `roundup_` to avoid name collisions. See # "Sandbox Test Runs" below for more insight. # Usage and Prerequisites # ----------------------- # Exit if any following command exits with a non-zero status. set -e # The current version is set during `make version`. Do not modify this line in # anyway unless you know what you're doing. ROUNDUP_VERSION="0.0.5" export ROUNDUP_VERSION # Usage is defined in a specific comment syntax. It is `grep`ed out of this file # when needed (i.e. The Tomayko Method). See # [shocco](http://rtomayko.heroku.com/shocco) for more detail. #/ usage: roundup [--help|-h] [--version|-v] [plan ...] roundup_usage() { grep '^#/' <"$0" | cut -c4- } while test "$#" -gt 0 do case "$1" in --help|-h) roundup_usage exit 0 ;; --version|-v) echo "roundup version $ROUNDUP_VERSION" exit 0 ;; --color) color=always shift ;; -) echo >&2 "roundup: unknown switch $1" exit 1 ;; *) break ;; esac done # Consider all scripts with names matching `*-test.sh` the plans to run unless # otherwise specified as arguments. if [ "$#" -gt "0" ] then roundup_plans="$@" else roundup_plans="$(ls *-test.sh)" fi : ${color:="auto"} # Create a temporary storage place for test output to be retrieved for display # after failing tests. roundup_tmp="$PWD/.roundup.$$" mkdir -p "$roundup_tmp" trap "rm -rf \"$roundup_tmp\"" EXIT INT # __Tracing failures__ roundup_trace() { # Delete the first two lines that represent roundups execution of the # test function. They are useless to the user. sed '1d' | # Delete the last line which is the "set +x" of the error trap sed '$d' | # Replace the rc=$? of the error trap with an verbose string appended # to the failing command trace line. sed '$s/.*rc=/exit code /' | # Trim the two left most `+` signs. They represent the depth at which # roundup executed the function. They also, are useless and confusing. sed 's/^++//' | # Indent the output by 4 spaces to align under the test name in the # summary. sed 's/^/ /' | # Highlight the last line in front of the exit code to bring notice to # where the error occurred. # # The sed magic puts every line into the hold buffer first, then # substitutes in the previous hold buffer content, prints that and starts # with the next cycle. At the end the last line (in the hold buffer) # is printed without substitution. sed -n "x;1!{ \$s/\(.*\)/$mag\1$clr/; };1!p;\$x;\$p" } # __Other helpers__ # Track the test stats while outputting a real-time report. This takes input on # **stdin**. Each input line must come in the format of: # # # The plan description to be displayed # d # # # A passing test # p # # # A failed test # f roundup_summarize() { set -e # __Colors for output__ # Use colors if we are writing to a tty device. if (test -t 1) || (test $color = always) then red=$(printf "\033[31m") grn=$(printf "\033[32m") mag=$(printf "\033[35m") clr=$(printf "\033[m") cols=$(tput cols) fi # Make these available to `roundup_trace`. export red grn mag clr ntests=0 passed=0 failed=0 : ${cols:=10} while read status name do case $status in p) ntests=$(expr $ntests + 1) passed=$(expr $passed + 1) printf " %-48s " "$name:" printf "$grn[PASS]$clr\n" ;; f) ntests=$(expr $ntests + 1) failed=$(expr $failed + 1) printf " %-48s " "$name:" printf "$red[FAIL]$clr\n" roundup_trace < "$roundup_tmp/$name" ;; d) printf "%s\n" "$name" ;; esac done # __Test Summary__ # # Display the summary now that all tests are finished. yes = | head -n 57 | tr -d '\n' printf "\n" printf "Tests: %3d | " $ntests printf "Passed: %3d | " $passed printf "Failed: %3d" $failed printf "\n" # Exit with an error if any tests failed test $failed -eq 0 || exit 2 } # Sandbox Test Runs # ----------------- # The above checks guarantee we have at least one test. We can now move through # each specified test plan, determine its test plan, and administer each test # listed in a isolated sandbox. for roundup_p in $roundup_plans do # Create a sandbox, source the test plan, run the tests, then leave # without a trace. ( # Consider the description to be the `basename` of the plan minus the # tailing -test.sh. roundup_desc=$(basename "$roundup_p" -test.sh) # Define functions for # [roundup(5)][r5] # A custom description is recommended, but optional. Use `describe` to # set the description to something more meaningful. # TODO: reimplement this. describe() { roundup_desc="$*" } # Provide default `before` and `after` functions that run only `:`, a # no-op. They may or may not be redefined by the test plan. before() { :; } after() { :; } # Seek test methods and aggregate their names, forming a test plan. # This is done before populating the sandbox with tests to avoid odd # conflicts. # TODO: I want to do this with sed only. Please send a patch if you # know a cleaner way. roundup_plan=$( grep "^it_.*()" $roundup_p | sed "s/\(it_[a-zA-Z0-9_]*\).*$/\1/g" ) # We have the test plan and are in our sandbox with [roundup(5)][r5] # defined. Now we source the plan to bring its tests into scope. . ./$roundup_p # Output the description signal printf "d %s" "$roundup_desc" | tr "\n" " " printf "\n" for roundup_test_name in $roundup_plan do # Any number of things are possible in `before`, `after`, and the # test. Drop into an subshell to contain operations that may throw # off roundup; such as `cd`. ( # Output `before` trace to temporary file. If `before` runs cleanly, # the trace will be overwritten by the actual test case below. { # redirect tracing output of `before` into file. { set -x # If `before` wasn't redefined, then this is `:`. before } &>"$roundup_tmp/$roundup_test_name" # disable tracing again. Its trace output goes to /dev/null. set +x } &>/dev/null # exit subshell with return code of last failing command. This # is needed to see the return code 253 on failed assumptions. # But, only do this if the error handling is activated. set -E trap 'rc=$?; set +x; set -o | grep "errexit.*on" >/dev/null && exit $rc' ERR # If `before` wasn't redefined, then this is `:`. before # Momentarily turn off auto-fail to give us access to the tests # exit status in `$?` for capturing. set +e ( # Set `-xe` before the test in the subshell. We want the # test to fail fast to allow for more accurate output of # where things went wrong but not in _our_ process because a # failed test should not immediately fail roundup. Each # tests trace output is saved in temporary storage. set -xe $roundup_test_name ) >"$roundup_tmp/$roundup_test_name" 2>&1 # We need to capture the exit status before returning the `set # -e` mode. Returning with `set -e` before we capture the exit # status will result in `$?` being set with `set`'s status # instead. roundup_result=$? # It's safe to return to normal operation. set -e # If `after` wasn't redefined, then this runs `:`. after # This is the final step of a test. Print its pass/fail signal # and name. if [ "$roundup_result" -ne 0 ] then printf "f" else printf "p" fi printf " $roundup_test_name\n" ) done ) done | # All signals are piped to this for summary. roundup_summarize