BASIS  r3148
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-basis'
00118 
00119 
00120 . "`cd -P -- \`dirname -- "${BASH_SOURCE}"\` && pwd`/core.sh" || exit 1 # match()
00121 
00122 readonly _SHTAP_FILENAME=''
00123 
00124 TODO=
00125 
00126 _shtap_plan_set=0
00127 _shtap_no_plan=0
00128 _shtap_skip_all=0
00129 _shtap_test_died=0
00130 _shtap_expected_tests=0 
00131 _shtap_executed_tests=0 
00132 _shtap_failed_tests=0
00133 
00134 # used to call _cleanup() on shell exit
00135 trap _shtap_exit EXIT
00136 trap _shtap_exit INT
00137 trap _shtap_exit TERM
00138 
00139 # ============================================================================
00140 # plan
00141 # ============================================================================
00142 
00143 # ----------------------------------------------------------------------------
00144 ## @brief Choose not to plan number of tests in advance.
00145 function plan_no_plan
00146 {
00147     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00148 
00149     _shtap_plan_set=1
00150     _shtap_no_plan=1
00151 
00152     return 0
00153 }
00154 
00155 # ----------------------------------------------------------------------------
00156 ## @brief Plan to skip all tests.
00157 function plan_skip_all
00158 {
00159     local reason=${1:-''}
00160 
00161     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00162 
00163     _shtap_print_plan 0 "Skip ${reason}"
00164 
00165     _shtap_skip_all=1
00166     _shtap_plan_set=1
00167     _shtap_exit 0
00168 
00169     return 0
00170 }
00171 
00172 # ----------------------------------------------------------------------------
00173 ## @brief Plan a certain number of tests and stick to it.
00174 function plan_tests
00175 {
00176     local tests=${1:?}
00177 
00178     [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
00179     [ ${tests} -eq 0 ] && _shtap_die "You said to run 0 tests!  You've got to run something."
00180 
00181     _shtap_print_plan ${tests}
00182     _shtap_expected_tests=${tests}
00183     _shtap_plan_set=1
00184 
00185     return ${tests}
00186 }
00187 
00188 # ----------------------------------------------------------------------------
00189 ## @brief Print plan.
00190 function _shtap_print_plan
00191 {
00192     local tests=${1:?}
00193     local directive=${2:-''}
00194 
00195     echo -n "1..${tests}"
00196     [[ -n "${directive}" ]] && echo -n " # ${directive}"
00197     echo
00198 }
00199 
00200 # ============================================================================
00201 # pass / fail
00202 # ============================================================================
00203 
00204 # ----------------------------------------------------------------------------
00205 ## @brief Pass in any case and print reason.
00206 function pass
00207 {
00208     local name=$1
00209     ok 0 "${name}"
00210 }
00211 
00212 # ----------------------------------------------------------------------------
00213 ## @brief Fail in any case and print reason.
00214 function fail
00215 {
00216     local name=$1
00217     ok 1 "${name}"
00218 }
00219 
00220 # ============================================================================
00221 # test
00222 # ============================================================================
00223 
00224 # ----------------------------------------------------------------------------
00225 ## @brief Evaluate test expression and fail if it does not evaluate to 0 or
00226 #         check return value of function.
00227 #
00228 # This is the workhorse method that actually prints the tests result.
00229 #
00230 # @param [in] expression Test expression or return value.
00231 # @param [in] name       Name of test.
00232 function ok
00233 {
00234     local expression=${1:?}
00235     local name=${2:-''}
00236 
00237     [ ${_shtap_plan_set} -eq 0 ] && _shtap_die "You tried to run a test without a plan!  Gotta have a plan."
00238 
00239     (( _shtap_executed_tests++ ))
00240 
00241     if [ -n "${name}" ]; then
00242         if match "${name}" "^[0-9]+$"; then
00243             diag "    You named your test '${name}'.  You shouldn't use numbers for your test names."
00244             diag "    Very confusing."
00245         fi
00246     fi
00247 
00248     local match=`expr "${expression}" : '\([0-9]*\)'`
00249     local result=0
00250     if [ -z "${expression}" ]; then
00251         result=1
00252     elif [ -n "${match}" -a "${expression}" = "${match}" ]; then
00253         [ ${expression} -ne 0 ] && result=1
00254     else
00255         ( eval ${expression} ) >/dev/null 2>&1
00256         [ $? -ne 0 ] && result=1
00257     fi
00258 
00259     if [ ${result} -ne 0 ]; then
00260         echo -n "not "
00261         (( _shtap_failed_tests++ ))
00262     fi
00263     echo -n "ok ${_shtap_executed_tests}"
00264 
00265     if [ -n "${name}" ]; then
00266         local ename=${name//\#/\\#}
00267         echo -n " - ${ename}"
00268     fi
00269 
00270     if [ -n "${TODO}" ]; then
00271         echo -n " # TODO ${TODO}" ;
00272         if [ ${result} -ne 0 ]; then
00273             (( _shtap_failed_tests-- ))
00274         fi
00275     fi
00276 
00277     echo
00278     if [ ${result} -ne 0 ]; then
00279         local file='_SHTAP_FILENAME'
00280         local func=
00281         local line=
00282 
00283         local i=0
00284         local bt=$(caller ${i})
00285         while match "${bt}" "${_SHTAP_FILENAME}$"; do
00286             (( i++ ))
00287             bt=$(caller ${i})
00288         done
00289         local backtrace=
00290         eval $(caller ${i} | (read line func file; echo "backtrace=\"${file}:${func}() at line ${line}.\""))
00291 
00292         local t=
00293         [ -n "${TODO}" ] && t="(TODO) "
00294 
00295         if [ -n "${name}" ]; then
00296             diag "  Failed ${t}test '${name}'"
00297             diag "  in ${backtrace}"
00298         else
00299             diag "  Failed ${t}test in ${backtrace}"
00300         fi
00301     fi
00302 
00303     return ${result}
00304 }
00305 
00306 # ----------------------------------------------------------------------------
00307 ## @brief Execute command and check return value.
00308 #
00309 # @param [in] command Command to run.
00310 function okx
00311 {
00312     local command="$@"
00313 
00314     local line=
00315     diag "Output of '${command}':"
00316     ${command} | while read line; do
00317         diag "${line}"
00318     done
00319     ok ${PIPESTATUS[0]} "${command}"
00320 }
00321 
00322 # ----------------------------------------------------------------------------
00323 ## @brief Compare actual and expected result.
00324 #
00325 # @param [in] result   Actual result.
00326 # @param [in] expected Expected result.
00327 function _shtap_equals
00328 {
00329     local result=$1
00330     local expected=$2
00331 
00332     if [[ "${result}" == "${expected}" ]] ; then
00333         return 0
00334     else 
00335         return 1
00336     fi
00337 }
00338 
00339 # ----------------------------------------------------------------------------
00340 ## @brief Diagnostic message for is().
00341 #
00342 # @param [in] result   Actual result.
00343 # @param [in] expected Expected result.
00344 function _shtap_is_diag
00345 {
00346     local result=$1
00347     local expected=$2
00348 
00349     diag "         got: '${result}'" 
00350     diag "    expected: '${expected}'"
00351 }
00352 
00353 # ----------------------------------------------------------------------------
00354 ## @brief Test whether a given result is equal to the expected result.
00355 #
00356 # @param [in] result   Actual result.
00357 # @param [in] expected Expected result.
00358 # @param [in] name     Optional name of test.
00359 #
00360 # @returns Whether the results are equal.
00361 #
00362 # @retval 0 On equality.
00363 # @retval 1 Otherwise.
00364 function is
00365 {
00366     local result=$1
00367     local expected=$2
00368     local name=${3:-''}
00369 
00370     _shtap_equals "${result}" "${expected}"
00371     [ $? -eq 0 ]
00372     ok $? "${name}"
00373     local r=$?
00374     [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
00375     return ${r}
00376 }
00377 
00378 # ----------------------------------------------------------------------------
00379 ## @brief Test whether a given result is not equal the expected result.
00380 #
00381 # @param [in] result   Actual result.
00382 # @param [in] expected Expected result.
00383 # @param [in] name     Optional name of test.
00384 #
00385 # @returns Whether the results were not equal.
00386 #
00387 # @retval 0 Otherwise.
00388 # @retval 1 On equality.
00389 function isnt
00390 {
00391     local result=$1
00392     local expected=$2
00393     local name=${3:-''}
00394 
00395     _shtap_equals "${result}" "${expected}"
00396     (( $? != 0 ))
00397     ok $? "${name}"
00398     local r=$?
00399     [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
00400     return ${r} 
00401 }
00402 
00403 # ----------------------------------------------------------------------------
00404 ## @brief Test whether a given result matches an expected pattern.
00405 #
00406 # @param [in] result  Actual result.
00407 # @param [in] pattern Expected pattern.
00408 # @param [in] name    Optional name of test.
00409 #
00410 # @returns Whether the result matched the pattern.
00411 #
00412 # @retval 0 On match.
00413 # @retval 1 Otherwise.
00414 function like
00415 {
00416     local result=$1
00417     local pattern=$2
00418     local name=${3:-''}
00419 
00420     match "${result}" "${pattern}"
00421     [ $? -eq 0 ]
00422     ok $? "${name}"
00423     local r=$?
00424     [ ${r} -ne 0 ] && diag "    '${result}' doesn't match '${pattern}'"
00425     return ${r}
00426 }
00427 
00428 # ----------------------------------------------------------------------------
00429 ## @brief Test whether a given result does not match an expected pattern.
00430 #
00431 # @param [in] result  Actual result.
00432 # @param [in] pattern Expected pattern.
00433 # @param [in] name    Optional name of test.
00434 #
00435 # @returns Whether the result did not match the pattern.
00436 #
00437 # @retval 0 Otherwise.
00438 # @retval 1 On match.
00439 function unlike
00440 {
00441     local result=$1
00442     local pattern=$2
00443     local name=${3:-''}
00444 
00445     match "${result}" "${pattern}"
00446     [ $? -ne 0 ]
00447     ok $? "${name}"
00448     local r=$?
00449     [ ${r} -ne 0 ] && diag "    '${result}' matches '${pattern}'"
00450     return ${r}
00451 }
00452 
00453 # ============================================================================
00454 # skip
00455 # ============================================================================
00456 
00457 # ----------------------------------------------------------------------------
00458 ## @brief Skip tests under a certain condition.
00459 #
00460 # @param [in] condition The condition for skipping the tests.
00461 #                       If 0, the tests are skipped, otherwise not.
00462 # @param [in] reason    An explanation for why skipping the tests.
00463 # @param [in] n         The number of tests which will be skipped.
00464 #
00465 # @returns Whether the tests were skipped.
00466 #
00467 # @retval 0 If tests are to be skipped.
00468 # @retval 1 Otherwise.
00469 function skip
00470 {
00471     local condition=${1:?}
00472     local reason=${2:-''}
00473     local n=${3:-1}
00474 
00475     if [ ${condition} -eq 0 ]; then
00476         local i=
00477         for (( i=0 ; i<$n ; i++ )); do
00478             (( _shtap_executed_tests++ ))
00479             echo "ok ${_shtap_executed_tests} # skip: ${reason}" 
00480         done
00481         return 0
00482     else
00483         return 1
00484     fi
00485 }
00486 
00487 # ============================================================================
00488 # diagnostics
00489 # ============================================================================
00490 
00491 # ----------------------------------------------------------------------------
00492 ## @brief Print diagnostic message.
00493 #
00494 # @param [in] msg Diagnostic message.
00495 #
00496 # @returns Always 1.
00497 function diag
00498 {
00499     local msg=${1:?}
00500 
00501     if [ -n "${msg}" ]; then
00502         echo "# ${msg}"
00503     fi
00504 
00505     return 1
00506 }
00507 
00508 # ============================================================================
00509 # termination
00510 # ============================================================================
00511 
00512 # ----------------------------------------------------------------------------
00513 ## @brief Bail out.
00514 #
00515 # @param [in] reason Reason for bailing out.
00516 #
00517 # @returns Nothing. Instead, exits the process with error code 255.
00518 function SHTAP_BAIL_OUT
00519 {
00520     local reason=${1:-''}
00521 
00522     echo "Bail out! ${reason}" >&2
00523     _shtap_exit 255
00524 }
00525 
00526 # ----------------------------------------------------------------------------
00527 ## @brief Abort test execution.
00528 #
00529 # @param [in] reason Reason for aborting the test execution.
00530 #
00531 # @returns Nothing. Instead, exits the process with error code 255.
00532 function _shtap_die
00533 {
00534     local reason=${1:-'<unspecified error>'}
00535 
00536     echo "${reason}" >&2
00537     _shtap_test_died=1
00538     _shtap_exit 255
00539 }
00540 
00541 # ----------------------------------------------------------------------------
00542 ## @brief Cleaning up after execution of tests and see if plan was fulfilled.
00543 function _shtap_cleanup
00544 {
00545     local rc=0 # return code
00546 
00547     if [ ${_shtap_plan_set} -eq 0 ]; then
00548         diag "Looks like your test died before it could output anything."
00549         return ${rc}
00550     fi
00551 
00552     if [ ${_shtap_test_died} -ne 0 ]; then
00553         diag "Looks like your test died just after ${_shtap_executed_tests}."
00554         return ${rc}
00555     fi
00556 
00557     if [ ${_shtap_skip_all} -eq 0 -a ${_shtap_no_plan} -ne 0 ]; then
00558         _shtap_print_plan ${_shtap_executed_tests}
00559     fi
00560 
00561     local s=
00562     if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
00563         s=''
00564         [ ${_shtap_expected_tests} -gt 1 ] && s='s'
00565         local extra=$(( _shtap_executed_tests - _shtap_expected_tests ))
00566         diag "Looks like you planned ${_shtap_expected_tests} test${s} but ran ${extra} extra."
00567         rc=-1 ;
00568     fi
00569 
00570     if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -gt ${_shtap_executed_tests} ]; then
00571         s=''
00572         [ ${_shtap_expected_tests} -gt 1 ] && s='s'
00573         diag "Looks like you planned ${_shtap_expected_tests} test${s} but only ran ${_shtap_executed_tests}."
00574     fi
00575 
00576     if [ ${_shtap_failed_tests} -gt 0 ]; then
00577         s=''
00578         [ ${_shtap_failed_tests} -gt 1 ] && s='s'
00579         diag "Looks like you failed ${_shtap_failed_tests} test${s} of ${_shtap_executed_tests}."
00580     fi
00581 
00582     return ${rc}
00583 }
00584 
00585 # ----------------------------------------------------------------------------
00586 ## @brief Calculate exit status indicating number of failed or extra tests.
00587 function _shtap_exit_status
00588 {
00589     if [ ${_shtap_no_plan} -ne 0 -o ${_shtap_plan_set} -eq 0 ]; then
00590         return ${_shtap_failed_tests}
00591     fi
00592 
00593     if [ ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
00594         return $(( _shtap_executed_tests - _shtap_expected_tests ))
00595     fi
00596 
00597     return $(( _shtap_failed_tests + ( _shtap_expected_tests - _shtap_executed_tests )))
00598 }
00599 
00600 # ----------------------------------------------------------------------------
00601 ## @brief Terminate execution of tests.
00602 function _shtap_exit
00603 {
00604     local rc=${1:-''}
00605     if [ -z "${rc}" ]; then
00606         _shtap_exit_status
00607         rc=$?
00608     fi
00609 
00610     _shtap_cleanup
00611     local alt_rc=$?
00612     [ ${alt_rc} -ne 0 ] && rc=${alt_rc}
00613     trap - EXIT
00614     exit ${rc}
00615 }