# Common definitions for all test suites. # Copyright 2012 The 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 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 [] # # Examines the standard output of the last command executed using "execute" or # any of its variants. If there is a line matching # "\n" then assigns the part matched by # into the shell variable called and returns 0. # Otherwise, leaves unchanged and returns 1. # # The default 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=$(replayStdout | $GREP "^$_label_re$_delim_re") local _value= local _return=1 if [ -n "$_line" ]; then _value="${_line#*$_delim}" _return=0 fi [ -n "$_var" ] && eval $_var="\$_value" 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 "$@" } # 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 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: # - pop an instance off the instance stack pop_instance() { local n=${#instance_stack[*]} [ $n -eq 0 ] && error "instance stack underflow" let --n unset instance_stack[$n] } # 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" ;; +[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" ;; *) error "malformed instance name argument, must be in form +[A-Z]" ;; 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 "$@"; 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 servald JNI fixtures: # - check that libservald.so is present # - set LD_LIBRARY_PATH so that libservald.so can be found setup_servald_so() { assert [ -r "$servald_build_root/libservald.so" ] export LD_LIBRARY_PATH="$servald_build_root" } # 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 # 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" ] wait_until grep -q "Server started" "$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 "$instance_dir/cache" || error "mkdir failed" SERVALD_SERVER_CHDIR="$instance_dir" SERVALD_LOG_FILE="$instance_servald_log" $servald start "$@" } # 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 # - 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 executeOk_servald keyring add "${servald_options[@]}" assert [ -e "$SERVALINSTANCE_PATH/serval.keyring" ] local sidvar=SID$instance_name$i local didvar=DID$instance_name$i local namevar=NAME$instance_name$i 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}" -o -n "${!namevar}" ]; 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" && tfw_log "$didvar=$(shellarg "${!didvar}")" extract_stdout_keyvalue_optional $namevar name ".*" && tfw_log "$namevar=$(shellarg "${!namevar}")" 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[@]}" assertStdoutLineCount '==' $N 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 } # 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() { : } # 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() { local DUMMY=dummy case "$1" in dummy*) DUMMY="$1"; shift;; esac push_instance tfw_log "# start servald instances DUMMY=$DUMMY $*" local DUMMYNET="$SERVALD_VAR/$DUMMY" >$DUMMYNET local I for I; do set_instance $I # These config settings can be overridden in a caller-supplied configure_servald_server(). # They are extremely useful for the majority of fixtures. executeOk_servald config \ set interfaces.1.file "$DUMMYNET" configure_servald_server start_servald_server eval DUMMY$instance_name="\$DUMMYNET" done # Now wait until they see each other. foreach_instance "$@" \ wait_until --sleep=0.25 has_seen_instances "$@" tfw_log "# dummynet file:" $(ls -l $DUMMYNET) 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() { local minversion="${1?}" local ver="$(curl --version | tr '\n' ' ')" case "$ver" in '') fail "curl(1) command is not present" ;; curl\ *\ Protocols:*\ http\ *) set -- $ver tfw_cmp_version "$2" 7 case $? in 0|2) unset http_proxy unset HTTP_PROXY unset HTTPS_PROXY unset ALL_PROXY return 0 ;; esac fail "curl(1) version $2 is not adequate (expecting $minversion or higher)" ;; esac fail "cannot parse output of curl --version: $ver" } # Setup function: # - ensure that version 1.2 or later of the jq(1) utility is available setup_jq() { local minversion="${1?}" local ver="$(jq --version 2>&1)" case "$ver" in '') fail "jq(1) command is not present" ;; jq\ version\ *) set -- $ver tfw_cmp_version "$3" "$minversion" case $? in 0|2) return 0;; esac fail "jq(1) version $3 is not adequate (need $minversion or higher)" ;; esac fail "cannot parse output of jq --version: $ver" }