Blob Blame History Raw
#!/bin/bash
#
# Copyright 2010 Patrick LeBoutillier <patrick.leboutillier@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.


_version='1.01'

_plan_set=0
_no_plan=0
_skip_all=0
_test_died=0
_expected_tests=0
_executed_tests=0
_failed_tests=0
TODO=


usage(){
    cat <<'USAGE'
tap-functions: A TAP-producing BASH library

PLAN:
  plan_no_plan
  plan_skip_all [REASON]
  plan_tests NB_TESTS

TEST:
  ok RESULT [NAME]
  okx COMMAND
  is RESULT EXPECTED [NAME]
  isnt RESULT EXPECTED [NAME]
  like RESULT PATTERN [NAME]
  unlike RESULT PATTERN [NAME]
  pass [NAME]
  fail [NAME]

SKIP:
  skip [CONDITION] [REASON] [NB_TESTS=1]

  skip $feature_not_present "feature not present" 2 || {
      is $a "a"
      is $b "b"
  }

TODO:
  Specify TODO mode by setting $TODO:
    TODO="not implemented yet"
    ok $result "some not implemented test"
    unset TODO

OTHER:
  diag MSG

EXAMPLE:
  #!/bin/bash

  . tap-functions

  plan_tests 7

  me=$USER
  is $USER $me "I am myself"
  like $HOME $me "My home is mine"
  like "`id`" $me "My id matches myself"

  /bin/ls $HOME 1>&2
  ok $? "/bin/ls $HOME"
  # Same thing using okx shortcut
  okx /bin/ls $HOME

  [[ "`id -u`" != "0" ]]
  i_am_not_root=$?
  skip $i_am_not_root "Must be root" || {
    okx ls /root
  }

  TODO="figure out how to become root..."
  okx [ "$HOME" == "/root" ]
  unset TODO
USAGE
    exit
}

opt=
set_u=
while getopts ":sx" opt ; do
    case $_opt in
        u) set_u=1 ;;
        *) usage ;;
    esac
done
shift $(( OPTIND - 1 ))
# Don't allow uninitialized variables if requested
[[ -n "$set_u" ]] && set -u
unset opt set_u

# Used to call _cleanup on shell exit
trap _exit EXIT


plan_no_plan(){
    (( _plan_set != 0 )) && "You tried to plan twice!"

    _plan_set=1
    _no_plan=1

    return 0
}


plan_skip_all(){
    local reason=${1:-''}

    (( _plan_set != 0 )) && _die "You tried to plan twice!"

    _print_plan 0 "Skip $reason"

    _skip_all=1
    _plan_set=1
    _exit 0

    return 0
}

plan_tests(){
    local tests=${1:?}

    (( _plan_set != 0 )) && _die "You tried to plan twice!"
    (( tests == 0 )) && _die "You said to run 0 tests!  You've got to run something."

    _print_plan $tests
    _expected_tests=$tests
    _plan_set=1

    return $tests
}


_print_plan(){
    local tests=${1:?}
    local directive=${2:-''}

    echo -n "1..$tests"
    [[ -n "$directive" ]] && echo -n " # $directive"
    echo
}


pass(){
    local name=$1
    ok 0 "$name"
}


fail(){
    local name=$1
    ok 1 "$name"
}

# This is the workhorse method that actually
# prints the tests result.
ok(){
    local result=${1:?}
    local name=${2:-''}

    (( _plan_set == 0 )) && _die "You tried to run a test without a plan!  Gotta have a plan."

    _executed_tests=$(( $_executed_tests + 1 ))

    if [[ -n "$name" ]] ; then
        if _matches "$name" "^[0-9]+$" ; then
            diag "    You named your test '$name'.  You shouldn't use numbers for your test names."
            diag "    Very confusing."
        fi
    fi

    if (( result != 0 )) ; then
        echo -n "not "
        _failed_tests=$(( _failed_tests + 1 ))
    fi
    echo -n "ok $_executed_tests"

    if [[ -n "$name" ]] ; then
        local ename=${name//\#/\\#}
        echo -n " - $ename"
    fi

    if [[ -n "$TODO" ]] ; then
        echo -n " # TODO $TODO" ;
        if (( result != 0 )) ; then
            _failed_tests=$(( _failed_tests - 1 ))
        fi
    fi

    echo
    if (( result != 0 )) ; then
        local file='tap-functions'
        local func=
        local line=

        local i=0
        local bt=$(caller $i)
        while _matches "$bt" "tap-functions$" ; do
            i=$(( $i + 1 ))
            bt=$(caller $i)
        done
        local backtrace=
        eval $(caller $i | (read line func file ; echo "backtrace=\"$file:$func() at line $line.\""))

        local t=
        [[ -n "$TODO" ]] && t="(TODO) "

        if [[ -n "$name" ]] ; then
            diag "  Failed ${t}test '$name'"
            diag "  in $backtrace"
        else
            diag "  Failed ${t}test in $backtrace"
        fi
    fi

    return $result
}


okx(){
    local command="$@"

    local line=
    diag "Output of '$command':"
    "$@" | while read line ; do
        diag "$line"
    done
    ok ${PIPESTATUS[0]} "$command"
}


_equals(){
    local result=${1:?}
    local expected=${2:?}

    if [[ "$result" == "$expected" ]] ; then
        return 0
    else
        return 1
    fi
}


# Thanks to Aaron Kangas for the patch to allow regexp matching
# under bash < 3.
 _bash_major_version=${BASH_VERSION%%.*}
_matches(){
    local result=${1:?}
    local pattern=${2:?}

    if [[ -z "$result" || -z "$pattern" ]] ; then
        return 1
    else
        if (( _bash_major_version >= 3 )) ; then
            [[ "$result" =~ "$pattern" ]]
        else
            echo "$result" | egrep -q "$pattern"
        fi
    fi
}


_is_diag(){
    local result=${1:?}
    local expected=${2:?}

    diag "         got: '$result'"
    diag "    expected: '$expected'"
}


is(){
    local result=${1:?}
    local expected=${2:?}
    local name=${3:-''}

    _equals "$result" "$expected"
    (( $? == 0 ))
    ok $? "$name"
    local r=$?
    (( r != 0 )) && _is_diag "$result" "$expected"
    return $r
}


isnt(){
    local result=${1:?}
    local expected=${2:?}
    local name=${3:-''}

    _equals "$result" "$expected"
    (( $? != 0 ))
    ok $? "$name"
    local r=$?
    (( r != 0 )) && _is_diag "$result" "$expected"
    return $r
}


like(){
    local result=${1:?}
    local pattern=${2:?}
    local name=${3:-''}

    _matches "$result" "$pattern"
    (( $? == 0 ))
    ok $? "$name"
    local r=$?
    (( r != 0 )) && diag "    '$result' doesn't match '$pattern'"
    return $r
}


unlike(){
    local result=${1:?}
    local pattern=${2:?}
    local name=${3:-''}

    _matches "$result" "$pattern"
    (( $? != 0 ))
    ok $? "$name"
    local r=$?
    (( r != 0 )) && diag "    '$result' matches '$pattern'"
    return $r
}


skip(){
    local condition=${1:?}
    local reason=${2:-''}
    local n=${3:-1}

    if (( condition == 0 )) ; then
        local i=
        for (( i=0 ; i<$n ; i++ )) ; do
            _executed_tests=$(( _executed_tests + 1 ))
            echo "ok $_executed_tests # skip: $reason"
        done
        return 0
    else
        return
    fi
}


diag(){
    local msg=${1:?}

    if [[ -n "$msg" ]] ; then
        echo "# $msg"
    fi

    return 1
}


_die(){
    local reason=${1:-'<unspecified error>'}

    echo "$reason" >&2
    _test_died=1
    _exit 255
}


BAIL_OUT(){
    local reason=${1:-''}

    echo "Bail out! $reason" >&2
    _exit 255
}


_cleanup(){
    local rc=0

    if (( _plan_set == 0 )) ; then
        diag "Looks like your test died before it could output anything."
        return $rc
    fi

    if (( _test_died != 0 )) ; then
        diag "Looks like your test died just after $_executed_tests."
        return $rc
    fi

    if (( _skip_all == 0 && _no_plan != 0 )) ; then
        _print_plan $_executed_tests
    fi

    local s=
    if (( _no_plan == 0 && _expected_tests < _executed_tests )) ; then
        s= ; (( _expected_tests > 1 )) && s=s
        local extra=$(( _executed_tests - _expected_tests ))
        diag "Looks like you planned $_expected_tests test$s but ran $extra extra."
        rc=1 ;
    fi

    if (( _no_plan == 0 && _expected_tests > _executed_tests )) ; then
        s= ; (( _expected_tests > 1 )) && s=s
        diag "Looks like you planned $_expected_tests test$s but only ran $_executed_tests."
    fi

    if (( _failed_tests > 0 )) ; then
        s= ; (( _failed_tests > 1 )) && s=s
        diag "Looks like you failed $_failed_tests test$s of $_executed_tests."
    fi

    return $rc
}


_exit_status(){
    if (( _no_plan != 0 || _plan_set == 0 )) ; then
        return $_failed_tests
    fi

    if (( _expected_tests < _executed_tests )) ; then
        return $(( _executed_tests - _expected_tests  ))
    fi

    return $(( _failed_tests + ( _expected_tests - _executed_tests )))
}


_exit(){
    local rc=${1:-''}
    if [[ -z "$rc" ]] ; then
        _exit_status
        rc=$?
    fi

    _cleanup
    local alt_rc=$?
    (( alt_rc != 0 )) && rc=$alt_rc
    trap - EXIT
    exit $rc
}