shtap.sh
Go to the documentation of this file.
1 # ============================================================================
2 # Copyright (c) Patrick LeBoutillier
3 # Copyright (c) 2011 University of Pennsylvania
4 # Copyright (c) 2011-2014 Andreas Schuh
5 # All rights reserved.
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # @sa http://testanything.org/wiki/index.php/Tap-functions
18 # ============================================================================
19 
20 ##############################################################################
21 # @file shtap.sh
22 # @brief Unit testing framework for BASH based on the Test Anything Protocol.
23 #
24 # @author Patrick LeBoutillier <patl at cpan.org>
25 #
26 # @note This file is a copy of the tap-functions file which is part of the
27 # JTap project (http://svn.solucorp.qc.ca/repos/solucorp/JTap/trunk/).
28 # The original implementation has been modified by Andreas Schuh as
29 # part of the BASIS project at SBIA.
30 #
31 # Plan:
32 # @code
33 # plan_no_plan
34 # plan_skip_all [REASON]
35 # plan_tests NB_TESTS
36 # @endcode
37 #
38 # Test:
39 # @code
40 # ok RESULT [NAME]
41 # okx COMMAND
42 # is RESULT EXPECTED [NAME]
43 # isnt RESULT EXPECTED [NAME]
44 # like RESULT PATTERN [NAME]
45 # unlike RESULT PATTERN [NAME]
46 # pass [NAME]
47 # fail [NAME]
48 # @endcode
49 #
50 # Skip:
51 # @code
52 # skip [CONDITION] [REASON] [NB_TESTS=1]
53 #
54 # skip $feature_not_present "feature not present" 2 ||
55 # {
56 # is $a "a"
57 # is $b "b"
58 # }
59 # @endcode
60 #
61 # Specify TODO mode by setting the TODO variable:
62 # @code
63 # TODO="not implemented yet"
64 # ok $result "some not implemented test"
65 # unset TODO
66 # @endcode
67 #
68 # Other:
69 # @code
70 # diag MSG
71 # @endcode
72 #
73 # Example:
74 # @code
75 # #! /usr/bin/env bash
76 #
77 # source shtap.sh
78 #
79 # plan_tests 7
80 #
81 # # test identity
82 # {
83 # me=${USER}
84 # is ${USER} ${me} "I am myself"
85 # like ${HOME} ${me} "My home is mine"
86 # like "`id`" ${me} "My id matches myself"
87 # }
88 #
89 # # test ls
90 # {
91 # ls ${HOME} 1>&2
92 # ok $? "ls ${HOME}"
93 # # same thing using okx shortcut
94 # okx ls ${HOME}
95 # }
96 #
97 # # test only for root
98 # {
99 # [[ "`id -u`" != "0" ]]
100 # i_am_not_root=$?
101 # skip ${i_am_not_root} "Must be root" ||
102 # {
103 # okx ls /root
104 # }
105 # }
106 #
107 # # test TODO
108 # {
109 # TODO="figure out how to become root..."
110 # okx [ "$HOME" == "/root" ]
111 # unset TODO
112 # }
113 # @endcode
114 ##############################################################################
115 
116 # return if already loaded
117 [ -n "${SHTAP_VERSION:-}" ] && return 0
118 readonly SHTAP_VERSION='1.02-basis'
119 
120 
121 . "`cd -P -- \`dirname -- "${BASH_SOURCE}"\` && pwd`/core.sh" || exit 1 # match()
122 
123 readonly _SHTAP_FILENAME=''
124 
125 TODO=
126 
127 _shtap_plan_set=0
128 _shtap_no_plan=0
129 _shtap_skip_all=0
130 _shtap_test_died=0
131 _shtap_expected_tests=0
132 _shtap_executed_tests=0
133 _shtap_failed_tests=0
134 
135 # used to call _cleanup() on shell exit
136 trap _shtap_exit EXIT
137 trap _shtap_exit INT
138 trap _shtap_exit TERM
139 
140 # ============================================================================
141 # plan
142 # ============================================================================
143 
144 # ----------------------------------------------------------------------------
145 ## @brief Choose not to plan number of tests in advance.
146 function plan_no_plan
147 {
148  [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
149 
150  _shtap_plan_set=1
151  _shtap_no_plan=1
152 
153  return 0
154 }
155 
156 # ----------------------------------------------------------------------------
157 ## @brief Plan to skip all tests.
158 function plan_skip_all
159 {
160  local reason=${1:-''}
161 
162  [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
163 
164  _shtap_print_plan 0 "Skip ${reason}"
165 
166  _shtap_skip_all=1
167  _shtap_plan_set=1
168  _shtap_exit 0
169 
170  return 0
171 }
172 
173 # ----------------------------------------------------------------------------
174 ## @brief Plan a certain number of tests and stick to it.
175 function plan_tests
176 {
177  local tests=${1:?}
178 
179  [ ${_shtap_plan_set} -ne 0 ] && _shtap_die "You tried to plan twice!"
180  [ ${tests} -eq 0 ] && _shtap_die "You said to run 0 tests! You've got to run something."
181 
182  _shtap_print_plan ${tests}
183  _shtap_expected_tests=${tests}
184  _shtap_plan_set=1
185 
186  return ${tests}
187 }
188 
189 # ----------------------------------------------------------------------------
190 ## @brief Print plan.
191 function _shtap_print_plan
192 {
193  local tests=${1:?}
194  local directive=${2:-''}
195 
196  echo -n "1..${tests}"
197  [[ -n "${directive}" ]] && echo -n " # ${directive}"
198  echo
199 }
200 
201 # ============================================================================
202 # pass / fail
203 # ============================================================================
204 
205 # ----------------------------------------------------------------------------
206 ## @brief Pass in any case and print reason.
207 function pass
208 {
209  local name=$1
210  ok 0 "${name}"
211 }
212 
213 # ----------------------------------------------------------------------------
214 ## @brief Fail in any case and print reason.
215 function fail
216 {
217  local name=$1
218  ok 1 "${name}"
219 }
220 
221 # ============================================================================
222 # test
223 # ============================================================================
224 
225 # ----------------------------------------------------------------------------
226 ## @brief Evaluate test expression and fail if it does not evaluate to 0 or
227 # check return value of function.
228 #
229 # This is the workhorse method that actually prints the tests result.
230 #
231 # @param [in] expression Test expression or return value.
232 # @param [in] name Name of test.
233 function ok
234 {
235  local expression=${1:?}
236  local name=${2:-''}
237 
238  [ ${_shtap_plan_set} -eq 0 ] && _shtap_die "You tried to run a test without a plan! Gotta have a plan."
239 
240  (( _shtap_executed_tests++ ))
241 
242  if [ -n "${name}" ]; then
243  if match "${name}" "^[0-9]+$"; then
244  diag " You named your test '${name}'. You shouldn't use numbers for your test names."
245  diag " Very confusing."
246  fi
247  fi
248 
249  local match=`expr "${expression}" : '\([0-9]*\)'`
250  local result=0
251  if [ -z "${expression}" ]; then
252  result=1
253  elif [ -n "${match}" -a "${expression}" = "${match}" ]; then
254  [ ${expression} -ne 0 ] && result=1
255  else
256  ( eval ${expression} ) >/dev/null 2>&1
257  [ $? -ne 0 ] && result=1
258  fi
259 
260  if [ ${result} -ne 0 ]; then
261  echo -n "not "
262  (( _shtap_failed_tests++ ))
263  fi
264  echo -n "ok ${_shtap_executed_tests}"
265 
266  if [ -n "${name}" ]; then
267  local ename=${name//\#/\\#}
268  echo -n " - ${ename}"
269  fi
270 
271  if [ -n "${TODO}" ]; then
272  echo -n " # TODO ${TODO}" ;
273  if [ ${result} -ne 0 ]; then
274  (( _shtap_failed_tests-- ))
275  fi
276  fi
277 
278  echo
279  if [ ${result} -ne 0 ]; then
280  local file='_SHTAP_FILENAME'
281  local func=
282  local line=
283 
284  local i=0
285  local bt=$(caller ${i})
286  while match "${bt}" "${_SHTAP_FILENAME}$"; do
287  (( i++ ))
288  bt=$(caller ${i})
289  done
290  local backtrace=
291  eval $(caller ${i} | (read line func file; echo "backtrace=\"${file}:${func}() at line ${line}.\""))
292 
293  local t=
294  [ -n "${TODO}" ] && t="(TODO) "
295 
296  if [ -n "${name}" ]; then
297  diag " Failed ${t}test '${name}'"
298  diag " in ${backtrace}"
299  else
300  diag " Failed ${t}test in ${backtrace}"
301  fi
302  fi
303 
304  return ${result}
305 }
306 
307 # ----------------------------------------------------------------------------
308 ## @brief Execute command and check return value.
309 #
310 # @param [in] command Command to run.
311 function okx
312 {
313  local command="$@"
314 
315  local line=
316  diag "Output of '${command}':"
317  ${command} | while read line; do
318  diag "${line}"
319  done
320  ok ${PIPESTATUS[0]} "${command}"
321 }
322 
323 # ----------------------------------------------------------------------------
324 ## @brief Compare actual and expected result.
325 #
326 # @param [in] result Actual result.
327 # @param [in] expected Expected result.
328 function _shtap_equals
329 {
330  local result=$1
331  local expected=$2
332 
333  if [[ "${result}" == "${expected}" ]] ; then
334  return 0
335  else
336  return 1
337  fi
338 }
339 
340 # ----------------------------------------------------------------------------
341 ## @brief Diagnostic message for is().
342 #
343 # @param [in] result Actual result.
344 # @param [in] expected Expected result.
345 function _shtap_is_diag
346 {
347  local result=$1
348  local expected=$2
349 
350  diag " got: '${result}'"
351  diag " expected: '${expected}'"
352 }
353 
354 # ----------------------------------------------------------------------------
355 ## @brief Test whether a given result is equal to the expected result.
356 #
357 # @param [in] result Actual result.
358 # @param [in] expected Expected result.
359 # @param [in] name Optional name of test.
360 #
361 # @returns Whether the results are equal.
362 #
363 # @retval 0 On equality.
364 # @retval 1 Otherwise.
365 function is
366 {
367  local result=$1
368  local expected=$2
369  local name=${3:-''}
370 
371  _shtap_equals "${result}" "${expected}"
372  [ $? -eq 0 ]
373  ok $? "${name}"
374  local r=$?
375  [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
376  return ${r}
377 }
378 
379 # ----------------------------------------------------------------------------
380 ## @brief Test whether a given result is not equal the expected result.
381 #
382 # @param [in] result Actual result.
383 # @param [in] expected Expected result.
384 # @param [in] name Optional name of test.
385 #
386 # @returns Whether the results were not equal.
387 #
388 # @retval 0 Otherwise.
389 # @retval 1 On equality.
390 function isnt
391 {
392  local result=$1
393  local expected=$2
394  local name=${3:-''}
395 
396  _shtap_equals "${result}" "${expected}"
397  (( $? != 0 ))
398  ok $? "${name}"
399  local r=$?
400  [ ${r} -ne 0 ] && _shtap_is_diag "${result}" "${expected}"
401  return ${r}
402 }
403 
404 # ----------------------------------------------------------------------------
405 ## @brief Test whether a given result matches an expected pattern.
406 #
407 # @param [in] result Actual result.
408 # @param [in] pattern Expected pattern.
409 # @param [in] name Optional name of test.
410 #
411 # @returns Whether the result matched the pattern.
412 #
413 # @retval 0 On match.
414 # @retval 1 Otherwise.
415 function like
416 {
417  local result=$1
418  local pattern=$2
419  local name=${3:-''}
420 
421  match "${result}" "${pattern}"
422  [ $? -eq 0 ]
423  ok $? "${name}"
424  local r=$?
425  [ ${r} -ne 0 ] && diag " '${result}' doesn't match '${pattern}'"
426  return ${r}
427 }
428 
429 # ----------------------------------------------------------------------------
430 ## @brief Test whether a given result does not match an expected pattern.
431 #
432 # @param [in] result Actual result.
433 # @param [in] pattern Expected pattern.
434 # @param [in] name Optional name of test.
435 #
436 # @returns Whether the result did not match the pattern.
437 #
438 # @retval 0 Otherwise.
439 # @retval 1 On match.
440 function unlike
441 {
442  local result=$1
443  local pattern=$2
444  local name=${3:-''}
445 
446  match "${result}" "${pattern}"
447  [ $? -ne 0 ]
448  ok $? "${name}"
449  local r=$?
450  [ ${r} -ne 0 ] && diag " '${result}' matches '${pattern}'"
451  return ${r}
452 }
453 
454 # ============================================================================
455 # skip
456 # ============================================================================
457 
458 # ----------------------------------------------------------------------------
459 ## @brief Skip tests under a certain condition.
460 #
461 # @param [in] condition The condition for skipping the tests.
462 # If 0, the tests are skipped, otherwise not.
463 # @param [in] reason An explanation for why skipping the tests.
464 # @param [in] n The number of tests which will be skipped.
465 #
466 # @returns Whether the tests were skipped.
467 #
468 # @retval 0 If tests are to be skipped.
469 # @retval 1 Otherwise.
470 function skip
471 {
472  local condition=${1:?}
473  local reason=${2:-''}
474  local n=${3:-1}
475 
476  if [ ${condition} -eq 0 ]; then
477  local i=
478  for (( i=0 ; i<$n ; i++ )); do
479  (( _shtap_executed_tests++ ))
480  echo "ok ${_shtap_executed_tests} # skip: ${reason}"
481  done
482  return 0
483  else
484  return 1
485  fi
486 }
487 
488 # ============================================================================
489 # diagnostics
490 # ============================================================================
491 
492 # ----------------------------------------------------------------------------
493 ## @brief Print diagnostic message.
494 #
495 # @param [in] msg Diagnostic message.
496 #
497 # @returns Always 1.
498 function diag
499 {
500  local msg=${1:?}
501 
502  if [ -n "${msg}" ]; then
503  echo "# ${msg}"
504  fi
505 
506  return 1
507 }
508 
509 # ============================================================================
510 # termination
511 # ============================================================================
512 
513 # ----------------------------------------------------------------------------
514 ## @brief Bail out.
515 #
516 # @param [in] reason Reason for bailing out.
517 #
518 # @returns Nothing. Instead, exits the process with error code 255.
519 function SHTAP_BAIL_OUT
520 {
521  local reason=${1:-''}
522 
523  echo "Bail out! ${reason}" >&2
524  _shtap_exit 255
525 }
526 
527 # ----------------------------------------------------------------------------
528 ## @brief Abort test execution.
529 #
530 # @param [in] reason Reason for aborting the test execution.
531 #
532 # @returns Nothing. Instead, exits the process with error code 255.
533 function _shtap_die
534 {
535  local reason=${1:-'<unspecified error>'}
536 
537  echo "${reason}" >&2
538  _shtap_test_died=1
539  _shtap_exit 255
540 }
541 
542 # ----------------------------------------------------------------------------
543 ## @brief Cleaning up after execution of tests and see if plan was fulfilled.
544 function _shtap_cleanup
545 {
546  local rc=0 # return code
547 
548  if [ ${_shtap_plan_set} -eq 0 ]; then
549  diag "Looks like your test died before it could output anything."
550  return ${rc}
551  fi
552 
553  if [ ${_shtap_test_died} -ne 0 ]; then
554  diag "Looks like your test died just after ${_shtap_executed_tests}."
555  return ${rc}
556  fi
557 
558  if [ ${_shtap_skip_all} -eq 0 -a ${_shtap_no_plan} -ne 0 ]; then
559  _shtap_print_plan ${_shtap_executed_tests}
560  fi
561 
562  local s=
563  if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
564  s=''
565  [ ${_shtap_expected_tests} -gt 1 ] && s='s'
566  local extra=$(( _shtap_executed_tests - _shtap_expected_tests ))
567  diag "Looks like you planned ${_shtap_expected_tests} test${s} but ran ${extra} extra."
568  rc=-1 ;
569  fi
570 
571  if [ ${_shtap_no_plan} -eq 0 -a ${_shtap_expected_tests} -gt ${_shtap_executed_tests} ]; then
572  s=''
573  [ ${_shtap_expected_tests} -gt 1 ] && s='s'
574  diag "Looks like you planned ${_shtap_expected_tests} test${s} but only ran ${_shtap_executed_tests}."
575  fi
576 
577  if [ ${_shtap_failed_tests} -gt 0 ]; then
578  s=''
579  [ ${_shtap_failed_tests} -gt 1 ] && s='s'
580  diag "Looks like you failed ${_shtap_failed_tests} test${s} of ${_shtap_executed_tests}."
581  fi
582 
583  return ${rc}
584 }
585 
586 # ----------------------------------------------------------------------------
587 ## @brief Calculate exit status indicating number of failed or extra tests.
588 function _shtap_exit_status
589 {
590  if [ ${_shtap_no_plan} -ne 0 -o ${_shtap_plan_set} -eq 0 ]; then
591  return ${_shtap_failed_tests}
592  fi
593 
594  if [ ${_shtap_expected_tests} -lt ${_shtap_executed_tests} ]; then
595  return $(( _shtap_executed_tests - _shtap_expected_tests ))
596  fi
597 
598  return $(( _shtap_failed_tests + ( _shtap_expected_tests - _shtap_executed_tests )))
599 }
600 
601 # ----------------------------------------------------------------------------
602 ## @brief Terminate execution of tests.
603 function _shtap_exit
604 {
605  local rc=${1:-''}
606  if [ -z "${rc}" ]; then
607  _shtap_exit_status
608  rc=$?
609  fi
610 
611  _shtap_cleanup
612  local alt_rc=$?
613  [ ${alt_rc} -ne 0 ] && rc=${alt_rc}
614  trap - EXIT
615  exit ${rc}
616 }
function SHTAP_BAIL_OUT(in reason)
Bail out.
function ok(in expression, in name)
Evaluate test expression and fail if it does not evaluate to 0 or check return value of function...
function is(in result, in expected, in name)
Test whether a given result is equal to the expected result.
string SHTAP_VERSION
Definition: shtap.sh:118
function isnt(in result, in expected, in name)
Test whether a given result is not equal the expected result.
function plan_tests()
Plan a certain number of tests and stick to it.
function unlike(in result, in pattern, in name)
Test whether a given result does not match an expected pattern.
function skip(in condition, in reason, in n)
Skip tests under a certain condition.
function plan_skip_all()
Plan to skip all tests.
function match(in value, in pattern)
This function implements a more portable way to do pattern matching.
function pass()
Pass in any case and print reason.
function plan_no_plan()
Choose not to plan number of tests in advance.
cmake msg
function like(in result, in pattern, in name)
Test whether a given result matches an expected pattern.
function okx(in command)
Execute command and check return value.
function diag(in msg)
Print diagnostic message.
function fail()
Fail in any case and print reason.