BASIS  version 1.2.3 (revision 2104)
shtap.sh
Go to the documentation of this file.
00001 ##############################################################################
00002 # @file  shtap.sh
00003 # @brief Unit testing framework for BASH based on the Test Anything Protocol.
00004 #
00005 # @author Patrick LeBoutillier <patl at cpan.org>
00006 #
00007 # @note This file is a copy of the tap-functions file which is part of the
00008 #       JTap project (http://svn.solucorp.qc.ca/repos/solucorp/JTap/trunk/).
00009 #       The original implementation has been modified by Andreas Schuh as
00010 #       part of the BASIS project at SBIA.
00011 #
00012 # Plan:
00013 # @code
00014 # plan_no_plan
00015 # plan_skip_all [REASON]
00016 # plan_tests NB_TESTS
00017 # @endcode
00018 #
00019 # Test:
00020 # @code
00021 # ok RESULT [NAME]
00022 # okx COMMAND
00023 # is RESULT EXPECTED [NAME]
00024 # isnt RESULT EXPECTED [NAME]
00025 # like RESULT PATTERN [NAME]
00026 # unlike RESULT PATTERN [NAME]
00027 # pass [NAME]
00028 # fail [NAME]
00029 # @endcode
00030 #
00031 # Skip:
00032 # @code
00033 # skip [CONDITION] [REASON] [NB_TESTS=1]
00034 #
00035 # skip $feature_not_present "feature not present" 2 ||
00036 # {
00037 #     is $a "a"
00038 #     is $b "b"
00039 # }
00040 # @endcode
00041 #
00042 # Specify TODO mode by setting the TODO variable:
00043 # @code
00044 # TODO="not implemented yet"
00045 # ok $result "some not implemented test"
00046 # unset TODO
00047 # @endcode
00048 #
00049 # Other:
00050 # @code
00051 # diag MSG
00052 # @endcode
00053 #
00054 # Example:
00055 # @code
00056 # #! /usr/bin/env bash
00057 #
00058 # source shtap.sh
00059 #
00060 # plan_tests 7
00061 #
00062 # # test identity
00063 # {
00064 #     me=${USER}
00065 #     is ${USER} ${me} "I am myself"
00066 #     like ${HOME} ${me} "My home is mine"
00067 #     like "`id`" ${me} "My id matches myself"
00068 # }
00069 #
00070 # # test ls
00071 # {
00072 #     ls ${HOME} 1>&2
00073 #     ok $? "ls ${HOME}"
00074 #     # same thing using okx shortcut
00075 #     okx ls ${HOME}
00076 # }
00077 #
00078 # # test only for root
00079 # {
00080 #     [[ "`id -u`" != "0" ]]
00081 #     i_am_not_root=$?
00082 #     skip ${i_am_not_root} "Must be root" ||
00083 #     {
00084 #         okx ls /root
00085 #     }
00086 # }
00087 #
00088 # # test TODO
00089 # {
00090 #     TODO="figure out how to become root..."
00091 #     okx [ "$HOME" == "/root" ]
00092 #     unset TODO
00093 # }
00094 # @endcode
00095 #
00096 # Copyright (c) Patrick LeBoutillier.<br />
00097 # Copyright (c) 2011, University of Pennsylvania.<br />
00098 # All rights reserved.
00099 #
00100 # This program is free software; you can redistribute it and/or modify
00101 # it under the terms of the GNU General Public License as published by
00102 # the Free Software Foundation; either version 2 of the License, or
00103 # any later version.
00104 #
00105 # This program is distributed in the hope that it will be useful,
00106 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00107 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00108 # GNU General Public License for more details.
00109 #
00110 # @sa http://testanything.org/wiki/index.php/Tap-functions
00111 #
00112 # Contact: SBIA Group <sbia-software at uphs.upenn.edu>
00113 ##############################################################################
00114 
00115 # return if already loaded
00116 [ -n "${SHTAP_VERSION:-}" ] && return 0
00117 readonly SHTAP_VERSION='1.02-sbia'
00118 
00119 
00120 readonly _SHTAP_DIR=$(cd -P -- "$(dirname -- "${BASH_SOURCE}")" && pwd -P)
00121 source "${_SHTAP_DIR}/./core.sh" # match()
00122 
00123 readonly _SHTAP_FILENAME='shtap.sh'
00124 
00125 TODO=
00126 
00127 _shtap_plan_set=0
00128 _shtap_no_plan=0
00129 _shtap_skip_all=0
00130 _shtap_test_died=0
00131 _shtap_expected_tests=0 
00132 _shtap_executed_tests=0 
00133 _shtap_failed_tests=0
00134 
00135 # used to call _cleanup() on shell exit
00136 trap _shtap_exit EXIT
00137 trap _shtap_exit INT
00138 trap _shtap_exit TERM
00139 
00140 # ============================================================================
00141 # plan
00142 # ============================================================================
00143 
00144 # ----------------------------------------------------------------------------
00145 ## @brief Choose not to plan number of tests in advance.
00146 function plan_no_plan
00147 {
00148     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00149 
00150     _shtap_plan_set=1
00151     _shtap_no_plan=1
00152 
00153     return 0
00154 }
00155 
00156 # ----------------------------------------------------------------------------
00157 ## @brief Plan to skip all tests.
00158 function plan_skip_all
00159 {
00160     local reason=${1:-''}
00161 
00162     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00163 
00164     _shtap_print_plan 0 "Skip ${reason}"
00165 
00166     _shtap_skip_all=1
00167     _shtap_plan_set=1
00168     _shtap_exit 0
00169 
00170     return 0
00171 }
00172 
00173 # ----------------------------------------------------------------------------
00174 ## @brief Plan a certain number of tests and stick to it.
00175 function plan_tests
00176 {
00177     local tests=${1:?}
00178 
00179     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00180     [ ${tests} -eq 0 ] && _shtap_die "You said to run 0 tests!  You've got to run something."
00181 
00182     _shtap_print_plan ${tests}
00183     _shtap_expected_tests=${tests}
00184     _shtap_plan_set=1
00185 
00186     return ${tests}
00187 }
00188 
00189 # ----------------------------------------------------------------------------
00190 ## @brief Print plan.
00191 function _shtap_print_plan
00192 {
00193     local tests=${1:?}
00194     local directive=${2:-''}
00195 
00196     echo -n "1..${tests}"
00197     [[ -n "${directive}" ]] && echo -n " # ${directive}"
00198     echo
00199 }
00200 
00201 # ============================================================================
00202 # pass / fail
00203 # ============================================================================
00204 
00205 # ----------------------------------------------------------------------------
00206 ## @brief Pass in any case and print reason.
00207 function pass
00208 {
00209     local name=$1
00210     ok 0 "${name}"
00211 }
00212 
00213 # ----------------------------------------------------------------------------
00214 ## @brief Fail in any case and print reason.
00215 function fail
00216 {
00217     local name=$1
00218     ok 1 "${name}"
00219 }
00220 
00221 # ============================================================================
00222 # test
00223 # ============================================================================
00224 
00225 # ----------------------------------------------------------------------------
00226 ## @brief Evaluate test expression and fail if it does not evaluate to 0 or
00227 #         check return value of function.
00228 #
00229 # This is the workhorse method that actually prints the tests result.
00230 #
00231 # @param [in] expression Test expression or return value.
00232 # @param [in] name       Name of test.
00233 function ok
00234 {
00235     local expression=${1:?}
00236     local name=${2:-''}
00237 
00238     [ ${_shtap_plan_set} -eq 0 ] && _shtap_die "You tried to run a test without a plan!  Gotta have a plan."
00239 
00240     (( _shtap_executed_tests++ ))
00241 
00242     if [ -n "${name}" ]; then
00243         if match "${name}" "^[0-9]+$"; then
00244             diag "    You named your test '${name}'.  You shouldn't use numbers for your test names."
00245             diag "    Very confusing."
00246         fi
00247     fi
00248 
00249     local match=`expr "${expression}" : '\([0-9]*\)'`
00250     local result=0
00251     if [ -z "${expression}" ]; then
00252         result=1
00253     elif [ -n "${match}" -a "${expression}" = "${match}" ]; then
00254         [ ${expression} -ne 0 ] && result=1
00255     else
00256         ( eval ${expression} ) >/dev/null 2>&1
00257         [ $? -ne 0 ] && result=1
00258     fi
00259 
00260     if [ ${result} -ne 0 ]; then
00261         echo -n "not "
00262         (( _shtap_failed_tests++ ))
00263     fi
00264     echo -n "ok ${_shtap_executed_tests}"
00265 
00266     if [ -n "${name}" ]; then
00267         local ename=${name//\#/\\#}
00268         echo -n " - ${ename}"
00269     fi
00270 
00271     if [ -n "${TODO}" ]; then
00272         echo -n " # TODO ${TODO}" ;
00273         if [ ${result} -ne 0 ]; then
00274             (( _shtap_failed_tests-- ))
00275         fi
00276     fi
00277 
00278     echo
00279     if [ ${result} -ne 0 ]; then
00280         local file='_SHTAP_FILENAME'
00281         local func=
00282         local line=
00283 
00284         local i=0
00285         local bt=$(caller ${i})
00286         while match "${bt}" "${_SHTAP_FILENAME}$"; do
00287             (( i++ ))
00288             bt=$(caller ${i})
00289         done
00290         local backtrace=
00291         eval $(caller ${i} | (read line func file; echo "backtrace=\"${file}:${func}() at line ${line}.\""))
00292 
00293         local t=
00294         [ -n "${TODO}" ] && t="(TODO) "
00295 
00296         if [ -n "${name}" ]; then
00297             diag "  Failed ${t}test '${name}'"
00298             diag "  in ${backtrace}"
00299         else
00300             diag "  Failed ${t}test in ${backtrace}"
00301         fi
00302     fi
00303 
00304     return ${result}
00305 }
00306 
00307 # ----------------------------------------------------------------------------
00308 ## @brief Execute command and check return value.
00309 #
00310 # @param [in] command Command to run.
00311 function okx
00312 {
00313     local command="$@"
00314 
00315     local line=
00316     diag "Output of '${command}':"
00317     ${command} | while read line; do
00318         diag "${line}"
00319     done
00320     ok ${PIPESTATUS[0]} "${command}"
00321 }
00322 
00323 # ----------------------------------------------------------------------------
00324 ## @brief Compare actual and expected result.
00325 #
00326 # @param [in] result   Actual result.
00327 # @param [in] expected Expected result.
00328 function _shtap_equals
00329 {
00330     local result=$1
00331     local expected=$2
00332 
00333     if [[ "${result}" == "${expected}" ]] ; then
00334         return 0
00335     else 
00336         return 1
00337     fi
00338 }
00339 
00340 # ----------------------------------------------------------------------------
00341 ## @brief Diagnostic message for is().
00342 #
00343 # @param [in] result   Actual result.
00344 # @param [in] expected Expected result.
00345 function _shtap_is_diag
00346 {
00347     local result=$1
00348     local expected=$2
00349 
00350     diag "         got: '${result}'" 
00351     diag "    expected: '${expected}'"
00352 }
00353 
00354 # ----------------------------------------------------------------------------
00355 ## @brief Test whether a given result is equal to the expected result.
00356 #
00357 # @param [in] result   Actual result.
00358 # @param [in] expected Expected result.
00359 # @param [in] name     Optional name of test.
00360 #
00361 # @returns Whether the results are equal.
00362 #
00363 # @retval 0 On equality.
00364 # @retval 1 Otherwise.
00365 function is
00366 {
00367     local result=$1
00368     local expected=$2
00369     local name=${3:-''}
00370 
00371     _shtap_equals "${result}" "${expected}"
00372     [ $? -eq 0 ]
00373     ok $? "${name}"
00374     local r=$?
00375     [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
00376     return ${r}
00377 }
00378 
00379 # ----------------------------------------------------------------------------
00380 ## @brief Test whether a given result is not equal the expected result.
00381 #
00382 # @param [in] result   Actual result.
00383 # @param [in] expected Expected result.
00384 # @param [in] name     Optional name of test.
00385 #
00386 # @returns Whether the results were not equal.
00387 #
00388 # @retval 0 Otherwise.
00389 # @retval 1 On equality.
00390 function isnt
00391 {
00392     local result=$1
00393     local expected=$2
00394     local name=${3:-''}
00395 
00396     _shtap_equals "${result}" "${expected}"
00397     (( $? != 0 ))
00398     ok $? "${name}"
00399     local r=$?
00400     [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
00401     return ${r} 
00402 }
00403 
00404 # ----------------------------------------------------------------------------
00405 ## @brief Test whether a given result matches an expected pattern.
00406 #
00407 # @param [in] result  Actual result.
00408 # @param [in] pattern Expected pattern.
00409 # @param [in] name    Optional name of test.
00410 #
00411 # @returns Whether the result matched the pattern.
00412 #
00413 # @retval 0 On match.
00414 # @retval 1 Otherwise.
00415 function like
00416 {
00417     local result=$1
00418     local pattern=$2
00419     local name=${3:-''}
00420 
00421     match "${result}" "${pattern}"
00422     [ $? -eq 0 ]
00423     ok $? "${name}"
00424     local r=$?
00425     [ ${r} -ne 0 ] && diag "    '${result}' doesn't match '${pattern}'"
00426     return ${r}
00427 }
00428 
00429 # ----------------------------------------------------------------------------
00430 ## @brief Test whether a given result does not match an expected pattern.
00431 #
00432 # @param [in] result  Actual result.
00433 # @param [in] pattern Expected pattern.
00434 # @param [in] name    Optional name of test.
00435 #
00436 # @returns Whether the result did not match the pattern.
00437 #
00438 # @retval 0 Otherwise.
00439 # @retval 1 On match.
00440 function unlike
00441 {
00442     local result=$1
00443     local pattern=$2
00444     local name=${3:-''}
00445 
00446     match "${result}" "${pattern}"
00447     [ $? -ne 0 ]
00448     ok $? "${name}"
00449     local r=$?
00450     [ ${r} -ne 0 ] && diag "    '${result}' matches '${pattern}'"
00451     return ${r}
00452 }
00453 
00454 # ============================================================================
00455 # skip
00456 # ============================================================================
00457 
00458 # ----------------------------------------------------------------------------
00459 ## @brief Skip tests under a certain condition.
00460 #
00461 # @param [in] condition The condition for skipping the tests.
00462 #                       If 0, the tests are skipped, otherwise not.
00463 # @param [in] reason    An explanation for why skipping the tests.
00464 # @param [in] n         The number of tests which will be skipped.
00465 #
00466 # @returns Whether the tests were skipped.
00467 #
00468 # @retval 0 If tests are to be skipped.
00469 # @retval 1 Otherwise.
00470 function skip
00471 {
00472     local condition=${1:?}
00473     local reason=${2:-''}
00474     local n=${3:-1}
00475 
00476     if [ ${condition} -eq 0 ]; then
00477         local i=
00478         for (( i=0 ; i<$n ; i++ )); do
00479             (( _shtap_executed_tests++ ))
00480             echo "ok ${_shtap_executed_tests} # skip: ${reason}" 
00481         done
00482         return 0
00483     else
00484         return 1
00485     fi
00486 }
00487 
00488 # ============================================================================
00489 # diagnostics
00490 # ============================================================================
00491 
00492 # ----------------------------------------------------------------------------
00493 ## @brief Print diagnostic message.
00494 #
00495 # @param [in] msg Diagnostic message.
00496 #
00497 # @returns Always 1.
00498 function diag
00499 {
00500     local msg=${1:?}
00501 
00502     if [ -n "${msg}" ]; then
00503         echo "# ${msg}"
00504     fi
00505 
00506     return 1
00507 }
00508 
00509 # ============================================================================
00510 # termination
00511 # ============================================================================
00512 
00513 # ----------------------------------------------------------------------------
00514 ## @brief Bail out.
00515 #
00516 # @param [in] reason Reason for bailing out.
00517 #
00518 # @returns Nothing. Instead, exits the process with error code 255.
00519 function SHTAP_BAIL_OUT
00520 {
00521     local reason=${1:-''}
00522 
00523     echo "Bail out! ${reason}" >&2
00524     _shtap_exit 255
00525 }
00526 
00527 # ----------------------------------------------------------------------------
00528 ## @brief Abort test execution.
00529 #
00530 # @param [in] reason Reason for aborting the test execution.
00531 #
00532 # @returns Nothing. Instead, exits the process with error code 255.
00533 function _shtap_die
00534 {
00535     local reason=${1:-'<unspecified error>'}
00536 
00537     echo "${reason}" >&2
00538     _shtap_test_died=1
00539     _shtap_exit 255
00540 }
00541 
00542 # ----------------------------------------------------------------------------
00543 ## @brief Cleaning up after execution of tests and see if plan was fulfilled.
00544 function _shtap_cleanup
00545 {
00546     local rc=0 # return code
00547 
00548     if [ ${_shtap_plan_set} -eq 0 ]; then
00549         diag "Looks like your test died before it could output anything."
00550         return ${rc}
00551     fi
00552 
00553     if [ ${_shtap_test_died} -ne 0 ]; then
00554         diag "Looks like your test died just after ${_shtap_executed_tests}."
00555         return ${rc}
00556     fi
00557 
00558     if [ ${_shtap_skip_all} -eq 0 -a ${_shtap_no_plan} -ne 0 ]; then
00559         _shtap_print_plan ${_shtap_executed_tests}
00560     fi
00561 
00562     local s=
00563     if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
00564         s=''
00565         [ ${_shtap_expected_tests} -gt 1 ] && s='s'
00566         local extra=$(( _shtap_executed_tests - _shtap_expected_tests ))
00567         diag "Looks like you planned ${_shtap_expected_tests} test${s} but ran ${extra} extra."
00568         rc=-1 ;
00569     fi
00570 
00571     if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -gt ${_shtap_executed_tests} ]; then
00572         s=''
00573         [ ${_shtap_expected_tests} -gt 1 ] && s='s'
00574         diag "Looks like you planned ${_shtap_expected_tests} test${s} but only ran ${_shtap_executed_tests}."
00575     fi
00576 
00577     if [ ${_shtap_failed_tests} -gt 0 ]; then
00578         s=''
00579         [ ${_shtap_failed_tests} -gt 1 ] && s='s'
00580         diag "Looks like you failed ${_shtap_failed_tests} test${s} of ${_shtap_executed_tests}."
00581     fi
00582 
00583     return ${rc}
00584 }
00585 
00586 # ----------------------------------------------------------------------------
00587 ## @brief Calculate exit status indicating number of failed or extra tests.
00588 function _shtap_exit_status
00589 {
00590     if [ ${_shtap_no_plan} -ne 0 -o ${_shtap_plan_set} -eq 0 ]; then
00591         return ${_shtap_failed_tests}
00592     fi
00593 
00594     if [ ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
00595         return $(( _shtap_executed_tests - _shtap_expected_tests ))
00596     fi
00597 
00598     return $(( _shtap_failed_tests + ( _shtap_expected_tests - _shtap_executed_tests )))
00599 }
00600 
00601 # ----------------------------------------------------------------------------
00602 ## @brief Terminate execution of tests.
00603 function _shtap_exit
00604 {
00605     local rc=${1:-''}
00606     if [ -z "${rc}" ]; then
00607         _shtap_exit_status
00608         rc=$?
00609     fi
00610 
00611     _shtap_cleanup
00612     local alt_rc=$?
00613     [ ${alt_rc} -ne 0 ] && rc=${alt_rc}
00614     trap - EXIT
00615     exit ${rc}
00616 }