| 1 | #!/usr/bin/env bash
 | 
| 2 | #
 | 
| 3 | # Show problems with errexit.
 | 
| 4 | #
 | 
| 5 | # Usage:
 | 
| 6 | #   ./errexit-confusion.sh <function name>
 | 
| 7 | 
 | 
| 8 | set -o nounset
 | 
| 9 | set -o pipefail
 | 
| 10 | set -o errexit
 | 
| 11 | 
 | 
| 12 | log() {
 | 
| 13 |   echo "$@" 1>&2
 | 
| 14 | }
 | 
| 15 | 
 | 
| 16 | die() {
 | 
| 17 |   log "$@"
 | 
| 18 |   exit 1
 | 
| 19 | }
 | 
| 20 | 
 | 
| 21 | all-passing() {
 | 
| 22 |   bin/osh --parse-and-print-arena foo  # This fails
 | 
| 23 |   echo status=$?  # succeeds
 | 
| 24 | }
 | 
| 25 | 
 | 
| 26 | # Copied from test/common.sh, to show the bug.
 | 
| 27 | 
 | 
| 28 | # The bug is that errexit is disabled within if.  Fixes tried:
 | 
| 29 | #
 | 
| 30 | # 1) Putting it on its own line: Then errexit triggers and you don't get the
 | 
| 31 | # failure message.
 | 
| 32 | #
 | 
| 33 | # $func_name 2>&1
 | 
| 34 | #
 | 
| 35 | # 2) Disabling with
 | 
| 36 | #
 | 
| 37 | # set +o errexit
 | 
| 38 | # $func_name 2>&1
 | 
| 39 | # status=$?
 | 
| 40 | # set -o errexit
 | 
| 41 | #
 | 
| 42 | # The problem is that the LAST line of all-passing succeeds.  We want it to
 | 
| 43 | # fail in the middle.
 | 
| 44 | 
 | 
| 45 | run-other-suite-for-release-OLD() {
 | 
| 46 |   local suite_name=$1
 | 
| 47 |   local func_name=$2
 | 
| 48 | 
 | 
| 49 |   local out=_tmp/other/${suite_name}.txt
 | 
| 50 |   mkdir -p $(dirname $out)
 | 
| 51 | 
 | 
| 52 |   echo
 | 
| 53 |   echo "*** Running test suite '$suite_name' ***"
 | 
| 54 |   echo
 | 
| 55 | 
 | 
| 56 |   if $func_name 2>&1; then
 | 
| 57 |     echo
 | 
| 58 |     log "Test suite '$suite_name' ran without errors.  Wrote $out"
 | 
| 59 |   else
 | 
| 60 |     echo
 | 
| 61 |     die "Test suite '$suite_name' failed (running $func_name)"
 | 
| 62 |   fi
 | 
| 63 | }
 | 
| 64 | 
 | 
| 65 | # This is an awkward rewrite that works.
 | 
| 66 | #
 | 
| 67 | # Again the argv dispatch pattern saves the day!  You can test if a function
 | 
| 68 | # failed while preserving its own errexit semantics!
 | 
| 69 | #
 | 
| 70 | # This composes!
 | 
| 71 | 
 | 
| 72 | run-other-suite-for-release-FIXED() {
 | 
| 73 |   local suite_name=$1
 | 
| 74 |   local func_name=$2
 | 
| 75 | 
 | 
| 76 |   local out=_tmp/other/${suite_name}.txt
 | 
| 77 |   mkdir -p $(dirname $out)
 | 
| 78 | 
 | 
| 79 |   echo
 | 
| 80 |   echo "*** Running test suite '$suite_name' ***"
 | 
| 81 |   echo
 | 
| 82 | 
 | 
| 83 |   local status=0
 | 
| 84 | 
 | 
| 85 |   # Run in a separate SHELL, not just in a separate process.  ( $func_name )
 | 
| 86 |   # doesn't work.
 | 
| 87 |   $0 $func_name 2>&1 | tee $out || status=$?
 | 
| 88 | 
 | 
| 89 |   if test $status -eq 0; then
 | 
| 90 |     echo
 | 
| 91 |     log "Test suite '$suite_name' ran without errors.  Wrote '$out'"
 | 
| 92 |   else
 | 
| 93 |     echo
 | 
| 94 |     die "Test suite '$suite_name' failed with status $status (running '$func_name', wrote '$out')"
 | 
| 95 |   fi
 | 
| 96 | }
 | 
| 97 | 
 | 
| 98 | run-for-release-OLD() {
 | 
| 99 |   run-other-suite-for-release-OLD example-failure all-passing
 | 
| 100 | }
 | 
| 101 | 
 | 
| 102 | run-for-release-FIXED() {
 | 
| 103 |   run-other-suite-for-release-FIXED example-failure all-passing
 | 
| 104 | }
 | 
| 105 | 
 | 
| 106 | # This could be a blog post:
 | 
| 107 | #
 | 
| 108 | # Conditions for the problem: 
 | 
| 109 | # - using errexit (pipefail)
 | 
| 110 | # - but you want to test if a FUNCTION failed.  If you disable errexit, you are
 | 
| 111 | #   changing the semantics of the function!
 | 
| 112 | #
 | 
| 113 | # Solution:
 | 
| 114 | #
 | 
| 115 | # 1. Use the argv dispatch pattern
 | 
| 116 | # 2. Use $0 $func_name || status=$?
 | 
| 117 | 
 | 
| 118 | # -----------------------------------------------------------------------------
 | 
| 119 | 
 | 
| 120 | # Another different that went away with the FIXED: A case where the stricter
 | 
| 121 | # behavior of OSH's errexit was triggered.
 | 
| 122 | 
 | 
| 123 | # Can't set 'errexit' in a context where it's disabled (if, !, && ||,
 | 
| 124 | # while/until conditions)
 | 
| 125 | #
 | 
| 126 | # Arguably this is exposing a bug?  errexit is already disabled?  May have to
 | 
| 127 | # revisit this.
 | 
| 128 | 
 | 
| 129 | test-case-that-sets-errexit() {
 | 
| 130 |   set +o errexit
 | 
| 131 |   echo hi
 | 
| 132 | }
 | 
| 133 | 
 | 
| 134 | osh-stricter() {
 | 
| 135 |   run-other-suite-for-release-OLD osh-stricter test-case-that-sets-errexit
 | 
| 136 | }
 | 
| 137 | 
 | 
| 138 | "$@"
 | 
| 139 | 
 |