1#
2# Automated Testing Framework (atf)
3#
4# Copyright (c) 2007 The NetBSD Foundation, Inc.
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
17# CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
18# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20# IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
21# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
25# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28#
29
30set -e
31
32# ------------------------------------------------------------------------
33# GLOBAL VARIABLES
34# ------------------------------------------------------------------------
35
36# Values for the expect property.
37Expect=pass
38Expect_Reason=
39
40# A boolean variable that indicates whether we are parsing a test case's
41# head or not.
42Parsing_Head=false
43
44# The program name.
45Prog_Name=${0##*/}
46
47# The file to which the test case will print its result.
48Results_File=
49
50# The test program's source directory: i.e. where its auxiliary data files
51# and helper utilities can be found.  Can be overriden through the '-s' flag.
52Source_Dir="$(dirname ${0})"
53
54# Indicates the test case we are currently processing.
55Test_Case=
56
57# List of meta-data variables for the current test case.
58Test_Case_Vars=
59
60# The list of all test cases provided by the test program.
61Test_Cases=
62
63# ------------------------------------------------------------------------
64# PUBLIC INTERFACE
65# ------------------------------------------------------------------------
66
67#
68# atf_add_test_case tc-name
69#
70#   Adds the given test case to the list of test cases that form the test
71#   program.  The name provided here must be accompanied by two functions
72#   named after it: <tc-name>_head and <tc-name>_body, and optionally by
73#   a <tc-name>_cleanup function.
74#
75atf_add_test_case()
76{
77    Test_Cases="${Test_Cases} ${1}"
78}
79
80#
81# atf_check cmd expcode expout experr
82#
83#   Executes atf-check with given arguments and automatically calls
84#   atf_fail in case of failure.
85#
86atf_check()
87{
88    ${Atf_Check} "${@}" || \
89        atf_fail "atf-check failed; see the output of the test for details"
90}
91
92#
93# atf_check_equal expr1 expr2
94#
95#   Checks that expr1's value matches expr2's and, if not, raises an
96#   error.  Ideally expr1 and expr2 should be provided quoted (not
97#   expanded) so that the error message is helpful; otherwise it will
98#   only show the values, not the expressions themselves.
99#
100atf_check_equal()
101{
102    eval _val1=\"${1}\"
103    eval _val2=\"${2}\"
104    test "${_val1}" = "${_val2}" || \
105        atf_fail "${1} != ${2} (${_val1} != ${_val2})"
106}
107
108#
109# atf_config_get varname [defvalue]
110#
111#   Prints the value of a configuration variable.  If it is not
112#   defined, prints the given default value.
113#
114atf_config_get()
115{
116    _varname="__tc_config_var_$(_atf_normalize ${1})"
117    if [ ${#} -eq 1 ]; then
118        eval _value=\"\${${_varname}-__unset__}\"
119        [ "${_value}" = __unset__ ] && \
120            _atf_error 1 "Could not find configuration variable \`${1}'"
121        echo ${_value}
122    elif [ ${#} -eq 2 ]; then
123        eval echo \${${_varname}-${2}}
124    else
125        _atf_error 1 "Incorrect number of parameters for atf_config_get"
126    fi
127}
128
129#
130# atf_config_has varname
131#
132#   Returns a boolean indicating if the given configuration variable is
133#   defined or not.
134#
135atf_config_has()
136{
137    _varname="__tc_config_var_$(_atf_normalize ${1})"
138    eval _value=\"\${${_varname}-__unset__}\"
139    [ "${_value}" != __unset__ ]
140}
141
142#
143# atf_expect_death reason
144#
145#   Sets the expectations to 'death'.
146#
147atf_expect_death()
148{
149    _atf_validate_expect
150
151    Expect=death
152    _atf_create_resfile "expected_death: ${*}"
153}
154
155#
156# atf_expect_timeout reason
157#
158#   Sets the expectations to 'timeout'.
159#
160atf_expect_timeout()
161{
162    _atf_validate_expect
163
164    Expect=timeout
165    _atf_create_resfile "expected_timeout: ${*}"
166}
167
168#
169# atf_expect_exit exitcode reason
170#
171#   Sets the expectations to 'exit'.
172#
173atf_expect_exit()
174{
175    _exitcode="${1}"; shift
176
177    _atf_validate_expect
178
179    Expect=exit
180    if [ "${_exitcode}" = "-1" ]; then
181        _atf_create_resfile "expected_exit: ${*}"
182    else
183        _atf_create_resfile "expected_exit(${_exitcode}): ${*}"
184    fi
185}
186
187#
188# atf_expect_fail reason
189#
190#   Sets the expectations to 'fail'.
191#
192atf_expect_fail()
193{
194    _atf_validate_expect
195
196    Expect=fail
197    Expect_Reason="${*}"
198}
199
200#
201# atf_expect_pass
202#
203#   Sets the expectations to 'pass'.
204#
205atf_expect_pass()
206{
207    _atf_validate_expect
208
209    Expect=pass
210    Expect_Reason=
211}
212
213#
214# atf_expect_signal signo reason
215#
216#   Sets the expectations to 'signal'.
217#
218atf_expect_signal()
219{
220    _signo="${1}"; shift
221
222    _atf_validate_expect
223
224    Expect=signal
225    if [ "${_signo}" = "-1" ]; then
226        _atf_create_resfile "expected_signal: ${*}"
227    else
228        _atf_create_resfile "expected_signal(${_signo}): ${*}"
229    fi
230}
231
232#
233# atf_expected_failure msg1 [.. msgN]
234#
235#   Makes the test case report an expected failure with the given error
236#   message.  Multiple words can be provided, which are concatenated with
237#   a single blank space.
238#
239atf_expected_failure()
240{
241    _atf_create_resfile "expected_failure: ${Expect_Reason}: ${*}"
242    exit 0
243}
244
245#
246# atf_fail msg1 [.. msgN]
247#
248#   Makes the test case fail with the given error message.  Multiple
249#   words can be provided, in which case they are joined by a single
250#   blank space.
251#
252atf_fail()
253{
254    case "${Expect}" in
255        fail)
256            atf_expected_failure "${@}"
257            ;;
258        pass)
259            _atf_create_resfile "failed: ${*}"
260            exit 1
261            ;;
262        *)
263            _atf_error 128 "Unreachable"
264            ;;
265    esac
266}
267
268#
269# atf_get varname
270#
271#   Prints the value of a test case-specific variable.  Given that one
272#   should not get the value of non-existent variables, it is fine to
273#   always use this function as 'val=$(atf_get var)'.
274#
275atf_get()
276{
277    eval echo \${__tc_var_${Test_Case}_$(_atf_normalize ${1})}
278}
279
280#
281# atf_get_srcdir
282#
283#   Prints the value of the test case's source directory.
284#
285atf_get_srcdir()
286{
287    echo ${Source_Dir}
288}
289
290#
291# atf_pass
292#
293#   Makes the test case pass.  Shouldn't be used in general, as a test
294#   case that does not explicitly fail is assumed to pass.
295#
296atf_pass()
297{
298    case "${Expect}" in
299        fail)
300            Expect=pass
301            atf_fail "Test case was expecting a failure but got a pass instead"
302            ;;
303        pass)
304            _atf_create_resfile passed
305            exit 0
306            ;;
307        *)
308            _atf_error 128 "Unreachable"
309            ;;
310    esac
311}
312
313#
314# atf_require_prog prog
315#
316#   Checks that the given program name (either provided as an absolute
317#   path or as a plain file name) can be found.  If it is not available,
318#   automatically skips the test case with an appropriate message.
319#
320#   Relative paths are not allowed because the test case cannot predict
321#   where it will be executed from.
322#
323atf_require_prog()
324{
325    _prog=
326    case ${1} in
327    /*)
328        _prog="${1}"
329        [ -x ${_prog} ] || \
330            atf_skip "The required program ${1} could not be found"
331        ;;
332    */*)
333        atf_fail "atf_require_prog does not accept relative path names \`${1}'"
334        ;;
335    *)
336        _prog=$(_atf_find_in_path "${1}")
337        [ -n "${_prog}" ] || \
338            atf_skip "The required program ${1} could not be found" \
339                     "in the PATH"
340        ;;
341    esac
342}
343
344#
345# atf_set varname val1 [.. valN]
346#
347#   Sets the test case's variable 'varname' to the specified values
348#   which are concatenated using a single blank space.  This function
349#   is supposed to be called form the test case's head only.
350#
351atf_set()
352{
353    ${Parsing_Head} || \
354        _atf_error 128 "atf_set called from the test case's body"
355
356    Test_Case_Vars="${Test_Case_Vars} ${1}"
357    _var=$(_atf_normalize ${1}); shift
358    eval __tc_var_${Test_Case}_${_var}=\"\${*}\"
359}
360
361#
362# atf_skip msg1 [.. msgN]
363#
364#   Skips the test case because of the reason provided.  Multiple words
365#   can be given, in which case they are joined by a single blank space.
366#
367atf_skip()
368{
369    _atf_create_resfile "skipped: ${*}"
370    exit 0
371}
372
373#
374# atf_test_case tc-name cleanup
375#
376#   Defines a new test case named tc-name.  The name provided here must be
377#   accompanied by two functions named after it: <tc-name>_head and
378#   <tc-name>_body.  If cleanup is set to 'cleanup', then this also expects
379#   a <tc-name>_cleanup function to be defined.
380#
381atf_test_case()
382{
383    eval "${1}_head() { :; }"
384    eval "${1}_body() { atf_fail 'Test case not implemented'; }"
385    if [ "${2}" = cleanup ]; then
386        eval __has_cleanup_${1}=true
387        eval "${1}_cleanup() { :; }"
388    else
389        eval "${1}_cleanup() {
390            _atf_error 1 'Test case ${1} declared without a cleanup routine'; }"
391    fi
392}
393
394# ------------------------------------------------------------------------
395# PRIVATE INTERFACE
396# ------------------------------------------------------------------------
397
398#
399# _atf_config_set varname val1 [.. valN]
400#
401#   Sets the test case's private variable 'varname' to the specified
402#   values which are concatenated using a single blank space.
403#
404_atf_config_set()
405{
406    _var=$(_atf_normalize ${1}); shift
407    eval __tc_config_var_${_var}=\"\${*}\"
408    Config_Vars="${Config_Vars} __tc_config_var_${_var}"
409}
410
411#
412# _atf_config_set_str varname=val
413#
414#   Sets the test case's private variable 'varname' to the specified
415#   value.  The parameter is of the form 'varname=val'.
416#
417_atf_config_set_from_str()
418{
419    _oldifs=${IFS}
420    IFS='='
421    set -- ${*}
422    _var=${1}
423    shift
424    _val="${@}"
425    IFS=${_oldifs}
426    _atf_config_set "${_var}" "${_val}"
427}
428
429#
430# _atf_create_resfile contents
431#
432#   Creates the results file.
433#
434_atf_create_resfile()
435{
436    if [ -n "${Results_File}" ]; then
437        echo "${*}" >"${Results_File}" || \
438            _atf_error 128 "Cannot create results file '${Results_File}'"
439    else
440        echo "${*}"
441    fi
442}
443
444#
445# _atf_error error_code [msg1 [.. msgN]]
446#
447#   Prints the given error message (which can be composed of multiple
448#   arguments, in which case are joined by a single space) and exits
449#   with the specified error code.
450#
451#   This must not be used by test programs themselves (hence making
452#   the function private) to indicate a test case's failure.  They
453#   have to use the atf_fail function.
454#
455_atf_error()
456{
457    _error_code="${1}"; shift
458
459    echo "${Prog_Name}: ERROR:" "$@" 1>&2
460    exit ${_error_code}
461}
462
463#
464# _atf_warning msg1 [.. msgN]
465#
466#   Prints the given warning message (which can be composed of multiple
467#   arguments, in which case are joined by a single space).
468#
469_atf_warning()
470{
471    echo "${Prog_Name}: WARNING:" "$@" 1>&2
472}
473
474#
475# _atf_find_in_path program
476#
477#   Looks for a program in the path and prints the full path to it or
478#   nothing if it could not be found.  It also returns true in case of
479#   success.
480#
481_atf_find_in_path()
482{
483    _prog="${1}"
484
485    _oldifs=${IFS}
486    IFS=:
487    for _dir in ${PATH}
488    do
489        if [ -x ${_dir}/${_prog} ]; then
490            IFS=${_oldifs}
491            echo ${_dir}/${_prog}
492            return 0
493        fi
494    done
495    IFS=${_oldifs}
496
497    return 1
498}
499
500#
501# _atf_has_tc name
502#
503#   Returns true if the given test case exists.
504#
505_atf_has_tc()
506{
507    for _tc in ${Test_Cases}; do
508        [ "${_tc}" != "${1}" ] || return 0
509    done
510    return 1
511}
512
513#
514# _atf_list_tcs
515#
516#   Describes all test cases and prints the list to the standard output.
517#
518_atf_list_tcs()
519{
520    echo 'Content-Type: application/X-atf-tp; version="1"'
521    echo
522
523    set -- ${Test_Cases}
524    while [ ${#} -gt 0 ]; do
525        _atf_parse_head ${1}
526
527        echo "ident: $(atf_get ident)"
528        for _var in ${Test_Case_Vars}; do
529	    # Elide ksh bug!
530	    set +e
531            [ "${_var}" != "ident" ] && echo "${_var}: $(atf_get ${_var})"
532	    set -e
533        done
534
535        [ ${#} -gt 1 ] && echo
536        shift
537    done
538}
539
540#
541# _atf_normalize str
542#
543#   Normalizes a string so that it is a valid shell variable name.
544#
545_atf_normalize()
546{
547    while :
548    do
549	case "${1}" in
550	(*.*)	set -- "${1%.*}_${1##*.}";;
551	(*-*)	set -- "${1%-*}_${1##*-}";;
552	(*)	break;;
553	esac
554    done
555    printf "%s\n" "$1"
556}
557
558#
559# _atf_parse_head tcname
560#
561#   Evaluates a test case's head to gather its variables and prepares the
562#   test program to run it.
563#
564_atf_parse_head()
565{
566    Parsing_Head=true
567
568    Test_Case="${1}"
569    Test_Case_Vars=
570
571    if _atf_has_cleanup "${1}"; then
572        atf_set has.cleanup "true"
573    fi
574
575    ${1}_head
576    atf_set ident "${1}"
577
578    Parsing_Head=false
579}
580
581#
582# _atf_run_tc tc
583#
584#   Runs the specified test case.  Prints its exit status to the
585#   standard output and returns a boolean indicating if the test was
586#   successful or not.
587#
588_atf_run_tc()
589{
590    case ${1} in
591    *:*)
592        _tcname=${1%%:*}
593        _tcpart=${1#*:}
594
595        if [ "${_tcpart}" != body -a "${_tcpart}" != cleanup ]; then
596            _atf_syntax_error "Unknown test case part \`${_tcpart}'"
597        fi
598        ;;
599
600    *)
601        _tcname=${1}
602        _tcpart=body
603        ;;
604    esac
605
606    _atf_has_tc "${_tcname}" || _atf_syntax_error "Unknown test case \`${1}'"
607
608    if [ "${__RUNNING_INSIDE_ATF_RUN}" != "internal-yes-value" ]; then
609        _atf_warning "Running test cases without atf-run(1) is unsupported"
610        _atf_warning "No isolation nor timeout control is being applied;" \
611            "you may get unexpected failures; see atf-test-case(4)"
612    fi
613
614    _atf_parse_head ${_tcname}
615
616    case ${_tcpart} in
617    body)
618        if ${_tcname}_body; then
619            _atf_validate_expect
620            _atf_create_resfile passed
621        else
622            Expect=pass
623            atf_fail "Test case body returned a non-ok exit code, but" \
624                "this is not allowed"
625        fi
626        ;;
627    cleanup)
628        if _atf_has_cleanup "${_tcname}"; then
629            ${_tcname}_cleanup || _atf_error 128 "The test case cleanup" \
630                "returned a non-ok exit code, but this is not allowed"
631        fi
632        ;;
633    *)
634        _atf_error 128 "Unknown test case part"
635        ;;
636    esac
637}
638
639#
640# _atf_syntax_error msg1 [.. msgN]
641#
642#   Formats and prints a syntax error message and terminates the
643#   program prematurely.
644#
645_atf_syntax_error()
646{
647    echo "${Prog_Name}: ERROR: ${@}" 1>&2
648    echo "${Prog_Name}: See atf-test-program(1) for usage details." 1>&2
649    exit 1
650}
651
652#
653# _atf_has_cleanup tc-name
654#
655#   Returns a boolean indicating if the given test case has a cleanup
656#   routine or not.
657#
658_atf_has_cleanup()
659{
660    _found=true
661    eval "[ x\"\${__has_cleanup_${1}}\" = xtrue ] || _found=false"
662    [ "${_found}" = true ]
663}
664
665#
666# _atf_validate_expect
667#
668#   Ensures that the current test case state is correct regarding the expect
669#   status.
670#
671_atf_validate_expect()
672{
673    case "${Expect}" in
674        death)
675            Expect=pass
676            atf_fail "Test case was expected to terminate abruptly but it" \
677                "continued execution"
678            ;;
679        exit)
680            Expect=pass
681            atf_fail "Test case was expected to exit cleanly but it continued" \
682                "execution"
683            ;;
684        fail)
685            Expect=pass
686            atf_fail "Test case was expecting a failure but none were raised"
687            ;;
688        pass)
689            ;;
690        signal)
691            Expect=pass
692            atf_fail "Test case was expected to receive a termination signal" \
693                "but it continued execution"
694            ;;
695        timeout)
696            Expect=pass
697            atf_fail "Test case was expected to hang but it continued execution"
698            ;;
699        *)
700            _atf_error 128 "Unreachable"
701            ;;
702    esac
703}
704
705#
706# _atf_warning [msg1 [.. msgN]]
707#
708#   Prints the given warning message (which can be composed of multiple
709#   arguments, in which case are joined by a single space).
710#
711#   This must not be used by test programs themselves (hence making
712#   the function private).
713#
714_atf_warning()
715{
716    echo "${Prog_Name}: WARNING:" "$@" 1>&2
717}
718
719#
720# main [options] test_case
721#
722#   Test program's entry point.
723#
724main()
725{
726    # Process command-line options first.
727    _numargs=${#}
728    _lflag=false
729    while getopts :lr:s:v: arg; do
730        case ${arg} in
731        l)
732            _lflag=true
733            ;;
734
735        r)
736            Results_File=${OPTARG}
737            ;;
738
739        s)
740            Source_Dir=${OPTARG}
741            ;;
742
743        v)
744            _atf_config_set_from_str "${OPTARG}"
745            ;;
746
747        \?)
748            _atf_syntax_error "Unknown option -${OPTARG}."
749            # NOTREACHED
750            ;;
751        esac
752    done
753    shift `expr ${OPTIND} - 1`
754
755    # First of all, make sure that the source directory is correct.  It
756    # doesn't matter if the user did not change it, because the default
757    # value may not work.  (TODO: It possibly should, even though it is
758    # not a big deal because atf-run deals with this.)
759    case ${Source_Dir} in
760        /*)
761            ;;
762        *)
763            Source_Dir=$(pwd)/${Source_Dir}
764            ;;
765    esac
766    [ -f ${Source_Dir}/${Prog_Name} ] || \
767        _atf_error 1 "Cannot find the test program in the source" \
768                     "directory \`${Source_Dir}'"
769
770    # Call the test program's hook to register all available test cases.
771    atf_init_test_cases
772
773    # Run or list test cases.
774    if `${_lflag}`; then
775        if [ ${#} -gt 0 ]; then
776            _atf_syntax_error "Cannot provide test case names with -l"
777        fi
778        _atf_list_tcs
779    else
780        if [ ${#} -eq 0 ]; then
781            _atf_syntax_error "Must provide a test case name"
782        elif [ ${#} -gt 1 ]; then
783            _atf_syntax_error "Cannot provide more than one test case name"
784        else
785            _atf_run_tc "${1}"
786        fi
787    fi
788}
789
790# vim: syntax=sh:expandtab:shiftwidth=4:softtabstop=4
791