# -*- mode: sh -*-
# mockup: a function to mockup other commands/functions
#
# this can be used to simulate a program that records calling arguments,
# provided input, produces specified output/errors, and exits with a specified
# return code.  very useful for unit testing!
#
# simple usage:
#  $ echo file1 file2 file3 > ls.output.txt
#  $ mockup -o ls.output.txt ls
#  $ ls
#  file1 file2 file3
#
# more advanced features are possible, such as multiple different invocations
# and support for running in parallel under pipes.  see the nonexistant
# documentation for examples
set -u

MOCKUP_WORKDIR=${MOCKUP_WORKDIR:-${TMPDIR:-}/mockup-$$}

# record internal mockup program failure
mockup_fail(){
    echo "    MOCKUP ERROR: ${@:-}" >> "${errorfile:-/dev/stderr}"
    return 127
}

# helpful usage stmts
mockup_help(){
    cat << EOF
mockup: create "mock" programs/functions for unit testing shell scripts.

usage:
  mockup -h
  mockup [-i] [-o file] [-e file] [-r code] command

  command:   command-line program or shell function to override.

  options:
  -h:        print this useful help message.
  -i:        record the input provided on stdin (to mockup.<n>.input.<cmd>)
  -o FILE:   print the contents of FILE on standard output.
  -e FILE:   print the contents of FILE on standard error.
  -r CODE:   return with status CODE.
EOF
}

# main function, called from test suite
mockup(){
    local args efile input ofile rcode
    local n cmd envfile
    args=$(getopt -o e:hio:r: -n mockup -- "${@:-}")
    test $? = 0 || mockup_fail invalid options passed to mockup
    eval set -- "$args"

    # do something that will always be true without using true
    # thus allowing even the "true" command to be mocked up :)
    while [ "true" ]; do
        case "$1" in
            -e)
                efile="$2"
                shift
                ;;
            -h)
                mockup_help
                return 0
                ;;
            -i)
                input=yes
                ;;
            -o)
                ofile="$2"
                shift
                ;;
            -r)
                rcode="$2"
                shift
                ;;
            --)
                shift
                break
                ;;
            *)
                mockup_help >&2;
                mockup_fail invalid arguments passed to mockup
                return 127
                ;;
        esac
        shift
    done
    if [ ! -e "$MOCKUP_WORKDIR" ]; then
        mockup_fail "${MOCKUP_WORKDIR} not found, create or set MOCKUP_WORKDIR"
        return 127
    elif [ $# -ne 1 ]; then
        mockup_help 2>&1
        mockup_fail "$# $* need command name to mockup"
        return 127
    fi

    cmd=$1

    # to support multiple invokations, organize filenames in a queue
    n=$(mockup_next_available "$MOCKUP_WORKDIR" "$cmd")

    mockup_print "$cmd" "$n"

    envfile=$MOCKUP_WORKDIR/$n.env.$cmd
    if [ -f "$envfile" ]; then
        . "$envfile"
    else
        mockup_fail "env file for $cmd (iteration $n) not found"
        return 127
    fi

    return 0
}

# utility function for the case where a program might be called
# multiple times in the same test suite but it's desired to
# have different behaviours on each invokation
mockup_next_available(){
    local wd="$1"
    local cmd="$2"
    local n=0;
    while [ -f "$wd/$n.script.$cmd" ] || [ -f "$wd/done.$n.script.$cmd" ]; do
        n=$(expr $n + 1)
    done
    echo $n
}

# likewise but opposite: find the next to be evaluated
mockup_next_evaluated(){
    local wd="$1"
    local cmd="$2"
    local n=0;
    local next oldnext

    n=$(ls "$wd" | grep -E "^[0-9]*.script.$cmd" 2>/dev/null | head -1)
    n=$(basename "$n" ".script.$cmd")
    echo ${n:-0}
}

# utility function to quote cmdline arguments distinctly wrt whitespace
# it can also replace certain environment variables with a template value,
# i.e. s/$TMPDIR/<TMPDIR>/ for easy comparison
mockup_escape(){
    local esc_tmpdir
    esc_tmpdir=$(echo $TMPDIR | sed -e 's,\\,\\\\,g')
    sed -e "s,$esc_tmpdir,<TMPDIR>,g" 2>"$errorfile" -e "s/'/'\\\\''/g" << EOF
$1
EOF
}

# create the mock wrappers etc for the selected program/function
mockup_print(){
    local n scriptfile cmdlinefile errorfile mockup_control envfile
    local cmd="$1"
    local next oldnext
    n="$2"

    # the actual hook
    scriptfile="$MOCKUP_WORKDIR/$n.script.$cmd"
    donescriptfile="$MOCKUP_WORKDIR/done.$n.script.$cmd"
    # where any received input is put
    inputfile="$MOCKUP_WORKDIR/$n.input.$cmd"
    doneinputfile="$MOCKUP_WORKDIR/done.$n.input.$cmd"
    # where the cmdline with which the hook was called is stored
    cmdlinefile="$MOCKUP_WORKDIR/$n.cmdline.$cmd"
    donecmdlinefile="$MOCKUP_WORKDIR/done.$n.cmdline.$cmd"
    # where the mockup-internal errors are stored
    errorfile="$MOCKUP_WORKDIR/$n.errors.$cmd"
    doneerrorfile="$MOCKUP_WORKDIR/done.$n.errors.$cmd"
    # env file (see below)
    envfile="$MOCKUP_WORKDIR/$n.env.$cmd"
    doneenvfile="$MOCKUP_WORKDIR/done.$n.env.$cmd"
    # master hook function control file for mockup scripts
    mockup_control="$MOCKUP_WORKDIR/mockup.control"

    # create an environment file to store all of this, but in the
    # form of after it's run (so it can be sourced for comparisons later)
    cat << EOF >"$envfile"
mockup_cmdline="$donecmdlinefile"
mockup_scriptfile="$donescriptfile"
mockup_errorfile="$doneerrorfile"
mockup_inputfile="$doneinputfile"
EOF

    # see if the mockup script already has a mockup hook registered
    # and if not, add it, and re-source it.  this hook doesn't actually
    # do the real work, as there's an extra level of indirection (argh,
    # meta-meta-shell programming makes teh hedz hurt) to support
    # multiple sequential invokations
    if ! grep -q "^$cmd(){" "$mockup_control" 2>/dev/null; then
        cat << EOF >> "${mockup_control}"

$cmd(){
  local next n oldnext ret
  local errorfile
  errorfile="$errorfile"
  unset -f mock_$cmd >/dev/null 2>&1
  n="\$(mockup_next_evaluated "\$MOCKUP_WORKDIR" "$cmd")"
  if [ \$? -ne 0 ] || [ -z "\$n" ]; then
    mockup_fail "no mockup script to execute for $cmd"
    return 127
  fi
  next="\$MOCKUP_WORKDIR/\$n.script.$cmd"
  if [ -f  "\$next" ]; then
    . "\${next}"
  else
    mockup_fail "sourcing \${next}"
    return 127
  fi
  mock_$cmd "\${@:-}"
  ret=\$?
  (
  cd \$MOCKUP_WORKDIR
  for f in \$n.*.$cmd; do
    mv "\$f" "done.\$f"
    if [ \$? -ne 0 ]; then
      mockup_fail "rotating out \${next}"
      return 127
    fi
  done
  )
  return \$ret
}

EOF
    fi
    . "$mockup_control"

    # okay, now this is the real meat and potatoes of the mockup work
    # this gets put into the sequential script file, and is called by
    # the main hook above.
    cat << EOF > "$scriptfile"
mock_$cmd(){
  local escaped cmdline
  local errorfile="$errorfile"

  # record the cmdline
  set $cmd "\${@:-}"
  while [ \$# -gt 0 ]; do
    escaped=\$(mockup_escape "\$1")
    if [ -n "\${cmdline:-}" ]; then
      cmdline="\$cmdline '\$escaped'"
    else
      cmdline="'\$escaped'"
    fi
    shift
  done
  set "\$cmdline"
  echo "\${@:-}" > "$cmdlinefile"
  if [ \$? -ne 0 ]; then
    mockup_fail failed recording cmdlinefile
    return 127
  fi

  # record input
  if [ -n "${input:-}" ]; then
    cat > "${inputfile:-}" 2>>"$errorfile"
    if [ \$? -ne 0 ]; then
      mockup_fail failed recording input
      return 127
    fi
  fi

  # produce output
  if [ -n "${ofile:-}" ]; then
    cat < "${ofile:-}" 2>>"$errorfile"
    if [ \$? -ne 0 ]; then
      mockup_fail failed producing output
      return 127
    fi
  fi

  # produce errors
  if [ -n "${efile:-}" ]; then
    if [ ! -f "${efile:-}" ]; then
      mockup_fail failed to find error output file
      return 127
    else
      cat < "${efile:-}" >&2
    fi
    if [ \$? -ne 0 ]; then
      mockup_fail failed producing error output
      return 127
    fi
  fi

  # and return requested value
  return ${rcode:-0}
}
EOF
}
