#!/usr/bin/env bash
# Define test functions.
# See numerous configuration variables later on in this file that you can set
# in the environment or the site.sh file. The definitions in the site.sh file 
# override 
#
# Create working dir as variable "dir"
# The functions are somewhat wildgrown, a little too many:
# - expectpart
# - expecteof
# - expecteof_netconf
# - expecteofx
# - expecteofeq
# - expecteof_file
# - expectwait
# - expectmatch

#set -e
# : ${A=B} vs : ${A:=B} # colon also checks for NULL

# Testfile (not including path)
: ${testfile:=$(basename $0)}

# SKIPLIST lists the filenames of the test files that you do *not* want to run.
# The format is a whitespace separated list of filenames. Specify the SKIPLIST
# either in the shell environment or in the site.sh file. Any SKIPLIST specified
# in site.sh overrides a SKIPLIST specified in the environment. If not specified
# in either the environment or the site.sh, then the default SKIPLIST is empty.
: ${SKIPLIST:=""}

>&2 echo "Running $testfile"

# Generated config file from autotools / configure
if [ -f ./config.sh ]; then
    . ./config.sh
    if [ $? -ne 0 ]; then
	return -1 # error
    fi
fi

# Test number from start
: ${testnr:=0}

# Test number in this test
testi=0

# Single test. Set by "new"
testname=

# Valgind memory leak check.
# The values are:
# 0: No valgrind check
# 1: Start valgrind at every new testcase. Check result every next new
# 2: Start valgrind every new backend start. Check when backend stops
# 3: Start valgrind every new restconf start. Check when restconf stops
# 4: Start valgrind every new snmp start. Check when snmp stops
# 
: ${valgrindtest=0}

# If set to 0, override starting of clixon_backend in test (you bring your own)
: ${BE:=1}

# If BE is set, some tests have a user timeout to show which params to set
# for starting a backend
: ${BETIMEOUT:=10}

# If set, enable debugging (of backend and restconf daemons)
: ${DBG:=0}

# If set to 0, override starting of clixon_restconf in test (you bring your own)
: ${RC:=1}

# Where to log restconf. Some systems may not have syslog,
# eg logging to a file: RCLOG="-l f/www-data/restconf.log"
: ${RCLOG:=}

# If set to 0, override starting of clixon_snmp in test (you bring your own)
: ${SN:=1}

# Namespace: netconf base
BASENS='urn:ietf:params:xml:ns:netconf:base:1.0'

# Namespace: Clixon config
CONFNS='xmlns="http://clicon.org/config"'

# Namespace: Clixon lib
LIBNS='xmlns="http://clicon.org/lib"'

# Namespace: Clixon restconf
RESTCONFNS='xmlns="http://clicon.org/restconf"'

# Default netconf namespace statement, typically as placed on top-level <rpc xmlns=""
DEFAULTONLY="xmlns=\"$BASENS\""

# Default netconf namespace + message-id
DEFAULTNS="$DEFAULTONLY message-id=\"42\""

# Minimal hello message as a prelude to netconf rpcs
DEFAULTHELLO="<?xml version=\"1.0\" encoding=\"UTF-8\"?><hello $DEFAULTNS><capabilities><capability>urn:ietf:params:netconf:base:1.0</capability><capability>urn:ietf:params:netconf:base:1.1</capability></capabilities></hello>]]>]]>"

# XXX cannot get this to work for all combinations of nc/netcat fcgi/native
# But leave it here for debugging where netcat works properly
if [ -n "$(type netcat 2> /dev/null)" ]; then
    netcat="netcat -w 1" # -N does not work on fcgi
# nc on freebsd does not work either
#elif [ -n "$(type nc 2> /dev/null)" ]; then
#    netcat=nc
else
    netcat=
fi

# SSL serv cert common name XXX should use DNS resolve
: ${SSLCN:="localhost"}

# Options passed to curl calls
# -s : silent
# -S : show error
# -i : Include HTTP response headers
# -k : insecure 
: ${CURLOPTS:="-Ssik"}
# Set HTTP version 1.1 or 2
if ${HAVE_LIBNGHTTP2}; then
    : ${HVER:=2}
else
    : ${HVER:=1.1}
fi

if [ ${HVER} = 2 ]; then
    if ${HAVE_HTTP1}; then
	# This is if http/1 is enabled (unset proto=HTTP_2 in restconf_accept_client)
	CURLOPTS="${CURLOPTS} --http2"
    else
	# This is if http/1 is disabled (set proto=HTTP_2 in restconf_accept_client)
	CURLOPTS="${CURLOPTS} --http2-prior-knowledge"
    fi
else
    CURLOPTS="${CURLOPTS} --http1.1"
fi

# Wait after daemons (backend/restconf) start. See mem.sh for valgrind
if [ "$(uname -m)" = "armv7l" ]; then
    : ${DEMWAIT:=8}
else
    : ${DEMWAIT:=2}
fi

# Multiplication factor to sleep less than whole seconds
DEMSLEEP=0.2

# Some sleep implementations cannot handle sub-seconds, change to 1s
sleep $DEMSLEEP || DEMSLEEP=1

# DEMWAIT is expressed in seconds, but really * DEMSLEEP
let DEMLOOP=5*DEMWAIT

# RESTCONF protocol, eg http or https

if [ "${WITH_RESTCONF}" = "fcgi" ]; then
    : ${RCPROTO:=http}
else
    : ${RCPROTO:=https}
fi

# www user (on linux typically www-data, freebsd www)
# Start restconf user, can be root which is dropped to wwwuser
: ${wwwstartuser:=root} 

# Some restconf tests can run IPv6, but its complicated because:
# - docker by default does not run IPv6
: ${IPv6:=false}

# Backend user
BUSER=clicon

# If set, unknown XML is treated as ANYDATA
# This would only happen if you set option YANG_UNKNOWN_ANYDATA to something else than default
: ${YANG_UNKNOWN_ANYDATA:=false}

# Follow the binary programs that can be parametrized (eg with valgrind)

: ${clixon_cli:=clixon_cli}

: ${clixon_netconf:=$(which clixon_netconf)}

: ${clixon_restconf:=clixon_restconf}

: ${clixon_backend:=clixon_backend}

: ${clixon_snmp:=$(type -p clixon_snmp)}

: ${clixon_snmp_pidfile:="/var/tmp/clixon_snmp.pid"}

# Source the site-specific definitions for test script variables, if site.sh
# exists. The variables defined in site.sh override any variables of the same
# names in the environment in the current execution.
if [ -f ./site.sh ]; then
    . ./site.sh
    if [ $? -ne 0 ]; then
	return -1 # skip
    fi
    # test skiplist.
    for f in $SKIPLIST; do
	if [ "$testfile" = "$f" ]; then
	    echo "...skipped (see site.sh)"
	    return -1 # skip
	fi
    done
fi

# Standard IETF RFC yang files. 
if [ ! -z ${YANG_STANDARD_DIR} ]; then
    : ${IETFRFC=$YANG_STANDARD_DIR/ietf/RFC}
fi

: ${SNMPCHECK:=true}

if $SNMPCHECK; then
    snmpget="$(type -p snmpget) -On -c public -v2c localhost "
    snmpbulkget="$(type -p snmpbulkget) -On -c public -v2c localhost "
    snmpset="$(type -p snmpset) -On -c public -v2c localhost "
    snmpgetstr="$(type -p snmpget) -c public -v2c localhost "
    snmpgetnext="$(type -p snmpgetnext) -On -c public -v2c localhost "
    snmpgetnextstr="$(type -p snmpgetnext) -c public -v2c localhost "
    snmptable="$(type -p snmptable) -c public -v2c localhost "
    snmpwalk="$(type -p snmpwalk) -c public -v2c localhost "
    snmptranslate="$(type -p snmptranslate) "

    if [ "${ENABLE_NETSNMP}" == "yes" ]; then
	    pgrep snmpd > /dev/null
        if [ $? != 0 ]; then
		    echo -e "\e[31m\nenable-netsnmp set but snmpd not running, start with:"
		    echo "systemctl start snmpd"
            echo ""
            echo "snmpd must be configured to use a Unix socket for agent communication"
            echo "and have a rwcommunity configured, make sure the following lines are"
            echo "added to /etc/snmp/snmpd.conf:"
            echo ""
            echo "  rwcommunity     public  localhost"
            echo "  agentXSocket    unix:/var/run/snmp.sock"
            echo "  agentxperms     777 777"
            echo ""
            echo "If you don't rely on systemd you can configure the lines above"
            echo "and start snmpd manually with 'snmpd -Lo -p /var/run/snmpd.pid'."
		    echo -e "\e[0m"
		    exit -1
        fi
    fi

    function validate_oid(){
        oid=$1
        oid2=$2
        type=$3
        value=$4
        result=$5

        name="$($snmptranslate $oid)"
        name2="$($snmptranslate $oid2)"

        if [[ $oid =~ ^([0-9]|\.)+$ ]]; then
            get=$snmpget
            getnext=$snmpgetnext
        else
            get=$snmpgetstr
            getnext=$snmpgetnextstr
        fi

        if [ $oid == $oid2 ]; then
            if [ -z "$result" ]; then
                result="$oid = $type: $value"
            fi

            new "Validating OID: $oid2 = $type: $value"
            expectpart "$($get $oid)" 0 "$result"
        else
            if [ -z "$result" ]; then
                result="$oid2 = $type: $value"
            fi

            new "Validating next OID: $oid2 = $type: $value"
            expectpart "$($getnext $oid)" 0 "$result"
        fi
    }
fi

# Check sanity between --with-restconf setting and if nginx is started by systemd or not
# This check is optional because some installs, such as vagrant make a non-systemd/direct
# start
: ${NGINXCHECK:=false}
# Sanity nginx running on systemd platforms
if $NGINXCHECK; then
    if systemctl > /dev/null 2>&1 ; then
	# even if systemd exists, nginx may be started in other ways
	nginxactive=$(systemctl show nginx |grep ActiveState=active)
	if [ "${WITH_RESTCONF}" = "fcgi" ]; then
	    if [ -z "$nginxactive"  -a ! -f /var/run/nginx.pid ]; then
		echo -e "\e[31m\nwith-restconf=fcgi set but nginx not running, start with:"
		echo "systemctl start nginx"
		echo -e "\e[0m"
		exit -1
	    fi
	else
	    if [ -n "$nginxactive" -o -f /var/run/nginx.pid ]; then
		echo -e "\e[31m\nwith-restconf=fcgi not set but nginx running, stop with:"
		echo "systemctl stop nginx"
		echo -e "\e[0m"
		exit -1
	    fi
	fi
    fi # systemctl
fi

# Temp directory where all tests write their data to
dir=/var/tmp/$0
if [ ! -d $dir ]; then
    mkdir $dir
fi

# Default restconf configuration: IPv4 
# Can be placed in clixon-config
# Note that https clause assumes there exists certs and keys in /etc/ssl,...
# Args:
# 1: auth-type (one of none, client-cert, user)
# 2: pretty (if true pretty-print restconf return values)
# [3: proto: http or https]
# [4: http_data: true or false] # Note feature http-data must be enabled
# Note, if AUTH=none then FEATURE clixon-restconf:allow-auth-none must be enabled
# Note if https, check if server cert/key exists, if not generate them
function restconf_config()
{
    AUTH=$1
    PRETTY=$2

    # Change this to fixed parameters
    if [ $# -gt 2 ]; then
	myproto=$3
    else
    	myproto=$RCPROTO
    fi
    if [ $# -gt 3 ]; then
	myhttpdata=$4
    else
    	myhttpdata=false
    fi
    
    echo -n "<CLICON_FEATURE>clixon-restconf:fcgi</CLICON_FEATURE>"
    if [ $myproto = http ]; then
	echo -n "<restconf><enable>true</enable>"
	if ${myhttpdata}; then
	    echo -n "<enable-http-data>true</enable-http-data>"
	fi
	echo "<auth-type>$AUTH</auth-type><pretty>$PRETTY</pretty><debug>$DBG</debug><socket><namespace>default</namespace><address>0.0.0.0</address><port>80</port><ssl>false</ssl></socket></restconf>"
    else
	certdir=$dir/certs
	if [ ! -f ${dir}/clixon-server-crt.pem ]; then
	    certdir=$dir/certs
	    test -d $certdir || mkdir $certdir
	    srvcert=${certdir}/clixon-server-crt.pem
	    srvkey=${certdir}/clixon-server-key.pem
	    cacert=${certdir}/clixon-ca-crt.pem
	    cakey=${certdir}/clixon-ca-key.pem
	    cacerts $cakey $cacert
	    servercerts $cakey $cacert $srvkey $srvcert
	fi
	echo -n "<restconf><enable>true</enable>"
	if ${myhttpdata}; then
	    echo -n "<enable-http-data>true</enable-http-data>"
	fi
	echo "<auth-type>$AUTH</auth-type><pretty>$PRETTY</pretty><server-cert-path>${certdir}/clixon-server-crt.pem</server-cert-path><server-key-path>${certdir}/clixon-server-key.pem</server-key-path><server-ca-cert-path>${certdir}/clixon-ca-crt.pem</server-ca-cert-path><debug>$DBG</debug><socket><namespace>default</namespace><address>0.0.0.0</address><port>443</port><ssl>true</ssl></socket></restconf>"
    fi
}

# Default autocli configuration
# Can be placed in clixon-config
# Exclude all modules instead as defined by arg1
# Args:
# 1: modname module name pattern to be included
# 2: list-keyword
# 3: treerefstate
function autocli_config()
{
    modname=$1
    listkw=$2
    state=$3

    TMP=$(cat <<EOF
  <autocli>
     <module-default>false</module-default>
     <list-keyword-default>$listkw</list-keyword-default>
     <treeref-state-default>$state</treeref-state-default>
     <rule>
       <name>include $modname</name>
       <operation>enable</operation>
       <module-name>$modname</module-name>
     </rule>
  </autocli>
EOF
   )
   echo "${TMP}"
}

# Some tests may set owner of testdir to something strange and quit, need
# to reset to me
if [ ! -G $dir ]; then 
    u=$(whoami)
    sudo chown $u $dir
    sudo chgrp $u $dir
fi

# If you bring your own backend BE=0 (it is already started), the backend may
# have created some files (eg unix socket) in $dir and therefore cannot
# be deleted.
# Same with RC=0
if [ $BE -ne 0 -a $RC -ne 0 ]; then
    rm -rf $dir/*
fi

# error and exit,
# arg1: expected
# arg2: errmsg[optional]
# Assumes: $dir and $expect are set
# see err1
function err(){
  echo -e "\e[31m\nError in Test$testnr [$testname]:"
  if [ $# -gt 0 ]; then 
      echo "Expected: $1"
      echo
  fi
  if [ $# -gt 1 ]; then 
      echo "Received: $2"
  fi
  echo -e "\e[0m"
  echo "Diff between Expected and Received:"
  diff <(echo "$ret"| od -t c) <(echo "$expect"| od -t c)

  exit -1 #$testnr
}

# Dont print diffs
function err1(){
  echo -e "\e[31m\nError in Test$testnr [$testname]:"
  if [ $# -gt 0 ]; then 
      echo "Expected: $1"
      echo
  fi
  if [ $# -gt 1 ]; then 
      echo "Received: $2"
  fi
  echo -e "\e[0m"
  exit -1 #$testnr
}

# Test is previous test had valgrind errors if so quit
function checkvalgrind(){
    if [ -f $valgrindfile ]; then
	res=$(cat $valgrindfile | grep -e "Invalid" |awk '{print  $4}' | grep -v '^0$')
	if [ -n "$res" ]; then
	    >&2 cat $valgrindfile
	    sudo rm -f $valgrindfile
	    exit -1	    
	fi
	res=$(cat $valgrindfile | grep -e "reachable" -e "lost:"|awk '{print  $4}' | grep -v '^0$')
	if [ -n "$res" ]; then
	    >&2 cat $valgrindfile
	    sudo rm -f $valgrindfile
	    exit -1	    
	fi
	sudo rm -f $valgrindfile
    fi
}

# Check if two RFC6242 NETCONF frames are equal
# Since they use \n you cannot just use =
function chunked_equal()
{
    echo "1:$1"
    echo "2:$2"
    if [ "$1" == "$2" ]; then
	return 0
    else
	return 255
    fi
}

# Given a string, add RFC6242 chunked franing around it
# Args:
# 0: string
function chunked_framing()
{
    str=$1
    length=${#str}
   
    printf "\n#%s\n%s\n##\n" ${length} "${str}"
}

# Start clixon_snmp
function start_snmp(){
    cfg=$1

    rm -f ${clixon_snmp_pidfile}
    
    $clixon_snmp -f $cfg -D $DBG &

    if [ $? -ne 0 ]; then
	    err
    fi
}

# Stop clixon_snmp and Valgrind if needed
function stop_snmp(){
    if [ $valgrindtest -eq 4 ]; then 
	pkill -f clixon_snmp
	sleep 1
	checkvalgrind
    else
	killall -q clixon_snmp
    fi
    rm -f ${clixon_snmp_pidfile}
}

# Start backend with all varargs.
# If valgrindtest == 2, start valgrind
function start_backend(){
    if [ $valgrindtest -eq 2 ]; then
	# Start in background since daemon version creates two traces: parent,
	# child. If background then only the single relevant.
	sudo $clixon_backend -F -D $DBG $* &
    else
	sudo $clixon_backend -D $DBG $*
    fi
    if [ $? -ne 0 ]; then
	err
    fi
}

function stop_backend(){
    sudo clixon_backend -z $*
    if [ $? -ne 0 ]; then
	err "kill backend"
    fi
    if [ $valgrindtest -eq 2 ]; then 
	sleep 1
	checkvalgrind
    fi
    sudo pkill -f clixon_backend # extra ($BUSER?)
}

# Wait for restconf to stop sending  502 Bad Gateway
function wait_backend(){
    freq=$(chunked_framing "<rpc $DEFAULTNS><ping $LIBNS/></rpc>")
    reply=$(echo "$freq" | $clixon_netconf -q1ef $cfg) 
#    freply=$(chunked_framing "<rpc-reply $DEFAULTNS><ok/></rpc-reply>")
#    chunked_equal "$reply" "$freply"
    let i=0;
    while [[ $reply != *"<rpc-reply"* ]]; do
#	echo "sleep $DEMSLEEP"
	sleep $DEMSLEEP
	reply=$(echo "<rpc $ÐEFAULTSNS $LIBNS><ping/></rpc>]]>]]>" | clixon_netconf -qef $cfg 2> /dev/null)
#	echo "reply:$reply"
	let i++;
#	echo "wait_backend  $i"
	if [ $i -ge $DEMLOOP ]; then
	    err "backend timeout $DEMWAIT seconds"
	fi
    done
}

# Start restconf daemon
# @see wait_restconf
function start_restconf(){
    # Start in background 
    echo "sudo -u $wwwstartuser -s $clixon_restconf $RCLOG -D $DBG $*"
    sudo -u $wwwstartuser -s $clixon_restconf $RCLOG -D $DBG $* </dev/null &>/dev/null &
    if [ $? -ne 0 ]; then
	err1 "expected 0" "$?"
    fi
}

# Stop restconf daemon before test
function stop_restconf_pre(){
    sudo pkill -f clixon_restconf
}

# Stop restconf daemon after test
# Some problems with pkill:
# 1) Dont use $clixon_restconf (dont work in valgrind)
# 2) Dont use -u $WWWUSER since clixon_restconf may drop privileges.
# 3) After fork, it seems to take some time before name is right
function stop_restconf(){
    sudo pkill -f clixon_restconf
    if [ $valgrindtest -eq 3 ]; then 
	sleep 1
	checkvalgrind
    fi
}

# Wait for restconf to stop sending  502 Bad Gateway
# @see start_restconf
# Reasons for not working: if you run native is nginx running?
# @note assumes port=80 if RCPROTO=http and port=443 if RCPROTO=https
# Args:
# 1: (optional) override RCPROTO with http or https
function wait_restconf(){
    if [ $# = 1 ]; then
	myproto=$1
    else
	myproto=${RCPROTO}
    fi
#    echo "curl $CURLOPTS -X GET $myproto://localhost/restconf"
    hdr=$(curl $CURLOPTS -X GET $myproto://localhost/restconf 2> /dev/null)
#    echo "hdr:\"$hdr\""
    let i=0;
    while [[ "$hdr" != *"200"* ]]; do
#	echo "wait_restconf $i"
	if [ $i -ge $DEMLOOP ]; then
	    err1 "restconf timeout $DEMWAIT seconds"
	fi
	sleep $DEMSLEEP
#	echo "curl $CURLOPTS -X GET $myproto://localhost/restconf"
	hdr=$(curl $CURLOPTS -X GET $myproto://localhost/restconf 2> /dev/null)
#	echo "hdr:\"$hdr\""
	let i++;
    done
    if [ $valgrindtest -eq 3 ]; then 
	sleep 2 # some problems with valgrind
    fi
}

# Wait for restconf to stop 
# @note assumes port=80 if RCPROTO=http and port=443 if RCPROTO=https
# @see wait_restconf
function wait_restconf_stopped(){
#    echo "curl $CURLOPTS $* $RCPROTO://localhost/restconf"
    hdr=$(curl $CURLOPTS $* $RCPROTO://localhost/restconf 2> /dev/null)
#    echo "hdr:\"$hdr\""
    let i=0;
    while [[ $hdr = *"200 OK"* ]]; do
#	echo "wait_restconf_stopped $i"
	if [ $i -ge $DEMLOOP ]; then
	    err1 "restconf timeout $DEMWAIT seconds"
	fi
	sleep $DEMSLEEP
	hdr=$(curl $CURLOPTS $* $RCPROTO://localhost/restconf 2> /dev/null)
#	echo "hdr:\"$hdr\""
	let i++;
    done
    if [ $valgrindtest -eq 3 ]; then 
	sleep 2 # some problems with valgrind
    fi
}

# Use pidfile to check snmp started. pidfile is created after init in clixon_snmp
function wait_snmp()
{
    let i=0;
    while [ ! -f ${clixon_snmp_pidfile} ]; do
	if [ $i -ge $DEMLOOP ]; then
	    err1 "snmp timeout $DEMWAIT seconds"
	fi
	sleep $DEMSLEEP
	let i++;
    done
}
    
# End of single test, final tests before normal exit of test
# Note this is a single test started by new, not a total test suite
function endtest()
{
    if [ $valgrindtest -eq 1 ]; then 
	checkvalgrind
    fi
}

# Increment test number and print a nice string
function new(){
    endtest # finalize previous test
    testnr=`expr $testnr + 1`
    testi=`expr $testi + 1`
    testname=$1
    >&2 echo "Test $testi($testnr) [$1]"
}

# End of complete test-suite, eg a test file, mem.sh
function endsuite()
{
    unset CURLOPTS
}

# Evaluate and return
# Example: expectpart $(fn arg) 0 "my return" -- "foo"
# - evaluated expression
# - expected command return value (0 if OK) or list of values, eg "55 56"
# - expected stdout outcome*
# - the token "--not--"
# - not expected stdout outcome*
# Example:
# expectpart "$(a-shell-cmd arg)" 0 'expected match 1' 'expected match 2' --not-- 'not expected 1'
# @note need to escape \[\]
function expectpart(){
  r=$?
  ret=$1
  retval=$2
  expect=$3

#  echo "r:$r"
#  echo "ret:\"$ret\""
#  echo "retval:$retval"
#  echo "expect:\"$expect\""
  if [ "$retval" -eq "$retval" 2> /dev/null ] ; then # single retval
      if [ $r != $retval ]; then 
	  echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
	  echo -e "\e[0m:"
	  exit -1
      fi
  else # List of retvals
      found=0
      for rv in $retval; do
	  if [ $r == $rv ]; then 
	      found=1
	  fi
      done
      if [ $found -eq 0 ]; then
	  echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
	  echo -e "\e[0m:"
	  exit -1
      fi
  fi
  if [ -z "$ret" -a -z "$expect" ]; then
      return
  fi
  # Loop over all variable args expect strings (skip first two args)
  # note that "expect" var is never actually used
  # Then test positive for strings, if the token --not-- is detected, then test negative for the rest
  positive=true;
  let i=0;
  for exp in "$@"; do
      if [ $i -gt 1 ]; then
	  if [ "$exp" == "--not--" ]; then
	      positive=false;
	  else
#	   echo "echo \"$ret\" | grep --null -o \"$exp"\"
	      match=$(echo "$ret" | grep --null -i -o "$exp") #-i ignore case XXX -EZo: -E cant handle {}
	      r=$? 
	      if $positive; then
		  if [ $r != 0 ]; then
		      err "$exp" "$ret"
		  fi
	      else
		  if [ $r == 0 ]; then
		      err "not $exp" "$ret"
		  fi
	      fi
	  fi
      fi
      let i++;
  done
#  if [[ "$ret" != "$expect" ]]; then
#      err "$expect" "$ret"
#  fi
}

# Pipe stdin to command
# Arguments:
# - Command
# - expected command return value (0 if OK) XXX SHOULD SWITCH w next
# - stdin input
# - expected stdout outcome
# - expected stderr outcome (can be null)
# Use this if you want regex eg  ^foo$
function expecteof(){
  cmd=$1
  retval=$2
  input=$3
  expect=$4
  if [ $# -gt 4 ]; then
      errfile=$(mktemp)
      expecterr=$5
# Do while read stuff
      ret=$($cmd 2> $errfile <<EOF 
$input
EOF
	 )
      r=$? 
  else
# Do while read stuff
      ret=$($cmd <<EOF 
$input
EOF
 )
  r=$? 
  fi
  if [ $r != $retval ]; then
      echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
      echo -e "\e[0m:"
      exit -1
  fi
  # If error dont match output strings (why not?)
#  if [ $r != 0 ]; then
#      return
#  fi
  # Match if both are empty string
  if [ -z "$ret" -a -z "$expect" ]; then
      : # null
  else
      # -G for basic regexp (eg ^$). -E for extended regular expression - differs in \
	  # --null for nul character, -x for implicit ^$ -q for quiet
      # -o only matching
      # Two variants: --null -Eo and -Fxq
      #  match=`echo "$ret" | grep --null -Fo "$expect"`
      if [ $# -gt 4 ]; then # stderr
	  rerr=$(cat $errfile)
	  rm -f $errfile
	  r=$(echo "$rerr" | grep --null -Go "$expecterr")
	  match=$?
	  if [ $match -ne 0 ]; then
	      err "$expecterr" "$rerr"
	  fi
      fi

      r=$(echo "$ret" | grep --null -Go "$expect")
      match=$?

#  echo "r:\"$r\""
#  echo "ret:\"$ret\""
#  echo "expect:\"$expect\""
#  echo "match:\"$match\""
      if [ $match -ne 0 ]; then
	  err "$expect" "$ret"
      fi
  fi
}

# Pipe stdin to command and also do chunked framing (netconf 1.1)
# Arguments:
# - Command
# - expected command return value (0 if OK)
# - stdin input1  This is NOT encoded, eg preamble/hello
# - stdin input2  This gets chunked encoding
# - expect1 stdout outcome, can be partial and contain regexps
# - expect2 stdout outcome This gets chunked encoding, must be complete netconf message
# Use this if you want regex eg  ^foo$
function expecteof_netconf(){
  cmd=$1
  retval=$2
  input1=$3
  input2=$4
  expect1=$5
  expect2=$6

  if [ -n "${input2}" ]; then
      inputenc=$(chunked_framing "${input2}")
  else
      inputenc=""
  fi
  if [ -n "${expect2}" ]; then
      expectenc=$(chunked_framing "${expect2}")
  else
      expectenc=""
  fi

#  echo "input1:$input1"
#  echo "input2:$input2"
#  echo "inputenc:$inputenc"
#  echo "expect1:$expect1"
#  echo "expect2:$expect2"
#  echo "expectenc:$expectenc"
# Do while read stuff
  ret=$($cmd <<EOF 
${input1}${inputenc}
EOF
 )
  r=$? 

  if [ $r != $retval ]; then
      echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
      echo -e "\e[0m:"
      exit -1
  fi
  # If error dont match output strings (why not?)
  # Match if both are empty string
  if [ -z "$ret" -a -z "$expect1" ]; then
      : # null
  else
      r=$(echo "$ret" | grep --null -Go "$expect1")
      match=$?
      if [ $match -ne 0 ]; then
	  err "$expect1" "$ret"
      fi
  fi
  if [ -z "$ret" -a -z "$expectenc" ]; then
      : # null
  else
      while read i
      do
	  r=$(echo "$ret" | grep --null -Go "$i")
	  match=$?
	  if [ $match -ne 0 ]; then
	      err "$expectenc" "$ret"
	  fi
      done <<< "$expectenc"
  fi
}

# Like expecteof but with grep -Fxq instead of -EZq. Ie implicit ^$
# Use this for fixed all line, ie must match exact.
# - Command
# - expected command return value (0 if OK)
# - stdin input
# - expected stdout outcome
function expecteofx(){
  cmd=$1
  retval=$2
  input=$3
  expect=$4

# Do while read stuff
ret=$($cmd<<EOF 
$input
EOF
)
  r=$? 
  if [ $r != $retval ]; then
      echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
      echo -e "\e[0m:"
      exit -1
  fi
  # If error dont match output strings (why not?)
#  if [ $r != 0 ]; then
#      return
#  fi

  # Match if both are empty string
  if [ -z "$ret" -a -z "$expect" ]; then
      return
  fi
  # -E for regexp (eg ^$). -Z for nul character, -x for implicit ^$ -q for quiet
  # -o only matching
  # Two variants: -EZo and -Fxq
  #  match=`echo "$ret" | grep -FZo "$expect"`
  r=$(echo "$ret" | grep -Fxq "$expect")
  match=$?
#  echo "ret:\"$ret\""
#  echo "expect:\"$expect\""
#  echo "match:\"$match\""
  if [ $match -ne 0 ]; then
      err "$expect" "$ret"
  fi
}

# Like expecteof/expecteofx but with test == instead of grep.
# No wildcards
# Use this for multi-lines
# # - Command
# - expected command return value (0 if OK)
# - stdin input
# - expected stdout outcome
function expecteofeq(){
  cmd=$1
  retval=$2
  input=$3
  expect=$4

# Do while read stuff
ret=$($cmd<<EOF 
$input
EOF
)
  r=$? 
  if [ $r != $retval ]; then
      echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
      echo -e "\e[0m:"
      exit -1
  fi
  # If error dont match output strings (why not?)
#  if [ $r != 0 ]; then
#      return
#  fi
  # Match if both are empty string
  if [ -z "$ret" -a -z "$expect" ]; then
      return
  fi
#  echo "ret:\"$ret\""
#  echo "expect:\"$expect\""
#  echo "match:\"$match\""
  if [ "$ret" != "$expect" ]; then
     err "$expect" "$ret"
  fi
}

# clixon tester read from file for large tests
# Arguments:
# - Command
# - Filename to pipe to stdin 
# - expected stdout outcome
function expecteof_file(){
  cmd=$1
  retval=$2
  file=$3
  expect=$4

  # Run the command, pipe stdin from file
  ret=$($cmd<$file)
  r=$?
  if [ $r != $retval ]; then
      echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
      echo -e "\e[0m:"
      exit -1
  fi
  # Match if both are empty string
  if [ -z "$ret" -a -z "$expect" ]; then
      return
  fi
  match=`echo "$ret" | grep -Eo "$expect"`
  if [ -z "$match" ]; then
      err "$expect" "$ret"
  fi
}

# test script with timeout, used for notificatin streams
# - (not-evaluated) expression
# - expected command return value (0 if OK) (NOOP for now)
# - stdin input1  This is NOT encoded, eg preamble/hello
# - stdin input2  This gets chunked encoding
# - wait time in seconds
# - expected stdout outcome*
# - the token "--not--"
# - not expected stdout outcome*
#
# Note: use of a file "result" as a way to collect results
function expectwait(){
  cmd=$1
  ret=$2
  input1=$3
  input2=$4
  wait=$5
  expect=$6

  if [ -n "${input2}" ]; then
      inputenc=$(chunked_framing "${input2}")
  else
      inputenc=""
  fi
#  echo "cmd:$cmd"
#  echo "ret:$ret"
#  echo "input1:$input1"
#  echo "inputenc:$inputenc"
#  echo "wait:$wait"
#  echo "expect:$expect"
# Do while read stuff
  echo timeout > $dir/expectwaitresult
  ret=""
  sleep $wait | cat <(echo "$input1$inputenc") -| $cmd | while [ 1 ] ; do
    read -t 20 r
#    echo "r:<$r>"
    if [ -z "$r" ]; then
	sleep 1
	continue
    fi
    # Append $r to $ret
    ret="$ret$r"
#    echo "ret:$ret"
    let i=0;
    positive=true;
    let ok=0
    let fail=0
    for exp in "$@"; do
	if [ $i -gt 4 ]; then
#	    echo "i:$i"
#	    echo "exp:$exp"
	    if [ "$exp" == "--not--" ]; then
		positive=false;
	    else
		match=$(echo "$ret" | grep --null -i -o "$exp")
#		match=$(echo "$ret" | grep -Eo "$exp");
		r=$?
		if $positive; then
		    if [ $r != 0 ]; then
#			echo "fail: $exp"
			let fail++
			break
		    fi
		else
		    if [ $r == 0 ]; then
#			echo "fail: $exp"
			let fail++
			break
		    fi
		fi
	    fi
	fi
	let i++;
    done # for exp
#    echo "fail:$fail"
    if [ $fail -eq 0 ]; then
#	echo ok
	echo ok > $dir/expectwaitresult	
	#	break
	exit 0
    fi
  done
#  cat $dir/expectwaitresult
  if [ $(cat $dir/expectwaitresult) != "ok" ]; then
      err "ok" "$(cat $dir/expectwaitresult)"
      cat $dir/expectwaitresult
      exit -1
  fi
}

function expectmatch(){
    ret=$1
    r=$2
    expret=$3
    expect=$4
#    echo "ret:$ret"
#    echo "ret:$r"
#    echo "expret:$expret"
#    echo "expect:$expect"
    if [ $r != $expret ]; then
	echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:"
	echo -e "\e[0m:"
	exit -1
    fi
    if [ -z "$ret" -a -z "$expect" ]; then
	echo > /dev/null
    else
	match=$(echo "$ret" | grep -Eo "$expect")
	if [ -z "$match" ]; then
	    err "$expect" "$ret"
	fi
	if [ -n "$expect2" ]; then
	    match=`echo "$ret" | grep --null -Eo "$expect2"`
	    if [ -z "$match" ]; then
		err $expect "$ret"
	    fi
	fi
    fi
}

# Create CA certs
# Output variables set as filenames on entry, set as cert/keys on exit:
# Vars:
# 1: cakey   filename
# 2: cacert  filename
function cacerts()
{
    if [ $# -ne 2 ]; then
	echo "cacerts function: Expected: cakey cacert"
	exit 1
    fi
    local cakey=$1
    local cacert=$2

    tmpdir=$dir/tmpcertdir

    test -d $tmpdir || mkdir $tmpdir

    # 1. CA
    cat<<EOF > $tmpdir/ca.cnf
[ ca ]
default_ca      = CA_default

[ CA_default ]
serial = ca-serial
crl = ca-crl.pem
database = ca-database.txt
name_opt = CA_default
cert_opt = CA_default
default_crl_days = 9999
default_md = md5

[ req ]
default_bits           = ${CERTKEYLEN}
days                   = 1
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
output_password        = password

[ req_distinguished_name ]
C                      = SE
L                      = Stockholm
O                      = Clixon
OU                     = clixon
CN                     = ca
emailAddress           = olof@hagsand.se

[ req_attributes ]
challengePassword      = test

EOF

    # Generate CA cert
    openssl req -new -x509 -days 1 -config $tmpdir/ca.cnf -keyout $cakey -out $cacert || err "Generate CA cert"

    rm -rf $tmpdir
}

# Create server certs
# Output variables set as filenames on entry, set as cert/keys on exit:
# Vars:
# 1: cakey   filename (input)
# 2: cacert  filename (input)
# 3: srvkey  filename (output)
# 4: srvcert filename (output)
function servercerts()
{
    if [ $# -ne 4 ]; then
	echo "servercerts function: Expected: cakey cacert srvkey srvcert"
	exit 1
    fi
    cakey=$1
    cacert=$2
    srvkey=$3
    srvcert=$4

    tmpdir=$dir/tmpcertdir

    test -d $tmpdir || mkdir $tmpdir

    cat<<EOF > $tmpdir/srv.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
[dn]
CN = ${SSLCN} # localhost
emailAddress = olof@hagsand.se
O = Clixon
L = Stockholm
C = SE
[ext]
subjectAltName = DNS:clicon.org
EOF

    # Generate server key
    openssl genpkey -algorithm RSA -out $srvkey  || err "Generate server key"

    # Generate CSR (signing request)
    openssl req -new -config $tmpdir/srv.cnf -key $srvkey -out $tmpdir/srv_csr.pem || err "Generate signing request"

    # Sign server cert by CA
    openssl x509 -req -extfile $tmpdir/srv.cnf -days 1 -passin "pass:password" -in $tmpdir/srv_csr.pem -CA $cacert -CAkey $cakey -CAcreateserial -out $srvcert || err "Sign server cert"

    rm -rf $tmpdir
}

