Repository: vmware/govmomi
Pull request #3078 (2023-03-23 18:20:39)

chore: add timeout to bats tests
## Description

The govc bats tests often hang when run in GH Actions. With this change:
- test_helper uses `head -n1` rather than `cat` to read env from the named pipe
- runs vcsim with a timeout, when triggered sends SIGQUIT to get a stacktrace
- upgrade to bats-core v1.9.0 fixes the hanging bats tests we've been seeing


Patch:
diff --git a/.github/workflows/govmomi-stale.yaml b/.github/workflows/govmomi-stale.yaml
index 8463adc0..fce14122 100644
--- a/.github/workflows/govmomi-stale.yaml
+++ b/.github/workflows/govmomi-stale.yaml
@@ -23,7 +23,7 @@ jobs:
     runs-on: ubuntu-20.04
 
     steps:
-      - uses: "actions/stale@v8"
+      - uses: "actions/stale@v7"
         with:
           repo-token: "${{ secrets.GITHUB_TOKEN }}" # No need to setup
 
diff --git a/Makefile b/Makefile
index a0e15722..d7df7766 100644
--- a/Makefile
+++ b/Makefile
@@ -143,16 +143,7 @@ go-test: ## Runs go unit tests with race detector enabled
 govc-test: install
 govc-test: ## Runs govc bats tests
 	./govc/test/images/update.sh
-	(cd govc/test && ./vendor/github.com/sstephenson/bats/libexec/bats -t .)
-
-.PHONY: govc-test-sso
-govc-test-sso: install
-	./govc/test/images/update.sh
-	(cd govc/test && SSO_BATS=1 ./vendor/github.com/sstephenson/bats/libexec/bats -t sso.bats)
-
-.PHONY: govc-test-sso-assert-cert
-govc-test-sso-assert-cert:
-	SSO_BATS_ASSERT_CERT=1 $(MAKE) govc-test-sso
+	(cd govc/test && ./vendor/github.com/bats-core/bats-core/bin/bats -t .)
 
 .PHONY: test
 test: go-test govc-test	## Runs go-test and govc-test
diff --git a/simulator/internal/server.go b/simulator/internal/server.go
index 1f814dea..877eca0d 100644
--- a/simulator/internal/server.go
+++ b/simulator/internal/server.go
@@ -286,15 +286,18 @@ func (s *Server) wrap() {
 	s.Config.ConnState = func(c net.Conn, cs http.ConnState) {
 		s.mu.Lock()
 		defer s.mu.Unlock()
+
 		switch cs {
 		case http.StateNew:
-			s.wg.Add(1)
 			if _, exists := s.conns[c]; exists {
 				panic("invalid state transition")
 			}
 			if s.conns == nil {
 				s.conns = make(map[net.Conn]http.ConnState)
 			}
+			// Add c to the set of tracked conns and increment it to the
+			// waitgroup.
+			s.wg.Add(1)
 			s.conns[c] = cs
 			if s.closed {
 				// Probably just a socket-late-binding dial from
@@ -321,7 +324,14 @@ func (s *Server) wrap() {
 				s.closeConn(c)
 			}
 		case http.StateHijacked, http.StateClosed:
-			s.forgetConn(c)
+			// Remove c from the set of tracked conns and decrement it from the
+			// waitgroup, unless it was previously removed.
+			if _, ok := s.conns[c]; ok {
+				delete(s.conns, c)
+				// Keep Close from returning until the user's ConnState hook
+				// (if any) finishes.
+				defer s.wg.Done()
+			}
 		}
 		if oldHook != nil {
 			oldHook(c, cs)
@@ -341,13 +351,3 @@ func (s *Server) closeConnChan(c net.Conn, done chan<- struct{}) {
 		done <- struct{}{}
 	}
 }
-
-// forgetConn removes c from the set of tracked conns and decrements it from the
-// waitgroup, unless it was previously removed.
-// s.mu must be held.
-func (s *Server) forgetConn(c net.Conn) {
-	if _, ok := s.conns[c]; ok {
-		delete(s.conns, c)
-		s.wg.Done()
-	}
-}


Test patch:
diff --git a/.github/workflows/govmomi-govc-tests.yaml b/.github/workflows/govmomi-govc-tests.yaml
index 8be9197c..e546f5d6 100644
--- a/.github/workflows/govmomi-govc-tests.yaml
+++ b/.github/workflows/govmomi-govc-tests.yaml
@@ -36,17 +36,6 @@ jobs:
         cmd: ["govc-test"]
         experimental: [false]
         timeout: [20]
-        include:
-          - go-version: "1.20"
-            platform: "ubuntu-20.04"
-            cmd: "govc-test-sso"
-            experimental: true
-            timeout: 3
-          - go-version: "1.20"
-            platform: "ubuntu-20.04"
-            cmd: "govc-test-sso-assert-cert"
-            experimental: true
-            timeout: 3
 
     runs-on: ${{ matrix.platform }}
     continue-on-error: ${{ matrix.experimental }}
diff --git a/govc/test/sso.bats b/govc/test/sso.bats
index 1d126fe1..aef6326c 100755
--- a/govc/test/sso.bats
+++ b/govc/test/sso.bats
@@ -3,10 +3,6 @@
 load test_helper
 
 @test "sso.service.ls" {
-  if [ ! "${SSO_BATS}" = "1" ]; then
-    skip "skip sso.service.ls by default"
-  fi
-
   vcsim_env
 
   sts=$(govc option.ls config.vpxd.sso.sts.uri | awk '{print $2}')
@@ -34,11 +30,9 @@ load test_helper
   run govc sso.service.ls -t sso:sts -U
   assert_success "$sts"
 
-  if [ "${SSO_BATS_ASSERT_CERT}" = "1" ]; then
-    cert=$(govc about.cert -show | grep -v CERTIFICATE | tr -d '\n')
-    trust=$(govc sso.service.ls -json -t sso:sts | jq -r .[].ServiceEndpoints[].SslTrust[0])
-    assert_equal "$cert" "$trust"
-  fi
+  cert=$(govc about.cert -show | grep -v CERTIFICATE | tr -d '\n')
+  trust=$(govc sso.service.ls -json -t sso:sts | jq -r .[].ServiceEndpoints[].SslTrust[0])
+  assert_equal "$cert" "$trust"
 
   govc sso.service.ls -t cs.identity | grep com.vmware.cis | grep -v https:
   govc sso.service.ls -t cs.identity -l | grep https:
diff --git a/govc/test/test_helper.bash b/govc/test/test_helper.bash
index 6bdb2970..a3d89696 100644
--- a/govc/test/test_helper.bash
+++ b/govc/test/test_helper.bash
@@ -22,6 +22,7 @@ fi
 readlink=$(type -p greadlink readlink | head -1)
 xargs=$(type -p gxargs xargs | head -1)
 mktemp=$(type -p gmktemp mktemp | head -1)
+timeout=$(type -p gtimeout timeout | head -1)
 
 BATS_TEST_DIRNAME=$($readlink -nf $BATS_TEST_DIRNAME)
 
@@ -54,15 +55,22 @@ vcsim_start() {
     export GOVC_SIM_ENV
     mkfifo "$GOVC_SIM_ENV"
 
-    vcsim -l 127.0.0.1:0 -E "$GOVC_SIM_ENV" "$@" &
+    if [ -z "$timeout" ] ; then
+      vcsim -l 127.0.0.1:0 -E "$GOVC_SIM_ENV" "$@" &
+    else
+      timeout -s QUIT 5m vcsim -l 127.0.0.1:0 -E "$GOVC_SIM_ENV" "$@" &
+    fi
+    pid=$!
 
-    eval "$(cat "$GOVC_SIM_ENV")"
+    eval "$(head -n1 "$GOVC_SIM_ENV")"
+    rm -f "$GOVC_SIM_ENV"
+    GOVC_SIM_PID=$pid
 }
 
 vcsim_stop() {
-  kill "$GOVC_SIM_PID"
-  wait "$GOVC_SIM_PID"
-  rm -f "$GOVC_SIM_ENV"
+  if kill "$GOVC_SIM_PID" 2>/dev/null ; then
+    wait "$GOVC_SIM_PID"
+  fi
   unset GOVC_SIM_PID
 }
 
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/bin/bats b/govc/test/vendor/github.com/bats-core/bats-core/bin/bats
new file mode 100755
index 00000000..892470c1
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/bin/bats
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if command -v greadlink >/dev/null; then
+  bats_readlinkf() {
+    greadlink -f "$1"
+  }
+else
+  bats_readlinkf() {
+    readlink -f "$1"
+  }
+fi
+
+fallback_to_readlinkf_posix() {
+  bats_readlinkf() {
+    [ "${1:-}" ] || return 1
+    max_symlinks=40
+    CDPATH='' # to avoid changing to an unexpected directory
+
+    target=$1
+    [ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes
+    [ -d "${target:-/}" ] && target="$target/"
+
+    cd -P . 2>/dev/null || return 1
+    while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do
+      if [ ! "$target" = "${target%/*}" ]; then
+        case $target in
+        /*) cd -P "${target%/*}/" 2>/dev/null || break ;;
+        *) cd -P "./${target%/*}" 2>/dev/null || break ;;
+        esac
+        target=${target##*/}
+      fi
+
+      if [ ! -L "$target" ]; then
+        target="${PWD%/}${target:+/}${target}"
+        printf '%s\n' "${target:-/}"
+        return 0
+      fi
+
+      # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n",
+      #   <file mode>, <number of links>, <owner name>, <group name>,
+      #   <size>, <date and time>, <pathname of link>, <contents of link>
+      # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html
+      link=$(ls -dl -- "$target" 2>/dev/null) || break
+      target=${link#*" $target -> "}
+    done
+    return 1
+  }
+}
+
+if ! BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}" 2>/dev/null); then
+  fallback_to_readlinkf_posix
+  BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}")
+fi
+
+export BATS_ROOT=${BATS_PATH%/*/*}
+export -f bats_readlinkf
+exec env BATS_ROOT="$BATS_ROOT" "$BATS_ROOT/libexec/bats-core/bats" "$@"
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/common.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/common.bash
new file mode 100755
index 00000000..a27f1ac8
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/common.bash
@@ -0,0 +1,249 @@
+#!/usr/bin/env bash
+
+bats_prefix_lines_for_tap_output() {
+  while IFS= read -r line; do
+    printf '# %s\n' "$line" || break # avoid feedback loop when errors are redirected into BATS_OUT (see #353)
+  done
+  if [[ -n "$line" ]]; then
+    printf '# %s\n' "$line"
+  fi
+}
+
+function bats_replace_filename() {
+  local line
+  while read -r line; do
+    printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}"
+  done
+  if [[ -n "$line" ]]; then
+    printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}"
+  fi
+}
+
+bats_quote_code() { # <var> <code>
+  printf -v "$1" -- "%s%s%s" "$BATS_BEGIN_CODE_QUOTE" "$2" "$BATS_END_CODE_QUOTE"
+}
+
+bats_check_valid_version() {
+  if [[ ! $1 =~ [0-9]+.[0-9]+.[0-9]+ ]]; then
+    printf "ERROR: version '%s' must be of format <major>.<minor>.<patch>!\n" "$1" >&2
+    exit 1
+  fi
+}
+
+# compares two versions. Return 0 when version1 < version2
+bats_version_lt() { # <version1> <version2>
+  bats_check_valid_version "$1"
+  bats_check_valid_version "$2"
+
+  local -a version1_parts version2_parts
+  IFS=. read -ra version1_parts <<<"$1"
+  IFS=. read -ra version2_parts <<<"$2"
+
+  for i in {0..2}; do
+    if ((version1_parts[i] < version2_parts[i])); then
+      return 0
+    elif ((version1_parts[i] > version2_parts[i])); then
+      return 1
+    fi
+  done
+  # if we made it this far, they are equal -> also not less then
+  return 2 # use other failing return code to distinguish equal from gt
+}
+
+# ensure a minimum version of bats is running or exit with failure
+bats_require_minimum_version() { # <required version>
+  local required_minimum_version=$1
+
+  if bats_version_lt "$BATS_VERSION" "$required_minimum_version"; then
+    printf "BATS_VERSION=%s does not meet required minimum %s\n" "$BATS_VERSION" "$required_minimum_version"
+    exit 1
+  fi
+
+  if bats_version_lt "$BATS_GUARANTEED_MINIMUM_VERSION" "$required_minimum_version"; then
+    BATS_GUARANTEED_MINIMUM_VERSION="$required_minimum_version"
+  fi
+}
+
+bats_binary_search() { # <search-value> <array-name>
+  if [[ $# -ne 2 ]]; then
+    printf "ERROR: bats_binary_search requires exactly 2 arguments: <search value> <array name>\n" >&2
+    return 2
+  fi
+
+  local -r search_value=$1 array_name=$2
+
+  # we'd like to test if array is set but we cannot distinguish unset from empty arrays, so we need to skip that
+
+  local start=0 mid end mid_value
+  # start is inclusive, end is exclusive ...
+  eval "end=\${#${array_name}[@]}"
+
+  # so start == end means empty search space
+  while ((start < end)); do
+    mid=$(((start + end) / 2))
+    eval "mid_value=\${${array_name}[$mid]}"
+    if [[ "$mid_value" == "$search_value" ]]; then
+      return 0
+    elif [[ "$mid_value" < "$search_value" ]]; then
+      # This branch excludes equality -> +1 to skip the mid element.
+      # This +1 also avoids endless recursion on odd sized search ranges.
+      start=$((mid + 1))
+    else
+      end=$mid
+    fi
+  done
+
+  # did not find it -> its not there
+  return 1
+}
+
+# store the values in ascending (string!) order in result array
+# Intended for short lists! (uses insertion sort)
+bats_sort() { # <result-array-name> <values to sort...>
+  local -r result_name=$1
+  shift
+
+  if (($# == 0)); then
+    eval "$result_name=()"
+    return 0
+  fi
+
+  local -a sorted_array=()
+  local -i i
+  while (( $# > 0 )); do # loop over input values
+    local current_value="$1"
+    shift
+    for ((i = ${#sorted_array[@]}; i >= 0; --i)); do # loop over output array from end
+      if (( i == 0 )) || [[ ${sorted_array[i - 1]} < $current_value ]]; then
+        # shift bigger elements one position to the end
+        sorted_array[i]=$current_value
+        break
+      else
+        # insert new element at (freed) desired location
+        sorted_array[i]=${sorted_array[i - 1]}
+      fi
+    done
+  done
+
+  eval "$result_name=(\"\${sorted_array[@]}\")"
+}
+
+# check if all search values (must be sorted!) are in the (sorted!) array
+# Intended for short lists/arrays!
+bats_all_in() { # <sorted-array> <sorted search values...>
+  local -r haystack_array=$1
+  shift
+
+  local -i haystack_length # just to appease shellcheck
+  eval "local -r haystack_length=\${#${haystack_array}[@]}"
+
+  local -i haystack_index=0         # initialize only here to continue from last search position
+  local search_value haystack_value # just to appease shellcheck
+  for ((i = 1; i <= $#; ++i)); do
+    eval "local search_value=${!i}"
+    for (( ; haystack_index < haystack_length; ++haystack_index)); do
+      eval "local haystack_value=\${${haystack_array}[$haystack_index]}"
+      if [[ $haystack_value > "$search_value" ]]; then
+        # we passed the location this value would have been at -> not found
+        return 1
+      elif [[ $haystack_value == "$search_value" ]]; then
+        continue 2 # search value found  -> try the next one
+      fi
+    done
+    return 1 # we ran of the end of the haystack without finding the value!
+  done
+
+  # did not return from loop above -> all search values were found
+  return 0
+}
+
+# check if any search value (must be sorted!) is in the (sorted!) array
+# intended for short lists/arrays
+bats_any_in() { # <sorted-array> <sorted search values>
+  local -r haystack_array=$1
+  shift
+
+  local -i haystack_length # just to appease shellcheck
+  eval "local -r haystack_length=\${#${haystack_array}[@]}"
+
+  local -i haystack_index=0         # initialize only here to continue from last search position
+  local search_value haystack_value # just to appease shellcheck
+  for ((i = 1; i <= $#; ++i)); do
+    eval "local search_value=${!i}"
+    for (( ; haystack_index < haystack_length; ++haystack_index)); do
+      eval "local haystack_value=\${${haystack_array}[$haystack_index]}"
+      if [[ $haystack_value > "$search_value" ]]; then
+        continue 2 # search value not in array! -> try next
+      elif [[ $haystack_value == "$search_value" ]]; then
+        return 0 # search value found
+      fi
+    done
+  done
+
+  # did not return from loop above -> no search value was found
+  return 1
+}
+
+bats_trim() {                                            # <output-variable> <string>
+  local -r bats_trim_ltrimmed=${2#"${2%%[![:space:]]*}"} # cut off leading whitespace
+  # shellcheck disable=SC2034 # used in eval!
+  local -r bats_trim_trimmed=${bats_trim_ltrimmed%"${bats_trim_ltrimmed##*[![:space:]]}"} # cut off trailing whitespace
+  eval "$1=\$bats_trim_trimmed"
+}
+
+# a helper function to work around unbound variable errors with ${arr[@]} on Bash 3
+bats_append_arrays_as_args() { # <array...> -- <command ...>
+  local -a trailing_args=()
+  while (($# > 0)) && [[ $1 != -- ]]; do
+    local array=$1
+    shift
+
+    if eval "(( \${#${array}[@]} > 0 ))"; then
+      eval "trailing_args+=(\"\${${array}[@]}\")"
+    fi
+  done
+  shift # remove -- separator
+
+  if (($# == 0)); then
+    printf "Error: append_arrays_as_args is missing a command or -- separator\n" >&2
+    return 1
+  fi
+
+  if ((${#trailing_args[@]} > 0)); then
+    "$@" "${trailing_args[@]}"
+  else
+    "$@"
+  fi
+}
+
+bats_format_file_line_reference() { # <output> <file> <line>
+  # shellcheck disable=SC2034 # will be used in subimplementation
+  local output="${1?}"
+  shift
+  "bats_format_file_line_reference_${BATS_LINE_REFERENCE_FORMAT?}" "$@"
+}
+
+bats_format_file_line_reference_comma_line() {
+  printf -v "$output" "%s, line %d" "$@"
+}
+
+bats_format_file_line_reference_colon() {
+  printf -v "$output" "%s:%d" "$@"
+}
+
+# approximate realpath without subshell
+bats_approx_realpath() { # <output-variable> <path>
+  local output=$1 path=$2
+  if [[ $path != /* ]]; then
+    path="$PWD/$path"
+  fi
+  # x/./y -> x/y
+  path=${path//\/.\//\/}
+  printf -v "$output" "%s" "$path"
+}
+
+bats_format_file_line_reference_uri() {
+  local filename=${1?} line=${2?}
+  bats_approx_realpath filename "$filename"
+  printf -v "$output" "file://%s:%d" "$filename" "$line"
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/formatter.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/formatter.bash
new file mode 100755
index 00000000..b774e167
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/formatter.bash
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+
+# reads (extended) bats tap streams from stdin and calls callback functions for each line
+#
+# Segmenting functions
+# ====================
+# bats_tap_stream_plan <number of tests>                                      -> when the test plan is encountered
+# bats_tap_stream_suite <file name>                                           -> when a new file is begun WARNING: extended only
+# bats_tap_stream_begin <test index> <test name>                              -> when a new test is begun WARNING: extended only
+#
+# Test result functions
+# =====================
+# If timing was enabled, BATS_FORMATTER_TEST_DURATION will be set to their duration in milliseconds
+# bats_tap_stream_ok <test index> <test name>                                 -> when a test was successful
+# bats_tap_stream_not_ok <test index> <test name>                             -> when a test has failed. If the failure was due to a timeout,
+#                                                                                BATS_FORMATTER_TEST_TIMEOUT is set to the timeout duration in seconds
+# bats_tap_stream_skipped <test index> <test name> <skip reason>              -> when a test was skipped
+#
+# Context functions
+# =================
+# bats_tap_stream_comment <comment text without leading '# '> <scope>         -> when a comment line was encountered,
+#                                                                                scope tells the last encountered of plan, begin, ok, not_ok, skipped, suite
+# bats_tap_stream_unknown <full line> <scope>                                 -> when a line is encountered that does not match the previous entries,
+#                                                                                scope @see bats_tap_stream_comment
+# forwards all input as is, when there is no TAP test plan header
+function bats_parse_internal_extended_tap() {
+  local header_pattern='[0-9]+\.\.[0-9]+'
+  IFS= read -r header
+
+  if [[ "$header" =~ $header_pattern ]]; then
+    bats_tap_stream_plan "${header:3}"
+  else
+    # If the first line isn't a TAP plan, print it and pass the rest through
+    printf '%s\n' "$header"
+    exec cat
+  fi
+
+  ok_line_regexpr="ok ([0-9]+) (.*)"
+  skip_line_regexpr="ok ([0-9]+) (.*) # skip( (.*))?$"
+  timeout_line_regexpr="not ok ([0-9]+) (.*) # timeout after ([0-9]+)s$"
+  not_ok_line_regexpr="not ok ([0-9]+) (.*)"
+
+  timing_expr="in ([0-9]+)ms$"
+  local test_name begin_index ok_index not_ok_index index scope
+  begin_index=0
+  index=0
+  scope=plan
+  while IFS= read -r line; do
+    unset BATS_FORMATTER_TEST_DURATION BATS_FORMATTER_TEST_TIMEOUT
+    case "$line" in
+    'begin '*) # this might only be called in extended tap output
+      ((++begin_index))
+      scope=begin
+      test_name="${line#* "$begin_index" }"
+      bats_tap_stream_begin "$begin_index" "$test_name"
+      ;;
+    'ok '*)
+      ((++index))
+      if [[ "$line" =~ $ok_line_regexpr ]]; then
+        ok_index="${BASH_REMATCH[1]}"
+        test_name="${BASH_REMATCH[2]}"
+        if [[ "$line" =~ $skip_line_regexpr ]]; then
+          scope=skipped
+          test_name="${BASH_REMATCH[2]}" # cut off name before "# skip"
+          local skip_reason="${BASH_REMATCH[4]}"
+          if [[ "$test_name" =~ $timing_expr ]]; then
+            local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
+            test_name="${test_name% in "${BATS_FORMATTER_TEST_DURATION}"ms}"
+            bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason"
+          else
+            bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason"
+          fi
+        else
+          scope=ok
+          if [[ "$line" =~ $timing_expr ]]; then
+            local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
+            bats_tap_stream_ok "$ok_index" "${test_name% in "${BASH_REMATCH[1]}"ms}"
+          else
+            bats_tap_stream_ok "$ok_index" "$test_name"
+          fi
+        fi
+      else
+        printf "ERROR: could not match ok line: %s" "$line" >&2
+        exit 1
+      fi
+      ;;
+    'not ok '*)
+      ((++index))
+      scope=not_ok
+      if [[ "$line" =~ $not_ok_line_regexpr ]]; then
+        not_ok_index="${BASH_REMATCH[1]}"
+        test_name="${BASH_REMATCH[2]}"
+        if [[ "$line" =~ $timeout_line_regexpr ]]; then
+          not_ok_index="${BASH_REMATCH[1]}"
+          test_name="${BASH_REMATCH[2]}"
+          # shellcheck disable=SC2034 # used in bats_tap_stream_ok
+          local BATS_FORMATTER_TEST_TIMEOUT="${BASH_REMATCH[3]}"
+        fi
+        if [[ "$test_name" =~ $timing_expr ]]; then
+          # shellcheck disable=SC2034 # used in bats_tap_stream_ok
+          local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
+          test_name="${test_name% in "${BASH_REMATCH[1]}"ms}"
+        fi
+        bats_tap_stream_not_ok "$not_ok_index" "$test_name"
+      else
+        printf "ERROR: could not match not ok line: %s" "$line" >&2
+        exit 1
+      fi
+      ;;
+    '# '*)
+      bats_tap_stream_comment "${line:2}" "$scope"
+      ;;
+    '#')
+      bats_tap_stream_comment "" "$scope"
+      ;;
+    'suite '*)
+      scope=suite
+      # pass on the
+      bats_tap_stream_suite "${line:6}"
+      ;;
+    *)
+      bats_tap_stream_unknown "$line" "$scope"
+      ;;
+    esac
+  done
+}
+
+normalize_base_path() { # <target variable> <base path>
+  # the relative path root to use for reporting filenames
+  # this is mainly intended for suite mode, where this will be the suite root folder
+  local base_path="$2"
+  # use the containing directory when --base-path is a file
+  if [[ ! -d "$base_path" ]]; then
+    base_path="$(dirname "$base_path")"
+  fi
+  # get the absolute path
+  base_path="$(cd "$base_path" && pwd)"
+  # ensure the path ends with / to strip that later on
+  if [[ "${base_path}" != *"/" ]]; then
+    base_path="$base_path/"
+  fi
+  printf -v "$1" "%s" "$base_path"
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/preprocessing.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/preprocessing.bash
new file mode 100755
index 00000000..5d9a7652
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/preprocessing.bash
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+BATS_TMPNAME="$BATS_RUN_TMPDIR/bats.$$"
+BATS_PARENT_TMPNAME="$BATS_RUN_TMPDIR/bats.$PPID"
+# shellcheck disable=SC2034
+BATS_OUT="${BATS_TMPNAME}.out" # used in bats-exec-file
+
+bats_preprocess_source() {
+  # export to make it visible to bats_evaluate_preprocessed_source
+  # since the latter runs in bats-exec-test's bash while this runs in bats-exec-file's
+  export BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
+  CHECK_BATS_COMMENT_COMMANDS=1 bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE"
+}
+
+bats_evaluate_preprocessed_source() {
+  if [[ -z "${BATS_TEST_SOURCE:-}" ]]; then
+    BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
+  fi
+  # Dynamically loaded user files provided outside of Bats.
+  # shellcheck disable=SC1090
+  source "$BATS_TEST_SOURCE"
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/semaphore.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/semaphore.bash
new file mode 100755
index 00000000..a196ac82
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/semaphore.bash
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+
+bats_run_under_flock() {
+  flock "$BATS_SEMAPHORE_DIR" "$@"
+}
+
+bats_run_under_shlock() {
+      local lockfile="$BATS_SEMAPHORE_DIR/shlock.lock"
+      while ! shlock -p $$ -f "$lockfile"; do
+        sleep 1
+      done
+      # we got the lock now, execute the command
+      "$@"
+      local status=$?
+      # free the lock
+      rm -f "$lockfile"
+      return $status
+    }
+
+# setup the semaphore environment for the loading file
+bats_semaphore_setup() {
+  export -f bats_semaphore_get_free_slot_count
+  export -f bats_semaphore_acquire_while_locked
+  export BATS_SEMAPHORE_DIR="$BATS_RUN_TMPDIR/semaphores"
+
+  if command -v flock >/dev/null; then
+    BATS_LOCKING_IMPLEMENTATION=flock
+  elif command -v shlock >/dev/null; then
+    BATS_LOCKING_IMPLEMENTATION=shlock
+  else
+    printf "ERROR: flock/shlock is required for parallelization within files!\n" >&2
+    exit 1
+  fi
+}
+
+# $1 - output directory for stdout/stderr
+# $@ - command to run
+# run the given command in a semaphore
+# block when there is no free slot for the semaphore
+# when there is a free slot, run the command in background
+# gather the output of the command in files in the given directory
+bats_semaphore_run() {
+  local output_dir=$1
+  shift
+  local semaphore_slot
+  semaphore_slot=$(bats_semaphore_acquire_slot)
+  bats_semaphore_release_wrapper "$output_dir" "$semaphore_slot" "$@" &
+  printf "%d\n" "$!"
+}
+
+# $1 - output directory for stdout/stderr
+# $@ - command to run
+# this wraps the actual function call to install some traps on exiting
+bats_semaphore_release_wrapper() {
+  local output_dir="$1"
+  local semaphore_name="$2"
+  shift 2 # all other parameters will be use for the command to execute
+
+  # shellcheck disable=SC2064 # we want to expand the semaphore_name right now!
+  trap "status=$?; bats_semaphore_release_slot '$semaphore_name'; exit $status" EXIT
+
+  mkdir -p "$output_dir"
+  "$@" 2>"$output_dir/stderr" >"$output_dir/stdout"
+  local status=$?
+
+  # bash bug: the exit trap is not called for the background process
+  bats_semaphore_release_slot "$semaphore_name"
+  trap - EXIT # avoid calling release twice
+  return $status
+}
+
+bats_semaphore_acquire_while_locked() {
+  if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
+    local slot=0
+    while [[ -e "$BATS_SEMAPHORE_DIR/slot-$slot" ]]; do
+      ((++slot))
+    done
+    if [[ $slot -lt $BATS_SEMAPHORE_NUMBER_OF_SLOTS ]]; then
+      touch "$BATS_SEMAPHORE_DIR/slot-$slot" && printf "%d\n" "$slot" && return 0
+    fi
+  fi
+  return 1
+}
+
+# block until a semaphore slot becomes free
+# prints the number of the slot that it received
+bats_semaphore_acquire_slot() {
+  mkdir -p "$BATS_SEMAPHORE_DIR"
+  # wait for a slot to become free
+  # TODO: avoid busy waiting by using signals -> this opens op prioritizing possibilities as well
+  while true; do
+    # don't lock for reading, we are fine with spuriously getting no free slot
+    if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
+      bats_run_under_"$BATS_LOCKING_IMPLEMENTATION" \
+        bash -c bats_semaphore_acquire_while_locked \
+      && break
+    fi
+    sleep 1
+  done
+}
+
+bats_semaphore_release_slot() {
+  # we don't need to lock this, since only our process owns this file
+  # and freeing a semaphore cannot lead to conflicts with others
+  rm "$BATS_SEMAPHORE_DIR/slot-$1" # this will fail if we had not acquired a semaphore!
+}
+
+bats_semaphore_get_free_slot_count() {
+  # find might error out without returning something useful when a file is deleted,
+  # while the directory is traversed ->  only continue when there was no error
+  until used_slots=$(find "$BATS_SEMAPHORE_DIR" -name 'slot-*' 2>/dev/null | wc -l); do :; done
+  echo $((BATS_SEMAPHORE_NUMBER_OF_SLOTS - used_slots))
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/test_functions.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/test_functions.bash
new file mode 100755
index 00000000..6ec6a918
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/test_functions.bash
@@ -0,0 +1,359 @@
+#!/usr/bin/env bash
+
+BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}"
+BATS_TEST_NAMES=()
+
+# shellcheck source=lib/bats-core/warnings.bash
+source "$BATS_ROOT/lib/bats-core/warnings.bash"
+
+# find_in_bats_lib_path echoes the first recognized load path to
+# a library in BATS_LIB_PATH or relative to BATS_TEST_DIRNAME.
+#
+# Libraries relative to BATS_TEST_DIRNAME take precedence over
+# BATS_LIB_PATH.
+#
+# Library load paths are recognized using find_library_load_path.
+#
+# If no library is found find_in_bats_lib_path returns 1.
+find_in_bats_lib_path() { # <return-var> <library-name>
+  local return_var="${1:?}"
+  local library_name="${2:?}"
+
+  local -a bats_lib_paths
+  IFS=: read -ra bats_lib_paths <<<"$BATS_LIB_PATH"
+
+  for path in "${bats_lib_paths[@]}"; do
+    if [[ -f "$path/$library_name" ]]; then
+      printf -v "$return_var" "%s" "$path/$library_name"
+      # A library load path was found, return
+      return 0
+    elif [[ -f "$path/$library_name/load.bash" ]]; then
+      printf -v "$return_var" "%s" "$path/$library_name/load.bash"
+      # A library load path was found, return
+      return 0
+    fi
+  done
+
+  return 1
+}
+
+# bats_internal_load expects an absolute path that is a library load path.
+#
+# If the library load path points to a file (a library loader) it is
+# sourced.
+#
+# If it points to a directory all files ending in .bash inside of the
+# directory are sourced.
+#
+# If the sourcing of the library loader or of a file in a library
+# directory fails bats_internal_load prints an error message and returns 1.
+#
+# If the passed library load path is not absolute or is not a valid file
+# or directory bats_internal_load prints an error message and returns 1.
+bats_internal_load() {
+  local library_load_path="${1:?}"
+
+  if [[ "${library_load_path:0:1}" != / ]]; then
+    printf "Passed library load path is not an absolute path: %s\n" "$library_load_path" >&2
+    return 1
+  fi
+
+  # library_load_path is a library loader
+  if [[ -f "$library_load_path" ]]; then
+    # shellcheck disable=SC1090
+    if ! source "$library_load_path"; then
+      printf "Error while sourcing library loader at '%s'\n" "$library_load_path" >&2
+      return 1
+    fi
+    return 0
+  fi
+
+  printf "Passed library load path is neither a library loader nor library directory: %s\n" "$library_load_path" >&2
+  return 1
+}
+
+# bats_load_safe accepts an argument called 'slug' and attempts to find and
+# source a library based on the slug.
+#
+# A slug can be an absolute path, a library name or a relative path.
+#
+# If the slug is an absolute path bats_load_safe attempts to find the library
+# load path using find_library_load_path.
+# What is considered a library load path is documented in the
+# documentation for find_library_load_path.
+#
+# If the slug is not an absolute path it is considered a library name or
+# relative path. bats_load_safe attempts to find the library load path using
+# find_in_bats_lib_path.
+#
+# If bats_load_safe can find a library load path it is passed to bats_internal_load.
+# If bats_internal_load fails bats_load_safe returns 1.
+#
+# If no library load path can be found bats_load_safe prints an error message
+# and returns 1.
+bats_load_safe() {
+  local slug="${1:?}"
+  if [[ ${slug:0:1} != / ]]; then # relative paths are relative to BATS_TEST_DIRNAME
+    slug="$BATS_TEST_DIRNAME/$slug"
+  fi
+
+  if [[ -f "$slug.bash" ]]; then
+    bats_internal_load "$slug.bash"
+    return $?
+  elif [[ -f "$slug" ]]; then
+    bats_internal_load "$slug"
+    return $?
+  fi
+
+  # loading from PATH (retained for backwards compatibility)
+  if [[ ! -f "$1" ]] && type -P "$1" >/dev/null; then
+    # shellcheck disable=SC1090
+    source "$1"
+    return $?
+  fi
+
+  # No library load path can be found
+  printf "bats_load_safe: Could not find '%s'[.bash]\n" "$slug" >&2
+  return 1
+}
+
+bats_load_library_safe() { # <slug>
+  local slug="${1:?}" library_path
+
+  # Check for library load paths in BATS_TEST_DIRNAME and BATS_LIB_PATH
+  if [[ ${slug:0:1} != / ]]; then
+    if ! find_in_bats_lib_path library_path "$slug"; then
+      printf "Could not find library '%s' relative to test file or in BATS_LIB_PATH\n" "$slug" >&2
+      return 1
+    fi
+  else
+    # absolute paths are taken as is
+    library_path="$slug"
+    if [[ ! -f "$library_path" ]]; then
+      printf "Could not find library on absolute path '%s'\n" "$library_path" >&2
+      return 1
+    fi
+  fi
+
+  bats_internal_load "$library_path"
+  return $?
+}
+
+# immediately exit on error, use bats_load_library_safe to catch and handle errors
+bats_load_library() { # <slug>
+  if ! bats_load_library_safe "$@"; then
+    exit 1
+  fi
+}
+
+# load acts like bats_load_safe but exits the shell instead of returning 1.
+load() {
+  if ! bats_load_safe "$@"; then
+    exit 1
+  fi
+}
+
+bats_redirect_stderr_into_file() {
+  "$@" 2>>"$bats_run_separate_stderr_file" # use >> to see collisions' content
+}
+
+bats_merge_stdout_and_stderr() {
+  "$@" 2>&1
+}
+
+# write separate lines from <input-var> into <output-array>
+bats_separate_lines() { # <output-array> <input-var>
+  local output_array_name="$1"
+  local input_var_name="$2"
+  if [[ $keep_empty_lines ]]; then
+    local bats_separate_lines_lines=()
+    if [[ -n "${!input_var_name}" ]]; then # avoid getting an empty line for empty input
+      while IFS= read -r line; do
+        bats_separate_lines_lines+=("$line")
+      done <<<"${!input_var_name}"
+    fi
+    eval "${output_array_name}=(\"\${bats_separate_lines_lines[@]}\")"
+  else
+    # shellcheck disable=SC2034,SC2206
+    IFS=$'\n' read -d '' -r -a "$output_array_name" <<<"${!input_var_name}" || true # don't fail due to EOF
+  fi
+}
+
+run() { # [!|-N] [--keep-empty-lines] [--separate-stderr] [--] <command to run...>
+  # This has to be restored on exit from this function to avoid leaking our trap INT into surrounding code.
+  # Non zero exits won't restore under the assumption that they will fail the test before it can be aborted,
+  # which allows us to avoid duplicating the restore code on every exit path
+  trap bats_interrupt_trap_in_run INT
+  local expected_rc=
+  local keep_empty_lines=
+  local output_case=merged
+  local has_flags=
+  # parse options starting with -
+  while [[ $# -gt 0 ]] && [[ $1 == -* || $1 == '!' ]]; do
+    has_flags=1
+    case "$1" in
+    '!')
+      expected_rc=-1
+      ;;
+    -[0-9]*)
+      expected_rc=${1#-}
+      if [[ $expected_rc =~ [^0-9] ]]; then
+        printf "Usage error: run: '-NNN' requires numeric NNN (got: %s)\n" "$expected_rc" >&2
+        return 1
+      elif [[ $expected_rc -gt 255 ]]; then
+        printf "Usage error: run: '-NNN': NNN must be <= 255 (got: %d)\n" "$expected_rc" >&2
+        return 1
+      fi
+      ;;
+    --keep-empty-lines)
+      keep_empty_lines=1
+      ;;
+    --separate-stderr)
+      output_case="separate"
+      ;;
+    --)
+      shift # eat the -- before breaking away
+      break
+      ;;
+    *)
+      printf "Usage error: unknown flag '%s'" "$1" >&2
+      return 1
+      ;;
+    esac
+    shift
+  done
+
+  if [[ -n $has_flags ]]; then
+    bats_warn_minimum_guaranteed_version "Using flags on \`run\`" 1.5.0
+  fi
+
+  local pre_command=
+
+  case "$output_case" in
+  merged) # redirects stderr into stdout and fills only $output/$lines
+    pre_command=bats_merge_stdout_and_stderr
+    ;;
+  separate) # splits stderr into own file and fills $stderr/$stderr_lines too
+    local bats_run_separate_stderr_file
+    bats_run_separate_stderr_file="$(mktemp "${BATS_TEST_TMPDIR}/separate-stderr-XXXXXX")"
+    pre_command=bats_redirect_stderr_into_file
+    ;;
+  esac
+
+  local origFlags="$-"
+  set +eET
+  if [[ $keep_empty_lines ]]; then
+    # 'output', 'status', 'lines' are global variables available to tests.
+    # preserve trailing newlines by appending . and removing it later
+    # shellcheck disable=SC2034
+    output="$(
+      "$pre_command" "$@"
+      status=$?
+      printf .
+      exit $status
+    )" && status=0 || status=$?
+    output="${output%.}"
+  else
+    # 'output', 'status', 'lines' are global variables available to tests.
+    # shellcheck disable=SC2034
+    output="$("$pre_command" "$@")" && status=0 || status=$?
+  fi
+
+  bats_separate_lines lines output
+
+  if [[ "$output_case" == separate ]]; then
+    # shellcheck disable=SC2034
+    read -d '' -r stderr <"$bats_run_separate_stderr_file" || true
+    bats_separate_lines stderr_lines stderr
+  fi
+
+  # shellcheck disable=SC2034
+  BATS_RUN_COMMAND="${*}"
+  set "-$origFlags"
+
+  bats_run_print_output() {
+    if [[ -n "$output" ]]; then
+      printf "%s\n" "$output"
+    fi
+    if [[ "$output_case" == separate && -n "$stderr" ]]; then
+      printf "stderr:\n%s\n" "$stderr"
+    fi
+  }
+
+  if [[ -n "$expected_rc" ]]; then
+    if [[ "$expected_rc" = "-1" ]]; then
+      if [[ "$status" -eq 0 ]]; then
+        BATS_ERROR_SUFFIX=", expected nonzero exit code!"
+        bats_run_print_output
+        return 1
+      fi
+    elif [ "$status" -ne "$expected_rc" ]; then
+      # shellcheck disable=SC2034
+      BATS_ERROR_SUFFIX=", expected exit code $expected_rc, got $status"
+      bats_run_print_output
+      return 1
+    fi
+  elif [[ "$status" -eq 127 ]]; then # "command not found"
+    bats_generate_warning 1 "$BATS_RUN_COMMAND"
+  fi
+
+  if [[ ${BATS_VERBOSE_RUN:-} ]]; then
+    bats_run_print_output
+  fi
+  
+  # don't leak our trap into surrounding code
+  trap bats_interrupt_trap INT
+}
+
+setup() {
+  return 0
+}
+
+teardown() {
+  return 0
+}
+
+skip() {
+  # if this is a skip in teardown ...
+  if [[ -n "${BATS_TEARDOWN_STARTED-}" ]]; then
+    # ... we want to skip the rest of teardown.
+    # communicate to bats_exit_trap that the teardown was completed without error
+    # shellcheck disable=SC2034
+    BATS_TEARDOWN_COMPLETED=1
+    # if we are already in the exit trap (e.g. due to previous skip) ...
+    if [[ "$BATS_TEARDOWN_STARTED" == as-exit-trap ]]; then
+      # ... we need to do the rest of the tear_down_trap that would otherwise be skipped after the next call to exit
+      bats_exit_trap
+      # and then do the exit (at the end of this function)
+    fi
+    # if we aren't in exit trap, the normal exit handling should suffice
+  else
+    # ... this is either skip in test or skip in setup.
+    # Following variables are used in bats-exec-test which sources this file
+    # shellcheck disable=SC2034
+    BATS_TEST_SKIPPED="${1:-1}"
+    # shellcheck disable=SC2034
+    BATS_TEST_COMPLETED=1
+  fi
+  exit 0
+}
+
+bats_test_begin() {
+  BATS_TEST_DESCRIPTION="$1"
+  if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then
+    printf 'begin %d %s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}$BATS_TEST_DESCRIPTION" >&3
+  fi
+  setup
+}
+
+bats_test_function() {
+  local test_name="$1"
+  BATS_TEST_NAMES+=("$test_name")
+}
+
+# decides whether a failed test should be run again
+bats_should_retry_test() {
+  # test try number starts at 1
+  # 0 retries means run only first try
+  ((BATS_TEST_TRY_NUMBER <= BATS_TEST_RETRIES))
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/tracing.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/tracing.bash
new file mode 100755
index 00000000..0e94ec30
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/tracing.bash
@@ -0,0 +1,399 @@
+#!/usr/bin/env bash
+
+# shellcheck source=lib/bats-core/common.bash
+source "$BATS_ROOT/lib/bats-core/common.bash"
+
+bats_capture_stack_trace() {
+  local test_file
+  local funcname
+  local i
+
+  BATS_DEBUG_LAST_STACK_TRACE=()
+
+  for ((i = 2; i != ${#FUNCNAME[@]}; ++i)); do
+    # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
+    # calling an exported function erases the test file's BASH_SOURCE entry.
+    test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
+    funcname="${FUNCNAME[$i]}"
+    BATS_DEBUG_LAST_STACK_TRACE+=("${BASH_LINENO[$((i - 1))]} $funcname $test_file")
+    case "$funcname" in
+    "${BATS_TEST_NAME-}" | setup | teardown | setup_file | teardown_file | setup_suite | teardown_suite)
+      break
+      ;;
+    esac
+    if [[ "${BASH_SOURCE[$i + 1]:-}" == *"bats-exec-file" ]] && [[ "$funcname" == 'source' ]]; then
+      break
+    fi
+  done
+}
+
+bats_get_failure_stack_trace() {
+  local stack_trace_var
+  # See bats_debug_trap for details.
+  if [[ -n "${BATS_DEBUG_LAST_STACK_TRACE_IS_VALID:-}" ]]; then
+    stack_trace_var=BATS_DEBUG_LAST_STACK_TRACE
+  else
+    stack_trace_var=BATS_DEBUG_LASTLAST_STACK_TRACE
+  fi
+  # shellcheck disable=SC2016
+  eval "$(printf \
+    '%s=(${%s[@]+"${%s[@]}"})' \
+    "${1}" \
+    "${stack_trace_var}" \
+    "${stack_trace_var}")"
+}
+
+bats_print_stack_trace() {
+  local frame
+  local index=1
+  local count="${#@}"
+  local filename
+  local lineno
+
+  for frame in "$@"; do
+    bats_frame_filename "$frame" 'filename'
+    bats_trim_filename "$filename" 'filename'
+    bats_frame_lineno "$frame" 'lineno'
+
+    printf '%s' "${BATS_STACK_TRACE_PREFIX-# }"
+    if [[ $index -eq 1 ]]; then
+      printf '('
+    else
+      printf ' '
+    fi
+
+    local fn
+    bats_frame_function "$frame" 'fn'
+    if [[ "$fn" != "${BATS_TEST_NAME-}" ]] &&
+      # don't print "from function `source'"",
+      # when failing in free code during `source $test_file` from bats-exec-file
+      ! [[ "$fn" == 'source' && $index -eq $count ]]; then
+      local quoted_fn
+      bats_quote_code quoted_fn "$fn"
+      printf "from function %s " "$quoted_fn"
+    fi
+
+    local reference
+    bats_format_file_line_reference reference "$filename" "$lineno"
+    if [[ $index -eq $count ]]; then
+      printf 'in test file %s)\n' "$reference"
+    else
+      printf 'in file %s,\n' "$reference"
+    fi
+
+    ((++index))
+  done
+}
+
+bats_print_failed_command() {
+  local stack_trace=("${@}")
+  if [[ ${#stack_trace[@]} -eq 0 ]]; then
+    return 0
+  fi
+  local frame="${stack_trace[${#stack_trace[@]} - 1]}"
+  local filename
+  local lineno
+  local failed_line
+  local failed_command
+
+  bats_frame_filename "$frame" 'filename'
+  bats_frame_lineno "$frame" 'lineno'
+  bats_extract_line "$filename" "$lineno" 'failed_line'
+  bats_strip_string "$failed_line" 'failed_command'
+  local quoted_failed_command
+  bats_quote_code quoted_failed_command "$failed_command"
+  printf '#   %s ' "${quoted_failed_command}"
+
+  if [[ "${BATS_TIMED_OUT-NOTSET}" != NOTSET ]]; then
+    # the other values can be safely overwritten here,
+    # as the timeout is the primary reason for failure
+    BATS_ERROR_SUFFIX=" due to timeout"
+  fi
+
+  if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then
+    printf 'failed%s\n' "$BATS_ERROR_SUFFIX"
+  else
+    printf 'failed with status %d%s\n' "$BATS_ERROR_STATUS" "$BATS_ERROR_SUFFIX"
+  fi
+}
+
+bats_frame_lineno() {
+  printf -v "$2" '%s' "${1%% *}"
+}
+
+bats_frame_function() {
+  local __bff_function="${1#* }"
+  printf -v "$2" '%s' "${__bff_function%% *}"
+}
+
+bats_frame_filename() {
+  local __bff_filename="${1#* }"
+  __bff_filename="${__bff_filename#* }"
+
+  if [[ "$__bff_filename" == "${BATS_TEST_SOURCE-}" ]]; then
+    __bff_filename="$BATS_TEST_FILENAME"
+  fi
+  printf -v "$2" '%s' "$__bff_filename"
+}
+
+bats_extract_line() {
+  local __bats_extract_line_line
+  local __bats_extract_line_index=0
+
+  while IFS= read -r __bats_extract_line_line; do
+    if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
+      printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
+      break
+    fi
+  done <"$1"
+}
+
+bats_strip_string() {
+  [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
+  printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+}
+
+bats_trim_filename() {
+  printf -v "$2" '%s' "${1#"$BATS_CWD"/}"
+}
+
+# normalize a windows path from e.g. C:/directory to /c/directory
+# The path must point to an existing/accessable directory, not a file!
+bats_normalize_windows_dir_path() { # <output-var> <path>
+  local output_var="$1" path="$2"
+  if [[ "$output_var" != NORMALIZED_INPUT ]]; then
+    local NORMALIZED_INPUT
+  fi
+  if [[ $path == ?:* ]]; then
+    NORMALIZED_INPUT="$(
+      cd "$path" || exit 1
+      pwd
+    )"
+  else
+    NORMALIZED_INPUT="$path"
+  fi
+  printf -v "$output_var" "%s" "$NORMALIZED_INPUT"
+}
+
+bats_emit_trace() {
+  if [[ $BATS_TRACE_LEVEL -gt 0 ]]; then
+    local line=${BASH_LINENO[1]}
+    # shellcheck disable=SC2016
+    if [[ $BASH_COMMAND != '"$BATS_TEST_NAME" >> "$BATS_OUT" 2>&1 4>&1' && $BASH_COMMAND != "bats_test_begin "* ]] && # don't emit these internal calls
+      [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || $line != "$BATS_LAST_BASH_LINENO" ]] &&
+      # avoid printing a function twice (at call site and at definition site)
+      [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || ${BASH_LINENO[2]} != "$BATS_LAST_BASH_LINENO" || ${BASH_SOURCE[3]} != "$BATS_LAST_BASH_SOURCE" ]]; then
+      local file="${BASH_SOURCE[2]}" # index 2: skip over bats_emit_trace and bats_debug_trap
+      if [[ $file == "${BATS_TEST_SOURCE}" ]]; then
+        file="$BATS_TEST_FILENAME"
+      fi
+      local padding='$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$'
+      if ((BATS_LAST_STACK_DEPTH != ${#BASH_LINENO[@]})); then
+        local reference
+        bats_format_file_line_reference reference "${file##*/}" "$line"
+        printf '%s [%s]\n' "${padding::${#BASH_LINENO[@]}-4}" "$reference" >&4
+      fi
+      printf '%s %s\n' "${padding::${#BASH_LINENO[@]}-4}" "$BASH_COMMAND" >&4
+      BATS_LAST_BASH_COMMAND="$BASH_COMMAND"
+      BATS_LAST_BASH_LINENO="$line"
+      BATS_LAST_BASH_SOURCE="${BASH_SOURCE[2]}"
+      BATS_LAST_STACK_DEPTH="${#BASH_LINENO[@]}"
+    fi
+  fi
+}
+
+# bats_debug_trap tracks the last line of code executed within a test. This is
+# necessary because $BASH_LINENO is often incorrect inside of ERR and EXIT
+# trap handlers.
+#
+# Below are tables describing different command failure scenarios and the
+# reliability of $BASH_LINENO within different the executed DEBUG, ERR, and EXIT
+# trap handlers. Naturally, the behaviors change between versions of Bash.
+#
+# Table rows should be read left to right. For example, on bash version
+# 4.0.44(2)-release, if a test executes `false` (or any other failing external
+# command), bash will do the following in order:
+# 1. Call the DEBUG trap handler (bats_debug_trap) with $BASH_LINENO referring
+#    to the source line containing the `false` command, then
+# 2. Call the DEBUG trap handler again, but with an incorrect $BASH_LINENO, then
+# 3. Call the ERR trap handler, but with a (possibly-different) incorrect
+#    $BASH_LINENO, then
+# 4. Call the DEBUG trap handler again, but with $BASH_LINENO set to 1, then
+# 5. Call the EXIT trap handler, with $BASH_LINENO set to 1.
+#
+# bash version 4.4.20(1)-release
+#  command     | first DEBUG | second DEBUG | ERR     | third DEBUG | EXIT
+# -------------+-------------+--------------+---------+-------------+--------
+#  false       | OK          | OK           | OK      | BAD[1]      | BAD[1]
+#  [[ 1 = 2 ]] | OK          | BAD[2]       | BAD[2]  | BAD[1]      | BAD[1]
+#  (( 1 = 2 )) | OK          | BAD[2]       | BAD[2]  | BAD[1]      | BAD[1]
+#  ! true      | OK          | ---          | BAD[4]  | ---         | BAD[1]
+#  $var_dne    | OK          | ---          | ---     | BAD[1]      | BAD[1]
+#  source /dne | OK          | ---          | ---     | BAD[1]      | BAD[1]
+#
+# bash version 4.0.44(2)-release
+#  command     | first DEBUG | second DEBUG | ERR     | third DEBUG | EXIT
+# -------------+-------------+--------------+---------+-------------+--------
+#  false       | OK          | BAD[3]       | BAD[3]  | BAD[1]      | BAD[1]
+#  [[ 1 = 2 ]] | OK          | ---          | BAD[3]  | ---         | BAD[1]
+#  (( 1 = 2 )) | OK          | ---          | BAD[3]  | ---         | BAD[1]
+#  ! true      | OK          | ---          | BAD[3]  | ---         | BAD[1]
+#  $var_dne    | OK          | ---          | ---     | BAD[1]      | BAD[1]
+#  source /dne | OK          | ---          | ---     | BAD[1]      | BAD[1]
+#
+# [1] The reported line number is always 1.
+# [2] The reported source location is that of the beginning of the function
+#     calling the command.
+# [3] The reported line is that of the last command executed in the DEBUG trap
+#     handler.
+# [4] The reported source location is that of the call to the function calling
+#     the command.
+bats_debug_trap() {
+  # on windows we sometimes get a mix of paths (when install via nmp install -g)
+  # which have C:/... or /c/... comparing them is going to be problematic.
+  # We need to normalize them to a common format!
+  local NORMALIZED_INPUT
+  bats_normalize_windows_dir_path NORMALIZED_INPUT "${1%/*}"
+  local file_excluded='' path
+  for path in "${BATS_DEBUG_EXCLUDE_PATHS[@]}"; do
+    if [[ "$NORMALIZED_INPUT" == "$path"* ]]; then
+      file_excluded=1
+      break
+    fi
+  done
+
+  # don't update the trace within library functions or we get backtraces from inside traps
+  # also don't record new stack traces while handling interruptions, to avoid overriding the interrupted command
+  if [[ -z "$file_excluded" &&
+    "${BATS_INTERRUPTED-NOTSET}" == NOTSET &&
+    "${BATS_TIMED_OUT-NOTSET}" == NOTSET ]]; then
+    BATS_DEBUG_LASTLAST_STACK_TRACE=(
+      ${BATS_DEBUG_LAST_STACK_TRACE[@]+"${BATS_DEBUG_LAST_STACK_TRACE[@]}"}
+    )
+
+    BATS_DEBUG_LAST_LINENO=(${BASH_LINENO[@]+"${BASH_LINENO[@]}"})
+    BATS_DEBUG_LAST_SOURCE=(${BASH_SOURCE[@]+"${BASH_SOURCE[@]}"})
+    bats_capture_stack_trace
+    bats_emit_trace
+  fi
+}
+
+# For some versions of Bash, the `ERR` trap may not always fire for every
+# command failure, but the `EXIT` trap will. Also, some command failures may not
+# set `$?` properly. See #72 and #81 for details.
+#
+# For this reason, we call `bats_check_status_from_trap` at the very beginning
+# of `bats_teardown_trap` and check the value of `$BATS_TEST_COMPLETED` before
+# taking other actions. We also adjust the exit status value if needed.
+#
+# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
+# isn't set properly during `teardown()` errors.
+bats_check_status_from_trap() {
+  local status="$?"
+  if [[ -z "${BATS_TEST_COMPLETED:-}" ]]; then
+    BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
+    if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
+      BATS_ERROR_STATUS=1
+    fi
+    trap - DEBUG
+  fi
+}
+
+bats_add_debug_exclude_path() { # <path>
+  if [[ -z "$1" ]]; then        # don't exclude everything
+    printf "bats_add_debug_exclude_path: Exclude path must not be empty!\n" >&2
+    return 1
+  fi
+  if [[ "$OSTYPE" == cygwin || "$OSTYPE" == msys ]]; then
+    local normalized_dir
+    bats_normalize_windows_dir_path normalized_dir "$1"
+    BATS_DEBUG_EXCLUDE_PATHS+=("$normalized_dir")
+  else
+    BATS_DEBUG_EXCLUDE_PATHS+=("$1")
+  fi
+}
+
+bats_setup_tracing() {
+  # Variables for capturing accurate stack traces. See bats_debug_trap for
+  # details.
+  #
+  # BATS_DEBUG_LAST_LINENO, BATS_DEBUG_LAST_SOURCE, and
+  # BATS_DEBUG_LAST_STACK_TRACE hold data from the most recent call to
+  # bats_debug_trap.
+  #
+  # BATS_DEBUG_LASTLAST_STACK_TRACE holds data from two bats_debug_trap calls
+  # ago.
+  #
+  # BATS_DEBUG_LAST_STACK_TRACE_IS_VALID indicates that
+  # BATS_DEBUG_LAST_STACK_TRACE contains the stack trace of the test's error. If
+  # unset, BATS_DEBUG_LAST_STACK_TRACE is unreliable and
+  # BATS_DEBUG_LASTLAST_STACK_TRACE should be used instead.
+  BATS_DEBUG_LASTLAST_STACK_TRACE=()
+  BATS_DEBUG_LAST_LINENO=()
+  BATS_DEBUG_LAST_SOURCE=()
+  BATS_DEBUG_LAST_STACK_TRACE=()
+  BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=
+  BATS_ERROR_SUFFIX=
+  BATS_DEBUG_EXCLUDE_PATHS=()
+  # exclude some paths by default
+  bats_add_debug_exclude_path "$BATS_ROOT/lib/"
+  bats_add_debug_exclude_path "$BATS_ROOT/libexec/"
+
+  exec 4<&1 # used for tracing
+  if [[ "${BATS_TRACE_LEVEL:-0}" -gt 0 ]]; then
+    # avoid undefined variable errors
+    BATS_LAST_BASH_COMMAND=
+    BATS_LAST_BASH_LINENO=
+    BATS_LAST_BASH_SOURCE=
+    BATS_LAST_STACK_DEPTH=
+    # try to exclude helper libraries if found, this is only relevant for tracing
+    while read -r path; do
+      bats_add_debug_exclude_path "$path"
+    done < <(find "$PWD" -type d -name bats-assert -o -name bats-support)
+  fi
+
+  local exclude_paths path
+  # exclude user defined libraries
+  IFS=':' read -r exclude_paths <<<"${BATS_DEBUG_EXCLUDE_PATHS:-}"
+  for path in "${exclude_paths[@]}"; do
+    if [[ -n "$path" ]]; then
+      bats_add_debug_exclude_path "$path"
+    fi
+  done
+
+  # turn on traps after setting excludes to avoid tracing the exclude setup
+  trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG
+  trap 'bats_error_trap' ERR
+}
+
+bats_error_trap() {
+  bats_check_status_from_trap
+
+  # If necessary, undo the most recent stack trace captured by bats_debug_trap.
+  # See bats_debug_trap for details.
+  if [[ "${BASH_LINENO[*]}" = "${BATS_DEBUG_LAST_LINENO[*]:-}" &&
+    "${BASH_SOURCE[*]}" = "${BATS_DEBUG_LAST_SOURCE[*]:-}" &&
+    -z "$BATS_DEBUG_LAST_STACK_TRACE_IS_VALID" ]]; then
+    BATS_DEBUG_LAST_STACK_TRACE=(
+      ${BATS_DEBUG_LASTLAST_STACK_TRACE[@]+"${BATS_DEBUG_LASTLAST_STACK_TRACE[@]}"}
+    )
+  fi
+  BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
+}
+
+bats_interrupt_trap() {
+  # mark the interruption, to handle during exit
+  BATS_INTERRUPTED=true
+  BATS_ERROR_STATUS=130
+  # debug trap fires before interrupt trap but gets wrong linenumber (line 1)
+  # -> use last stack trace instead of BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=true
+}
+
+# this is used inside run()
+bats_interrupt_trap_in_run() {
+  # mark the interruption, to handle during exit
+  BATS_INTERRUPTED=true
+  BATS_ERROR_STATUS=130
+  BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=true
+  exit 130
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/validator.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/validator.bash
new file mode 100755
index 00000000..59fc2c1e
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/validator.bash
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+bats_test_count_validator() {
+  trap '' INT # continue forwarding
+  header_pattern='[0-9]+\.\.[0-9]+'
+  IFS= read -r header
+  # repeat the header
+  printf "%s\n" "$header"
+
+  # if we detect a TAP plan
+  if [[ "$header" =~ $header_pattern ]]; then
+    # extract the number of tests ...
+    local expected_number_of_tests="${header:3}"
+    # ... count the actual number of [not ] oks...
+    local actual_number_of_tests=0
+    while IFS= read -r line; do
+      # forward line
+      printf "%s\n" "$line"
+      case "$line" in
+      'ok '*)
+        ((++actual_number_of_tests))
+        ;;
+      'not ok'*)
+        ((++actual_number_of_tests))
+        ;;
+      esac
+    done
+    # ... and error if they are not the same
+    if [[ "${actual_number_of_tests}" != "${expected_number_of_tests}" ]]; then
+      printf '# bats warning: Executed %s instead of expected %s tests\n' "$actual_number_of_tests" "$expected_number_of_tests"
+      return 1
+    fi
+  else
+    # forward output unchanged
+    cat
+  fi
+}
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/warnings.bash b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/warnings.bash
new file mode 100755
index 00000000..fbb5186a
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/lib/bats-core/warnings.bash
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+# shellcheck source=lib/bats-core/tracing.bash
+source "$BATS_ROOT/lib/bats-core/tracing.bash"
+
+# generate a warning report for the parent call's call site
+bats_generate_warning() { # <warning number> [--no-stacktrace] [<printf args for warning string>...]
+  local warning_number="${1-}" padding="00"
+  shift
+  local no_stacktrace=
+  if [[ ${1-} == --no-stacktrace ]]; then
+    no_stacktrace=1
+    shift
+  fi
+  if [[ $warning_number =~ [0-9]+ ]] && ((warning_number < ${#BATS_WARNING_SHORT_DESCS[@]})); then
+    {
+      printf "BW%s: ${BATS_WARNING_SHORT_DESCS[$warning_number]}\n" "${padding:${#warning_number}}${warning_number}" "$@"
+      if [[ -z "$no_stacktrace" ]]; then
+        bats_capture_stack_trace
+        BATS_STACK_TRACE_PREFIX='      ' bats_print_stack_trace "${BATS_DEBUG_LAST_STACK_TRACE[@]}"
+      fi
+    } >>"$BATS_WARNING_FILE" 2>&3
+  else
+    printf "Invalid Bats warning number '%s'. It must be an integer between 1 and %d." "$warning_number" "$((${#BATS_WARNING_SHORT_DESCS[@]} - 1))" >&2
+    exit 1
+  fi
+}
+
+# generate a warning if the BATS_GUARANTEED_MINIMUM_VERSION is not high enough
+bats_warn_minimum_guaranteed_version() { # <feature> <minimum required version>
+  if bats_version_lt "$BATS_GUARANTEED_MINIMUM_VERSION" "$2"; then
+    bats_generate_warning 2 "$1" "$2" "$2"
+  fi
+}
+
+# put after functions to avoid line changes in tests when new ones get added
+BATS_WARNING_SHORT_DESCS=(
+  # to start with 1
+  'PADDING'
+  # see issue #578 for context
+  "\`run\`'s command \`%s\` exited with code 127, indicating 'Command not found'. Use run's return code checks, e.g. \`run -127\`, to fix this message."
+  "%s requires at least BATS_VERSION=%s. Use \`bats_require_minimum_version %s\` to fix this message."
+  "\`setup_suite\` is visible to test file '%s', but was not executed. It belongs into 'setup_suite.bash' to be picked up automatically."
+)
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats
new file mode 100755
index 00000000..9b5f0c43
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats
@@ -0,0 +1,504 @@
+#!/usr/bin/env bash
+set -e
+
+export BATS_VERSION='1.9.0'
+VALID_FORMATTERS="pretty, junit, tap, tap13"
+
+version() {
+  printf 'Bats %s\n' "$BATS_VERSION"
+}
+
+abort() {
+  local print_usage=1
+  if [[ ${1:-} == --no-print-usage ]]; then
+    print_usage=
+    shift
+  fi
+  printf 'Error: %s\n' "$1" >&2
+  if [[ -n $print_usage ]]; then
+    usage >&2
+  fi
+  exit 1
+}
+
+usage() {
+  local cmd="${0##*/}"
+  local line
+
+  cat <<HELP_TEXT_HEADER
+Usage: ${cmd} [OPTIONS] <tests>
+       ${cmd} [-h | -v]
+
+HELP_TEXT_HEADER
+
+  cat <<'HELP_TEXT_BODY'
+  <tests> is the path to a Bats test file, or the path to a directory
+  containing Bats test files (ending with ".bats")
+
+  -c, --count               Count test cases without running any tests
+  --code-quote-style <style>
+                            A two character string of code quote delimiters
+                            or 'custom' which requires setting $BATS_BEGIN_CODE_QUOTE and 
+                            $BATS_END_CODE_QUOTE. Can also be set via $BATS_CODE_QUOTE_STYLE
+  --line-reference-format   Controls how file/line references e.g. in stack traces are printed:
+                              - comma_line (default): a.bats, line 1
+                              - colon:  a.bats:1
+                              - uri: file:///tests/a.bats:1
+                              - custom: provide your own via defining bats_format_file_line_reference_custom
+                                        with parameters <filename> <line>, store via `printf -v "$output"`
+  -f, --filter <regex>      Only run tests that match the regular expression
+  --filter-status <status>  Only run tests with the given status in the last completed (no CTRL+C/SIGINT) run.
+                            Valid <status> values are:
+                              failed - runs tests that failed or were not present in the last run
+                              missed - runs tests that were not present in the last run
+  --filter-tags <comma-separated-tag-list>
+                            Only run tests that match all the tags in the list (&&).
+                            You can negate a tag via prepending '!'.
+                            Specifying this flag multiple times allows for logical or (||):
+                            `--filter-tags A,B --filter-tags A,!C` matches tags (A && B) || (A && !C)
+  -F, --formatter <type>    Switch between formatters: pretty (default),
+                              tap (default w/o term), tap13, junit, /<absolute path to formatter>
+  --gather-test-outputs-in <directory>
+                            Gather the output of failing *and* passing tests
+                            as files in directory (if existing, must be empty)
+  -h, --help                Display this help message
+  -j, --jobs <jobs>         Number of parallel jobs (requires GNU parallel)
+  --no-tempdir-cleanup      Preserve test output temporary directory
+  --no-parallelize-across-files
+                            Serialize test file execution instead of running
+                            them in parallel (requires --jobs >1)
+  --no-parallelize-within-files
+                            Serialize test execution within files instead of
+                            running them in parallel (requires --jobs >1)
+  --report-formatter <type> Switch between reporters (same options as --formatter)
+  -o, --output <dir>        Directory to write report files (must exist)
+  -p, --pretty              Shorthand for "--formatter pretty"
+  --print-output-on-failure Automatically print the value of `$output` on failed tests
+  -r, --recursive           Include tests in subdirectories
+  --show-output-of-passing-tests
+                            Print output of passing tests
+  -t, --tap                 Shorthand for "--formatter tap"
+  -T, --timing              Add timing information to tests
+  -x, --trace               Print test commands as they are executed (like `set -x`)
+  --verbose-run             Make `run` print `$output` by default
+  -v, --version             Display the version number
+
+  For more information, see https://github.com/bats-core/bats-core
+HELP_TEXT_BODY
+}
+
+expand_path() {
+  local path="${1%/}"
+  local dirname="${path%/*}"
+  if [[ -z "$dirname" ]]; then
+    dirname=/
+  fi
+  local result="$2"
+  local OLDPWD="$PWD"
+
+  if [[ "$dirname" == "$path" ]]; then
+    dirname="$PWD"
+  else
+    cd "$dirname"
+    dirname="$PWD"
+    cd "$OLDPWD"
+  fi
+  printf -v "$result" '%s/%s' "$dirname" "${path##*/}"
+}
+
+BATS_LIBEXEC="$(
+  cd "$(dirname "$(bats_readlinkf "${BASH_SOURCE[0]}")")"
+  pwd
+)"
+export BATS_LIBEXEC
+export BATS_CWD="$PWD"
+export BATS_TEST_FILTER=
+export PATH="$BATS_LIBEXEC:$PATH"
+export BATS_ROOT_PID=$$
+export BATS_TMPDIR="${TMPDIR:-/tmp}"
+BATS_TMPDIR=${BATS_TMPDIR%/} # chop off trailing / to avoid duplication
+export BATS_RUN_TMPDIR=
+export BATS_GUARANTEED_MINIMUM_VERSION=0.0.0
+export BATS_LIB_PATH=${BATS_LIB_PATH-/usr/lib/bats}
+BATS_REPORT_OUTPUT_DIR=${BATS_REPORT_OUTPUT_DIR-.}
+export BATS_LINE_REFERENCE_FORMAT=${BATS_LINE_REFERENCE_FORMAT-comma_line}
+
+if [[ ! -d "${BATS_TMPDIR}" ]]; then
+  printf "Error: BATS_TMPDIR (%s) does not exist or is not a directory" "${BATS_TMPDIR}" >&2
+  exit 1
+elif [[ ! -w "${BATS_TMPDIR}" ]]; then
+  printf "Error: BATS_TMPDIR (%s) is not writable" "${BATS_TMPDIR}" >&2
+  exit 1
+fi
+
+arguments=()
+
+# Unpack single-character options bundled together, e.g. -cr, -pr.
+for arg in "$@"; do
+  if [[ "$arg" =~ ^-[^-]. ]]; then
+    index=1
+    while option="${arg:$((index++)):1}"; do
+      if [[ -z "$option" ]]; then
+        break
+      fi
+      arguments+=("-$option")
+    done
+  else
+    arguments+=("$arg")
+  fi
+  shift
+done
+
+set -- "${arguments[@]}"
+arguments=()
+
+unset flags recursive formatter_flags
+flags=('--dummy-flag')           # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
+formatter_flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
+formatter=${BATS_FORMATTER:-'tap'}
+report_formatter=''
+recursive=
+setup_suite_file=''
+export BATS_TEMPDIR_CLEANUP=1
+if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
+  formatter='pretty'
+fi
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  -h | --help)
+    version
+    usage
+    exit 0
+    ;;
+  -v | --version)
+    version
+    exit 0
+    ;;
+  -c | --count)
+    flags+=('-c')
+    ;;
+  -f | --filter)
+    shift
+    flags+=('-f' "$1")
+    ;;
+  -F | --formatter)
+    shift
+    # allow cat formatter to see extended output but don't advertise to users
+    if [[ $1 =~ ^(pretty|junit|tap|tap13|cat|/.*)$ ]]; then
+      formatter="$1"
+    else
+      printf "Unknown formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
+      exit 1
+    fi
+    ;;
+  --report-formatter)
+    shift
+    if [[ $1 =~ ^(cat|pretty|junit|tap|tap13|/.*)$ ]]; then
+      report_formatter="$1"
+    else
+      printf "Unknown report formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
+      exit 1
+    fi
+    ;;
+  -o | --output)
+    shift
+    BATS_REPORT_OUTPUT_DIR="$1"
+    ;;
+  -p | --pretty)
+    formatter='pretty'
+    ;;
+  -j | --jobs)
+    shift
+    flags+=('-j' "$1")
+    ;;
+  -r | --recursive)
+    recursive=1
+    ;;
+  -t | --tap)
+    formatter='tap'
+    ;;
+  -T | --timing)
+    flags+=('-T')
+    formatter_flags+=('-T')
+    ;;
+  # this flag is now a no-op, as it is the parallel default
+  --parallel-preserve-environment) ;;
+
+  --no-parallelize-across-files)
+    flags+=("--no-parallelize-across-files")
+    ;;
+  --no-parallelize-within-files)
+    flags+=("--no-parallelize-within-files")
+    ;;
+  --no-tempdir-cleanup)
+    BATS_TEMPDIR_CLEANUP=''
+    ;;
+  --tempdir) # for internal test consumption only!
+    BATS_RUN_TMPDIR="$2"
+    shift
+    ;;
+  -x | --trace)
+    flags+=(--trace)
+    ;;
+  --print-output-on-failure)
+    flags+=(--print-output-on-failure)
+    ;;
+  --show-output-of-passing-tests)
+    flags+=(--show-output-of-passing-tests)
+    ;;
+  --verbose-run)
+    flags+=(--verbose-run)
+    ;;
+  --gather-test-outputs-in)
+    shift
+    output_dir="$1"
+    if [ -d "$output_dir" ]; then
+      if ! find "$output_dir" -mindepth 1 -exec false {} + 2>/dev/null; then
+        abort --no-print-usage "Directory '$output_dir' must be empty for --gather-test-outputs-in"
+      fi
+    elif ! mkdir "$output_dir" 2>/dev/null; then
+      abort --no-print-usage "Could not create '$output_dir' for --gather-test-outputs-in"
+    fi
+    flags+=(--gather-test-outputs-in "$output_dir")
+    ;;
+  --setup-suite-file)
+    shift
+    setup_suite_file="$1"
+    ;;
+  --code-quote-style)
+    shift
+    BATS_CODE_QUOTE_STYLE="$1"
+    ;;
+  --filter-status)
+    shift
+    flags+=('--filter-status' "$1")
+    ;;
+  --filter-tags)
+    shift
+    flags+=('--filter-tags' "$1")
+    ;;
+  --line-reference-format)
+    shift
+    BATS_LINE_REFERENCE_FORMAT=$1
+    ;;
+  -*)
+    abort "Bad command line option '$1'"
+    ;;
+  *)
+    arguments+=("$1")
+    ;;
+  esac
+  shift
+done
+
+if [[ ! $BATS_LINE_REFERENCE_FORMAT =~ (custom|comma_line|colon|uri) ]]; then
+  abort "Invalid BATS_LINE_REFERENCE_FORMAT '$BATS_LINE_REFERENCE_FORMAT' (e.g. via --line-reference-format)"
+fi
+
+if [[ -n "${BATS_RUN_TMPDIR:-}" ]]; then
+  if [[ -d "$BATS_RUN_TMPDIR" ]]; then
+    printf "Error: BATS_RUN_TMPDIR (%s) already exists\n" "$BATS_RUN_TMPDIR" >&2
+    printf "Reusing old run directories can lead to unexpected results ... aborting!\n" >&2
+    exit 1
+  elif ! mkdir -p "$BATS_RUN_TMPDIR"; then
+    printf "Error: Failed to create BATS_RUN_TMPDIR (%s)\n" "$BATS_RUN_TMPDIR" >&2
+    exit 1
+  fi
+elif ! BATS_RUN_TMPDIR=$(mktemp -d "${BATS_TMPDIR}/bats-run-XXXXXX"); then
+  printf "Error: Failed to create BATS_RUN_TMPDIR (%s) with mktemp\n" "${BATS_TMPDIR}/bats-run-XXXXXX" >&2
+  exit 1
+fi
+
+export BATS_WARNING_FILE="${BATS_RUN_TMPDIR}/warnings.log"
+
+bats_exit_trap() {
+  if [[ -s "$BATS_WARNING_FILE" ]]; then
+    local pre_cat='' post_cat=''
+    if [[ $formatter == pretty ]]; then
+      pre_cat=$'\x1B[31m'
+      post_cat=$'\x1B[0m'
+    fi
+    printf "\nThe following warnings were encountered during tests:\n%s" "$pre_cat"
+    cat "$BATS_WARNING_FILE"
+    printf "%s" "$post_cat"
+  fi >&2
+
+  if [[ -n "$BATS_TEMPDIR_CLEANUP" ]]; then
+    rm -rf "$BATS_RUN_TMPDIR"
+  else
+    printf "BATS_RUN_TMPDIR: %s\n" "$BATS_RUN_TMPDIR" >&2
+  fi
+}
+
+trap bats_exit_trap EXIT
+
+if [[ "$formatter" != "tap" ]]; then
+  flags+=('-x')
+fi
+
+if [[ -n "$report_formatter" && "$report_formatter" != "tap" ]]; then
+  flags+=('-x')
+fi
+
+if [[ "$formatter" == "junit" ]]; then
+  flags+=('-T')
+  formatter_flags+=('--base-path' "${arguments[0]}")
+fi
+if [[ "$report_formatter" == "junit" ]]; then
+  flags+=('-T')
+  report_formatter_flags+=('--base-path' "${arguments[0]}")
+fi
+
+if [[ "$formatter" == "pretty" ]]; then
+  formatter_flags+=('--base-path' "${arguments[0]}")
+fi
+
+# if we don't need to filter extended syntax, use the faster formatter
+if [[ "$formatter" == tap && -z "$report_formatter" ]]; then
+  formatter="cat"
+fi
+
+bats_check_formatter() { # <formatter-path>
+  local -r formatter="$1"
+  if [[ ! -f "$formatter" ]]; then
+    printf "ERROR: Formatter '%s' is not readable!\n" "$formatter"
+    exit 1
+  elif [[ ! -x "$formatter" ]]; then
+    printf "ERROR: Formatter '%s' is not executable!\n" "$formatter"
+    exit 1
+  fi
+}
+
+if [[ $formatter == /* ]]; then # absolute paths are direct references to formatters
+  bats_check_formatter "$formatter"
+  interpolated_formatter="$formatter"
+else
+  interpolated_formatter="bats-format-${formatter}"
+fi
+
+if [[ "${#arguments[@]}" -eq 0 ]]; then
+  abort 'Must specify at least one <test>'
+fi
+
+if [[ -n "$report_formatter" ]]; then
+  if [[ ! -w "${BATS_REPORT_OUTPUT_DIR}" ]]; then
+    abort "Output path ${BATS_REPORT_OUTPUT_DIR} is not writeable"
+  fi
+  # only set BATS_REPORT_FILENAME if none was given
+  if [[ -z "${BATS_REPORT_FILENAME:-}" ]]; then
+    case "$report_formatter" in
+    tap | tap13)
+      BATS_REPORT_FILENAME="report.tap"
+      ;;
+    junit)
+      BATS_REPORT_FILENAME="report.xml"
+      ;;
+    *)
+      BATS_REPORT_FILENAME="report.log"
+      ;;
+    esac
+  fi
+fi
+
+if [[ $report_formatter == /* ]]; then # absolute paths are direct references to formatters
+  bats_check_formatter "$report_formatter"
+  interpolated_report_formatter="${report_formatter}"
+else
+  interpolated_report_formatter="bats-format-${report_formatter}"
+fi
+
+if [[ "${BATS_CODE_QUOTE_STYLE-BATS_CODE_QUOTE_STYLE_UNSET}" == BATS_CODE_QUOTE_STYLE_UNSET ]]; then
+  BATS_CODE_QUOTE_STYLE="\`'"
+fi
+
+case "${BATS_CODE_QUOTE_STYLE}" in
+??)
+  BATS_BEGIN_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE::1}"
+  BATS_END_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE:1:1}"
+  export BATS_BEGIN_CODE_QUOTE BATS_END_CODE_QUOTE
+  ;;
+custom)
+  if [[ ${BATS_BEGIN_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ||
+    ${BATS_END_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ]]; then
+    printf "ERROR: BATS_CODE_QUOTE_STYLE=custom requires BATS_BEGIN_CODE_QUOTE and BATS_END_CODE_QUOTE to be set\n" >&2
+    exit 1
+  fi
+  ;;
+*)
+  printf "ERROR: Unknown BATS_CODE_QUOTE_STYLE: %s\n" "$BATS_CODE_QUOTE_STYLE" >&2
+  exit 1
+  ;;
+esac
+
+if [[ -n "$setup_suite_file" && ! -f "$setup_suite_file" ]]; then
+  abort "--setup-suite-file $setup_suite_file does not exist!"
+fi
+
+filenames=()
+for filename in "${arguments[@]}"; do
+  expand_path "$filename" 'filename'
+
+  if [[ -z "$setup_suite_file" ]]; then
+    if [[ -d "$filename" ]]; then
+      dirname="$filename"
+    else
+      dirname="${filename%/*}"
+    fi
+    potential_setup_suite_file="$dirname/setup_suite.bash"
+    if [[ -e "$potential_setup_suite_file" ]]; then
+      setup_suite_file="$potential_setup_suite_file"
+    fi
+  fi
+
+  if [[ -d "$filename" ]]; then
+    shopt -s nullglob
+    if [[ "$recursive" -eq 1 ]]; then
+      while IFS= read -r -d $'\0' file; do
+        filenames+=("$file")
+      done < <(find -L "$filename" -type f -name "*.${BATS_FILE_EXTENSION:-bats}" -print0 | sort -z)
+    else
+      for suite_filename in "$filename"/*."${BATS_FILE_EXTENSION:-bats}"; do
+        filenames+=("$suite_filename")
+      done
+    fi
+    shopt -u nullglob
+  else
+    filenames+=("$filename")
+  fi
+done
+
+if [[ -n "$setup_suite_file" ]]; then
+  flags+=("--setup-suite-file" "$setup_suite_file")
+fi
+
+# shellcheck source=lib/bats-core/validator.bash
+source "$BATS_ROOT/lib/bats-core/validator.bash"
+
+trap 'BATS_INTERRUPTED=true' INT # let the lower levels handle the interruption
+
+set -o pipefail execfail
+
+# pipe stdin into command and to stdout
+# pipe command stdout to file
+bats_tee() { # <output-file> <command...>
+  local output_file=$1 status=0
+  shift
+  exec 3<&1 # use FD3 to get around pipe
+  tee >(cat >&3) | "$@" >"$output_file" || status=$?
+  if (( status != 0 )); then
+    printf "ERROR: command \`%s\` failed with status %d\n" "$*" "$status" >&2
+  fi
+  return $status
+}
+
+if [[ -n "$report_formatter" ]]; then
+  exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
+    bats_tee "${BATS_REPORT_OUTPUT_DIR}/${BATS_REPORT_FILENAME}" "$interpolated_report_formatter" "${report_formatter_flags[@]}" |
+    bats_test_count_validator |
+    "$interpolated_formatter" "${formatter_flags[@]}"
+else
+  exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
+    bats_test_count_validator |
+    "$interpolated_formatter" "${formatter_flags[@]}"
+fi
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-file b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-file
new file mode 100755
index 00000000..7db57640
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-file
@@ -0,0 +1,383 @@
+#!/usr/bin/env bash
+set -eET
+
+flags=('--dummy-flag')
+num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS:-1}
+extended_syntax=''
+BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}"
+declare -r BATS_RETRY_RETURN_CODE=126
+export BATS_TEST_RETRIES=0 # no retries by default
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  -j)
+    shift
+    num_jobs="$1"
+    ;;
+  -T)
+    flags+=('-T')
+    ;;
+  -x)
+    flags+=('-x')
+    extended_syntax=1
+    ;;
+  --no-parallelize-within-files)
+    # use singular to allow for users to override in file
+    BATS_NO_PARALLELIZE_WITHIN_FILE=1
+    ;;
+  --dummy-flag) ;;
+
+  --trace)
+    flags+=('--trace')
+    ;;
+  --print-output-on-failure)
+    flags+=(--print-output-on-failure)
+    ;;
+  --show-output-of-passing-tests)
+    flags+=(--show-output-of-passing-tests)
+    ;;
+  --verbose-run)
+    flags+=(--verbose-run)
+    ;;
+  --gather-test-outputs-in)
+    shift
+    flags+=(--gather-test-outputs-in "$1")
+    ;;
+  *)
+    break
+    ;;
+  esac
+  shift
+done
+
+filename="$1"
+TESTS_FILE="$2"
+
+if [[ ! -f "$filename" ]]; then
+  printf 'Testfile "%s" not found\n' "$filename" >&2
+  exit 1
+fi
+
+export BATS_TEST_FILENAME="$filename"
+
+# shellcheck source=lib/bats-core/preprocessing.bash
+# shellcheck disable=SC2153
+source "$BATS_ROOT/lib/bats-core/preprocessing.bash"
+
+bats_run_setup_file() {
+  # shellcheck source=lib/bats-core/tracing.bash
+  # shellcheck disable=SC2153
+  source "$BATS_ROOT/lib/bats-core/tracing.bash"
+  # shellcheck source=lib/bats-core/test_functions.bash
+  # shellcheck disable=SC2153
+  source "$BATS_ROOT/lib/bats-core/test_functions.bash"
+
+  exec 3<&1
+
+  # these are defined only to avoid errors when referencing undefined variables down the line
+  # shellcheck disable=2034
+  BATS_TEST_NAME= # used in tracing.bash
+  # shellcheck disable=2034
+  BATS_TEST_COMPLETED= # used in tracing.bash
+
+  BATS_SOURCE_FILE_COMPLETED=
+  BATS_SETUP_FILE_COMPLETED=
+  BATS_TEARDOWN_FILE_COMPLETED=
+  # shellcheck disable=2034
+  BATS_ERROR_STATUS= # used in tracing.bash
+  touch "$BATS_OUT"
+  bats_setup_tracing
+  trap 'bats_file_teardown_trap' EXIT
+
+  local status=0
+  # get the setup_file/teardown_file functions for this file (if it has them)
+  # shellcheck disable=SC1090
+  source "$BATS_TEST_SOURCE"
+
+  BATS_SOURCE_FILE_COMPLETED=1
+
+  setup_file >>"$BATS_OUT" 2>&1
+
+  BATS_SETUP_FILE_COMPLETED=1
+}
+
+bats_run_teardown_file() {
+  local bats_teardown_file_status=0
+  # avoid running the therdown trap due to errors in teardown_file
+  trap 'bats_file_exit_trap' EXIT
+
+  # rely on bats_error_trap to catch failures
+  teardown_file >>"$BATS_OUT" 2>&1 || bats_teardown_file_status=$?
+
+  if ((bats_teardown_file_status == 0)); then
+    BATS_TEARDOWN_FILE_COMPLETED=1
+  elif [[ -n "${BATS_SETUP_FILE_COMPLETED:-}" ]]; then
+    BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
+    BATS_ERROR_STATUS=$bats_teardown_file_status
+    return $BATS_ERROR_STATUS
+  fi
+}
+
+# shellcheck disable=SC2317
+bats_file_teardown_trap() {
+  bats_run_teardown_file
+  bats_file_exit_trap in-teardown_trap
+}
+
+# shellcheck source=lib/bats-core/common.bash
+source "$BATS_ROOT/lib/bats-core/common.bash"
+
+# shellcheck disable=SC2317
+bats_file_exit_trap() {
+  local -r last_return_code=$?
+  if [[ ${1:-} != in-teardown_trap ]]; then
+    BATS_ERROR_STATUS=$last_return_code
+  fi
+  trap - ERR EXIT
+  local failure_reason
+  local -i failure_test_index=$((BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE + 1))
+  if [[ -n "${BATS_TEST_SKIPPED-}" ]]; then
+    export BATS_TEST_SKIPPED # indicate to exec-test that it should skip
+    # print skip message for each test (use this to avoid reimplementing filtering)
+    bats_run_tests 1<&3 # restore original stdout (this is running in setup_file's redirection to BATS_OUT)
+    bats_exec_file_status=0 # this should not lead to errors
+  elif [[ -z "$BATS_SETUP_FILE_COMPLETED" || -z "$BATS_TEARDOWN_FILE_COMPLETED" ]]; then
+    if [[ -z "$BATS_SETUP_FILE_COMPLETED" ]]; then
+      failure_reason='setup_file'
+    elif [[ -z "$BATS_TEARDOWN_FILE_COMPLETED" ]]; then
+      failure_reason='teardown_file'
+      failure_test_index=$((BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE + ${#tests_to_run[@]} + 1))
+    elif [[ -z "$BATS_SOURCE_FILE_COMPLETED" ]]; then
+      failure_reason='source'
+    else
+      failure_reason='unknown internal'
+    fi
+    printf "not ok %d %s\n" "$failure_test_index" "$failure_reason failed" >&3
+    local stack_trace
+    bats_get_failure_stack_trace stack_trace
+    bats_print_stack_trace "${stack_trace[@]}" >&3
+    bats_print_failed_command "${stack_trace[@]}" >&3
+    bats_prefix_lines_for_tap_output <"$BATS_OUT" | bats_replace_filename >&3
+    rm -rf "$BATS_OUT"
+    bats_exec_file_status=1
+  fi
+
+  # setup_file not executed but defined in this test file? -> might be defined in the wrong file
+  if [[ -z "${BATS_SETUP_SUITE_COMPLETED-}" ]] && declare -F setup_suite >/dev/null; then
+    bats_generate_warning 3 --no-stacktrace "$BATS_TEST_FILENAME"
+  fi
+
+  exit "$bats_exec_file_status"
+}
+
+function setup_file() {
+  return 0
+}
+
+function teardown_file() {
+  return 0
+}
+
+bats_forward_output_of_parallel_test() {
+  local test_number_in_suite=$1
+  local status=0
+  wait "$(cat "$output_folder/$test_number_in_suite/pid")" || status=1
+  cat "$output_folder/$test_number_in_suite/stdout"
+  cat "$output_folder/$test_number_in_suite/stderr" >&2
+  return $status
+}
+
+bats_is_next_parallel_test_finished() {
+  local PID
+  # get the pid of the next potentially finished test
+  PID=$(cat "$output_folder/$((test_number_in_suite_of_last_finished_test + 1))/pid")
+  # try to send a signal to this process
+  # if it fails, the process exited,
+  # if it succeeds, the process is still running
+  if kill -0 "$PID" 2>/dev/null; then
+    return 1
+  fi
+}
+
+# prints output from all tests in the order they were started
+# $1 == "blocking": wait for a test to finish before printing
+#    != "blocking": abort printing, when a test has not finished
+bats_forward_output_for_parallel_tests() {
+  local status=0
+  # was the next test already started?
+  while ((test_number_in_suite_of_last_finished_test + 1 <= test_number_in_suite)); do
+    # if we are okay with waiting or if the test has already been finished
+    if [[ "$1" == "blocking" ]] || bats_is_next_parallel_test_finished; then
+      ((++test_number_in_suite_of_last_finished_test))
+      bats_forward_output_of_parallel_test "$test_number_in_suite_of_last_finished_test" || status=$?
+    else
+      # non-blocking and the process has not finished -> abort the printing
+      break
+    fi
+  done
+  return $status
+}
+
+bats_run_test_with_retries() { # <args>
+  local status=0
+  local should_try_again=1 try_number
+  for ((try_number = 1; should_try_again; ++try_number)); do
+    if "$BATS_LIBEXEC/bats-exec-test" "$@" "$try_number"; then
+      should_try_again=0
+    else
+      status=$?
+      if ((status == BATS_RETRY_RETURN_CODE)); then
+        should_try_again=1
+        status=0 # this is not the last try -> reset status
+      else
+        should_try_again=0
+        bats_exec_file_status=$status
+      fi
+    fi
+  done
+  return $status
+}
+
+bats_run_tests_in_parallel() {
+  local output_folder="$BATS_RUN_TMPDIR/parallel_output"
+  local status=0
+  mkdir -p "$output_folder"
+  # shellcheck source=lib/bats-core/semaphore.bash
+  source "$BATS_ROOT/lib/bats-core/semaphore.bash"
+  bats_semaphore_setup
+  # the test_number_in_file is not yet incremented -> one before the next test to run
+  local test_number_in_suite_of_last_finished_test="$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE" # stores which test was printed last
+  local test_number_in_file=0 test_number_in_suite=$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE
+  for test_name in "${tests_to_run[@]}"; do
+    # Only handle non-empty lines
+    if [[ $test_name ]]; then
+      ((++test_number_in_suite))
+      ((++test_number_in_file))
+      mkdir -p "$output_folder/$test_number_in_suite"
+      bats_semaphore_run "$output_folder/$test_number_in_suite" \
+        bats_run_test_with_retries "${flags[@]}" "$filename" "$test_name" "$test_number_in_suite" "$test_number_in_file" \
+        >"$output_folder/$test_number_in_suite/pid"
+    fi
+    # print results early to get interactive feedback
+    bats_forward_output_for_parallel_tests non-blocking || status=1 # ignore if we did not finish yet
+  done
+  bats_forward_output_for_parallel_tests blocking || status=1
+  return $status
+}
+
+bats_read_tests_list_file() {
+  local line_number=0
+  tests_to_run=()
+  # the global test number must be visible to traps -> not local
+  local test_number_in_suite=''
+  while read -r test_line; do
+    # check if the line begins with filename
+    # filename might contain some hard to parse characters,
+    # use simple string operations to work around that issue
+    if [[ "$filename" == "${test_line::${#filename}}" ]]; then
+      # get the rest of the line without the separator \t
+      test_name=${test_line:$((1 + ${#filename}))}
+      tests_to_run+=("$test_name")
+      # save the first test's number for later iteration
+      # this assumes that tests for a file are stored consecutive in the file!
+      if [[ -z "$test_number_in_suite" ]]; then
+        test_number_in_suite=$line_number
+      fi
+    fi
+    ((++line_number))
+  done <"$TESTS_FILE"
+  BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE="$test_number_in_suite"
+  declare -ri BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE # mark readonly (cannot merge assignment, because value would be lost)
+}
+
+bats_run_tests() {
+  bats_exec_file_status=0
+
+  if [[ "${BATS_RUN_TESTS_SKIPPED-}" ]]; then
+    # shellcheck disable=SC2317
+    bats_test_begin() {
+      printf "ok %d %s # skip %s\n" "$test_number_in_suite" "$1" "$BATS_RUN_TESTS_SKIPPED_REASON"  >&3
+      return 1
+    }
+    local test_number_in_suite=0
+    for test_name in "${tests_to_run[@]}"; do
+      ((++test_number_in_suite))
+      eval "$test_name" || true
+    done
+    exit 0
+  fi
+
+  if [[ "$num_jobs" -lt 1 ]]; then
+    printf 'Invalid number of jobs: %s\n' "$num_jobs" >&2
+    exit 1
+  fi
+
+  if [[ "$num_jobs" != 1 && "${BATS_NO_PARALLELIZE_WITHIN_FILE-False}" == False ]]; then
+    export BATS_SEMAPHORE_NUMBER_OF_SLOTS="$num_jobs"
+    bats_run_tests_in_parallel "$BATS_RUN_TMPDIR/parallel_output" || bats_exec_file_status=1
+  else
+    local test_number_in_suite=$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE \
+      test_number_in_file=0
+    for test_name in "${tests_to_run[@]}"; do
+      if [[ "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then
+        bats_exec_file_status=130 # bash's code for SIGINT exits
+        break
+      fi
+      # Only handle non-empty lines
+      if [[ -n ${test_name-} ]]; then
+        ((++test_number_in_suite))
+        ((++test_number_in_file))
+        bats_run_test_with_retries "${flags[@]}" "$filename" "$test_name" \
+          "$test_number_in_suite" "$test_number_in_file" || bats_exec_file_status=$?
+      fi
+    done
+  fi
+}
+
+bats_create_file_tempdirs() {
+  local bats_files_tmpdir="${BATS_RUN_TMPDIR}/file"
+  if ! mkdir -p "$bats_files_tmpdir"; then
+    printf 'Failed to create %s\n' "$bats_files_tmpdir" >&2
+    exit 1
+  fi
+  BATS_FILE_TMPDIR="$bats_files_tmpdir/${BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE?}"
+  if ! mkdir "$BATS_FILE_TMPDIR"; then
+    printf 'Failed to create BATS_FILE_TMPDIR=%s\n' "$BATS_FILE_TMPDIR" >&2
+    exit 1
+  fi
+  ln -s "$BATS_TEST_FILENAME" "$BATS_FILE_TMPDIR-$(basename "$BATS_TEST_FILENAME").source_file"
+  export BATS_FILE_TMPDIR
+}
+
+trap 'BATS_INTERRUPTED=true' INT
+
+if [[ -n "$extended_syntax" ]]; then
+  printf "suite %s\n" "$filename"
+fi
+
+BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE=0 # predeclare as Bash 3.2 does not support declare -g
+bats_read_tests_list_file
+
+# don't run potentially expensive setup/teardown_file
+# when there are no tests to run
+if [[ ${#tests_to_run[@]} -eq 0 ]]; then
+  exit 0
+fi
+
+# requires the test list to be read but not empty
+bats_create_file_tempdirs
+
+bats_preprocess_source "$filename"
+
+trap bats_interrupt_trap INT
+bats_run_setup_file
+
+# during tests, we don't want to get backtraces from this level
+# just wait for the test to be interrupted and display their trace
+trap 'BATS_INTERRUPTED=true' INT
+bats_run_tests
+
+trap bats_interrupt_trap INT
+bats_run_teardown_file
+
+exit $bats_exec_file_status
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-suite b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-suite
new file mode 100755
index 00000000..0708d169
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-suite
@@ -0,0 +1,467 @@
+#!/usr/bin/env bash
+set -e
+
+count_only_flag=''
+filter=''
+num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS:-1}
+bats_no_parallelize_across_files=${BATS_NO_PARALLELIZE_ACROSS_FILES-}
+bats_no_parallelize_within_files=
+filter_status=''
+filter_tags_list=()
+flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
+setup_suite_file=''
+BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}"
+BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=
+
+abort() {
+  printf 'Error: %s\n' "$1" >&2
+  exit 1
+}
+
+# shellcheck source=lib/bats-core/common.bash disable=SC2153
+source "$BATS_ROOT/lib/bats-core/common.bash"
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  -c)
+    count_only_flag=1
+    ;;
+  -f)
+    shift
+    filter="$1"
+    ;;
+  -j)
+    shift
+    num_jobs="$1"
+    flags+=('-j' "$num_jobs")
+    ;;
+  -T)
+    flags+=('-T')
+    ;;
+  -x)
+    flags+=('-x')
+    ;;
+  --no-parallelize-across-files)
+    bats_no_parallelize_across_files=1
+    ;;
+  --no-parallelize-within-files)
+    bats_no_parallelize_within_files=1
+    flags+=("--no-parallelize-within-files")
+    ;;
+  --filter-status)
+    shift
+    filter_status="$1"
+    ;;
+  --filter-tags)
+    shift
+    IFS=, read -ra tags <<<"$1" || true
+    if ((${#tags[@]} > 0)); then
+      for ((i = 0; i < ${#tags[@]}; ++i)); do
+        bats_trim "tags[$i]" "${tags[$i]}"
+      done
+      bats_sort sorted_tags "${tags[@]}"
+      IFS=, filter_tags_list+=("${sorted_tags[*]}")
+    else
+      filter_tags_list+=("")
+    fi
+    ;;
+  --dummy-flag) ;;
+
+  --trace)
+    flags+=('--trace')
+    ((++BATS_TRACE_LEVEL)) # avoid returning 0
+    ;;
+  --print-output-on-failure)
+    flags+=(--print-output-on-failure)
+    ;;
+  --show-output-of-passing-tests)
+    flags+=(--show-output-of-passing-tests)
+    BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=1
+    ;;
+  --verbose-run)
+    flags+=(--verbose-run)
+    ;;
+  --gather-test-outputs-in)
+    shift
+    flags+=(--gather-test-outputs-in "$1")
+    ;;
+  --setup-suite-file)
+    shift
+    setup_suite_file="$1"
+    ;;
+  *)
+    break
+    ;;
+  esac
+  shift
+done
+
+if [[ "$num_jobs" != 1 ]]; then
+  if ! type -p parallel >/dev/null && parallel --version &>/dev/null && [[ -z "$bats_no_parallelize_across_files" ]]; then
+    abort "Cannot execute \"${num_jobs}\" jobs without GNU parallel"
+  fi
+  # shellcheck source=lib/bats-core/semaphore.bash
+  source "${BATS_ROOT}/lib/bats-core/semaphore.bash"
+  bats_semaphore_setup
+fi
+
+# create a file that contains all (filtered) tests to run from all files
+TESTS_LIST_FILE="${BATS_RUN_TMPDIR}/test_list_file.txt"
+focus_mode=
+bats_gather_tests() {
+  local line test_line tags
+  all_tests=()
+  for filename in "$@"; do
+    if [[ ! -f "$filename" ]]; then
+      abort "Test file \"${filename}\" does not exist"
+    fi
+
+    test_names=()
+    test_dupes=()
+    while read -r line; do
+      if [[ ! "$line" =~ ^bats_test_function\  ]]; then
+        continue
+      fi
+      line="${line%$'\r'}"
+      line="${line#* }"
+      TAG_REGEX="--tags '(.*)' (.*)"
+      if [[ "$line" =~ $TAG_REGEX ]]; then
+        IFS=, read -ra tags <<<"${BASH_REMATCH[1]}" || true
+        line="${BASH_REMATCH[2]}"
+      else
+        tags=()
+      fi
+      # is this test focused?
+      if bats_all_in tags 'bats:focus'; then
+        if [[ $focus_mode == 1 ]]; then
+          # focused tests in focus mode should just be registered
+          :
+        else
+          # the current test enables focus mode ...
+          focus_mode=1
+          # ... -> remove previously found, unfocused tests
+          all_tests=()
+          : > "$TESTS_LIST_FILE"
+        fi
+      elif [[ $focus_mode == 1 ]]; then
+        # the current test is not focused but focus mode is enabled -> filter out
+        continue
+        # no else -> unfocused tests outside focus mode should just be registered
+      fi
+      if [[ ${#filter_tags_list[@]} -gt 0 ]]; then
+        local match=
+        for filter_tags in "${filter_tags_list[@]}"; do
+          # empty search tags only match empty test tags!
+          if [[ -z "$filter_tags" ]]; then
+            if [[ ${#tags[@]} -eq 0 ]]; then
+              match=1
+              break
+            fi
+            continue
+          fi
+          local -a positive_filter_tags=() negative_filter_tags=()
+          IFS=, read -ra filter_tags <<<"$filter_tags" || true
+          for filter_tag in "${filter_tags[@]}"; do
+            if [[ $filter_tag == !* ]]; then
+              bats_trim filter_tag "${filter_tag#!}"
+              negative_filter_tags+=("${filter_tag}")
+            else
+              positive_filter_tags+=("${filter_tag}")
+            fi
+          done
+          if bats_append_arrays_as_args positive_filter_tags -- bats_all_in tags &&
+            ! bats_append_arrays_as_args negative_filter_tags -- bats_any_in tags; then
+            match=1
+          fi
+        done
+        if [[ -z "$match" ]]; then
+          continue
+        fi
+      fi
+      test_line=$(printf "%s\t%s" "$filename" "$line")
+      all_tests+=("$test_line")
+      printf "%s\n" "$test_line" >>"$TESTS_LIST_FILE"
+      # avoid unbound variable errors on empty array expansion with old bash versions
+      if [[ ${#test_names[@]} -gt 0 && " ${test_names[*]} " == *" $line "* ]]; then
+        test_dupes+=("$line")
+        continue
+      fi
+      test_names+=("$line")
+    done < <(BATS_TEST_FILTER="$filter" bats-preprocess "$filename")
+
+    if [[ "${#test_dupes[@]}" -ne 0 ]]; then
+      abort "Duplicate test name(s) in file \"${filename}\": ${test_dupes[*]}"
+    fi
+  done
+
+  test_count="${#all_tests[@]}"
+}
+
+TEST_ROOT=${1-}
+TEST_ROOT=${TEST_ROOT%/*}
+BATS_RUN_LOGS_DIRECTORY="$TEST_ROOT/.bats/run-logs"
+if [[ ! -d "$BATS_RUN_LOGS_DIRECTORY" ]]; then
+  if [[ -n "$filter_status" ]]; then
+    printf "Error: --filter-status needs '%s/' to save failed tests. Please create this folder, add it to .gitignore and try again.\n" "$BATS_RUN_LOGS_DIRECTORY"
+    exit 1
+  else
+    BATS_RUN_LOGS_DIRECTORY=
+  fi
+  # discard via sink instead of having a conditional later
+  export BATS_RUNLOG_FILE='/dev/null'
+else
+  # use UTC (-u) to avoid problems with TZ changes
+  BATS_RUNLOG_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
+  export BATS_RUNLOG_FILE="$BATS_RUN_LOGS_DIRECTORY/${BATS_RUNLOG_DATE}.log"
+  if [[ ! -w "$BATS_RUN_LOGS_DIRECTORY" ]]; then
+    printf "WARNING: Cannot write in %s. This run will not write a log!\n" "$BATS_RUN_LOGS_DIRECTORY" >&2
+    BATS_RUNLOG_FILE='/dev/null' # disable runlog file
+  fi
+fi
+
+bats_gather_tests "$@"
+
+if [[ -n "$filter_status" ]]; then
+  case "$filter_status" in
+  failed)
+    bats_filter_test_by_status() { # <line>
+      ! bats_binary_search "$1" "passed_tests"
+    }
+    ;;
+  passed)
+    bats_filter_test_by_status() {
+      ! bats_binary_search "$1" "failed_tests"
+    }
+    ;;
+  missed)
+    bats_filter_test_by_status() {
+      ! bats_binary_search "$1" "failed_tests" && ! bats_binary_search "$1" "passed_tests"
+    }
+    ;;
+  *)
+    printf "Error: Unknown value '%s' for --filter-status. Valid values are 'failed' and 'missed'.\n" "$filter_status" >&2
+    exit 1
+    ;;
+  esac
+
+  if IFS='' read -d $'\n' -r BATS_PREVIOUS_RUNLOG_FILE < <(ls -1r "$BATS_RUN_LOGS_DIRECTORY"); then
+    BATS_PREVIOUS_RUNLOG_FILE="$BATS_RUN_LOGS_DIRECTORY/$BATS_PREVIOUS_RUNLOG_FILE"
+    if [[ $BATS_PREVIOUS_RUNLOG_FILE == "$BATS_RUNLOG_FILE" ]]; then
+      count=$(find "$BATS_RUN_LOGS_DIRECTORY" -name "$BATS_RUNLOG_DATE*" | wc -l)
+      BATS_RUNLOG_FILE="$BATS_RUN_LOGS_DIRECTORY/${BATS_RUNLOG_DATE}-$count.log"
+    fi
+    failed_tests=()
+    passed_tests=()
+    # store tests that were already filtered out in the last run for the same filter reason
+    last_filtered_tests=()
+    i=0
+    while read -rd $'\n' line; do
+      ((++i))
+      case "$line" in
+      "passed "*)
+        passed_tests+=("${line#passed }")
+        ;;
+      "failed "*)
+        failed_tests+=("${line#failed }")
+        ;;
+      "status-filtered $filter_status"*) # pick up tests that were filtered in the last round for the same status
+        last_filtered_tests+=("${line#status-filtered "$filter_status" }")
+        ;;
+      "status-filtered "*) # ignore other status-filtered lines
+        ;;
+      "#"*) # allow for comments
+        ;;
+      *)
+        printf "Error: %s:%d: Invalid format: %s\n" "$BATS_PREVIOUS_RUNLOG_FILE" "$i" "$line" >&2
+        exit 1
+        ;;
+      esac
+    done < <(sort "$BATS_PREVIOUS_RUNLOG_FILE")
+
+    filtered_tests=()
+    for line in "${all_tests[@]}"; do
+      if bats_filter_test_by_status "$line" && ! bats_binary_search "$line" last_filtered_tests; then
+        printf "%s\n" "$line"
+        filtered_tests+=("$line")
+      else
+        printf "status-filtered %s %s\n" "$filter_status" "$line" >>"$BATS_RUNLOG_FILE"
+      fi
+    done >"$TESTS_LIST_FILE"
+
+    # save filtered tests to exclude them again in next round
+    for test_line in "${last_filtered_tests[@]}"; do
+      printf "status-filtered %s %s\n" "$filter_status" "$test_line"
+    done >>"$BATS_RUNLOG_FILE"
+
+    test_count="${#filtered_tests[@]}"
+    if [[ ${#failed_tests[@]} -eq 0 && ${#filtered_tests[@]} -eq 0 ]]; then
+      printf "There where no failed tests in the last recorded run.\n" >&2
+    fi
+  else
+    printf "No recording of previous runs found. Running all tests!\n" >&2
+  fi
+fi
+
+if [[ -n "$count_only_flag" ]]; then
+  printf '%d\n' "${test_count}"
+  exit
+fi
+
+if [[ -n "$bats_no_parallelize_across_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
+  abort "The flag --no-parallelize-across-files requires at least --jobs 2"
+fi
+
+if [[ -n "$bats_no_parallelize_within_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
+  abort "The flag --no-parallelize-across-files requires at least --jobs 2"
+fi
+
+# only abort on the lowest levels
+trap 'BATS_INTERRUPTED=true' INT
+
+if [[ -n "$focus_mode" ]]; then
+  printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n"
+fi
+
+bats_exec_suite_status=0
+printf '1..%d\n' "${test_count}"
+
+# No point on continuing if there's no tests.
+if [[ "${test_count}" == 0 ]]; then
+  exit
+fi
+
+export BATS_SUITE_TMPDIR="${BATS_RUN_TMPDIR}/suite"
+if ! mkdir "$BATS_SUITE_TMPDIR"; then
+  printf '%s\n' "Failed to create BATS_SUITE_TMPDIR" >&2
+  exit 1
+fi
+
+# Deduplicate filenames (without reordering) to avoid running duplicate tests n by n times.
+# (see https://github.com/bats-core/bats-core/issues/329)
+# If a file was specified multiple times, we already got it repeatedly in our TESTS_LIST_FILE.
+# Thus, it suffices to bats-exec-file it once to run all repeated tests on it.
+IFS=$'\n' read -d '' -r -a BATS_UNIQUE_TEST_FILENAMES < <(printf "%s\n" "$@" | nl | sort -k 2 | uniq -f 1 | sort -n | cut -f 2-) || true
+
+# shellcheck source=lib/bats-core/tracing.bash
+source "$BATS_ROOT/lib/bats-core/tracing.bash"
+bats_setup_tracing
+
+trap bats_suite_exit_trap EXIT
+
+exec 3<&1
+
+# shellcheck disable=SC2317
+bats_suite_exit_trap() {
+  local print_bats_out="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS}"
+  if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" || -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then
+    if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" ]]; then
+      printf "not ok 1 setup_suite\n"
+    elif [[ -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then
+      printf "not ok %d teardown_suite\n" $((test_count + 1))
+    fi
+    local stack_trace
+    bats_get_failure_stack_trace stack_trace
+    bats_print_stack_trace "${stack_trace[@]}"
+    bats_print_failed_command "${stack_trace[@]}"
+    print_bats_out=1
+    bats_exec_suite_status=1
+  fi
+
+  if [[ -n "$print_bats_out" ]]; then
+    bats_prefix_lines_for_tap_output <"$BATS_OUT"
+  fi
+
+  if [[ ${BATS_INTERRUPTED-NOTSET} != NOTSET ]]; then
+    printf "\n# Received SIGINT, aborting ...\n\n"
+  fi
+
+  if [[ -d "$BATS_RUN_LOGS_DIRECTORY" && -n "${BATS_INTERRUPTED:-}" ]]; then
+    # aborting a test run with CTRL+C does not save the runlog file
+    [[ "$BATS_RUNLOG_FILE" != /dev/null ]] && rm "$BATS_RUNLOG_FILE"
+  fi
+  exit "$bats_exec_suite_status"
+} >&3
+
+bats_run_teardown_suite() {
+  local bats_teardown_suite_status=0
+  # avoid being called twice, in case this is not called through bats_teardown_suite_trap
+  # but from the end of file
+  trap bats_suite_exit_trap EXIT
+  BATS_TEARDOWN_SUITE_COMPLETED=
+  teardown_suite >>"$BATS_OUT" 2>&1 || bats_teardown_suite_status=$?
+  if ((bats_teardown_suite_status == 0)); then
+    BATS_TEARDOWN_SUITE_COMPLETED=1
+  elif [[ -n "${BATS_SETUP_SUITE_COMPLETED:-}" ]]; then
+    BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
+    BATS_ERROR_STATUS=$bats_teardown_suite_status
+    return $BATS_ERROR_STATUS
+  fi
+}
+
+# shellcheck disable=SC2317
+bats_teardown_suite_trap() {
+  bats_run_teardown_suite
+  bats_suite_exit_trap
+}
+
+teardown_suite() {
+  :
+}
+
+trap bats_teardown_suite_trap EXIT
+
+BATS_OUT="$BATS_RUN_TMPDIR/suite.out"
+if [[ -n "$setup_suite_file" ]]; then
+  setup_suite() {
+    printf "%s does not define \`setup_suite()\`\n" "$setup_suite_file" >&2
+    return 1
+  }
+
+  # shellcheck disable=SC2034 # will be used in the sourced file below
+  BATS_TEST_FILENAME="$setup_suite_file"
+  # shellcheck source=lib/bats-core/test_functions.bash
+  source "$BATS_ROOT/lib/bats-core/test_functions.bash"
+
+  # shellcheck disable=SC1090
+  source "$setup_suite_file"
+
+  set -eET
+  export BATS_SETUP_SUITE_COMPLETED=
+  setup_suite >>"$BATS_OUT" 2>&1
+  BATS_SETUP_SUITE_COMPLETED=1
+  set +ET
+else
+  # prevent exit trap from printing an error because of incomplete setup_suite,
+  # when there was none to execute
+  BATS_SETUP_SUITE_COMPLETED=1
+fi
+
+if [[ "$num_jobs" -gt 1 ]] && [[ -z "$bats_no_parallelize_across_files" ]]; then
+  # run files in parallel to get the maximum pool of parallel tasks
+  # shellcheck disable=SC2086,SC2068
+  # we need to handle the quoting of ${flags[@]} ourselves,
+  # because parallel can only quote it as one
+  parallel --keep-order --jobs "$num_jobs" bats-exec-file "$(printf "%q " "${flags[@]}")" "{}" "$TESTS_LIST_FILE" ::: "${BATS_UNIQUE_TEST_FILENAMES[@]}" 2>&1 || bats_exec_suite_status=1
+else
+  for filename in "${BATS_UNIQUE_TEST_FILENAMES[@]}"; do
+    if [[ "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then
+      bats_exec_suite_status=130 # bash's code for SIGINT exits
+      break
+    fi
+    bats-exec-file "${flags[@]}" "$filename" "${TESTS_LIST_FILE}" || bats_exec_suite_status=1
+  done
+fi
+
+set -eET
+bats_run_teardown_suite
+
+if [[ "$focus_mode" == 1 && $bats_exec_suite_status -eq 0 ]]; then 
+  if [[ ${BATS_NO_FAIL_FOCUS_RUN-} == 1 ]]; then
+    printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n"
+  else
+    printf "Marking test run as failed due to \`bats:focus\` tag. (Set \`BATS_NO_FAIL_FOCUS_RUN=1\` to disable.)\n" >&2
+    bats_exec_suite_status=1
+  fi
+fi
+
+exit "$bats_exec_suite_status" # the actual exit code will be set by the exit trap using bats_exec_suite_status
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-test b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-test
new file mode 100755
index 00000000..ad528c44
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-exec-test
@@ -0,0 +1,345 @@
+#!/usr/bin/env bash
+set -eET
+
+# Variables used in other scripts.
+BATS_ENABLE_TIMING=''
+BATS_EXTENDED_SYNTAX=''
+BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}"
+BATS_PRINT_OUTPUT_ON_FAILURE="${BATS_PRINT_OUTPUT_ON_FAILURE:-}"
+BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS:-}"
+BATS_VERBOSE_RUN="${BATS_VERBOSE_RUN:-}"
+BATS_GATHER_TEST_OUTPUTS_IN="${BATS_GATHER_TEST_OUTPUTS_IN:-}"
+BATS_TEST_NAME_PREFIX="${BATS_TEST_NAME_PREFIX:-}"
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  -T)
+    BATS_ENABLE_TIMING='-T'
+    ;;
+  -x)
+    # shellcheck disable=SC2034
+    BATS_EXTENDED_SYNTAX='-x'
+    ;;
+  --dummy-flag) ;;
+
+  --trace)
+    ((++BATS_TRACE_LEVEL)) # avoid returning 0
+    ;;
+  --print-output-on-failure)
+    BATS_PRINT_OUTPUT_ON_FAILURE=1
+    ;;
+  --show-output-of-passing-tests)
+    BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=1
+    ;;
+  --verbose-run)
+    BATS_VERBOSE_RUN=1
+    ;;
+  --gather-test-outputs-in)
+    shift
+    BATS_GATHER_TEST_OUTPUTS_IN="$1"
+    ;;
+  *)
+    break
+    ;;
+  esac
+  shift
+done
+
+export BATS_TEST_FILENAME="$1"
+export BATS_TEST_NAME="$2"
+export BATS_SUITE_TEST_NUMBER="$3"
+export BATS_TEST_NUMBER="$4"
+BATS_TEST_TRY_NUMBER="$5"
+
+if [[ -z "$BATS_TEST_FILENAME" ]]; then
+  printf 'usage: bats-exec-test <filename>\n' >&2
+  exit 1
+elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then
+  printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2
+  exit 1
+fi
+
+bats_create_test_tmpdirs() {
+  local tests_tmpdir="${BATS_RUN_TMPDIR}/test"
+  if ! mkdir -p "$tests_tmpdir"; then
+    printf 'Failed to create: %s\n' "$tests_tmpdir" >&2
+    exit 1
+  fi
+
+  BATS_TEST_TMPDIR="$tests_tmpdir/$BATS_SUITE_TEST_NUMBER"
+  if ! mkdir "$BATS_TEST_TMPDIR"; then
+    printf 'Failed to create BATS_TEST_TMPDIR%d: %s\n' "$BATS_TEST_TRY_NUMBER" "$BATS_TEST_TMPDIR" >&2
+    exit 1
+  fi
+
+  printf "%s\n" "$BATS_TEST_NAME" >"$BATS_TEST_TMPDIR.name"
+
+  export BATS_TEST_TMPDIR
+}
+
+# load the test helper functions like `load` or `run` that are needed to run a (preprocessed) .bats file without bash errors
+# shellcheck source=lib/bats-core/test_functions.bash disable=SC2153
+source "$BATS_ROOT/lib/bats-core/test_functions.bash"
+# shellcheck source=lib/bats-core/tracing.bash disable=SC2153
+source "$BATS_ROOT/lib/bats-core/tracing.bash"
+
+bats_teardown_trap() {
+  bats_check_status_from_trap
+  local bats_teardown_trap_status=0
+
+  # `bats_teardown_trap` is always called with one parameter (BATS_TEARDOWN_STARTED)
+  # The second parameter is optional and corresponds for to the killer pid
+  # that will be forwarded to the exit trap to kill it if necesary
+  local killer_pid=${2:-}
+
+  # mark the start of this function to distinguish where skip is called
+  # parameter 1 will signify the reason why this function was called
+  # this is used to identify when this is called as exit trap function
+  BATS_TEARDOWN_STARTED=${1:-1}
+  teardown >>"$BATS_OUT" 2>&1 || bats_teardown_trap_status="$?"
+
+  if [[ $bats_teardown_trap_status -eq 0 ]]; then
+    BATS_TEARDOWN_COMPLETED=1
+  elif [[ -n "$BATS_TEST_COMPLETED" ]]; then
+    BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
+    BATS_ERROR_STATUS="$bats_teardown_trap_status"
+  fi
+
+  bats_exit_trap "$killer_pid"
+}
+
+# shellcheck source=lib/bats-core/common.bash
+source "$BATS_ROOT/lib/bats-core/common.bash"
+
+bats_exit_trap() {
+  local status
+  local exit_metadata=''
+  local killer_pid=${1:-}
+  trap - ERR EXIT
+  if [[ -n "${BATS_TEST_TIMEOUT:-}" ]]; then
+    # Kill the watchdog in the case of of kernel finished before the timeout
+    bats_abort_timeout_countdown "$killer_pid" || status=1
+  fi
+
+  if [[ -n "$BATS_TEST_SKIPPED" ]]; then
+    exit_metadata=' # skip'
+    if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then
+      exit_metadata+=" $BATS_TEST_SKIPPED"
+    fi
+  elif [[ "${BATS_TIMED_OUT-NOTSET}" != NOTSET ]]; then
+    exit_metadata=" # timeout after ${BATS_TEST_TIMEOUT}s"
+  fi
+
+  BATS_TEST_TIME=''
+  if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+    BATS_TEST_TIME=" in "$(($(get_mills_since_epoch) - BATS_TEST_START_TIME))"ms"
+  fi
+
+  local print_bats_out="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS}"
+
+  local should_retry=''
+  if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" || "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then
+    if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
+      # For some versions of bash, `$?` may not be set properly for some error
+      # conditions before triggering the EXIT trap directly (see #72 and #81).
+      # Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such
+      # errors if they happen during `teardown()` when `bats_perform_test` calls
+      # `bats_teardown_trap` directly after the test itself passes.
+      #
+      # If instead the test fails, and the `teardown()` error happens while
+      # `bats_teardown_trap` runs as the EXIT trap, the test will fail with no
+      # output, since there's no way to reach the `bats_exit_trap` call.
+      BATS_ERROR_STATUS=1
+    fi
+    if bats_should_retry_test; then
+      should_retry=1
+      status=126                # signify retry
+      rm -r "$BATS_TEST_TMPDIR" # clean up for retry
+    else
+      printf 'not ok %d %s%s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" "$exit_metadata" >&3
+      local stack_trace
+      bats_get_failure_stack_trace stack_trace
+      bats_print_stack_trace "${stack_trace[@]}" >&3
+      bats_print_failed_command "${stack_trace[@]}" >&3
+
+      if [[ $BATS_PRINT_OUTPUT_ON_FAILURE ]]; then
+        if [[ -n "${output:-}" ]]; then
+          printf "Last output:\n%s\n" "$output"
+        fi
+        if [[ -n "${stderr:-}" ]]; then
+          printf "Last stderr: \n%s\n" "$stderr"
+        fi
+      fi >>"$BATS_OUT"
+
+      print_bats_out=1
+      status=1
+      local state=failed
+    fi
+  else
+    printf 'ok %d %s%s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" \
+      "$exit_metadata" >&3
+    status=0
+    local state=passed
+  fi
+
+  if [[ -z "$should_retry" ]]; then
+    printf "%s %s\t%s\n" "$state" "$BATS_TEST_FILENAME" "$BATS_TEST_NAME" >>"$BATS_RUNLOG_FILE"
+
+    if [[ $print_bats_out ]]; then
+      bats_prefix_lines_for_tap_output <"$BATS_OUT" | bats_replace_filename >&3
+    fi
+  fi
+  if [[ $BATS_GATHER_TEST_OUTPUTS_IN ]]; then
+    local try_suffix=
+    if [[ -n "$should_retry" ]]; then
+      try_suffix="-try$BATS_TEST_TRY_NUMBER"
+    fi
+    cp "$BATS_OUT" "$BATS_GATHER_TEST_OUTPUTS_IN/$BATS_SUITE_TEST_NUMBER$try_suffix-$BATS_TEST_DESCRIPTION.log"
+  fi
+  rm -f "$BATS_OUT"
+  exit "$status"
+}
+
+# Marks the test as failed due to timeout.
+# The actual termination of subprocesses is done via pkill in the background
+# process in bats_start_timeout_countdown
+# shellcheck disable=SC2317
+bats_timeout_trap() {
+  BATS_TIMED_OUT=1
+  BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=
+  exit 1
+}
+
+bats_get_child_processes_of() { # <parent-pid>
+  local -ri parent_pid=${1?}
+  {
+    read -ra header
+    local pid_col ppid_col
+    for ((i = 0; i < ${#header[@]}; ++i)); do
+      if [[ ${header[$i]} == "PID" ]]; then
+        pid_col=$i
+      fi
+      if [[ ${header[$i]} == "PPID" ]]; then
+        ppid_col=$i
+      fi
+    done
+    while read -ra row; do
+      if ((${row[$ppid_col]} == parent_pid)); then
+        printf "%d\n" "${row[$pid_col]}"
+      fi
+    done
+  } < <(ps -ef "$parent_pid")
+}
+
+bats_kill_childprocesses_of() { # <parent-pid>
+  local -ir parent_pid="${1?}"
+  if command -v pkill; then
+    pkill -P "$parent_pid"
+  else
+    # kill in reverse order (latest first)
+    while read -r pid; do
+      kill "$pid"
+    done < <(bats_get_child_processes_of "$parent_pid" | sort -r)
+  fi >/dev/null
+}
+
+# sets a timeout for this process
+#
+# using SIGABRT for interprocess communication.
+# Ruled out:
+# USR1/2 - not available on Windows
+# SIGALRM - interferes with sleep:
+#           "sleep(3) may be implemented using SIGALRM; mixing calls to alarm()
+#           and sleep(3) is a bad idea." ~ https://linux.die.net/man/2/alarm
+bats_start_timeout_countdown() { # <timeout>
+  local -ri timeout=$1
+  local -ri target_pid=$$
+  # shellcheck disable=SC2064
+  trap "bats_timeout_trap $target_pid" ABRT
+  if ! (command -v ps || command -v pkill) >/dev/null; then
+    printf "Error: Cannot execute timeout because neither pkill nor ps are available on this system!\n" >&2
+    exit 1
+  fi
+  # Start another process to kill the children of this process
+  (
+    sleep "$timeout" &
+    # sleep won't recieve signals, so we use wait below
+    # and kill sleep explicitly when signalled to do so
+    # shellcheck disable=SC2064
+    trap "kill $!; exit 0" ABRT
+    wait
+    if kill -ABRT "$target_pid"; then
+      # get rid of signal blocking child processes (like sleep)
+      bats_kill_childprocesses_of "$target_pid"
+    fi &>/dev/null
+  ) &
+}
+
+bats_abort_timeout_countdown() {
+  # kill the countdown process, don't care if its still there
+  kill -ABRT "$1" &>/dev/null || true
+}
+
+get_mills_since_epoch() {
+  local ms_since_epoch
+  ms_since_epoch=$(date +%s%N)
+  if [[ "$ms_since_epoch" == *N || "${#ms_since_epoch}" -lt 19 ]]; then
+    ms_since_epoch=$(($(date +%s) * 1000))
+  else
+    ms_since_epoch=$((ms_since_epoch / 1000000))
+  fi
+
+  printf "%d\n" "$ms_since_epoch"
+}
+
+bats_perform_test() {
+  if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then
+    local quoted_test_name
+    bats_quote_code quoted_test_name "$BATS_TEST_NAME"
+    printf "bats: unknown test name %s\n" "$quoted_test_name" >&2
+    exit 1
+  fi
+
+  # is this skipped from outside ?
+  if [[ -n "${BATS_TEST_SKIPPED-}" ]]; then
+    # forward skip (with message) by overriding setup
+    # shellcheck disable=SC2317
+    setup() {
+      skip "$BATS_TEST_SKIPPED"
+    }
+  fi
+
+  local BATS_killer_pid=''
+  if [[ -n "${BATS_TEST_TIMEOUT:-}" ]]; then
+    bats_start_timeout_countdown "$BATS_TEST_TIMEOUT"
+    BATS_killer_pid=$!
+  fi
+  BATS_TEST_COMPLETED=
+  BATS_TEST_SKIPPED=${BATS_TEST_SKIPPED-}
+  BATS_TEARDOWN_COMPLETED=
+  BATS_ERROR_STATUS=
+  bats_setup_tracing
+  # use parameter to mark this call as trap call
+  # shellcheck disable=SC2064
+  trap "bats_teardown_trap as-exit-trap $BATS_killer_pid" EXIT
+
+  BATS_TEST_START_TIME=$(get_mills_since_epoch)
+  "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 4>&1
+
+  BATS_TEST_COMPLETED=1
+  # shellcheck disable=SC2064
+  trap "bats_exit_trap $BATS_killer_pid" EXIT
+  bats_teardown_trap "" "$BATS_killer_pid" # pass empty parameter to signify call outside trap
+}
+
+trap bats_interrupt_trap INT
+
+# shellcheck source=lib/bats-core/preprocessing.bash
+source "$BATS_ROOT/lib/bats-core/preprocessing.bash"
+
+exec 3<&1
+
+bats_create_test_tmpdirs
+# Run the given test.
+bats_evaluate_preprocessed_source
+bats_perform_test
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-cat b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-cat
new file mode 100755
index 00000000..a6f8abc9
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-cat
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+trap '' INT
+
+cat
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-junit b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-junit
new file mode 100755
index 00000000..5436157e
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-junit
@@ -0,0 +1,250 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# shellcheck source=lib/bats-core/formatter.bash
+source "$BATS_ROOT/lib/bats-core/formatter.bash"
+
+BASE_PATH=.
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  --base-path)
+    shift
+    normalize_base_path BASE_PATH "$1"
+    ;;
+  esac
+  shift
+done
+
+init_suite() {
+  suite_test_exec_time=0
+  # since we have to print the suite header before its contents but we don't know the contents before the header,
+  # we have to buffer the contents
+  _suite_buffer=""
+  test_result_state="" # declare for the first flush, when no test has been encountered
+}
+
+_buffer_log=
+init_file() {
+  file_count=0
+  file_failures=0
+  file_skipped=0
+  file_exec_time=0
+  test_exec_time=0
+  name=""
+  _buffer=""
+  _buffer_log=""
+  _system_out_log=""
+  test_result_state="" # mark that no test has run in this file so far
+}
+
+host() {
+  local hostname="${HOST:-}"
+  [[ -z "$hostname" ]] && hostname="${HOSTNAME:-}"
+  [[ -z "$hostname" ]] && hostname="$(uname -n)"
+  [[ -z "$hostname" ]] && hostname="$(hostname -f)"
+
+  echo "$hostname"
+}
+
+# convert $1 (time in milliseconds) to seconds
+milliseconds_to_seconds() {
+  # we cannot rely on having bc for this calculation
+  full_seconds=$(($1 / 1000))
+  remaining_milliseconds=$(($1 % 1000))
+  if [[ $remaining_milliseconds -eq 0 ]]; then
+    printf "%d" "$full_seconds"
+  else
+    printf "%d.%03d" "$full_seconds" "$remaining_milliseconds"
+  fi
+}
+
+suite_header() {
+  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<testsuites time=\"%s\">\n" "$(milliseconds_to_seconds "${suite_test_exec_time}")"
+}
+
+file_header() {
+  timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S")
+  printf "<testsuite name=\"%s\" tests=\"%s\" failures=\"%s\" errors=\"0\" skipped=\"%s\" time=\"%s\" timestamp=\"%s\" hostname=\"%s\">\n" \
+    "$(xml_escape "${class}")" "${file_count}" "${file_failures}" "${file_skipped}" "$(milliseconds_to_seconds "${file_exec_time}")" "${timestamp}" "$(host)"
+}
+
+file_footer() {
+  printf "</testsuite>\n"
+}
+
+suite_footer() {
+  printf "</testsuites>\n"
+}
+
+print_test_case() {
+  if [[ "$test_result_state" == ok && -z "$_system_out_log" && -z "$_buffer_log" ]]; then
+    # pass and no output can be shortened
+    printf "    <testcase classname=\"%s\" name=\"%s\" time=\"%s\" />\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
+  else
+    printf "    <testcase classname=\"%s\" name=\"%s\" time=\"%s\">\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
+    if [[ -n "$_system_out_log" ]]; then
+      printf "        <system-out>%s</system-out>\n" "$(xml_escape "${_system_out_log}")"
+    fi
+    if [[ -n "$_buffer_log" || "$test_result_state" == not_ok ]]; then
+      printf "        <failure type=\"failure\">%s</failure>\n" "$(xml_escape "${_buffer_log}")"
+    fi
+    if [[ "$test_result_state" == skipped ]]; then
+      printf "        <skipped>%s</skipped>\n" "$(xml_escape "$test_skip_message")"
+    fi
+    printf "    </testcase>\n"
+  fi
+}
+
+xml_escape() {
+  output=${1//&/\&amp;}
+  output=${output//</\&lt;}
+  output=${output//>/\&gt;}
+  output=${output//'"'/\&quot;}
+  output=${output//\'/\&#39;}
+  local CONTROL_CHAR=$'\033'
+  output=${output//$CONTROL_CHAR/\&#27;}
+  printf "%s" "$output"
+}
+
+suite_buffer() {
+  local output
+  output="$(
+    "$@"
+    printf "x"
+  )" # use x marker to avoid losing trailing newlines
+  _suite_buffer="${_suite_buffer}${output%x}"
+}
+
+suite_flush() {
+  echo -n "${_suite_buffer}"
+  _suite_buffer=""
+}
+
+buffer() {
+  local output
+  output="$(
+    "$@"
+    printf "x"
+  )" # use x marker to avoid losing trailing newlines
+  _buffer="${_buffer}${output%x}"
+}
+
+flush() {
+  echo -n "${_buffer}"
+  _buffer=""
+}
+
+log() {
+  if [[ -n "$_buffer_log" ]]; then
+    _buffer_log="${_buffer_log}
+$1"
+  else
+    _buffer_log="$1"
+  fi
+}
+
+flush_log() {
+  if [[ -n "$test_result_state" ]]; then
+    buffer print_test_case
+  fi
+  _buffer_log=""
+  _system_out_log=""
+}
+
+log_system_out() {
+  if [[ -n "$_system_out_log" ]]; then
+    _system_out_log="${_system_out_log}
+$1"
+  else
+    _system_out_log="$1"
+  fi
+}
+
+finish_file() {
+  if [[ "${class-JUNIT_FORMATTER_NO_FILE_ENCOUNTERED}" != JUNIT_FORMATTER_NO_FILE_ENCOUNTERED ]]; then
+    file_header
+    printf "%s\n" "${_buffer}"
+    file_footer
+  fi
+}
+
+finish_suite() {
+  flush_log
+  suite_header
+  suite_flush
+  finish_file # must come after suite flush to not print the last file before the others
+  suite_footer
+}
+
+bats_tap_stream_plan() { #  <number of tests>
+  :
+}
+
+init_suite
+trap finish_suite EXIT
+trap '' INT
+
+bats_tap_stream_begin() { # <test index> <test name>
+  flush_log
+  # set after flushing to avoid overriding name of test
+  name="$2"
+}
+
+bats_tap_stream_ok() { # <test index> <test name>
+  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
+  ((file_count += 1))
+  test_result_state='ok'
+  file_exec_time="$((file_exec_time + test_exec_time))"
+  suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
+}
+
+bats_tap_stream_skipped() { # <test index> <test name> <skip reason>
+  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
+  ((file_count += 1))
+  ((file_skipped += 1))
+  test_result_state='skipped'
+  test_exec_time=0
+  test_skip_message="$3"
+}
+
+bats_tap_stream_not_ok() { # <test index> <test name>
+  test_exec_time=${BATS_FORMATTER_TEST_DURATION:-0}
+  ((file_count += 1))
+  ((file_failures += 1))
+  test_result_state=not_ok
+  file_exec_time="$((file_exec_time + test_exec_time))"
+  suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
+}
+
+bats_tap_stream_comment() { # <comment text without leading '# '> <scope>
+  local comment="$1" scope="$2"
+  case "$scope" in
+  begin)
+    # everything that happens between begin and [not] ok is FD3 output from the test
+    log_system_out "$comment"
+    ;;
+  ok)
+    # non failed tests can produce FD3 output
+    log_system_out "$comment"
+    ;;
+  *)
+    # everything else is considered error output
+    log "$1"
+    ;;
+  esac
+}
+
+bats_tap_stream_suite() { # <file name>
+  flush_log
+  suite_buffer finish_file
+  init_file
+  class="${1/$BASE_PATH/}"
+}
+
+bats_tap_stream_unknown() { # <full line>
+  :
+}
+
+bats_parse_internal_extended_tap
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-pretty b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-pretty
new file mode 100755
index 00000000..2b03eed8
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-pretty
@@ -0,0 +1,348 @@
+#!/usr/bin/env bash
+set -e
+
+# shellcheck source=lib/bats-core/formatter.bash
+source "$BATS_ROOT/lib/bats-core/formatter.bash"
+
+BASE_PATH=.
+BATS_ENABLE_TIMING=
+
+while [[ "$#" -ne 0 ]]; do
+  case "$1" in
+  -T)
+    BATS_ENABLE_TIMING="-T"
+    ;;
+  --base-path)
+    shift
+    normalize_base_path BASE_PATH "$1"
+    ;;
+  esac
+  shift
+done
+
+update_count_column_width() {
+  count_column_width=$((${#count} * 2 + 2))
+  if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+    # additional space for ' in %s sec'
+    count_column_width=$((count_column_width + ${#SECONDS} + 8))
+  fi
+  # also update dependent value
+  update_count_column_left
+}
+
+update_screen_width() {
+  screen_width="$(tput cols)"
+  # also update dependent value
+  update_count_column_left
+}
+
+update_count_column_left() {
+  count_column_left=$((screen_width - count_column_width))
+}
+
+# avoid unset variables
+count=0
+screen_width=80
+update_count_column_width
+update_screen_width
+test_result=
+
+trap update_screen_width WINCH
+
+begin() {
+  test_result= # reset to avoid carrying over result state from previous test
+  line_backoff_count=0
+  go_to_column 0
+  update_count_column_width
+  buffer_with_truncation $((count_column_left - 1)) '   %s' "$name"
+  clear_to_end_of_line
+  go_to_column $count_column_left
+  if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+    buffer "%${#count}s/${count} in %s sec" "$index" "$SECONDS"
+  else
+    buffer "%${#count}s/${count}" "$index"
+  fi
+  go_to_column 1
+}
+
+finish_test() {
+  move_up $line_backoff_count
+  go_to_column 0
+  buffer "$@"
+  if [[ -n "${TIMEOUT-}" ]]; then
+    set_color 2
+    if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+      buffer ' [%s (timeout: %s)]' "$TIMING" "$TIMEOUT"
+    else
+      buffer ' [timeout: %s]' "$TIMEOUT"
+    fi
+  else
+    if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+      set_color 2
+      buffer ' [%s]' "$TIMING"
+    fi
+  fi
+  advance
+  move_down $((line_backoff_count - 1))
+}
+
+pass() {
+  local TIMING="${1:-}"
+  finish_test ' ✓ %s' "$name"
+  test_result=pass
+}
+
+skip() {
+  local reason="$1" TIMING="${2:-}"
+  if [[ -n "$reason" ]]; then
+    reason=": $reason"
+  fi
+  finish_test ' - %s (skipped%s)' "$name" "$reason"
+  test_result=skip
+}
+
+fail() {
+  local TIMING="${1:-}"
+  set_color 1 bold
+  finish_test ' ✗ %s' "$name"
+  test_result=fail
+}
+
+timeout() {
+  local TIMING="${1:-}"
+  set_color 3 bold
+  TIMEOUT="${2:-}" finish_test ' ✗ %s' "$name"
+  test_result=timeout
+}
+
+log() {
+  case ${test_result} in
+  pass)
+    clear_color
+    ;;
+  fail)
+    set_color 1
+    ;;
+  timeout)
+    set_color 3
+    ;;
+  esac
+  buffer '   %s\n' "$1"
+  clear_color
+}
+
+summary() {
+  if [ "$failures" -eq 0 ]; then
+    set_color 2 bold
+  else
+    set_color 1 bold
+  fi
+
+  buffer '\n%d test' "$count"
+  if [[ "$count" -ne 1 ]]; then
+    buffer 's'
+  fi
+
+  buffer ', %d failure' "$failures"
+  if [[ "$failures" -ne 1 ]]; then
+    buffer 's'
+  fi
+
+  if [[ "$skipped" -gt 0 ]]; then
+    buffer ', %d skipped' "$skipped"
+  fi
+
+  if ((timed_out > 0)); then
+    buffer ', %d timed out' "$timed_out"
+  fi
+
+  not_run=$((count - passed - failures - skipped - timed_out))
+  if [[ "$not_run" -gt 0 ]]; then
+    buffer ', %d not run' "$not_run"
+  fi
+
+  if [[ -n "$BATS_ENABLE_TIMING" ]]; then
+    buffer " in $SECONDS seconds"
+  fi
+
+  buffer '\n'
+  clear_color
+}
+
+buffer_with_truncation() {
+  local width="$1"
+  shift
+  local string
+
+  # shellcheck disable=SC2059
+  printf -v 'string' -- "$@"
+
+  if [[ "${#string}" -gt "$width" ]]; then
+    buffer '%s...' "${string:0:$((width - 4))}"
+  else
+    buffer '%s' "$string"
+  fi
+}
+
+move_up() {
+  if [[ $1 -gt 0 ]]; then # avoid moving if we got 0
+    buffer '\x1B[%dA' "$1"
+  fi
+}
+
+move_down() {
+  if [[ $1 -gt 0 ]]; then # avoid moving if we got 0
+    buffer '\x1B[%dB' "$1"
+  fi
+}
+
+go_to_column() {
+  local column="$1"
+  buffer '\x1B[%dG' $((column + 1))
+}
+
+clear_to_end_of_line() {
+  buffer '\x1B[K'
+}
+
+advance() {
+  clear_to_end_of_line
+  buffer '\n'
+  clear_color
+}
+
+set_color() {
+  local color="$1"
+  local weight=22
+
+  if [[ "${2:-}" == 'bold' ]]; then
+    weight=1
+  fi
+  buffer '\x1B[%d;%dm' "$((30 + color))" "$weight"
+}
+
+clear_color() {
+  buffer '\x1B[0m'
+}
+
+_buffer=
+
+buffer() {
+  local content
+  # shellcheck disable=SC2059
+  printf -v content -- "$@"
+  _buffer+="$content"
+}
+
+prefix_buffer_with() {
+  local old_buffer="$_buffer"
+  _buffer=''
+  "$@"
+  _buffer="$_buffer$old_buffer"
+}
+
+flush() {
+  printf '%s' "$_buffer"
+  _buffer=
+}
+
+finish() {
+  flush
+  printf '\n'
+}
+
+trap finish EXIT
+trap '' INT
+
+bats_tap_stream_plan() {
+  count="$1"
+  index=0
+  passed=0
+  failures=0
+  skipped=0
+  timed_out=0
+  name=
+  update_count_column_width
+}
+
+bats_tap_stream_begin() {
+  index="$1"
+  name="$2"
+  begin
+  flush
+}
+
+bats_tap_stream_ok() {
+  index="$1"
+  name="$2"
+  ((++passed))
+
+  pass "${BATS_FORMATTER_TEST_DURATION:-}"
+}
+
+bats_tap_stream_skipped() {
+  index="$1"
+  name="$2"
+  ((++skipped))
+  skip "$3" "${BATS_FORMATTER_TEST_DURATION:-}"
+}
+
+bats_tap_stream_not_ok() {
+  index="$1"
+  name="$2"
+
+  if [[ ${BATS_FORMATTER_TEST_TIMEOUT-x} != x ]]; then
+    timeout "${BATS_FORMATTER_TEST_DURATION:-}" "${BATS_FORMATTER_TEST_TIMEOUT}s"
+    ((++timed_out))
+  else
+    fail "${BATS_FORMATTER_TEST_DURATION:-}"
+    ((++failures))
+  fi
+
+}
+
+bats_tap_stream_comment() { # <comment> <scope>
+  local scope=$2
+  # count the lines we printed after the begin text,
+  if [[ $line_backoff_count -eq 0 && $scope == begin ]]; then
+    # if this is the first line after begin, go down one line
+    buffer "\n"
+    ((++line_backoff_count)) # prefix-increment to avoid "error" due to returning 0
+  fi
+
+  ((++line_backoff_count))
+  ((line_backoff_count += ${#1} / screen_width)) # account for linebreaks due to length
+  log "$1"
+}
+
+bats_tap_stream_suite() {
+  #test_file="$1"
+  line_backoff_count=0
+  index=
+  # indicate filename for failures
+  local file_name="${1#"$BASE_PATH"}"
+  name="File $file_name"
+  set_color 4 bold
+  buffer "%s\n" "$file_name"
+  clear_color
+}
+
+line_backoff_count=0
+bats_tap_stream_unknown() { # <full line> <scope>
+  local scope=$2
+  # count the lines we printed after the begin text, (or after suite, in case of syntax errors)
+  if [[ $line_backoff_count -eq 0 && ($scope == begin || $scope == suite) ]]; then
+    # if this is the first line after begin, go down one line
+    buffer "\n"
+    ((++line_backoff_count)) # prefix-increment to avoid "error" due to returning 0
+  fi
+
+  ((++line_backoff_count))
+  ((line_backoff_count += ${#1} / screen_width)) # account for linebreaks due to length
+  buffer "%s\n" "$1"
+  flush
+}
+
+bats_parse_internal_extended_tap
+
+summary
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap
new file mode 100755
index 00000000..4aed2c14
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+set -e
+trap '' INT
+
+# shellcheck source=lib/bats-core/formatter.bash
+source "$BATS_ROOT/lib/bats-core/formatter.bash"
+
+bats_tap_stream_plan() {
+  printf "1..%d\n" "$1"
+}
+
+bats_tap_stream_begin() { #<test index> <test name>
+  :
+}
+
+bats_tap_stream_ok() { # [<test index> <test name>
+  printf "ok %d %s" "$1" "$2"
+  if [[ "${BATS_FORMATTER_TEST_DURATION-x}" != x ]]; then
+    printf " # in %d ms" "$BATS_FORMATTER_TEST_DURATION"
+  fi
+  printf "\n"
+}
+
+bats_tap_stream_not_ok() { # <test index> <test name>
+  printf "not ok %d %s" "$1" "$2"
+  if [[ "${BATS_FORMATTER_TEST_DURATION-x}" != x ]]; then
+    printf " # in %d ms" "$BATS_FORMATTER_TEST_DURATION"
+  fi
+  if [[ "${BATS_FORMATTER_TEST_TIMEOUT-x}" != x ]]; then
+    printf " # timeout after %d s" "${BATS_FORMATTER_TEST_TIMEOUT}"
+  fi
+  printf "\n"
+}
+
+bats_tap_stream_skipped() { # <test index> <test name> <reason>
+  if [[ $# -eq 3 ]]; then
+    printf "ok %d %s # skip %s\n" "$1" "$2" "$3"
+  else
+    printf "ok %d %s # skip\n" "$1" "$2"
+  fi
+}
+
+bats_tap_stream_comment() { # <comment text without leading '# '>
+  printf "# %s\n" "$1"
+}
+
+bats_tap_stream_suite() { # <file name>
+  :
+}
+
+bats_tap_stream_unknown() { # <full line>
+  printf "%s\n" "$1"
+}
+
+bats_parse_internal_extended_tap
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap13 b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap13
new file mode 100755
index 00000000..98a3ffaf
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-format-tap13
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+set -e
+
+yaml_block_open=''
+add_yaml_entry() {
+  if [[ -z "$yaml_block_open" ]]; then
+    printf "  ---\n"
+  fi
+  printf "  %s: %s\n" "$1" "$2"
+  yaml_block_open=1
+}
+
+close_previous_yaml_block() {
+  if [[ -n "$yaml_block_open" ]]; then
+    printf "  ...\n"
+    yaml_block_open=''
+  fi
+}
+
+trap '' INT
+
+number_of_printed_log_lines_for_this_test_so_far=0
+
+# shellcheck source=lib/bats-core/formatter.bash
+source "$BATS_ROOT/lib/bats-core/formatter.bash"
+
+bats_tap_stream_plan() {
+  printf "TAP version 13\n"
+  printf "1..%d\n" "$1"
+}
+
+bats_tap_stream_begin() { #<test index> <test name>
+  :
+}
+
+bats_tap_stream_ok() { # <test index> <test name>
+  close_previous_yaml_block
+  number_of_printed_log_lines_for_this_test_so_far=0
+  printf "ok %d %s\n" "$1" "$2"
+  if [[ "${BATS_FORMATTER_TEST_DURATION-x}" != x ]]; then
+    add_yaml_entry "duration_ms" "${BATS_FORMATTER_TEST_DURATION}"
+  fi
+}
+
+pass_on_optional_data() {
+  if [[ "${BATS_FORMATTER_TEST_DURATION-x}" != x ]]; then
+    add_yaml_entry "duration_ms" "${BATS_FORMATTER_TEST_DURATION}"
+  fi
+  if [[ "${BATS_FORMATTER_TEST_TIMEOUT-x}" != x ]]; then
+    add_yaml_entry "timeout_sec" "${BATS_FORMATTER_TEST_TIMEOUT}"
+  fi
+}
+
+bats_tap_stream_not_ok() { # <test index> <test name>
+  close_previous_yaml_block
+  number_of_printed_log_lines_for_this_test_so_far=0
+
+  printf "not ok %d %s\n" "$1" "$2"
+  pass_on_optional_data
+}
+
+bats_tap_stream_skipped() { # <test index> <test name> <reason>
+  close_previous_yaml_block
+  number_of_printed_log_lines_for_this_test_so_far=0
+
+  printf "not ok %d %s # SKIP %s\n" "$1" "$2" "$3"
+  pass_on_optional_data
+}
+
+bats_tap_stream_comment() { # <comment text without leading '# '>
+  if [[ $number_of_printed_log_lines_for_this_test_so_far -eq 0 ]]; then
+    add_yaml_entry "message" "|" # use a multiline string for this entry
+  fi
+  ((++number_of_printed_log_lines_for_this_test_so_far))
+  printf "    %s\n" "$1"
+}
+
+bats_tap_stream_suite() { # <file name>
+  :
+}
+
+bats_tap_stream_unknown() { # <full line>
+  :
+}
+
+bats_parse_internal_extended_tap
+
+# close the final block if there was one
+close_previous_yaml_block
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-preprocess b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-preprocess
new file mode 100755
index 00000000..5ef83104
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/libexec/bats-core/bats-preprocess
@@ -0,0 +1,119 @@
+#!/usr/bin/env bash
+set -e
+
+bats_encode_test_name() {
+  local name="$1"
+  local result='test_'
+  local hex_code
+
+  if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then
+    name="${name//_/-5f}"
+    name="${name//-/-2d}"
+    name="${name// /_}"
+    result+="$name"
+  else
+    local length="${#name}"
+    local char i
+
+    for ((i = 0; i < length; i++)); do
+      char="${name:$i:1}"
+      if [[ "$char" == ' ' ]]; then
+        result+='_'
+      elif [[ "$char" =~ [[:alnum:]] ]]; then
+        result+="$char"
+      else
+        printf -v 'hex_code' -- '-%02x' \'"$char"
+        result+="$hex_code"
+      fi
+    done
+  fi
+
+  printf -v "$2" '%s' "$result"
+}
+
+BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$"
+BATS_TEST_PATTERN_COMMENT="[[:blank:]]*([^[:blank:]()]+)[[:blank:]]*\(?\)?[[:blank:]]+\{[[:blank:]]+#[[:blank:]]*@test[[:blank:]]*\$"
+BATS_COMMENT_COMMAND_PATTERN="^[[:blank:]]*#[[:blank:]]*bats[[:blank:]]+(.*)$"
+BATS_VALID_TAG_PATTERN="[-_:[:alnum:]]+"
+BATS_VALID_TAGS_PATTERN="^ *($BATS_VALID_TAG_PATTERN)?( *, *$BATS_VALID_TAG_PATTERN)* *$"
+
+# shellcheck source=lib/bats-core/common.bash
+source "$BATS_ROOT/lib/bats-core/common.bash"
+
+extract_tags() { # <tag_type/return_var> <tags-string>
+  local -r tag_type=$1 tags_string=$2
+  local -a tags=()
+
+  if [[ $tags_string =~ $BATS_VALID_TAGS_PATTERN ]]; then
+    IFS=, read -ra tags <<<"$tags_string"
+    local -ri length=${#tags[@]}
+    for ((i = 0; i < length; ++i)); do
+      local element="tags[$i]"
+      bats_trim "$element" "${!element}" 2>/dev/null # printf on bash 3 will complain but work anyways
+      if [[ -z "${!element}" && -n "${CHECK_BATS_COMMENT_COMMANDS:-}" ]]; then
+        printf "%s:%d: Error: Invalid %s: '%s'. " "$test_file" "$line_number" "$tag_type" "$tags_string"
+        printf "Tags must not be empty. Please remove redundant commas!\n"
+        exit_code=1
+      fi
+    done
+  elif [[ -n "${CHECK_BATS_COMMENT_COMMANDS:-}" ]]; then
+    printf "%s:%d: Error: Invalid %s: '%s'. " "$test_file" "$line_number" "$tag_type" "$tags_string"
+    printf "Valid tags must match %s and be separated with comma (and optional spaces)\n" "$BATS_VALID_TAG_PATTERN"
+    exit_code=1
+  fi >&2
+  if ((${#tags[@]} > 0)); then
+    eval "$tag_type=(\"\${tags[@]}\")"
+  else
+    eval "$tag_type=()"
+  fi
+}
+
+test_file="$1"
+tests=()
+test_tags=()
+# shellcheck disable=SC2034 # used in `bats_sort tags`/`extract_tags``
+file_tags=()
+line_number=0
+exit_code=0
+{
+  while IFS= read -r line; do
+    ((++line_number))
+    line="${line//$'\r'/}"
+    if [[ "$line" =~ $BATS_TEST_PATTERN ]] || [[ "$line" =~ $BATS_TEST_PATTERN_COMMENT ]]; then
+      name="${BASH_REMATCH[1]#[\'\"]}"
+      name="${name%[\'\"]}"
+      body="${BASH_REMATCH[2]:-}"
+      bats_encode_test_name "$name" 'encoded_name'
+      printf '%s() { bats_test_begin "%s"; %s\n' "${encoded_name:?}" "$name" "$body" || :
+
+      bats_append_arrays_as_args \
+        test_tags file_tags \
+        -- bats_sort tags
+      if [[ -z "$BATS_TEST_FILTER" || "$name" =~ $BATS_TEST_FILTER ]]; then
+        IFS=,
+        tests+=("--tags '${tags[*]-}' $encoded_name")
+      fi
+      # shellcheck disable=SC2034 # used in `bats_sort tags`/`extract_tags`
+      test_tags=() # reset test tags for next test
+    else
+      if [[ "$line" =~ $BATS_COMMENT_COMMAND_PATTERN ]]; then
+        command=${BASH_REMATCH[1]}
+        case $command in
+        'test_tags='*)
+          extract_tags test_tags "${command#test_tags=}"
+          ;;
+        'file_tags='*)
+          extract_tags file_tags "${command#file_tags=}"
+          ;;
+        esac
+      fi
+      printf '%s\n' "$line"
+    fi
+  done
+} <<<"$(<"$test_file")"$'\n'
+
+for test_name in "${tests[@]}"; do
+  printf 'bats_test_function %s\n' "$test_name"
+done
+
+exit $exit_code
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/share/man/man1/bats.1 b/govc/test/vendor/github.com/bats-core/bats-core/share/man/man1/bats.1
new file mode 100644
index 00000000..c806f575
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/share/man/man1/bats.1
@@ -0,0 +1,143 @@
+.\" generated with Ronn-NG/v0.9.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
+.TH "BATS" "1" "November 2022" "bats-core" "Bash Automated Testing System"
+.SH "NAME"
+\fBbats\fR \- Bash Automated Testing System
+.SH "SYNOPSIS"
+Usage: bats [OPTIONS] \fItests\fR bats [\-h | \-v]
+.P
+\fItests\fR is the path to a Bats test file, or the path to a directory containing Bats test files (ending with "\.bats")
+.SH "DESCRIPTION"
+Bats is a TAP\-compliant testing framework for Bash\. It provides a simple way to verify that the UNIX programs you write behave as expected\.
+.P
+A Bats test file is a Bash script with special syntax for defining test cases\. Under the hood, each test case is just a function with a description\.
+.P
+Test cases consist of standard shell commands\. Bats makes use of Bash\'s \fBerrexit\fR (\fBset \-e\fR) option when running test cases\. If every command in the test case exits with a \fB0\fR status code (success), the test passes\. In this way, each line is an assertion of truth\.
+.P
+See \fBbats\fR(7) for more information on writing Bats tests\.
+.SH "RUNNING TESTS"
+To run your tests, invoke the \fBbats\fR interpreter with a path to a test file\. The file\'s test cases are run sequentially and in isolation\. If all the test cases pass, \fBbats\fR exits with a \fB0\fR status code\. If there are any failures, \fBbats\fR exits with a \fB1\fR status code\.
+.P
+You can invoke the \fBbats\fR interpreter with multiple test file arguments, or with a path to a directory containing multiple \fB\.bats\fR files\. Bats will run each test file individually and aggregate the results\. If any test case fails, \fBbats\fR exits with a \fB1\fR status code\.
+.SH "FILTERING TESTS"
+There are multiple mechanisms to filter which tests to execute:
+.IP "\[ci]" 4
+\fB\-\-filter <regex>\fR to filter by test name
+.IP "\[ci]" 4
+\fB\-\-filter\-status <status>\fR to filter by the test\'s status in the last run
+.IP "\[ci]" 4
+\fB\-\-filter\-tags <tag\-list>\fR to filter by the tags of a test
+.IP "" 0
+.SH "\-\-FILTER\-TAGS <var>TAG\-LIST</var>"
+Tags can be used for finegrained filtering of which tests to run via \fB\-\-filter\-tags\fR\. This accepts a comma separated list of tags\. Only tests that match all of these tags will be executed\. For example, \fBbats \-\-filter\-tags a,b,c\fR will pick up tests with tags \fBa,b,c\fR, but not tests that miss one or more of those tags\.
+.P
+Additionally, you can specify negative tags via \fBbats \-\-filter\-tags a,!b,c\fR, which now won\'t match tests with tags \fBa,b,c\fR, due to the \fBb\fR, but will select \fBa,c\fR\. To put it more formally, \fB\-\-filter\-tags\fR is a boolean conjunction\.
+.P
+To allow for more complex queries, you can specify multiple \fB\-\-filter\-tags\fR\. A test will be executed, if it matches at least one of them\. This means multiple \fB\-\-filter\-tags\fR form a boolean disjunction\.
+.P
+A query of \fB\-\-filter\-tags a,!b \-\-filter\-tags b,c\fR can be translated to: Execute only tests that (have tag a, but not tag b) or (have tag b and c)\.
+.P
+An empty tag list matches tests without tags\.
+.SH "OPTIONS"
+.TP
+\fB\-c\fR, \fB\-\-count\fR
+Count the number of test cases without running any tests
+.TP
+\fB\-\-code\-quote\-style <style>\fR
+A two character string of code quote delimiters or \fBcustom\fR which requires setting \fB$BATS_BEGIN_CODE_QUOTE\fR and \fB$BATS_END_CODE_QUOTE\fR\. Can also be set via \fB$BATS_CODE_QUOTE_STYLE\fR\.
+.TP
+\fB\-f\fR, \fB\-\-filter <regex>\fR
+Filter test cases by names matching the regular expression
+.TP
+\fB\-F\fR, \fB\-\-formatter <type>\fR
+Switch between formatters: pretty (default), tap (default w/o term), tap13, junit, \fB/<absolute path to formatter>\fR
+.TP
+\fB\-\-filter\-status <status>\fR
+Only run tests with the given status in the last completed (no CTRL+C/SIGINT) run\. Valid \fIstatus\fR values are: failed \- runs tests that failed or were not present in the last run missed \- runs tests that were not present in the last run
+.TP
+\fB\-\-filter\-tags <comma\-separated\-tag\-list>\fR
+Only run tests that match all the tags in the list (\fB&&\fR)\. You can negate a tag via prepending \fB!\fR\. Specifying this flag multiple times allows for logical or (\fB||\fR): \fB\-\-filter\-tags A,B \-\-filter\-tags A,!C\fR matches tags \fB(A && B) || (A && !C)\fR
+.TP
+\fB\-\-gather\-test\-outputs\-in <directory>\fR
+Gather the output of failing \fIand\fR passing tests as files in directory
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Display this help message
+.TP
+\fB\-j\fR, \fB\-\-jobs <jobs>\fR
+Number of parallel jobs (requires GNU parallel)
+.TP
+\fB\-\-no\-tempdir\-cleanup\fR
+Preserve test output temporary directory
+.TP
+\fB\-\-no\-parallelize\-across\-files\fR
+Serialize test file execution instead of running them in parallel (requires \-\-jobs >1)
+.TP
+\fB\-\-no\-parallelize\-within\-files\fR
+Serialize test execution within files instead of running them in parallel (requires \-\-jobs >1)
+.TP
+\fB\-\-report\-formatter <type>\fR
+Switch between reporters (same options as \-\-formatter)
+.TP
+\fB\-o\fR, \fB\-\-output <dir>\fR
+Directory to write report files
+.TP
+\fB\-p\fR, \fB\-\-pretty\fR
+Shorthand for "\-\-formatter pretty"
+.TP
+\fB\-\-print\-output\-on\-failure\fR
+Automatically print the value of \fB$output\fR on failed tests
+.TP
+\fB\-r\fR, \fB\-\-recursive\fR
+Include tests in subdirectories
+.TP
+\fB\-\-show\-output\-of\-passing\-tests\fR
+Print output of passing tests
+.TP
+\fB\-t\fR, \fB\-\-tap\fR
+Shorthand for "\-\-formatter tap"
+.TP
+\fB\-T\fR, \fB\-\-timing\fR
+Add timing information to tests
+.TP
+\fB\-x\fR, \fB\-\-trace\fR
+Print test commands as they are executed (like \fBset \-x\fR)
+.TP
+\fB\-\-verbose\-run\fR
+Make \fBrun\fR print \fB$output\fR by default
+.TP
+\fB\-v\fR, \fB\-\-version\fR
+Display the version number
+.SH "OUTPUT"
+When you run Bats from a terminal, you\'ll see output as each test is performed, with a check\-mark next to the test\'s name if it passes or an "X" if it fails\.
+.IP "" 4
+.nf
+$ bats addition\.bats
+ ✓ addition using bc
+ ✓ addition using dc
+
+2 tests, 0 failures
+.fi
+.IP "" 0
+.P
+If Bats is not connected to a terminal\-\-in other words, if you run it from a continuous integration system or redirect its output to a file\-\-the results are displayed in human\-readable, machine\-parsable TAP format\. You can force TAP output from a terminal by invoking Bats with the \fB\-\-tap\fR option\.
+.IP "" 4
+.nf
+$ bats \-\-tap addition\.bats
+1\.\.2
+ok 1 addition using bc
+ok 2 addition using dc
+.fi
+.IP "" 0
+.SH "EXIT STATUS"
+The \fBbats\fR interpreter exits with a value of \fB0\fR if all test cases pass, or \fB1\fR if one or more test cases fail\.
+.SH "SEE ALSO"
+Bats wiki: \fIhttps://github\.com/bats\-core/bats\-core/wiki/\fR
+.P
+\fBbash\fR(1), \fBbats\fR(7)
+.SH "COPYRIGHT"
+(c) 2017\-2022 bats\-core organization
+.br
+(c) 2011\-2016 Sam Stephenson
+.P
+Bats is released under the terms of an MIT\-style license\.
diff --git a/govc/test/vendor/github.com/bats-core/bats-core/share/man/man7/bats.7 b/govc/test/vendor/github.com/bats-core/bats-core/share/man/man7/bats.7
new file mode 100644
index 00000000..a1b77388
--- /dev/null
+++ b/govc/test/vendor/github.com/bats-core/bats-core/share/man/man7/bats.7
@@ -0,0 +1,239 @@
+.\" generated with Ronn-NG/v0.9.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
+.TH "BATS" "7" "November 2022" "bats-core" "Bash Automated Testing System"
+.SH "NAME"
+\fBbats\fR \- Bats test file format
+.SH "DESCRIPTION"
+A Bats test file is a Bash script with special syntax for defining test cases\. Under the hood, each test case is just a function with a description\.
+.IP "" 4
+.nf
+#!/usr/bin/env bats
+
+@test "addition using bc" {
+  result="$(echo 2+2 | bc)"
+  [ "$result" \-eq 4 ]
+}
+
+@test "addition using dc" {
+  result="$(echo 2 2+p | dc)"
+  [ "$result" \-eq 4 ]
+}
+.fi
+.IP "" 0
+.P
+Each Bats test file is evaluated n+1 times, where \fIn\fR is the number of test cases in the file\. The first run counts the number of test cases, then iterates over the test cases and executes each one in its own process\.
+.SH "Tagging tests"
+Each test has a list of tags attached to it\. Without specification, this list is empty\. Tags can be defined in two ways\. The first being \fB# bats test_tags=\fR:
+.P
+# bats test_tags=tag:1, tag:2, tag:3 @test "second test" { # \|\.\|\.\|\. }
+.P
+@test "second test" { # \|\.\|\.\|\. }
+.P
+These tags (\fBtag:1\fR, \fBtag:2\fR, \fBtag:3\fR) will be attached to the test \fBfirst test\fR\. The second test will have no tags attached\. Values defined in the \fB# bats test_tags=\fR directive will be assigned to the next \fB@test\fR that is being encountered in the file and forgotten after that\. Only the value of the last \fB# bats test_tags=\fR directive before a given test will be used\.
+.P
+Sometimes, we want to give all tests in a file a set of the same tags\. This can be achieved via \fB# bats file_tags=\fR\. They will be added to all tests in the file after that directive\. An additional \fB# bats file_tags=\fR directive will override the previously defined values:
+.IP "" 4
+.nf
+@test "Zeroth test" {
+  # will have no tags
+}
+
+# bats file_tags=a:b
+# bats test_tags=c:d
+
+@test "First test" {
+  # will be tagged a:b, c:d
+}
+
+# bats file_tags=
+
+@test "Second test" {
+  # will have no tags
+}
+.fi
+.IP "" 0
+.P
+Tags are case sensitive and must only consist of alphanumeric characters and \fB_\fR, \fB\-\fR, or \fB:\fR\. They must not contain whitespaces! The colon is intended as a separator for (recursive) namespacing\.
+.P
+Tag lists must be separated by commas and are allowed to contain whitespace\. They must not contain empty tags like \fBtest_tags=,b\fR (first tag is empty), \fBtest_tags=a,,c\fR, \fBtest_tags=a, ,c\fR (second tag is only whitespace/empty), \fBtest_tags=a,b,\fR (third tag is empty)\.
+.P
+Every tag starting with \fBbats:\fR (case insensitive!) is reserved for Bats\' internal use:
+.TP
+\fBbats:focus\fR
+If any test with the tag \fBbats:focus\fR is encountered in a test suite, only those tagged with this tag will be executed\. To prevent the CI from silently running on a subset of tests due to an accidentally commited \fBbats:focus\fR tag, the exit code of successful runs will be overriden to 1\.
+.IP
+Should you require the true exit code, e\.g\. for a \fBgit bisect\fR operation, you can disable this behavior by setting \fBBATS_NO_FAIL_FOCUS_RUN=1\fR when running \fBbats\fR, but make sure to not commit this to CI!
+.SH "THE RUN HELPER"
+Usage: run [OPTIONS] [\-\-]
+.P
+Many Bats tests need to run a command and then make assertions about its exit status and output\. Bats includes a \fBrun\fR helper that invokes its arguments as a command, saves the exit status and output into special global variables, and (optionally) checks exit status against a given expected value\. If successful, \fBrun\fR returns with a \fB0\fR status code so you can continue to make assertions in your test case\.
+.P
+For example, let\'s say you\'re testing that the \fBfoo\fR command, when passed a nonexistent filename, exits with a \fB1\fR status code and prints an error message\.
+.IP "" 4
+.nf
+@test "invoking foo with a nonexistent file prints an error" {
+  run \-1 foo nonexistent_filename
+  [ "$output" = "foo: no such file \'nonexistent_filename\'" ]
+}
+.fi
+.IP "" 0
+.P
+The \fB\-1\fR as first argument tells \fBrun\fR to expect 1 as an exit status, and to fail if the command exits with any other value\. On failure, both actual and expected values will be displayed, along with the invoked command and its output:
+.IP "" 4
+.nf
+(in test file test\.bats, line 2)
+ `run \-1 foo nonexistent_filename\' failed, expected exit code 1, got 127
+.fi
+.IP "" 0
+.P
+This error indicates a possible problem with the installation or configuration of \fBfoo\fR; note that a simple \fB[ $status != 0 ]\fR test would not have caught this kind of failure\.
+.P
+The \fB$status\fR variable contains the status code of the command, and the \fB$output\fR variable contains the combined contents of the command\'s standard output and standard error streams\.
+.P
+A third special variable, the \fB$lines\fR array, is available for easily accessing individual lines of output\. For example, if you want to test that invoking \fBfoo\fR without any arguments prints usage information on the first line:
+.IP "" 4
+.nf
+@test "invoking foo without arguments prints usage" {
+  run \-1 foo
+  [ "${lines[0]}" = "usage: foo <filename>" ]
+}
+.fi
+.IP "" 0
+.P
+By default \fBrun\fR leaves out empty lines in \fB${lines[@]}\fR\. Use \fBrun \-\-keep\-empty\-lines\fR to retain them\.
+.P
+Additionally, you can use \fB\-\-separate\-stderr\fR to split stdout and stderr into \fB$output\fR/\fB$stderr\fR and \fB${lines[@]}\fR/\fB${stderr_lines[@]}\fR\.
+.P
+All additional parameters to run should come before the command\. If you want to run a command that starts with \fB\-\fR, prefix it with \fB\-\-\fR to prevent \fBrun\fR from parsing it as an option\.
+.SH "THE LOAD COMMAND"
+You may want to share common code across multiple test files\. Bats includes a convenient \fBload\fR command for sourcing a Bash source file relative to the location of the current test file\. For example, if you have a Bats test in \fBtest/foo\.bats\fR, the command
+.IP "" 4
+.nf
+load test_helper
+.fi
+.IP "" 0
+.P
+will source the script \fBtest/test_helper\.bash\fR in your test file\. This can be useful for sharing functions to set up your environment or load fixtures\.
+.SH "THE BATS_LOAD_LIBRARY COMMAND"
+Some libraries are installed on the system, e\.g\. by \fBnpm\fR or \fBbrew\fR\. These should not be \fBload\fRed, as their path depends on the installation method\. Instead, one should use \fBbats_load_library\fR together with setting \fBBATS_LIB_PATH\fR, a \fBPATH\fR\-like colon\-delimited variable\.
+.P
+\fBbats_load_library\fR has two modes of resolving requests:
+.IP "1." 4
+by relative path from the \fBBATS_LIB_PATH\fR to a file in the library
+.IP "2." 4
+by library name, expecting libraries to have a \fBload\.bash\fR entrypoint
+.IP "" 0
+.P
+For example if your \fBBATS_LIB_PATH\fR is set to \fB~/\.bats/libs:/usr/lib/bats\fR, then \fBbats_load_library test_helper\fR would look for existing files with the following paths:
+.IP "\[ci]" 4
+\fB~/\.bats/libs/test_helper\fR
+.IP "\[ci]" 4
+\fB~/\.bats/libs/test_helper/load\.bash\fR
+.IP "\[ci]" 4
+\fB/usr/lib/bats/test_helper\fR
+.IP "\[ci]" 4
+\fB/usr/lib/bats/test_helper/load\.bash\fR
+.IP "" 0
+.P
+The first existing file in this list will be sourced\.
+.P
+If you want to load only part of a library or the entry point is not named \fBload\.bash\fR, you have to include it in the argument: \fBbats_load_library library_name/file_to_load\fR will try
+.IP "\[ci]" 4
+\fB~/\.bats/libs/library_name/file_to_load\fR
+.IP "\[ci]" 4
+\fB~/\.bats/libs/library_name/file_to_load/load\.bash\fR
+.IP "\[ci]" 4
+\fB/usr/lib/bats/library_name/file_to_load\fR
+.IP "\[ci]" 4
+\fB/usr/lib/bats/library_name/file_to_load/load\.bash\fR
+.IP "" 0
+.P
+Apart from the changed lookup rules, \fBbats_load_library\fR behaves like \fBload\fR\.
+.P
+\fBNote\fR: As seen above \fBload\.bash\fR is the entry point for libraries and meant to load more files from its directory or other libraries\.
+.P
+\fBNote\fR: Obviously, the actual \fBBATS_LIB_PATH\fR is highly dependent on the environment\. To maintain a uniform location across systems, (distribution) package maintainers are encouraged to use \fB/usr/lib/bats/\fR as the install path for libraries where possible\. However, if the package manager has another preferred location, like \fBnpm\fR or \fBbrew\fR, you should use this instead\.
+.SH "THE SKIP COMMAND"
+Tests can be skipped by using the \fBskip\fR command at the point in a test you wish to skip\.
+.IP "" 4
+.nf
+@test "A test I don\'t want to execute for now" {
+  skip
+  run \-0 foo
+}
+.fi
+.IP "" 0
+.P
+Optionally, you may include a reason for skipping:
+.IP "" 4
+.nf
+@test "A test I don\'t want to execute for now" {
+  skip "This command will return zero soon, but not now"
+  run \-0 foo
+}
+.fi
+.IP "" 0
+.P
+Or you can skip conditionally:
+.IP "" 4
+.nf
+@test "A test which should run" {
+  if [ foo != bar ]; then
+    skip "foo isn\'t bar"
+  fi
+
+  run \-0 foo
+}
+.fi
+.IP "" 0
+.SH "THE BATS_REQUIRE_MINIMUM_VERSION COMMAND"
+Code for newer versions of Bats can be incompatible with older versions\. In the best case this will lead to an error message and a failed test suite\. In the worst case, the tests will pass erroneously, potentially masking a failure\.
+.P
+Use \fBbats_require_minimum_version <Bats version number>\fR to avoid this\. It communicates in a concise manner, that you intend the following code to be run under the given Bats version or higher\.
+.P
+Additionally, this function will communicate the current Bats version floor to subsequent code, allowing e\.g\. Bats\' internal warning to give more informed warnings\.
+.P
+\fBNote\fR: By default, calling \fBbats_require_minimum_version\fR with versions before Bats 1\.7\.0 will fail regardless of the required version as the function is not available\. However, you can use the bats\-backports plugin (https://github\.com/bats\-core/bats\-backports) to make your code usable with older versions, e\.g\. during migration while your CI system is not yet upgraded\.
+.SH "SETUP AND TEARDOWN FUNCTIONS"
+You can define special \fBsetup\fR and \fBteardown\fR functions which run before and after each test case, respectively\. Use these to load fixtures, set up your environment, and clean up when you\'re done\.
+.SH "CODE OUTSIDE OF TEST CASES"
+You can include code in your test file outside of \fB@test\fR functions\. For example, this may be useful if you want to check for dependencies and fail immediately if they\'re not present\. However, any output that you print in code outside of \fB@test\fR, \fBsetup\fR or \fBteardown\fR functions must be redirected to \fBstderr\fR (\fB>&2\fR)\. Otherwise, the output may cause Bats to fail by polluting the TAP stream on \fBstdout\fR\.
+.SH "SPECIAL VARIABLES"
+There are several global variables you can use to introspect on Bats tests:
+.IP "\[ci]" 4
+\fB$BATS_TEST_FILENAME\fR is the fully expanded path to the Bats test file\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_DIRNAME\fR is the directory in which the Bats test file is located\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_NAMES\fR is an array of function names for each test case\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_NAME\fR is the name of the function containing the current test case\.
+.IP "\[ci]" 4
+\fBBATS_TEST_NAME_PREFIX\fR will be prepended to the description of each test on stdout and in reports\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_DESCRIPTION\fR is the description of the current test case\.
+.IP "\[ci]" 4
+\fBBATS_TEST_RETRIES\fR is the maximum number of additional attempts that will be made on a failed test before it is finally considered failed\. The default of 0 means the test must pass on the first attempt\.
+.IP "\[ci]" 4
+\fBBATS_TEST_TIMEOUT\fR is the number of seconds after which a test (including setup) will be aborted and marked as failed\. Updates to this value in \fBsetup()\fR or \fB@test\fR cannot change the running timeout countdown, so the latest useful update location is \fBsetup_file()\fR\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test file\.
+.IP "\[ci]" 4
+\fB$BATS_SUITE_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test suite (over all files)\.
+.IP "\[ci]" 4
+\fB$BATS_TMPDIR\fR is the base temporary directory used by bats to create its temporary files / directories\. (default: \fB$TMPDIR\fR\. If \fB$TMPDIR\fR is not set, \fB/tmp\fR is used\.)
+.IP "\[ci]" 4
+\fB$BATS_RUN_TMPDIR\fR is the location to the temporary directory used by bats to store all its internal temporary files during the tests\. (default: \fB$BATS_TMPDIR/bats\-run\-$BATS_ROOT_PID\-XXXXXX\fR)
+.IP "\[ci]" 4
+\fB$BATS_FILE_EXTENSION\fR (default: \fBbats\fR) specifies the extension of test files that should be found when running a suite (via \fBbats [\-r] suite_folder/\fR)
+.IP "\[ci]" 4
+\fB$BATS_SUITE_TMPDIR\fR is a temporary directory common to all tests of a suite\. Could be used to create files required by multiple tests\.
+.IP "\[ci]" 4
+\fB$BATS_FILE_TMPDIR\fR is a temporary directory common to all tests of a test file\. Could be used to create files required by multiple tests in the same test file\.
+.IP "\[ci]" 4
+\fB$BATS_TEST_TMPDIR\fR is a temporary directory unique for each test\. Could be used to create files required only for specific tests\.
+.IP "\[ci]" 4
+\fB$BATS_VERSION\fR is the version of Bats running the test\.
+.IP "" 0
+.SH "SEE ALSO"
+\fBbash\fR(1), \fBbats\fR(1)
diff --git a/govc/test/vendor/github.com/sstephenson/bats/LICENSE b/govc/test/vendor/github.com/sstephenson/bats/LICENSE
deleted file mode 100644
index bac4eb29..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2014 Sam Stephenson
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats b/govc/test/vendor/github.com/sstephenson/bats/libexec/bats
deleted file mode 100755
index 71f392f7..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-version() {
-  echo "Bats 0.4.0"
-}
-
-usage() {
-  version
-  echo "Usage: bats [-c] [-p | -t] <test> [<test> ...]"
-}
-
-help() {
-  usage
-  echo
-  echo "  <test> is the path to a Bats test file, or the path to a directory"
-  echo "  containing Bats test files."
-  echo
-  echo "  -c, --count    Count the number of test cases without running any tests"
-  echo "  -h, --help     Display this help message"
-  echo "  -p, --pretty   Show results in pretty format (default for terminals)"
-  echo "  -t, --tap      Show results in TAP format"
-  echo "  -v, --version  Display the version number"
-  echo
-  echo "  For more information, see https://github.com/sstephenson/bats"
-  echo
-}
-
-resolve_link() {
-  $(type -p greadlink readlink | head -1) "$1"
-}
-
-abs_dirname() {
-  local cwd="$(pwd)"
-  local path="$1"
-
-  while [ -n "$path" ]; do
-    cd "${path%/*}"
-    local name="${path##*/}"
-    path="$(resolve_link "$name" || true)"
-  done
-
-  pwd
-  cd "$cwd"
-}
-
-expand_path() {
-  { cd "$(dirname "$1")" 2>/dev/null
-    local dirname="$PWD"
-    cd "$OLDPWD"
-    echo "$dirname/$(basename "$1")"
-  } || echo "$1"
-}
-
-BATS_LIBEXEC="$(abs_dirname "$0")"
-export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")"
-export BATS_CWD="$(abs_dirname .)"
-export PATH="$BATS_LIBEXEC:$PATH"
-
-options=()
-arguments=()
-for arg in "$@"; do
-  if [ "${arg:0:1}" = "-" ]; then
-    if [ "${arg:1:1}" = "-" ]; then
-      options[${#options[*]}]="${arg:2}"
-    else
-      index=1
-      while option="${arg:$index:1}"; do
-        [ -n "$option" ] || break
-        options[${#options[*]}]="$option"
-        let index+=1
-      done
-    fi
-  else
-    arguments[${#arguments[*]}]="$arg"
-  fi
-done
-
-unset count_flag pretty
-[ -t 0 ] && [ -t 1 ] && pretty="1"
-[ -n "$CI" ] && pretty=""
-
-for option in "${options[@]}"; do
-  case "$option" in
-  "h" | "help" )
-    help
-    exit 0
-    ;;
-  "v" | "version" )
-    version
-    exit 0
-    ;;
-  "c" | "count" )
-    count_flag="-c"
-    ;;
-  "t" | "tap" )
-    pretty=""
-    ;;
-  "p" | "pretty" )
-    pretty="1"
-    ;;
-  * )
-    usage >&2
-    exit 1
-    ;;
-  esac
-done
-
-if [ "${#arguments[@]}" -eq 0 ]; then
-  usage >&2
-  exit 1
-fi
-
-filenames=()
-for filename in "${arguments[@]}"; do
-  if [ -d "$filename" ]; then
-    shopt -s nullglob
-    for suite_filename in "$(expand_path "$filename")"/*.bats; do
-      filenames["${#filenames[@]}"]="$suite_filename"
-    done
-    shopt -u nullglob
-  else
-    filenames["${#filenames[@]}"]="$(expand_path "$filename")"
-  fi
-done
-
-if [ "${#filenames[@]}" -eq 1 ]; then
-  command="bats-exec-test"
-else
-  command="bats-exec-suite"
-fi
-
-if [ -n "$pretty" ]; then
-  extended_syntax_flag="-x"
-  formatter="bats-format-tap-stream"
-else
-  extended_syntax_flag=""
-  formatter="cat"
-fi
-
-set -o pipefail execfail
-exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter"
diff --git a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-suite b/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-suite
deleted file mode 100755
index 29ab255d..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-suite
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-count_only_flag=""
-if [ "$1" = "-c" ]; then
-  count_only_flag=1
-  shift
-fi
-
-extended_syntax_flag=""
-if [ "$1" = "-x" ]; then
-  extended_syntax_flag="-x"
-  shift
-fi
-
-trap "kill 0; exit 1" int
-
-count=0
-for filename in "$@"; do
-  let count+="$(bats-exec-test -c "$filename")"
-done
-
-if [ -n "$count_only_flag" ]; then
-  echo "$count"
-  exit
-fi
-
-echo "1..$count"
-status=0
-offset=0
-for filename in "$@"; do
-  index=0
-  {
-    IFS= read -r # 1..n
-    while IFS= read -r line; do
-      case "$line" in
-      "begin "* )
-        let index+=1
-        echo "${line/ $index / $(($offset + $index)) }"
-        ;;
-      "ok "* | "not ok "* )
-        [ -n "$extended_syntax_flag" ] || let index+=1
-        echo "${line/ $index / $(($offset + $index)) }"
-        [ "${line:0:6}" != "not ok" ] || status=1
-        ;;
-      * )
-        echo "$line"
-        ;;
-      esac
-    done
-  } < <( bats-exec-test $extended_syntax_flag "$filename" )
-  offset=$(($offset + $index))
-done
-
-exit "$status"
diff --git a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-test b/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-test
deleted file mode 100755
index 8f3bd510..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-exec-test
+++ /dev/null
@@ -1,346 +0,0 @@
-#!/usr/bin/env bash
-set -e
-set -E
-set -T
-
-BATS_COUNT_ONLY=""
-if [ "$1" = "-c" ]; then
-  BATS_COUNT_ONLY=1
-  shift
-fi
-
-BATS_EXTENDED_SYNTAX=""
-if [ "$1" = "-x" ]; then
-  BATS_EXTENDED_SYNTAX="$1"
-  shift
-fi
-
-BATS_TEST_FILENAME="$1"
-if [ -z "$BATS_TEST_FILENAME" ]; then
-  echo "usage: bats-exec <filename>" >&2
-  exit 1
-elif [ ! -f "$BATS_TEST_FILENAME" ]; then
-  echo "bats: $BATS_TEST_FILENAME does not exist" >&2
-  exit 1
-else
-  shift
-fi
-
-BATS_TEST_DIRNAME="$(dirname "$BATS_TEST_FILENAME")"
-BATS_TEST_NAMES=()
-
-load() {
-  local name="$1"
-  local filename
-
-  if [ "${name:0:1}" = "/" ]; then
-    filename="${name}"
-  else
-    filename="$BATS_TEST_DIRNAME/${name}.bash"
-  fi
-
-  [ -f "$filename" ] || {
-    echo "bats: $filename does not exist" >&2
-    exit 1
-  }
-
-  source "${filename}"
-}
-
-run() {
-  local e E T oldIFS
-  [[ ! "$-" =~ e ]] || e=1
-  [[ ! "$-" =~ E ]] || E=1
-  [[ ! "$-" =~ T ]] || T=1
-  set +e
-  set +E
-  set +T
-  output="$("$@" 2>&1)"
-  status="$?"
-  oldIFS=$IFS
-  IFS=$'\n' lines=($output)
-  [ -z "$e" ] || set -e
-  [ -z "$E" ] || set -E
-  [ -z "$T" ] || set -T
-  IFS=$oldIFS
-}
-
-setup() {
-  true
-}
-
-teardown() {
-  true
-}
-
-skip() {
-  BATS_TEST_SKIPPED=${1:-1}
-  BATS_TEST_COMPLETED=1
-  exit 0
-}
-
-bats_test_begin() {
-  BATS_TEST_DESCRIPTION="$1"
-  if [ -n "$BATS_EXTENDED_SYNTAX" ]; then
-    echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3
-  fi
-  setup
-}
-
-bats_test_function() {
-  local test_name="$1"
-  BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]="$test_name"
-}
-
-bats_capture_stack_trace() {
-  BATS_PREVIOUS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" )
-  BATS_CURRENT_STACK_TRACE=()
-
-  local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE"
-  local setup_pattern=" setup $BATS_TEST_SOURCE"
-  local teardown_pattern=" teardown $BATS_TEST_SOURCE"
-
-  local frame
-  local index=1
-
-  while frame="$(caller "$index")"; do
-    BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame"
-    if [[ "$frame" = *"$test_pattern"     || \
-          "$frame" = *"$setup_pattern"    || \
-          "$frame" = *"$teardown_pattern" ]]; then
-      break
-    else
-      let index+=1
-    fi
-  done
-
-  BATS_SOURCE="$(bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}")"
-  BATS_LINENO="$(bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}")"
-}
-
-bats_print_stack_trace() {
-  local frame
-  local index=1
-  local count="${#@}"
-
-  for frame in "$@"; do
-    local filename="$(bats_trim_filename "$(bats_frame_filename "$frame")")"
-    local lineno="$(bats_frame_lineno "$frame")"
-
-    if [ $index -eq 1 ]; then
-      echo -n "# ("
-    else
-      echo -n "#  "
-    fi
-
-    local fn="$(bats_frame_function "$frame")"
-    if [ "$fn" != "$BATS_TEST_NAME" ]; then
-      echo -n "from function \`$fn' "
-    fi
-
-    if [ $index -eq $count ]; then
-      echo "in test file $filename, line $lineno)"
-    else
-      echo "in file $filename, line $lineno,"
-    fi
-
-    let index+=1
-  done
-}
-
-bats_print_failed_command() {
-  local frame="$1"
-  local status="$2"
-  local filename="$(bats_frame_filename "$frame")"
-  local lineno="$(bats_frame_lineno "$frame")"
-
-  local failed_line="$(bats_extract_line "$filename" "$lineno")"
-  local failed_command="$(bats_strip_string "$failed_line")"
-  echo -n "#   \`${failed_command}' "
-
-  if [ $status -eq 1 ]; then
-    echo "failed"
-  else
-    echo "failed with status $status"
-  fi
-}
-
-bats_frame_lineno() {
-  local frame="$1"
-  local lineno="${frame%% *}"
-  echo "$lineno"
-}
-
-bats_frame_function() {
-  local frame="$1"
-  local rest="${frame#* }"
-  local fn="${rest%% *}"
-  echo "$fn"
-}
-
-bats_frame_filename() {
-  local frame="$1"
-  local rest="${frame#* }"
-  local filename="${rest#* }"
-
-  if [ "$filename" = "$BATS_TEST_SOURCE" ]; then
-    echo "$BATS_TEST_FILENAME"
-  else
-    echo "$filename"
-  fi
-}
-
-bats_extract_line() {
-  local filename="$1"
-  local lineno="$2"
-  sed -n "${lineno}p" "$filename"
-}
-
-bats_strip_string() {
-  local string="$1"
-  printf "%s" "$string" | sed -e "s/^[ "$'\t'"]*//" -e "s/[ "$'\t'"]*$//"
-}
-
-bats_trim_filename() {
-  local filename="$1"
-  local length="${#BATS_CWD}"
-
-  if [ "${filename:0:length+1}" = "${BATS_CWD}/" ]; then
-    echo "${filename:length+1}"
-  else
-    echo "$filename"
-  fi
-}
-
-bats_debug_trap() {
-  if [ "$BASH_SOURCE" != "$1" ]; then
-    bats_capture_stack_trace
-  fi
-}
-
-bats_error_trap() {
-  BATS_ERROR_STATUS="$?"
-  BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" )
-  trap - debug
-}
-
-bats_teardown_trap() {
-  trap "bats_exit_trap" exit
-  local status=0
-  teardown >>"$BATS_OUT" 2>&1 || status="$?"
-
-  if [ $status -eq 0 ]; then
-    BATS_TEARDOWN_COMPLETED=1
-  elif [ -n "$BATS_TEST_COMPLETED" ]; then
-    BATS_ERROR_STATUS="$status"
-    BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" )
-  fi
-
-  bats_exit_trap
-}
-
-bats_exit_trap() {
-  local status
-  local skipped
-  trap - err exit
-
-  skipped=""
-  if [ -n "$BATS_TEST_SKIPPED" ]; then
-    skipped=" # skip"
-    if [ "1" != "$BATS_TEST_SKIPPED" ]; then
-      skipped+=" ($BATS_TEST_SKIPPED)"
-    fi
-  fi
-
-  if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then
-    echo "not ok $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3
-    bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3
-    bats_print_failed_command "${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" "$BATS_ERROR_STATUS" >&3
-    sed -e "s/^/# /" < "$BATS_OUT" >&3
-    status=1
-  else
-    echo "ok ${BATS_TEST_NUMBER}${skipped} ${BATS_TEST_DESCRIPTION}" >&3
-    status=0
-  fi
-
-  rm -f "$BATS_OUT"
-  exit "$status"
-}
-
-bats_perform_tests() {
-  echo "1..$#"
-  test_number=1
-  status=0
-  for test_name in "$@"; do
-    "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1
-    let test_number+=1
-  done
-  exit "$status"
-}
-
-bats_perform_test() {
-  BATS_TEST_NAME="$1"
-  if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then
-    BATS_TEST_NUMBER="$2"
-    if [ -z "$BATS_TEST_NUMBER" ]; then
-      echo "1..1"
-      BATS_TEST_NUMBER="1"
-    fi
-
-    BATS_TEST_COMPLETED=""
-    BATS_TEARDOWN_COMPLETED=""
-    trap "bats_debug_trap \"\$BASH_SOURCE\"" debug
-    trap "bats_error_trap" err
-    trap "bats_teardown_trap" exit
-    "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1
-    BATS_TEST_COMPLETED=1
-
-  else
-    echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2
-    exit 1
-  fi
-}
-
-if [ -z "$TMPDIR" ]; then
-  BATS_TMPDIR="/tmp"
-else
-  BATS_TMPDIR="${TMPDIR%/}"
-fi
-
-BATS_TMPNAME="$BATS_TMPDIR/bats.$$"
-BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID"
-BATS_OUT="${BATS_TMPNAME}.out"
-
-bats_preprocess_source() {
-  BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
-  { tr -d '\r' < "$BATS_TEST_FILENAME"; echo; } | bats-preprocess > "$BATS_TEST_SOURCE"
-  trap "bats_cleanup_preprocessed_source" err exit
-  trap "bats_cleanup_preprocessed_source; exit 1" int
-}
-
-bats_cleanup_preprocessed_source() {
-  rm -f "$BATS_TEST_SOURCE"
-}
-
-bats_evaluate_preprocessed_source() {
-  if [ -z "$BATS_TEST_SOURCE" ]; then
-    BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
-  fi
-  source "$BATS_TEST_SOURCE"
-}
-
-exec 3<&1
-
-if [ "$#" -eq 0 ]; then
-  bats_preprocess_source
-  bats_evaluate_preprocessed_source
-
-  if [ -n "$BATS_COUNT_ONLY" ]; then
-    echo "${#BATS_TEST_NAMES[@]}"
-  else
-    bats_perform_tests "${BATS_TEST_NAMES[@]}"
-  fi
-else
-  bats_evaluate_preprocessed_source
-  bats_perform_test "$@"
-fi
diff --git a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-format-tap-stream b/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-format-tap-stream
deleted file mode 100755
index 614768f4..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-format-tap-stream
+++ /dev/null
@@ -1,165 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-# Just stream the TAP output (sans extended syntax) if tput is missing
-command -v tput >/dev/null || exec grep -v "^begin "
-
-header_pattern='[0-9]+\.\.[0-9]+'
-IFS= read -r header
-
-if [[ "$header" =~ $header_pattern ]]; then
-  count="${header:3}"
-  index=0
-  failures=0
-  skipped=0
-  name=""
-  count_column_width=$(( ${#count} * 2 + 2 ))
-else
-  # If the first line isn't a TAP plan, print it and pass the rest through
-  printf "%s\n" "$header"
-  exec cat
-fi
-
-update_screen_width() {
-  screen_width="$(tput cols)"
-  count_column_left=$(( $screen_width - $count_column_width ))
-}
-
-trap update_screen_width WINCH
-update_screen_width
-
-begin() {
-  go_to_column 0
-  printf_with_truncation $(( $count_column_left - 1 )) "   %s" "$name"
-  clear_to_end_of_line
-  go_to_column $count_column_left
-  printf "%${#count}s/${count}" "$index"
-  go_to_column 1
-}
-
-pass() {
-  go_to_column 0
-  printf " ✓ %s" "$name"
-  advance
-}
-
-skip() {
-  local reason="$1"
-  [ -z "$reason" ] || reason=": $reason"
-  go_to_column 0
-  printf " - %s (skipped%s)" "$name" "$reason"
-  advance
-}
-
-fail() {
-  go_to_column 0
-  set_color 1 bold
-  printf " ✗ %s" "$name"
-  advance
-}
-
-log() {
-  set_color 1
-  printf "   %s\n" "$1"
-  clear_color
-}
-
-summary() {
-  printf "\n%d test%s" "$count" "$(plural "$count")"
-
-  printf ", %d failure%s" "$failures" "$(plural "$failures")"
-
-  if [ "$skipped" -gt 0 ]; then
-    printf ", %d skipped" "$skipped"
-  fi
-
-  printf "\n"
-}
-
-printf_with_truncation() {
-  local width="$1"
-  shift
-  local string="$(printf "$@")"
-
-  if [ "${#string}" -gt "$width" ]; then
-    printf "%s..." "${string:0:$(( $width - 4 ))}"
-  else
-    printf "%s" "$string"
-  fi
-}
-
-go_to_column() {
-  local column="$1"
-  printf "\x1B[%dG" $(( $column + 1 ))
-}
-
-clear_to_end_of_line() {
-  printf "\x1B[K"
-}
-
-advance() {
-  clear_to_end_of_line
-  echo
-  clear_color
-}
-
-set_color() {
-  local color="$1"
-  local weight="$2"
-  printf "\x1B[%d;%dm" $(( 30 + $color )) "$( [ "$weight" = "bold" ] && echo 1 || echo 22 )"
-}
-
-clear_color() {
-  printf "\x1B[0m"
-}
-
-plural() {
-  [ "$1" -eq 1 ] || echo "s"
-}
-
-_buffer=""
-
-buffer() {
-  _buffer="${_buffer}$("$@")"
-}
-
-flush() {
-  printf "%s" "$_buffer"
-  _buffer=""
-}
-
-finish() {
-  flush
-  printf "\n"
-}
-
-trap finish EXIT
-
-while IFS= read -r line; do
-  case "$line" in
-  "begin "* )
-    let index+=1
-    name="${line#* $index }"
-    buffer begin
-    flush
-    ;;
-  "ok "* )
-    skip_expr="ok $index # skip (\(([^)]*)\))?"
-    if [[ "$line" =~ $skip_expr ]]; then
-      let skipped+=1
-      buffer skip "${BASH_REMATCH[2]}"
-    else
-      buffer pass
-    fi
-    ;;
-  "not ok "* )
-    let failures+=1
-    buffer fail
-    ;;
-  "# "* )
-    buffer log "${line:2}"
-    ;;
-  esac
-done
-
-buffer summary
diff --git a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-preprocess b/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-preprocess
deleted file mode 100755
index 04297ed0..00000000
--- a/govc/test/vendor/github.com/sstephenson/bats/libexec/bats-preprocess
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-encode_name() {
-  local name="$1"
-  local result="test_"
-
-  if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then
-    name="${name//_/-5f}"
-    name="${name//-/-2d}"
-    name="${name// /_}"
-    result+="$name"
-  else
-    local length="${#name}"
-    local char i
-
-    for ((i=0; i<length; i++)); do
-      char="${name:$i:1}"
-      if [ "$char" = " " ]; then
-        result+="_"
-      elif [[ "$char" =~ [[:alnum:]] ]]; then
-        result+="$char"
-      else
-        result+="$(printf -- "-%02x" \'"$char")"
-      fi
-    done
-  fi
-
-  echo "$result"
-}
-
-tests=()
-index=0
-pattern='^ *@test  *([^ ].*)  *\{ *(.*)$'
-
-while IFS= read -r line; do
-  let index+=1
-  if [[ "$line" =~ $pattern ]]; then
-    quoted_name="${BASH_REMATCH[1]}"
-    body="${BASH_REMATCH[2]}"
-    name="$(eval echo "$quoted_name")"
-    encoded_name="$(encode_name "$name")"
-    tests["${#tests[@]}"]="$encoded_name"
-    echo "${encoded_name}() { bats_test_begin ${quoted_name} ${index}; ${body}"
-  else
-    printf "%s\n" "$line"
-  fi
-done
-
-for test_name in "${tests[@]}"; do
-  echo "bats_test_function ${test_name}"
-done
diff --git a/govc/test/vm.bats b/govc/test/vm.bats
index efafa41a..75588174 100755
--- a/govc/test/vm.bats
+++ b/govc/test/vm.bats
@@ -600,7 +600,7 @@ load test_helper
   assert_success
   result=$(govc device.ls -vm "$vm" | grep -c disk-)
   [ "$result" -eq 1 ]
-  govc device.info -json -vm "$vm" disk-* | jq .Devices[].Backing.Sharing | grep -v sharingMultiWriter
+  govc device.info -json -vm "$vm" disk-* | jq .Devices[].backing.sharing | grep -v sharingMultiWriter
 
   name=$(new_id)
 
@@ -618,7 +618,7 @@ load test_helper
 
   run govc vm.disk.create -vm "$vm" -name "$vm/shared.vmdk" -size 1G -eager -thick -sharing sharingMultiWriter
   assert_success
-  govc device.info -json -vm "$vm" disk-* | jq .Devices[].Backing.Sharing | grep sharingMultiWriter
+  govc device.info -json -vm "$vm" disk-* | jq .Devices[].backing.sharing | grep sharingMultiWriter
 
   run govc vm.power -on "$vm"
   assert_success