# Copyright (c) 2017 The NetBSD Foundation, Inc. # All rights reserved. # # This code is derived from software contributed to The NetBSD Foundation # by Johnny C. Lam. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # NAME # unittest.subr -- run unit tests and produce TAP output # # SYNOPSIS # task_requires_root # # task_load_test ... # # task_run_tests [-q] [] # # DESCRIPTION # The task_requires_root function checks whether the the user is # "root" and sets the "describe" environment variable to a "skip" # directive. # # The task_load_test function sources the test module from the # ${TASK_TESTS_DIR} directory and passes it all of the remaining # parameters. # # The task_run_tests function runs all functions named "test" in # order, starting with "test1", and it produces output in Test # Anything Protocol (TAP) format. # # The options are as follows: # # -q Quiet; Close the standard output and standard error before # running the test. If the "-q" option is not present, then # the standard output and standard error are left open. # # Only run "test". # # task_run_tests sets the environment variable ${TEST_CURDIR} to the # location where each test function is run. # # A test function must set the "describe" environment variable to # the "TAP test line" description for the test. # # If the test_setup and test_teardown functions are present, then # they are called respectively before and after each test function # is called. # # RETURN VALUES # The task_requires_root function returns 0 if the user is root, # and >0 otherwise. # # The task_load_test function returns 0 on success, and >0 if an # error occurs. # # The task_run_tests function returns the number of tests that failed # when run. # # ENVIRONMENT # The following variables are used if they are set: # # ID The name or path to the id(1) utility. # # RM The name or path to the rm(1) utility. # # TASK_TESTS_DIR # The path to the directory containing the tests. # # EXAMPLE # # # Load unittest functions. # task_load unittest # # # test_setup() is run before each test. # test_setup() { # var="visible in all test functions" # } # # test1() { # describe="guaranteed success" # echo "This is visible in the output unless -q is given." # # copied group file is automatically removed after test is run # cp /etc/group . # return 0 # } # # task_run_tests "$@" # __task_unittest__="yes" task_load taskfunc # task_is_function task_load maketemp task_load say task_requires_root() { : ${ID:=id} if [ -z "$__task_uid__" ]; then __task_uid__=$( ${ID} -u ) fi if [ "$__task_uid__" != 0 ]; then describe="# skip $describe (requires root)" return 1 fi return 0 } task_load_test() { : ${TASK_TESTS_DIR:=.} [ $# -gt 0 ] || return 127 local module="$1"; shift local found= local test_module_path suffix for suffix in "" ".sh"; do test_module_path="${TASK_TESTS_DIR}/$module$suffix" if [ -f "$test_module_path" ]; then found=yes break fi done if [ -n "$found" ]; then unset found module suffix . "$test_module_path" else echo 1>&2 "Error: Loading test $module failed: $test_module_path" return 1 fi } task_run_tests() { # If the first parameter is "-s", then short-circuit return 0. case $1 in -s) return 0 ;; esac _task_tap_test "$@" | task_tap_harness } _task_tap_test() { local run_args= local arg local OPTIND=1 while getopts ":q" arg "$@"; do case $arg in q) run_args="$run_args -$arg" ;; *) return 127 ;; esac done shift $(( ${OPTIND} - 1 )) # Remaining arguments are tests to run. local setup_fn="test_setup" local teardown_fn="test_teardown" task_is_function "$setup_fn" || setup_fn=":" task_is_function "$teardown_fn" || teardown_fn=":" local failures=0 if [ $# -gt 0 ]; then # Explicit list of tests to run. local max=0 local number test_fn for number; do test_fn="test$number" task_is_function "$test_fn" || break max=$(( $max + 1 )) done # Write the TAP plan before the test output. echo "1..$max" # Run the tests. for number; do _task_tap_line $run_args $number $setup_fn $teardown_fn || failures=$(( $failures + 1 )) done else local max=0 local number=1 local test_fn while : ; do test_fn="test$number" task_is_function "$test_fn" || break max=$number number=$(( $number + 1 )) done # Write the TAP plan before the test output. echo "1..$max" number=1 run_args="$run_args -n" # Run the tests. while [ $number -le $max ]; do _task_tap_line $run_args $number $setup_fn $teardown_fn || failures=$(( $failures + 1 )) number=$(( $number + 1 )) done fi return $failures } _task_tap_line() { : ${RM:=rm} local quiet= local write_number= local arg local OPTIND=1 while getopts ":nq" arg "$@"; do case $arg in n) write_number="yes" ;; q) quiet="yes" ;; *) return 127 ;; esac done shift $(( ${OPTIND} - 1 )) [ $# -eq 3 ] || return 127 local number="$1" local setup_fn="$2" local teardown_fn="$3" shift 3 local test_fn="test$number" local TEST_CURDIR=$( task_maketemp -d -t "pkgtasks.$test_fn" ) ( # Redirect fd 6 to standard output. exec 6>&1 # Close standard output and standard error if quiet. if [ -n "$quiet" ]; then exec 1>&- exec 2>&- fi # setup > test > teardown local describe= cd "${TEST_CURDIR}" && eval $setup_fn && eval $test_fn local test_result=$? eval $teardown_fn local testline # ok or noe ok. case $test_result in 0) testline="ok" ;; *) testline="not ok" esac # Append test number. case $write_number in "") : "don't append number" ;; *) testline="$testline $number" ;; esac # Put a dash "-" before $describe if it's missing. case $describe in "") : "testline is unchanged" ;; "- "*|"#"*) testline="$testline $describe" ;; *) testline="$testline - $describe" ;; esac # Write the proper TAP test line. echo 1>&6 "$testline" return $test_result ) local result=$? ${RM} -fr "${TEST_CURDIR}" return $result } task_tap_harness() { local ws="[ ]" local count=0 local fail=0 local pass=0 local broken=0 local fixed=0 local skip=0 local status= local msgtype="none" local line while IFS= read line; do case $line in 1..[0-9]*) # TAP plan status="plan" count=${line#1..} msgtype="info" ;; "not ok"|"not ok"$ws*) # failed test status="not_ok" fail=$(( $fail + 1 )) msgtype="error" ;; ok|"ok"$ws*) # passed test status="ok" pass=$(( $pass + 1 )) msgtype="none" ;; esac case $status in ok|not_ok) case $line in *$ws"#"$ws[Ss][Kk][Ii][Pp]|\ *$ws"#"$ws[Ss][Kk][Ii][Pp]$ws*) # skip directive; consider as passed pass=$(( $pass + 1 )) skip=$(( $skip + 1 )) msgtype="skip" ;; *$ws"#"$ws[Tt][Oo][Dd][Oo]|\ *$ws"#"$ws[Tt][Oo][Dd][Oo]$ws*) # todo directive case $status in ok) fixed=$(( $fixed + 1 )) ;; not_ok) broken=$(( $broken + 1 )) fail=$(( fail - 1 )) ;; esac msgtype="warn" ;; esac esac task_say $msgtype "$line" done # summary if [ $skip -gt 0 ]; then task_say skip "# skipped $skip test(s)" fi if [ $fixed -gt 0 ]; then task_say error \ "# $fixed known breakage(s) vanished; please update test(s)" fi if [ $broken -gt 0 ]; then task_say warn "# still have $broken known breakage(s)" fi local msg if [ $broken -eq 0 -a $fixed -eq 0 ]; then msg="$count test(s)" else count=$(( $count - $broken - $fixed )) msg="remaining $count test(s)" fi if [ $fail -gt 0 ]; then task_say error "# failed $fail among $msg" else task_say pass "# passed all $msg" fi # return the number of failed tests return $fail } # Static variable for the user ID of the user executing the test. __task_uid__=