# Common definitions for all test suites.
# Copyright 2012 Serval Project Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

shopt -s extglob

testdefs_sh=$(abspath "${BASH_SOURCE[0]}")
servald_source_root="${testdefs_sh%/*}"
servald_build_root="$servald_source_root"
servald_basename="servald"
servald_build_executable="$servald_build_root/$servald_basename"
export TFW_LOGDIR="${TFW_LOGDIR:-$servald_build_root/testlog}"
addr_localhost="127.0.0.1"

declare -a instance_stack=()

# Some useful string constants.
LF='
'
CR='
'
HT='	'

# Some useful regular expressions.  These must work in grep(1) as basic
# expressions, and also in sed(1).
rexp_sid='[0-9a-fA-F]\{64\}'
rexp_did='[0-9+#]\{5,\}'

# Utility function for extracting information from the output of servald
# commands that return "key:value\n" pairs.
#
#     extract_stdout_keyvalue_optional <varname> <key> [<delimiter>] <regular-expression>
#
# Examines the standard output of the last command executed using "execute" or
# any of its variants.  If there is a line matching
# "<key><delimiter><regular-expression>\n" then assigns the part matched by
# <regular-expression> into the shell variable called <varname> and returns 0.
# Otherwise, leaves <varname> unchanged and returns 1.
#
# The default <delimimter> is ':'.
#
extract_stdout_keyvalue_optional() {
   local _var="$1"
   local _label="$2"
   local _delim=':'
   local _rexp="$3"
   case $# in
   3) ;;
   4) _delim="$3"; _rexp="$4";;
   *) error "invalid number of args";;
   esac
   local _label_re=$(escape_grep_basic "$_label")
   local _delim_re=$(escape_grep_basic "$_delim")
   local _line=$($GREP "^$_label_re$_delim_re" "$TFWSTDOUT")
   local _value=
   local _return=1
   if [ -n "$_line" ]; then
      _value="${_line#*$_delim}"
      _return=0
      if [ -n "$_var" ]; then
         eval $_var="\$_value"
         eval tfw_log "$_var=\$(shellarg "\${$_var}")"
      fi
   fi
   return $_return
}

# Utility function for extracting information from the output of servald
# commands that return "key:value\n" pairs.
extract_stdout_keyvalue() {
   local _label="$2"
   assert --message="stdout of ($executed) contains valid '$_label:' line" --stdout extract_stdout_keyvalue_optional "$@"
}

# Parse the standard result set output produced by the immediately preceding command
# command into the following shell variables:
#  NCOLS the number of columns
#  NROWS the number of data rows (not counting headers)
#  HEADER[c] the C-th header label, 0 <= C <= NCOLS-1
#  <label>[R] where <label> is a header label with all non-alphanumerics
#  replaced by underscore '_' and all alphas converted to upper case, eg,
#  .author -> _AUTHOR, is the value of that column in the R-th row, 0 <=
#  R < NROWS
#
# Warning: overwrites existing shell variables.  Names of overwritten shell
# variables are derived directly from the output of the command, so cannot be
# controlled.  If a prefix is supplied, all variables are prefixed with that.
unpack_stdout_list() {
   local prefix="$1"
   {
      local n
      read n
      assertExpr --stdout --message="line 1: malformed list" "$n" '~' '^[0-9]\+$'
      eval ${prefix}NCOLS=\"\$n\"
      declare -a ${prefix}HEADER
      local -a header
      local oIFS="$IFS"
      IFS=:
      read -r -a header
      IFS="$oIFS"
      assertExpr --stdout --message="line 2: malformed list" "${#header[*]}" == "$n"
      eval ${prefix}HEADER="(\"\${header[@]}\")"
      local hdr
      local -a colvars=()
      for hdr in "${header[@]}"; do
         hdr="${hdr//[^A-Za-z0-9_]/_}"
         # hdr="${hdr^^*}" would do in Bash-4.0 and later
         hdr="$(echo "$hdr" | tr a-z A-Z)"
         colvars+=("$hdr")
      done
      local -a row
      IFS=:
      local i=0
      while eval read -r -a row; do
         local j=0
         local val
         for val in "${row[@]}"; do
            eval ${prefix}${colvars[$j]}[$i]=\"\$val\"
            let ++j
         done
         let ++i
      done
      IFS="$oIFS"
      eval ${prefix}NROWS=$i
   } <"$TFWSTDOUT"
}

# Utility function for creating servald fixtures:
#  - set $servald variable (executable under test)
#  - set the current instance to be "Z"
setup_servald() {
   if ! [ -x "$servald_build_executable" ]; then
      error "servald executable not present: $servald"
      return 1
   fi
   export SERVALD_VAR="$TFWVAR/servald"
   mkdir "$SERVALD_VAR"
   servald="$SERVALD_VAR/$servald_basename" # The servald executable under test
   cp -f "$servald_build_executable" "$servald"
   unset SERVALD_OUTPUT_DELIMITER
   unset SERVALD_SERVER_START_DELAY
   unset SERVALD_SERVER_CHDIR
   unset SERVALD_START_POST_SLEEP
   unset SERVALD_LOG_FILE
   unset SERVALD_KEYRING_PATH
   unset SERVALD_KEYRING_READONLY
   servald_instances_dir="$SERVALD_VAR/instance"
   set_instance +Z
}

# Utility function for running servald:
#  - executes $servald with the given arguments
execute_servald() {
   execute --core-backtrace --executable=$servald "$@"
}

# Utility function for running servald and asserting no errors:
#  - executes $servald with the given arguments
#  - asserts that standard error contains no error messages
executeOk_servald() {
   executeOk --core-backtrace --executable=$servald "$@"
   assertStderrGrep --matches=0 --message="stderr of ($executed) contains no error messages" '^ERROR:'
}

# Utility function:
#  - if the argument is a prefixed instance name "+X", then call set_instance
#    "X" and return 0, otherwise leave the current instance unchanged and return 1
# Designed for use in functions that take an optional instance name as their
# first argument:
#     func() {
#        push_instance
#        set_instance_fromarg "$1" && shift
#        ...
#        pop_instance
#     }
# would be invoked as:
#     func +A blah blah
#     func +B wow wow
#     func foo bar
set_instance_fromarg() {
   case "$1" in
   +[A-Z]) set_instance "$1"; return 0;;
   esac
   return 1
}

# Utility function:
#  - push the current instance on the instance stack
push_instance() {
   instance_stack+=("$instance_name")
}

# Utility function:
#  - push the current instance on the instance stack and set the instance to the
#    given arg
#  - if set_instance fails, pops the instance stack before returning
#    set_instance's exit status
push_and_set_instance() {
   push_instance
   set_instance "$@" && return 0
   status=$?
   pop_instance
   return $status
}

# Utility function:
#  - pop an instance off the instance stack
pop_instance() {
   local n=${#instance_stack[*]}
   if [ $n -eq 0 ]; then
      error "instance stack underflow"
      return $?
   fi
   let --n
   set_instance +${instance_stack[$n]}
   unset instance_stack[$n]
   return 0
}

# Utility function:
#  - create a temporary directory to contain all per-instance test files
#  - set SERVALINSTANCE_PATH environment variable to name a directory within
#    the per-instance test directory (but do not create it)
#  - set other environment variables to support other functions defined in this
#    script
set_instance() {
   case "$1" in
   '')
      error "missing instance name argument"
      return 1
      ;;
   +[A-Z])
      instance_arg="${1}"
      instance_name="${instance_arg#+}"
      instance_number=$((36#$instance_name - 9))
      local servald_binary_var=servald${instance_name}
      if [ -z "${!servald_binary_var}" ]; then
         servald="$SERVALD_VAR/$servald_basename"
      else
         servald="$SERVALD_VAR/${!servald_binary_var}"
      fi
      if ! [ -x "$servald" ]; then
         error "servald executable not present: $servald"
         return 1
      fi
      tfw_log "# set instance = $instance_name, number = $instance_number"
      export instance_dir="${servald_instances_dir?:}/$instance_name"
      mkdir -p "$instance_dir"
      export SERVALINSTANCE_PATH="$instance_dir/servald"
      instance_servald_log="$instance_dir/servald.log"
      instance_servald_pidfile="$SERVALINSTANCE_PATH/servald.pid"
      return 0
      ;;
   *)
      error "malformed instance name argument, must be in form +[A-Z]"
      return 1
      ;;
   esac
}

# Composition function:
#  - invoke a command once in many instances
#  - in "--all" mode (default), returns the count of commands that returned
#    nonzero (ie, failure count); this returns zero only if all commands in all
#    instances return zero, ie, is an AND relation on success; this is
#    guaranteed to invoke the command in all instances (unless terminated
#    by a failed assertion)
#  - in "--any" mode, returns zero as soon as any command returns zero; ie, is
#    an OR relation on success; N.B. this may not invoke the command in all
#    instances
foreach_instance() {
   mode=all
   case "$1" in
   --any) mode=any; shift;;
   --all) mode=all; shift;;
   esac
   local -a instances=()
   while [ $# -ne 0 ]; do
      case "$1" in
      +[A-Z]) instances+=("$1"); shift;;
      *) break;;
      esac
   done
   push_instance
   local ret=0
   local I
   for I in ${instances[*]}; do
      set_instance $I
      if tfw_run "$@"; then
         case $mode in
         any) break;;
         esac
      else
         case $mode in
         all) let ++ret;;
         esac
      fi
   done
   pop_instance
   return $ret
}

# Composition function:
#  - invoke a command once in every instance that contains a server pidfile
#  - takes the same options as foreach_instance()
foreach_instance_with_pidfile() {
   push_instance
   local -a instances=()
   if pushd "${servald_instances_dir?:}" >/dev/null; then
      for name in *; do
         set_instance "+$name"
         get_servald_server_pidfile && instances+=("+$name")
      done
      popd >/dev/null
   fi
   pop_instance
   foreach_instance "${instances[@]}" "$@"
}

# Utility function for setting up a fixture with a servald server process:
#  - start a servald server process
#  - assert that the pidfile is created and correct
#  - set $servald_pid to the PID of the started server process
#  - assert that the reported PID is actually a running servald process
start_servald_server() {
   push_instance
   set_instance_fromarg "$1" && shift
   configure_servald_server
   # Start servald server
   local -a before_pids
   local -a after_pids
   get_servald_pids before_pids
   tfw_log "# before_pids=$before_pids"
   executeOk --core-backtrace servald_start "$@"
   extract_stdout_keyvalue start_instance_path instancepath '.*'
   extract_stdout_keyvalue start_pid pid '[0-9]\+'
   assert [ "$start_instance_path" = "$SERVALINSTANCE_PATH" ]
   get_servald_pids after_pids
   tfw_log "# after_pids=$after_pids"
   assert_servald_server_pidfile servald_pid
   # Assert that the servald pid file is present.
   assert --message="servald pidfile was created" [ -s "$instance_servald_pidfile" ]
   assert --message="servald pidfile contains a valid pid" --dump-on-fail="$instance_servald_log" kill -0 "$servald_pid"
   assert --message="servald start command returned correct pid" [ "$start_pid" -eq "$servald_pid" ]
   # Assert that there is at least one new servald process running.
   local apid bpid
   local new_pids=
   local pidfile_running=false
   for apid in ${after_pids[*]}; do
      local isnew=true
      for bpid in ${before_pids[*]}; do
         if [ "$apid" -eq "$bpid" ]; then
            isnew=false
            break
         fi
      done
      if [ "$apid" -eq "$servald_pid" ]; then
         tfw_log "# started servald process: pid=$servald_pid"
         new_pids="$new_pids $apid"
         pidfile_running=true
      elif $isnew; then
         tfw_log "# unknown new servald process: pid=$apid"
         new_pids="$new_pids $apid"
      fi
   done
   eval LOG$instance_name=$instance_servald_log
   assert --message="a new servald process is running" --dump-on-fail="$instance_servald_log" [ -n "$new_pids" ]
   assert --message="servald pidfile process is running" --dump-on-fail="$instance_servald_log" $pidfile_running
   assert --message="servald log file $instance_servald_log is present" [ -r "$instance_servald_log" ]
   tfw_log "# Started servald server process $instance_name, pid=$servald_pid"
   pop_instance
}

# Utility function:
#  - invoke "servald start" command with given args and suitable environment
servald_start() {
   rm -r -f "$instance_dir/cache"
   mkdir -p "$instance_dir/cache" || error "mkdir failed"
   SERVALD_SERVER_CHDIR="$instance_dir" SERVALD_LOG_FILE="$instance_servald_log" $servald start "$@"
}

# Utility function:
#  - test whether the daemon's HTTP server has started
servald_http_server_started() {
   local logvar=LOG${1#+}
   $GREP 'HTTP SERVER START.*port=[0-9]' "${!logvar}"
}

# Utility function:
#  - test whether the daemon's HTTP server has started
servald_restful_http_server_started() {
   local logvar=LOG${1#+}
   $GREP 'HTTP SERVER START.*port=[0-9].*services=[^ ]*\<RESTful\>' "${!logvar}"
}

# Utility function:
#  - fetch the daemon's HTTP server port number
get_servald_http_server_port() {
   local _instance="$2"
   [ -z "$_instance" ] || push_and_set_instance $_instance || return $?
   local _var="$1"
   local _port=$(<"$SERVALINSTANCE_PATH/proc/http_port")
   assert --message="instance $instance_name HTTP server port number is known" [ -n "$_port" ]
   if [ -n "$_var" ]; then
      eval "$_var=\$_port"
      tfw_log "$_var=$_port"
   fi
   [ -z "$_instance" ] || pop_instance
   return 0
}

get_servald_restful_http_server_port() {
   get_servald_http_server_port "$@"
}

# Utility function:
#  - stop a servald server process instance in an orderly fashion
#  - cat its log file into the test log
stop_servald_server() {
   push_instance
   set_instance_fromarg "$1" && shift
   # Stop servald server
   get_servald_server_pidfile servald_pid
   local -a before_pids
   local -a after_pids
   get_servald_pids before_pids
   tfw_log "# before_pids=$before_pids"
   execute --core-backtrace $servald stop "$@"
   extract_stdout_keyvalue stop_instance_path instancepath '.*'
   assert [ "$stop_instance_path" = "$SERVALINSTANCE_PATH" ]
   if [ -n "$servald_pid" ]; then
      assertExitStatus '==' 0
      extract_stdout_keyvalue stop_pid pid '[0-9]\+'
      assert [ "$stop_pid" = "$servald_pid" ]
   fi
   tfw_log "# Stopped servald server process $instance_name, pid=${servald_pid:-unknown}"
   get_servald_pids after_pids
   tfw_log "# after_pids=$after_pids"
   # Assert that the servald pid file is gone.
   assert --message="servald pidfile was removed" [ ! -e "$instance_servald_pidfile" ]
   # Assert that the servald process identified by the pidfile is no longer running.
   local apid bpid
   if [ -n "$servald_pid" ]; then
      for apid in ${after_pids[*]}; do
         assert --message="servald process still running" [ "$apid" -ne "$servald_pid" ]
      done
   fi
   # Check there is at least one fewer servald processes running.
   for bpid in ${before_pids[*]}; do
      local isgone=true
      for apid in ${after_pids[*]}; do
         if [ "$apid" -eq "$bpid" ]; then
            isgone=false
            break
         fi
      done
      if $isgone; then
         tfw_log "# ended servald process: pid=$bpid"
      fi
   done
   pop_instance
}

# Utility function:
#  - cat a servald server log file and core dump information into the test log
report_servald_server() {
   # Append the server log file to the test log.
   [ -s "$instance_servald_log" ] && tfw_cat "$instance_servald_log"
   # Append a core dump backtrace to the test log.
   if [ -s "$instance_dir/core" ]; then
      tfw_core_backtrace "$servald" "$instance_dir/core"
      rm -f "$instance_dir/core"
   fi
}

# Utility function:
#  - test whether the pidfile for a given server instance exists and is valid
#  - if it exists and is valid, set named variable to PID (and second named
#    variable to path of pidfile) and return 0
#  - otherwise return 1
get_servald_server_pidfile() {
   local _pidvar="$1"
   local _pidfilevar="$2"
   push_instance
   set_instance_fromarg "$1" && shift
   local _pidfile="$instance_servald_pidfile"
   pop_instance
   [ -n "$_pidfilevar" ] && eval $_pidfilevar="\$_pidfile"
   local _pid=$(cat "$_pidfile" 2>/dev/null)
   case "$_pid" in
   +([0-9]))
      [ -n "$_pidvar" ] && eval $_pidvar="\$_pid"
      return 0
      ;;
   '')
      if [ -e "$_pidfile" ]; then
         tfw_log "# empty pidfile $_pidfile"
      else
         tfw_log "# missing pidfile $_pidfile"
      fi
      ;;
   *)
      tfw_log "# invalid pidfile $_pidfile"
      tfw_cat "$_pidfile"
      ;;
   esac
   return 1
}

# Assertion function:
#  - asserts that the servald server pidfile exists and contains a valid PID
#  - does NOT check whether a process with that PID exists or whether that
#    process is a servald process
assert_servald_server_pidfile() {
   assert get_servald_server_pidfile "$@"
}

# Assertion function:
#  - assert that the given instance's server has the given status ('running' or 'stopped')
assert_servald_server_status() {
   push_instance
   set_instance_fromarg "$1" && shift
   [ $# -eq 1 ] || error "invalid arguments"
   executeOk_servald status
   local status
   extract_stdout_keyvalue status status '.*'
   assert --message="instance +$instance_name servald server status is '$1'" [ "$status" = "$1" ]
   pop_instance
}

# Assertion function:
#  - asserts that all servald instances with a pidfile have a server in a given
#    state
assert_status_all_servald_servers() {
   foreach_instance_with_pidfile assert_servald_server_status "$@"
}

# Utility function for tearing down servald fixtures:
#  - stop all servald server process instances in an orderly fashion
stop_all_servald_servers() {
   foreach_instance_with_pidfile stop_servald_server
}

# Utility function for tearing down servald fixtures:
#  - log a report of the execution of all servald server process instances
report_all_servald_servers() {
   foreach_instance +{A..Z} report_servald_server
}

# Utility function for tearing down servald fixtures:
#  - send a given signal to all running servald processes, identified by name
#  - return 1 if no processes were present, 0 if any signal was sent
signal_all_servald_processes() {
   local sig="$1"
   local servald_pids
   get_servald_pids servald_pids
   local pid
   local ret=1
   for pid in $servald_pids; do
      if kill -$sig "$pid"; then
         tfw_log "# Sent SIG$sig to servald process pid=$pid"
         ret=0
      else
         tfw_log "# servald process pid=$pid not running -- SIG$sig not sent"
      fi
   done
   return $ret
}

# Utility function for tearing down servald fixtures:
#  - wait while any servald processes remain
#  - return 0 if no processes are present
#  - 1 if the timeout elapses first
wait_all_servald_processes() {
   local timeout="${1:-1000000}"
   sleep $timeout &
   sleep_pid=$!
   while get_servald_pids; do
      kill -0 $sleep_pid 2>/dev/null || return 1
      sleep 0.1
   done
   kill -TERM $sleep_pid 2>/dev/null
   return 0
}

# Utility function for tearing down servald fixtures:
#  - terminate all running servald processes, identified by name, by sending
#    two SIGHUPs 100ms apart, then another SIGHUP after 2 seconds, finally
#    SIGKILL after 2 seconds
#  - return 0 if no more processes are running, nonzero otherwise
kill_all_servald_processes() {
   for delay in 0.1 2 2; do
      signal_all_servald_processes HUP || return 0
      wait_all_servald_processes $delay && return 0
   done
   signal_all_servald_processes KILL || return 0
   return 1
}

# Utility function:
#  - return the PIDs of all servald processes the current test is running, by
#    assigning to the named array variable if given
#  - return 0 if there are any servald processes running, 1 if not
get_servald_pids() {
   local var="$1"
   if [ -z "$servald" ]; then
      error "\$servald is not set"
      return 1
   fi
   local mypid=$$
   # XXX The following line will not find any PIDs if there are spaces in "$servald".
   local pids=$(ps -u$UID -o pid,args | $AWK -v mypid="$mypid" -v servald="$servald" '$1 != mypid && $2 == servald {print $1}')
   [ -n "$var" ] && eval "$var=(\$pids)"
   [ -n "$pids" ]
}

# Assertion function:
#  - assert there are no existing servald server processes
assert_no_servald_processes() {
   local pids
   get_servald_pids pids
   assert --message="no $servald_basename process(es) running" [ -z "$pids" ]
   return 0
}

# Assertion function:
#  - assert the given instance's servald server log contains no errors
assert_servald_server_no_errors() {
   push_instance
   set_instance_fromarg "$1" && shift
   assertGrep --matches=0 --message="stderr of $servald_basename $instance_name contains no error messages" "$instance_servald_log" '^ERROR:'
   pop_instance
}

# Assertion function:
#  - assert that all instances of servald server logs contain no errors
assert_all_servald_servers_no_errors() {
   push_instance
   if pushd "${servald_instances_dir?:}" >/dev/null; then
      for name in *; do
         set_instance "+$name"
         assertGrep --matches=0 --message="stderr of $servald_basename $instance_name contains no error messages" "$instance_servald_log" '^ERROR:'
      done
      popd >/dev/null
   fi
   pop_instance
}

# Utility function
#
# create_single_identity [--option]... [ DID [ Name ]]
#
#  - create an identity in the current instance {I} by invoking the command:
#    servald keyring add [--option]...
#  - assign a phone number (DID) and name to the new identity, use defaults
#    if not specified by arg1 and arg2
#  - assert the new identity is the only one in this instance
#  - set the SID{I} variable, eg SIDA, to the SID of the new identity
#  - set the DID{I} variable, eg DIDA, to the phone number of the new identity
#  - set the NAME{I} variable, eg NAMEA, to the name of the new identity
create_single_identity() {
   local servald_options=()
   while [ $# -gt 0 ]; do
      case "$1" in
      --*) servald_options+=("$1"); shift;;
      *) break;;
      esac
   done
   local sidvar=SID${instance_name}1
   local didvar=DID${instance_name}1
   local namevar=NAME${instance_name}1
   eval "$didvar=\"\${1-\$((5550000 + \$instance_number))}\""
   eval "$namevar=\"\${2-Agent \$instance_name Smith}\""
   create_identities "${servald_options[@]}" 1
   eval "SID$instance_name=\"\${!sidvar}\""
   eval "DID$instance_name=\"\${!didvar}\""
   eval "NAME$instance_name=\"\${!namevar}\""
   sidvar=SID${instance_name}
   didvar=DID${instance_name}
   namevar=NAME${instance_name}
   tfw_log "SID$instance_name=$(shellarg "${!sidvar}")"
   tfw_log "DID$instance_name=$(shellarg "${!didvar}")"
   tfw_log "NAME$instance_name=$(shellarg "${!namevar}")"
}

# Utility function:
# 
# create_identities [--option]... N
#
#  - create N identities in the current instance {I} using N consecutive
#    invocations of: servald keyring add [--option]...
#  - pass [args...] to the keyring add
#  - if variables DID{I}{1..N} and/or NAME{I}{1..N} are already set, then use
#    them to set the DIDs and names of each identity
#  - if variables PIN{I}{1..N} are already set, then use them as the --entry-pin
#    option to set the DIDs and names of each identity
#  - assert that all SIDs are unique
#  - assert that all SIDs appear in keyring list
#  - set variables SID{I}{1..N} to SIDs of identities, eg, SIDA1, SIDA2...
#  - set variables DID{I}{1..N} to DIDs of identities, eg, DIDA1, DIDA2...
#  - set variables NAME{I}{1..N} to names of identities, eg, NAMEA1, NAMEA2...
create_identities() {
   local servald_options=()
   while [ $# -gt 0 ]; do
      case "$1" in
      --*) servald_options+=("$1"); shift;;
      *) break;;
      esac
   done
   local N="$1"
   case "$N" in
   +([0-9]));;
   *) error "invalid arg1: $N";;
   esac
   shift
   local i j
   for ((i = 1; i <= N; ++i)); do
      local pinvar=PIN$instance_name$i
      local pin="${!pinvar}"
      servald_options+=(${pin:+--entry-pin="$pin"})
   done
   for ((i = 1; i <= N; ++i)); do
      local pinvar=PIN$instance_name$i
      local sidvar=SID$instance_name$i
      local didvar=DID$instance_name$i
      local namevar=NAME$instance_name$i
      local pin="${!pinvar}"
      [ -n "$pin" ] && tfw_log "$pinvar=$(shellarg "$pin")"
      executeOk_servald keyring add "${servald_options[@]}" "$pin"
      assert [ -e "$SERVALINSTANCE_PATH/serval.keyring" ]
      extract_stdout_keyvalue $sidvar sid "$rexp_sid"
      tfw_log "$sidvar=${!sidvar}"
      # If the DID and/or NAME is already specified in the variables, then use
      # them, otherwise extract the DID and NAME automatically generated by
      # servald.
      if [ -n "${!didvar+x}" -o -n "${!namevar+x}" ]; then
         executeOk_servald keyring set did "${servald_options[@]}" "${!sidvar}" "${!didvar}" "${!namevar}"
         eval "$didvar=\${!didvar}"
         eval "$namevar=\${!namevar}"
         tfw_log "$didvar=$(shellarg "${!didvar}")"
         tfw_log "$namevar=$(shellarg "${!namevar}")"
      else
         extract_stdout_keyvalue_optional $didvar did "$rexp_did"
         extract_stdout_keyvalue_optional $namevar name ".*"
      fi
   done
   for ((i = 1; i <= N; ++i)); do
      for ((j = 1; j <= N; ++j)); do
         [ $i -ne $j ] && eval assert [ "\$SID$instance_name$i" != "\$SID$instance_name$j" ]
      done
   done
   executeOk_servald keyring list "${servald_options[@]}"
   for ((i = 1; i <= N; ++i)); do
      local sidvar=SID$instance_name$i
      local didvar=DID$instance_name$i
      local namevar=NAME$instance_name$i
      local re_name=$(escape_grep_basic "${!namevar}")
      assertStdoutGrep --matches=1 "^${!sidvar}:${!didvar}:${re_name}\$"
   done
}

# Assertion function:
# - asserts that the list contains N identities that have the correct format
assert_keyring_list() {
   unpack_stdout_list __X
   assert --stdout --stderr [ $__XNROWS -eq $1 ]
   assertStdoutGrep --stderr --matches=$1 "^$rexp_sid:\($rexp_did\)\?:.*\$"
}

# Utility function, to be overridden as needed:
#  - set up the configuration immediately prior to starting a servald server process
#  - called by start_servald_instances
configure_servald_server() {
   add_servald_interface
}

add_servald_interface() {
   local SOCKET_TYPE="dgram"
   local INTERFACE="1"
   local TYPE="wifi"
   while [ $# -ne 0 ]; do
      case "$1" in
      --wifi) TYPE="wifi"; shift;;
      --ethernet) TYPE="ethernet"; shift;;
      --file) SOCKET_TYPE="file"; shift;;
      *) INTERFACE="$1"; shift;;
      esac
   done
   if [ "${SOCKET_TYPE}" == "file" ]; then
      >>$SERVALD_VAR/dummy$INTERFACE
      executeOk_servald config \
         set server.interface_path $SERVALD_VAR \
         set interfaces.$INTERFACE.socket_type $SOCKET_TYPE \
         set interfaces.$INTERFACE.file dummy$INTERFACE \
         set interfaces.$INTERFACE.type $TYPE \
         set interfaces.$INTERFACE.dummy_address 127.0.$INTERFACE.$instance_number \
         set interfaces.$INTERFACE.dummy_netmask 255.255.255.224
   else
      mkdir -p "$SERVALD_VAR/dummy$INTERFACE/"
      executeOk_servald config \
         set server.interface_path $SERVALD_VAR \
         set interfaces.$INTERFACE.socket_type $SOCKET_TYPE \
         set interfaces.$INTERFACE.file dummy$INTERFACE/$instance_name \
         set interfaces.$INTERFACE.type $TYPE
   fi
}

# Utility function:
#  - start a set of servald server processes running on a shared file interface
#    and with its own private monitor and MDP abstract socket names
#  - set variables DUMMYx to the full path name of shared dummy interface
#  - set variables LOGx to the full path of server log file for instance x: LOGA,
#    LOGB, etc,
#  - wait for all instances to detect each other
#  - assert that all instances are in each others' peer lists
start_servald_instances() {
   push_instance
   tfw_log "# start servald instances $*"
   foreach_instance "$@" \
      start_servald_server
   # Now wait until they see each other.
   foreach_instance "$@" \
      wait_until --sleep=0.25 has_seen_instances "$@"
   pop_instance
}

# Assertion function:
# - asserts that the current instance reports a peer list that contains all the
#   SIDs of all the other instances
# - uses the SID{I}{1..N} variables set by create_instances()
assert_peers_are_instances() {
   local I N
   executeOk_servald id allpeers
   for I; do
      for ((N=1; 1; ++N)); do
         local sidvar=SID${I#+}$N
         [ -n "${!sidvar}" ] || break
         assertStdoutGrep "${!sidvar}"
      done
   done
}

# Predicate function:
# - useful in combination with assert() and wait_until()
# - return true if the current instance has logged that it has seen all other instances via the
#   selfannounce mechanism
has_seen_instances() {
   local I N
   executeOk_servald route print
   for I; do
      [ $I = $instance_arg ] && continue
      for ((N=1; 1; ++N)); do
         local sidvar=SID${I#+}$N
         [ -n "${!sidvar}" ] || break
         if ! grep "^${!sidvar}" $_tfw_tmp/stdout; then
            return 1
         fi
      done
   done
   return 0
}

# Predicate function:
# - useful in combination with assert() and wait_until()
# - return true if all instances have logged that they have seen all other instances via the
#   selfannounce mechanism
instances_see_each_other() {
   foreach_instance "$@" has_seen_instances "$@"
}

# Setup function:
# - ensure that the given version of the curl(1) utility is available
# - remove all proxy settings
setup_curl() {
   CURL=$(type -P curl) || error "curl(1) command is not present"
   local minversion="${1?}"
   local ver="$("$CURL" --version | tr '\n' ' ')"
   case "$ver" in
   curl\ *\ Protocols:*\ http\ *)
      set -- $ver
      tfw_cmp_version "$2" "$minversion"
      case $? in
      0|2)
         export CURL
         unset http_proxy
         unset HTTP_PROXY
         unset HTTPS_PROXY
         unset ALL_PROXY
         return 0
         ;;
      esac
      error "$CURL version $2 is not adequate (expecting $minversion or higher)"
      ;;
   esac
   error "cannot parse output of $CURL --version: $ver"
}

# Guard function:
CURL=
curl() {
   [ -x "$CURL" ] || error "missing call to setup_curl in the fixture"
   "$CURL" "$@"
}

# Setup function:
# - ensure that the netcat6 nc6(1) utility is available
setup_netcat6() {
   NETCAT6=$(type -P nc6) || error "nc6(1) command is not present"
   local minversion="${1:-1.0}"
   local ver="$("$NETCAT6" --version | tr '\n' ' ')"
   case "$ver" in
   nc6\ version\ *)
      set -- $ver
      tfw_cmp_version "$3" "$minversion"
      case $? in
      0|2)
         export NETCAT6
         return 0
         ;;
      esac
      error "$NETCAT6 version $3 is not adequate (expecting $minversion or higher)"
      ;;
   esac
   error "cannot parse output of $NETCAT6 --version: $ver"
}

# Guard functions.
NETCAT6=
nc6() {
   [ -x "$NETCAT6" ] || error "missing call to setup_netcat6 in the fixture"
   "$NETCAT6" "$@"
}
nc() {
   error "do not use nc; instead use nc6(1) after calling setup_netcat6 in the fixture"
}
netcat() {
   error "do not use netcat; instead use nc6(1) after calling setup_netcat6 in the fixture"
}