#!/bin/sh
#
# vmm tests. TAP output, no framework.
#
#   ./test            run every file in tests/
#   ./test config     run tests/config only
#
# Every test runs against a throwaway VMMDIR under $TMPDIR. Tests that
# need to boot a guest are skipped when /dev/kvm is not usable, so this
# exits 0 on a machine without virtualisation.

# shellcheck disable=SC2329  # the helpers below are called from tests/*
set -eu
umask 077

ROOT=$(cd "$(dirname "$0")" && pwd)
VMM=$ROOT/vmm
[ -x "$VMM" ] || { printf 'Bail out! not executable: %s\n' "$VMM"; exit 1; }

WORK=$(mktemp -d "${TMPDIR:-/tmp}/vmm-tests.XXXXXX")
trap 'cleanup' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

cleanup() {
	trap - EXIT HUP INT TERM
	for cmdline in /proc/[0-9]*/cmdline; do
		p=${cmdline#/proc/}
		p=${p%/cmdline}
		line=
		IFS= read -r line 2>/dev/null < "$cmdline" || :
		case $line in
		qemu-system-*"-pidfile$WORK/vms/"*"/pid" | \
		/*/qemu-system-*"-pidfile$WORK/vms/"*"/pid")
			kill -9 "$p" 2>/dev/null || :
			;;
		esac
	done
	rm -rf "$WORK"
}

VMMDIR=$WORK/vms
export VMMDIR
SHUTDOWN_TIMEOUT=3
export SHUTDOWN_TIMEOUT
EDITOR=true
export EDITOR

N=0
FAILED=0

ok() {
	N=$((N + 1))
	printf 'ok %s - %s\n' "$N" "$1"
}

notok() {
	N=$((N + 1))
	FAILED=$((FAILED + 1))
	printf 'not ok %s - %s\n' "$N" "$1"
	shift
	# Line by line: a diagnostic carrying vmm's output is usually several
	# lines, and a bare one is not a comment. It is read as a result, so an
	# error message mentioning 'ok' would invent a test that never ran.
	for l in "$@"; do
		printf '%s\n' "$l" | sed 's/^/# /'
	done
}

# skip N REASON - N assertions this host cannot make
skip() {
	n=$1
	shift
	while [ "$n" -gt 0 ]; do
		N=$((N + 1))
		printf 'ok %s - # SKIP %s\n' "$N" "$*"
		n=$((n - 1))
	done
}

# holds DESC CMD... - require a command to succeed. 'test ! -e x' says
# the negative, so there is only the one.
holds() {
	desc=$1
	shift
	if "$@" >/dev/null 2>&1; then
		ok "$desc"
	else
		notok "$desc" "failed: $*"
	fi
}

# exits CODE DESC ARGS...  - run vmm and require an exact exit code
exits() {
	want=$1
	desc=$2
	shift 2
	out=$("$VMM" "$@" 2>&1) && got=0 || got=$?
	if [ "$got" = "$want" ]; then
		ok "$desc"
	else
		notok "$desc" "wanted exit $want, got $got" "argv: $*" "output: $out"
	fi
}

# outputs PATTERN DESC ARGS... - run vmm and require a matching line
outputs() {
	pat=$1
	desc=$2
	shift 2
	out=$("$VMM" "$@" 2>&1) && got=0 || got=$?
	if [ "$got" = 0 ] && printf '%s\n' "$out" | grep -q -- "$pat"; then
		ok "$desc"
	else
		notok "$desc" "exit $got, wanted 0 and a match for: $pat" \
			"argv: $*" "output: $out"
	fi
}

# omits PATTERN DESC ARGS... - run vmm and require no matching line
omits() {
	pat=$1
	desc=$2
	shift 2
	out=$("$VMM" "$@" 2>&1) && got=0 || got=$?
	if [ "$got" != 0 ]; then
		notok "$desc" "exit $got, wanted 0" "argv: $*" "output: $out"
	elif printf '%s\n' "$out" | grep -q -- "$pat"; then
		notok "$desc" "unwanted match for: $pat" "argv: $*" "output: $out"
	else
		ok "$desc"
	fi
}

# refuses PATTERN DESC ARGS... - require a NON-ZERO exit AND a matching
# message. The message alone is not enough: an error downgraded to a
# warning prints the same words and still exits 0.
refuses() {
	pat=$1
	desc=$2
	shift 2
	out=$("$VMM" "$@" 2>&1) && got=0 || got=$?
	if [ "$got" = 0 ]; then
		notok "$desc" "expected a refusal, got exit 0" "argv: $*" "output: $out"
		return 0
	fi
	if printf '%s\n' "$out" | grep -q -- "$pat"; then
		ok "$desc"
	else
		notok "$desc" "exit $got but no match for: $pat" "argv: $*" "output: $out"
	fi
}

cfg() {
	cat > "$VMMDIR/$1/config"
}

# The list is an order, cheapest checks first. The loop refuses a file
# it does not name, which would otherwise never run and never be missed.
if [ $# -eq 0 ]; then
	set -- static usage create config argv arch list clone lock env \
		boot net console viewer
	for f in "$ROOT"/tests/*; do
		case " $* " in
		*" ${f##*/} "*)	continue ;;
		esac
		printf 'Bail out! tests/%s is in no run list\n' "${f##*/}"
		exit 1
	done
fi

for t in "$@"; do
	if [ ! -f "$ROOT/tests/$t" ]; then
		printf 'Bail out! no such test: %s\n' "$t"
		exit 1
	fi
	# shellcheck source=/dev/null
	. "$ROOT/tests/$t"
done

printf '1..%s\n' "$N"
[ "$FAILED" = 0 ] || exit 1
exit 0
