#!/bin/bash

# uses bash's define

set -e

declare -a makeflags

# gmake isn't on debian?
gnumake="make -C $(realpath $(dirname $0))/testing/kvm"

print-kvm-variable() {
    ${gnumake} --no-print-directory print-kvm-variable VARIABLE=$1 "${makeflags[@]}"
}

kvm_platforms=$(print-kvm-variable KVM_PLATFORMS)

kvm_test_host_names=$(print-kvm-variable KVM_TEST_HOST_NAMES)

kvm_testingdir=$(print-kvm-variable KVM_TESTINGDIR)
kvm_prefix=($(print-kvm-variable KVM_PREFIX))
kvm_sourcedir=$(print-kvm-variable KVM_SOURCEDIR)
kvm_pidfile=$(print-kvm-variable KVM_PIDFILE)

# repo under test
rutdir=${kvm_testingdir:+$(realpath $(dirname "${kvm_testingdir}"))}


# eat newlines

hosts="${kvm_test_host_names}"
for platform in ${kvm_platforms} ; do
    hosts="${hosts} ${platform} ${platform}-base ${platform}-upgrade"
done

# Note: pass1help() is fed into AWK.  Anything that matches
# '^____[-a-z]' is considered a Pass 1 command ('_' denotes a space).

pass1help()
{
    cat <<EOF
Modifiers:

  Use the test directories that have been modified when running and
  comparing tests:

    modified
	apply operation to modified tests (default is all tests)

  In addition, failed and unresolved, update the current test
  directory.  For instance "unresolved results" only lists unresolved
  tests.

EOF
}

pass1=$(pass1help | awk '/^    [-a-z]/ { printf " %s ", $1 }' ; echo ${host})

# Note: pass2help() is fed into AWK.  Anything that matches
# '^____[-a-z]' is considered a valid Pass 2 command (_ denotes a
# space).

pass2help()
{
    cat <<EOF
  Set up the test environment, run the testsuite:

    install [PLATFORM]
	if needed, create the build domains
	install libreswan on the build domains
	clone the build domains to create the test domains
	control with Makefile.inc.local:KVM_{FEDORA,FREEBSD,NETBSD,OPENBSD}=true
    check [ <test-directory> ... ]
	run the testsuite

  Examine the test results:

    diffs [ <test-directory> ... ]
	show differences (for the specified tests)
	exit non-zero when differences are found
    results [ <test-directory> ... ]
	list test results (for the specified tests)
	exit non-zero when failures are found
    failed [ <test-directory> ... ]
	list failed and unresolved test results (for the specified tests)
        subsequent commands are applied to the resulting list
    passed [ <test-directory> ... ]
	list passed test results (for the specified tests)
        subsequent commands are applied to the resulting list
    unresolved [ <test-directory> ... ]
	list unresolved test results (for the specified tests)
        subsequent commands are applied to the resulting list

  Re-run, or run a-new the testsuite:

    recheck [ <test-directory> ... ]
	re-run the testsuite
    uninstall
	deletes test domains
	deletes build domains
	leaves test results and keys alone
    clean
	deletes test domains
	deletes build domains
	deletes test results
	deletes test keys
    check-clean
	delete the test results (leave the build trees and domains alone)
    keys
	deletes test keys, and then rebuilds them

  Update the expected test results (and GIT repository):

    patch [ <test-directory> ... ]
	apply test differences (to the specified test directories)
    add [ <test-directory> ... ]
	<<git add>> (the specified test directories)

  Manipulate the kvmrunner process:

    status
	report the status of the running test
    kill
	kill the running testsuite
    nohup
	run in the background writing output to a NEW nohup.out

  Step wize create/delete the domains (just use ./kvm install):

    gateway
	create the gateway (swandefault) used by the VMs

    base [PLATFORM]
	create a domain containing the base OS
	creates: ${kvm_base_host_names}
    upgrade [PLATFORM]
	create the upgrade domain from the base domain
	installs missing packages
        updates existing packages
	creates: ${kvm_upgrade_host_names}
    transmogrify [PLATFORM]
	create the build domain from the upgrade domain
	transmogrifies the domain ready for building and testing
	creates: ${kvm_platforms}
    build [PLATFORM]
	build/install libreswan on the build domains
	uses: ${kvm_platforms}
    install [PLATFORM]
	build/install libreswan
	clones build domains to create the test domains

  Stepwize delete the domains (just use ./kvm purge):

    shutdown [PLATFORM]
	shutdown all domains
    uninstall [PLATFORM]
	delete test-domains, build domain
	leaves base domains and upgrade domains alone
    downgrade [PLATFORM]
	also deletes upgrade domains
	also deletes test keys
	also deletes test results
    purge [PLATFORM]
	also deletes base domains
    demolish [PLATFORM]
	also deletes shared gateway

  Libvirt breakage:

    restart
	restart libvirt to workaround catatonic daemon performance

  To log into a domain:

    sh <host> [ <command> ]
	start a shell on <host> which can be a:
	  test domain: ${kvm_test_host_names}
	  build domain: ${kvm_build_host_names}
	  base domain: ${kvm_base_host_names}
	  upgrade domain: ${kvm_upgrade_host_names}

  Configuration:

    config
	show the configuration

  Namespaces - broken:

    nsinstall
	install namespaces
    nsreinstall
	re-install namespaces
    nsrun [ <test-directory> ... ]
	run testsuite using namespaces on the fedora domain

EOF
}

pass2=$(pass2help | awk -v "plat=${kvm_platforms}" -- '
BEGIN {
	split(plat, platforms)
}
/^    [a-z]* .PLATFORM.$/ {
	t=$1
	sub(/[^a-z]*$/,"",t)
	printf " %s ", t
	for (i in platforms) {
		printf " %s-%s ", t, platforms[i]
	}
	next
}
/^    [-a-z]*/ {
	printf " %s ", $1
}')


# Invoked by completer with:
#
#   $0 <command==$0> <word> <previous>?
#
# ${pass1}, ${pass2} and ${hosts} contain completion values.

if test "$1" = $0 -a $# -eq 3 ; then
    command=$1
    word=$2
    previous=$3
    # hack to detect first vs later argument
    if test "${previous}" = "${command}" ; then
	# first command
	compgen -W "${pass1} ${pass2}" "${word}" | sort
    elif test "${previous}" = sh ; then
	# sh hostname
	compgen -W "${hosts}" "${word}"
    elif [[ " ${pass2}" =~ " ${word}" ]] ; then
	# word looks to be matching a command (or is empty),
	# expand to either <command> or <directory>
	compgen -o plusdirs -W "${pass2}" "${word}"
    else
	# doesn't match a command, so throw in the testing directory
	# as a quick expansion
	compgen -o plusdirs -W "${pass2}" -G "$(realpath --relative-base $PWD ${kvm_testingdir}/pluto)/${word}*" "${word}"
    fi
    exit 0
fi

# Handle ../../../kvm [...]
#
# No arguments implies "check $PWD".  Operations but no directory
# implies $PWD is the test.
#
# Set relative to the directory to interpret paths from.

relative=

if test $(realpath $(dirname $0)) != $(realpath ${PWD}) ; then
    if test $# -eq 0 ; then
	set -- check
    fi
    relative=$PWD
    cd $(dirname $0)
fi

# Finally is there at least one parameter?

if test $# -eq 0; then
    cat <<EOF
Usage:

   <modifier> ... <operation> ... <test-directory> <test-directory> ...

EOF
    pass1help
    echo
    pass2help
cat <<EOF

To enable completion, add these lines to .bashrc:

  complete -o filenames -C        ./kvm        ./kvm
  complete -o filenames -C ../../../kvm ../../../kvm

(the first is for top-level, the second for within a directory)
EOF
    exit 1
fi

# Accumulate pass 2 commands; execute pass 1 commands immediately.
#
# XXX: should "sh" be delayed so that "downgrade install sh netbsd"
# DTRT?

echo=
modified=
declare -a directories
declare -a ops
declare -a platforms

while test $# -gt 0 ; do

    # pass 1; look for modifiers  commands

    case "$1" in

	-n )
	    echo=echo
	    ;;

	nohup )
	    if test "${#ops[@]}" -gt 0 ; then
		echo "nohup must be first command" 1>&2
		exit 1
	    fi
	    if test "$#" -eq 1 ; then
		echo "expecting a command after nohup" 1>&2
		exit 1
	    fi
	    # avoid race tail can try to open nohup.out before nohup
	    # creates it.
	    cp /dev/null nohup.out
	    shift
	    nohup ./kvm "$@" &
	    exec tail -f nohup.out
	    ;;

	# aliases for pass2 commands
	diff )       ops[${#ops[@]}]=diffs ;;
	result )     ops[${#ops[@]}]=results ;;
	failed )     ops[${#ops[@]}]=failed ;;
	passed )     ops[${#ops[@]}]=passed ;;
	unresolved ) ops[${#ops[@]}]=unresolved ;;
	test   )     ops[${#ops[@]}]=check ;;
	retest )     ops[${#ops[@]}]=recheck ;;
	test-clean ) ops[${#ops[@]}]=check-clean ;;

	sh )
	    # remainder of line is host + optional commands
	    shift # drop SH
	    if test $# -lt 1 ; then
		echo "missing hostname" 1>&2
		exit 1
	    fi
	    # try matching without prefix
	    if [[ " ${hosts} " =~ " $1 " ]] ; then
		host=${kvm_prefix}$1
		shift # drop HOST; add prefix
		echo "Connecting to ${host}" 1>&2
		exec testing/utils/kvmsh.py ${host} "$@"
	    fi
	    # try matching with prefix?
	    echo "unrecognized hostname" 1>&2
	    exit 1
	    ;;

	# wrappers
	modified )
	    modified=$(git status testing/pluto/ \
			   | awk '/(modified|deleted|renamed|new file):/ { print $NF }' \
			   | grep '/.*/.*/' \
			   | cut -d/ -f1-3 \
			   | sort -u)
	    if test -z "${modified}" ; then
		echo "no modified tests" 1>&2
		exit 1
	    fi
	    ;;

	*=* ) # capture make variables
	    makeflags[${#makeflags[@]}]=$1
	    ;;

	/* ) # an absolute directory path
	    directories[${#directories[@]}]=$(realpath --relative-to=$PWD "$1")
	    ;;

	* ) # a directory, a pass2 command, or a platform

	    if [[ " ${pass2} " =~ " $1 " ]] ; then
	       # a pass2 command, accumulate
		ops[${#ops[@]}]=$1
	    elif [[ " ${kvm_platforms} " =~ " $1 " ]] ; then
		# platform, accumulate
		platforms[${#platforms[@]}]=$1
	    elif test -n "${relative}" -a -d ${relative}/$1 ; then
		directories[${#directories[@]}]=$(realpath --relative-to=$PWD ${relative}/$1)
	    elif test -z "${relative}" -a -d "$1" ; then
		directories[${#directories[@]}]=$(realpath --relative-to=$PWD "$1")
	    elif test -z "${relative}" -a -d "${rutdir}/$1" ; then
		directories[${#directories[@]}]=$(realpath --relative-to=${rutdir} "${rutdir}/$1")
	    elif test -z "${relative}" -a -d "${rutdir}/testing/pluto/$1" ; then
		directories[${#directories[@]}]=$(realpath --relative-to=${rutdir} "${rutdir}/testing/pluto/$1")
	    elif test -z "${ops[*]}" ; then
		# first argument, should be a command
		echo "unrecognized command: $1" 1>&2
	    else
		echo "not a directory: $1" 1>&2
	    fi
	    ;;
    esac
    shift
done

if test -n "${modified}" ; then
    if test ${#ops[@]} -eq 0 ; then
	echo "${modified}"
	exit 0
    elif test ${#directories[@]} -gt 0 ; then
	echo "both modified and tests specified" 1>&2
	exit 1
    fi
    directories=(${modified})
elif test ${#directories[@]} -eq 0 ; then
    if test -n "${relative}" ; then
	directories=(${relative})
    else
	directories=(${kvm_testingdir})
    fi
fi

if test -z "${ops[*]}" ; then
    echo "nothing to do!" 1>&2
    exit 1
fi

# Now that makeflags[] is set, extract more variables so they can be
# passed to kvmresults.
#
# For instance ./kvm results KVM_NETBSD=true adds netbsd to platform
# and status flags.

kvm_test_status=$(print-kvm-variable KVM_TEST_STATUS)
kvm_test_platform=$(print-kvm-variable KVM_TEST_PLATFORM)
kvm_test_name=$(print-kvm-variable KVM_TEST_NAME)
kvm_test_flags=$(print-kvm-variable KVM_TEST_FLAGS)

run()
{
    echo "$@" 1>&2
    if test -z "${echo}" ; then
	"$@"
    fi
}

run_pid()
{
    if test -r ${kvm_pidfile} ; then
	local pid=$(cat ${kvm_pidfile})
	if ps www --no-headers ${pid} ; then
	    echo "it looks like KVM is already running?"
	    exit 1
	fi
    fi

    if test -n "${echo}" ; then
	echo "$@"
	return
    fi

    echo "$@" 1>&2
    trap "set -x ; rm -f ${kvm_pidfile}" EXIT
    echo $$ > ${kvm_pidfile}
    if "$@" ; then
	# clean up after every command
	rm ${kvm_pidfile}
    else
	status=$?
	rm ${kvm_pidfile}
	exit ${status}
    fi
}

kvmresults()
{
    ./testing/utils/kvmresults.py \
	${kvm_testingdir:+--testing-directory ${kvm_testingdir}} \
	${kvm_test_status:+--test-status "${kvm_test_status// /|}"} \
	${kvm_test_platform:+--test-platform "${kvm_test_platform// /|}"} \
	${kvm_test_name:+--test-name "${kvm_test_name}"} \
	${kvm_test_flags} \
	"$@"
}

results_command()
{
    kvmresults "$@"
    status=$?
}

diffs_command()
{
    kvmresults --no-summary --print diffs "${directories[@]}"
    status=$?
}

kill_command()
{
    if test ! -r ${kvm_pidfile} ; then
	echo "no ${kvm_pidfile} file" 1>&2
	exit 1
    fi
    local pid=$(cat ${kvm_pidfile})
    echo "killing ${pid}" 1>&2
    rm -f ${kvm_pidfile}
    kill ${pid}
}

status_command()
{
    if test ! -r ${kvm_pidfile} ; then
	echo "no ${kvm_pidfile} file" 1>&2
	exit 1
    fi
    local pid=$(cat ${kvm_pidfile})
    ps="ps www --no-headers ${pid}"
    echo "${ps}"
    ${ps}
}

# second pass
status=0
i=0

while test $i -lt ${#ops[@]} ; do

    op=${ops[${i}]}
    i=$((i + 1))
    # note [ $i -lt ${#ops[@]} ] -> more to come

    case ${op} in

	add )
	    run git add "${directories[@]}"
	    ;;

	kill )
	    kill_command
	    ;;
	status )
	    status_command
	    ;;

	base | upgrade | transmogrify | build | install | \
        shutdown | uninstall | downgrade | demolish | purge )
	    if test "${#platforms[@]}" -gt 0 ; then
		targets=$(for platform in ${platforms[@]} ; do
			      echo kvm-${op}-${platform}
			  done)
		run_pid ${gnumake} ${targets} "${makeflags[@]}"
	    else
		run_pid ${gnumake} kvm-${op} "${makeflags[@]}"
	    fi
	    ;;
	base* | upgrade* | transmogrify* | build* | install* | \
        shutdown* | uninstall* | downgrade* | demolish* | purge* )
	    run_pid ${gnumake} kvm-${op} "${makeflags[@]}"
	    ;;
	gateway | config | check-clean | clean )
	    run_pid ${gnumake} kvm-${op} "${makeflags[@]}"
	    ;;
	check | recheck )
	    run_pid ${gnumake} kvm-${op} "${makeflags[@]}" KVM_TESTS="${directories[*]}"
	    ;;
	diffs )
	    run diffs_command "${directories[@]}"
	    ;;
	results )
	    run results_command "${directories[@]}"
	    ;;
	passed | failed | unresolved )
	    # should leave: failed and/or unresolved
	    case ${op} in
		passed )     match="--skip failed --skip unsupported --skip untested" ;;
		failed )     match="--skip passed --skip unsupported --skip untested" ;;
		unresolved ) match="--result unresolved" ;;
	    esac
	    if test  ${i} -lt ${#ops[@]} ; then
		run set -- $(results_command ${match} "${directories[@]}" 2>/dev/null)
	    else
		run results_command ${match} "${directories[@]}"
	    fi
	    ;;
	patch )
	    run diffs_command "${directories[@]}" | patch -p1
	    ;;
	keys )
	    run_pid ${gnumake} kvm-keys-clean kvm-keys "${vars[@]}"
	    ;;
	restart )
	    run sudo systemctl restart libvirtd
	    ;;
	nsinstall )
	    run testing/utils/kvmsh.py --chdir /source ${prefix}fedora -- gmake nsinstall
	    ;;
	nsreinstall )
	    run testing/utils/kvmsh.py --chdir /source ${prefix}fedora -- gmake nsreinstall
	    ;;
	nsrun )
	    run testing/utils/kvmsh.py --chdir /source ${prefix}fedora -- gmake nsrun
	    ;;
    esac
done

exit ${status}
