#!/bin/sh -X # no runnable script, just for editors
# set -eu is assumed.

if [ -n "${APT_TEST_FRAMEWORK_WITHOUT_REPO_SOURCED:+yes}" ]; then
    return
fi
readonly APT_TEST_FRAMEWORK_WITHOUT_REPO_SOURCED=1

# Why no repeat it?..
set -eu

# Under set -x, don't mess the trace with other commands' output (possibly
# redirected), rather send it directly to the global stderr.
exec 5>&2
BASH_XTRACEFD=5

# the mode common for our test scripts
set -o pipefail
# and some more strictness (bash 4.4):
#set -o inherit_errexit

EXIT_CODE=0

# During the most lifetime of the script (until the exiting traps), this var is
# just used to count the failed tests. Better do this via a dedicated function:
XFAILED=0
one_more_failure() {
	if [ "$APT_TEST_XFAIL" = '' ]; then
		EXIT_CODE=$((EXIT_CODE+1))
	else
		XFAILED=$((XFAILED+1))
	fi
}

# we all like colorful messages
if [ "$MSGCOLOR" != 'NO' ]; then
	if [ ! -t 1 ]; then # but check that we output to a terminal
		export MSGCOLOR='NO'
	fi
fi

CERROR=
CWARNING=
CMSG=
CINFO=
CDEBUG=
CNORMAL=
CDONE=
CPASS=
CFAIL=
CCMD=

if [ "$MSGCOLOR" != 'NO' ]; then
	CERROR=$'\033[1;31m' # red
	CWARNING=$'\033[1;33m' # yellow
	CMSG=$'\033[1;32m' # green
	CINFO=$'\033[1;96m' # light blue
	CDEBUG=$'\033[1;94m' # blue
	CNORMAL=$'\033[0;39m' # default system console color
	CDONE=$'\033[1;32m' # green
	CPASS=$'\033[1;32m' # green
	CFAIL=$'\033[1;31m' # red
	CCMD=$'\033[1;35m' # pink
fi

# Our messages shouldn't be messed up with commands' stdout and stderr,
# therefore we use a special FD for them.
exec 6>&2

wait_until() {
    local -r cond="$1"; shift
    local -r ms="$1"; shift
    local msg="$*"
    if [ -z "$msg" ]; then
	msg="$cond"
    fi
    local -r msg

    local -r n=$(( ms / 50 ))

    # The message 'Waiting...' is intentionally printed regardless of $MSGLEVEL;
    # otherwise it would be difficult to see why the process is hanging;
    # it can appear only in extra-ordinary situations (under heavy load).
    eval "$cond" ||
	{ # need to group the alternative commands after a ||
	    printf >&6 'Waiting for %s\n' "$msg" &&
		local i=0 &&
		while ! eval "$cond"; do
			sleep 0.05 &&
			let '++i' &&
			{ (( i < n )) || {
			      msgwarn "Waited too long (${ms}ms)!"
			      return 1
			  }
			}
		done &&
		printf >&6 'Waited %s * 0.05s.\n' "$i" # count&print i out of curiosity
	}
}

wait_for_file() {
    local -r f="$1"; shift

    local cond
    printf -v cond '[ -s %q ]' "$f"
    local -r cond

    wait_until "$cond" 50000 "$f" || msgdie "Timeout waiting for $f"
}

wait_for_nofile() {
    local -r f="$1"; shift
    local -r time="$1"; shift

    local cond
    printf -v cond '! [ -s %q ]' "no $f"
    local -r cond

    wait_until "$cond" "$time" "$f"
}

# Not to hang on waiting, we start a parallell subshell which
# will kill it after a timeout (sleep). (Or which would just sleep,
# and we'd wait -n on either processes, then kill it in the main
# script.) Idea: https://stackoverflow.com/a/22384727/94687 .
ensure_it_is_killed_in_a_while() {
    local -r old_pid="$1"; shift

    local killer_pid=
    kill -0 "$old_pid" &>/dev/null &&
	{ (sleep 5 &&
	       kill "$old_pid" &&
	       kill -0 "$old_pid" &>/dev/null &&
	       sleep 5 &&
	       kill -0 "$old_pid" &>/dev/null &&
	       kill -KILL "$old_pid" ||: ) &
	  killer_pid=$!
	}
    local -r killer_pid
    wait "$old_pid" ||:
    [ -z "$killer_pid" ] || { kill -0 "$killer_pid" &>/dev/null && kill "$killer_pid"; } ||:
}

msgdie() { printf "${CERROR}E: %s${CNORMAL}\n" "$1" >&6; exit 1; }
msgwarn() { printf "${CWARNING}W: %s${CNORMAL}\n" "$1" >&6; }
msgmsg() { printf "${CMSG}%s${CNORMAL}\n" "$1" >&6; }
msginfo() { printf "${CINFO}I: %s${CNORMAL}\n" "$1" >&6; }
msgdebug() { printf "${CDEBUG}D: %s${CNORMAL}\n" "$1" >&6; }
msgdone() { printf "${CDONE}%s${CNORMAL}\n" 'DONE' >&6; }
msgnwarn() { printf "${CWARNING}W: %s${CNORMAL}" "$1" >&6; }
msgnmsg() { printf "${CMSG}%s${CNORMAL}" "$1" >&6; }
msgninfo() { printf "${CINFO}I: %s${CNORMAL}" "$1" >&6; }
msgndebug() { printf "${CDEBUG}D: %s${CNORMAL}" "$1" >&6; }
msgtest() {
	while [ $# -gt 0 ]; do
		printf "${CINFO}%s${CCMD} %s${CNORMAL} " "$1" "$(sed -e 's#^apt\([cgfs]\)#apt-\1#' <<<"${2-}")" >&6
		shift
		if [ $# -gt 0 ]; then
			shift
		else
			break
		fi
	done
	printf "${CINFO}…${CNORMAL} " >&6
}
msgpass() { printf "${CPASS}PASS${CNORMAL}\n" >&6; }
msgskip() { printf "${CWARNING}SKIP${CNORMAL}\n" >&6; }
msgfail() {
	if [ $# -gt 0 ]; then printf "${CFAIL}FAIL: %s${CNORMAL}\n" "$*" >&6;
	else printf "${CFAIL}FAIL${CNORMAL}\n" >&6; fi
	one_more_failure
}

# enable / disable Debugging
if [ $MSGLEVEL -le 0 ]; then
	# Although this override never happens with nonnegative MSGLEVEL, let's not
	# change the semantics: this msg*() function doesn't just print a message.
	msgdie() { exit 1; }
fi
if [ $MSGLEVEL -le 1 ]; then
	msgwarn() { true; }
	msgnwarn() { true; }
fi
if [ $MSGLEVEL -le 2 ]; then
	msgmsg() { true; }
	msgnmsg() { true; }
	msgtest() { true; }
	msgpass() { printf " ${CPASS}P${CNORMAL}" >&6; }
	msgskip() { printf " ${CWARNING}S${CNORMAL}" >&6; }
	if [ -n "$CFAIL" ]; then
		msgfail() { printf " ${CFAIL}FAIL${CNORMAL}" >&6; one_more_failure; }
	else
		msgfail() { printf ' ###FAILED###' >&6; one_more_failure; }
	fi
fi
if [ $MSGLEVEL -le 3 ]; then
	msginfo() { true; }
	msgninfo() { true; }
fi
if [ $MSGLEVEL -le 4 ]; then
	msgdebug() { true; }
	msgndebug() { true; }
fi
msgdone() {
	if [ "$1" = 'debug' -a $MSGLEVEL -le 4 ] ||
	   [ "$1" = 'info' -a $MSGLEVEL -le 3 ] ||
	   [ "$1" = 'msg' -a $MSGLEVEL -le 2 ] ||
	   [ "$1" = 'warn' -a $MSGLEVEL -le 1 ] ||
	   [ "$1" = 'die' -a $MSGLEVEL -le 0 ]; then
		true;
	else
		printf "${CDONE}DONE${CNORMAL}\n" >&6;
	fi
}
runapt() {
	msgdebug "CWD: $PWD"
	msgdebug "Executing: APT_CONFIG=./aptconfig.conf ${CCMD}$*${CDEBUG} "
	local CMD="$1"
	shift
	APT_CONFIG=./aptconfig.conf $CMD "$@"
}
aptconfig() { runapt apt-config "$@"; }
aptcache() { runapt apt-cache "$@"; }
aptget() { runapt apt-get "${CDROM_OPTS[@]}" "$@"; }
aptmark() { runapt apt-mark "$@"; }

exitwithstatus() {
		if [ $EXIT_CODE -gt 0 ]; then
			exit $((EXIT_CODE <= 253 ? EXIT_CODE : 253));
		elif [ $XFAILED -gt 0 ]; then
			exit 255
		elif [ -n "$APT_TEST_XFAIL" ]; then
			exit 254 # XPASS: uneXpected PASS
		fi
}

shellsetedetector() {
	local exit_status=$?
	if [ "$exit_status" != '0' ]; then
		printf >&6 "${CERROR}E: Looks like the testcases ended prematurely with exitcode: %d${CNORMAL}\n" "${exit_status}"
		if [ "$EXIT_CODE" = '0' ]; then
			EXIT_CODE="$exit_status"
		fi
	fi
}

CURRENTTRAP=
addtrap() {
	if [ "$1" = 'prefix' ]; then
		shift
		local -r fmt="$1"; shift
		printf -v CURRENTTRAP "$fmt\n%s" "$@" "$CURRENTTRAP"
	else
		local -r fmt="$1"; shift
		printf -v CURRENTTRAP "%s$fmt\n" "$CURRENTTRAP" "$@"
	fi
	trap "shellsetedetector; $CURRENTTRAP trap - 0; exitwithstatus; exit 0;" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
}

# Source a script whose effect is a prerequisite.
# Do not continue, if that script has failed or x-failed.
prereq() {
	. "$1"
	if [ $EXIT_CODE -gt 0 ] || [ $XFAILED -gt 0 ]; then
	    exit
	fi
}

setupenvironment() {
	TESTDIRECTORY=$(readlink -f $(dirname $0))
	addtrap '' # unconditionally set up our basic traps

	if [ -n "$APT_TEST_WORKINGDIRECTORY" ]; then
		TMPWORKINGDIRECTORY="$APT_TEST_WORKINGDIRECTORY"
	else
		TMPWORKINGDIRECTORY=$(mktemp -d)
		addtrap 'cd /; rm -rf %q;' "$TMPWORKINGDIRECTORY"
	fi

	# The place for build logs and metainfo, and built pkgs,
	# and other intermediate prerequisites for the tests.
	# Is there a place for persistent storage accross test runs?
	# If no, make it temporary.
	if [ -z "$APT_TEST_INTERMEDIATES" ]; then
	    APT_TEST_INTERMEDIATES=$TMPWORKINGDIRECTORY/intermediates
	fi

	msgninfo "Preparing environment for ${CCMD}$(basename $0)${CINFO} in ${TMPWORKINGDIRECTORY}… "

	cd $TMPWORKINGDIRECTORY
	mkdir rootdir aptarchive keys
	cd rootdir
	mkdir -p \
		  etc/apt/apt.conf.d etc/apt/sources.list.d \
		  etc/apt/trusted.gpg.d etc/apt/preferences.d
	mkdir -p var/cache var/lib/apt var/log tmp
	mkdir -p var/lib/rpm
	# global var to be conveniently re-used in tests
	readonly APT_LISTS_DIR="$PWD/var/lib/apt/lists"
	mkdir -p "$APT_LISTS_DIR"/partial var/cache/apt/archives/partial
	cd ..

	{
		echo "Dir \"${TMPWORKINGDIRECTORY}/rootdir\";"
		echo 'Debug::NoLocking "true";'
		echo "Dir::Etc::sourcelist \"${TMPWORKINGDIRECTORY}/rootdir/etc/apt/sources.list\";"
		echo 'RPM::Options { "--justdb"; };'

		if [ -n "$METHODSDIR" ] ; then
			echo "Dir::Bin::methods \"$METHODSDIR\";"
		fi
	} > aptconfig.conf

	cat > rootdir/etc/apt/pkgpriorities << END
Important:
  basesystem
Required:
  apt
  systemd-sysvinit
  sysvinit
  openssh-server
Standard:
  postfix
END

	# just an empty sources.list for the beginning
	echo > rootdir/etc/apt/sources.list

	# global var to be conveniently re-used in tests, where cert-pinning is
	# involved (so that all edit the same place)
	readonly APT_TEST_PINNING_CONF=$TMPWORKINGDIRECTORY/rootdir/etc/apt/apt.conf.d/81https-pinning.conf

	# global var to be conveniently re-used in tests, where proxy for HTTP is
	# involved (so that all edit the same place)
	readonly APT_TEST_PROXY_FOR_HTTP_CONF=$TMPWORKINGDIRECTORY/rootdir/etc/apt/apt.conf.d/81http-proxy.conf

	readonly APT_DEBUG_CONNECT_DIR=$TMPWORKINGDIRECTORY/debug/connect

	# cleanup the environment a bit
	export PATH="${PATH}:/usr/local/sbin:/usr/sbin:/sbin"
	export LC_ALL=C.UTF-8
	unset LANGUAGE APT_CONFIG
	unset GREP_OPTIONS

	# The configuration for rpm (install)
	mkdir -p $TMPWORKINGDIRECTORY/home_rooter
	export HOME=$TMPWORKINGDIRECTORY/home_rooter
	echo >"$HOME"/.rpmmacros
	# Not using the common tmppath helps to avoid conflicts
	# (among other things: a race in rpmioMkpath)
	echo "%_tmppath $TMPWORKINGDIRECTORY/rootdir/tmp" >>"$HOME"/.rpmmacros
	echo "%_dbpath $TMPWORKINGDIRECTORY/rootdir/var/lib/rpm" >>"$HOME"/.rpmmacros

	# Initialize rpmdb
	rpmdb --initdb

	# Include a special kind of pkg in the db to see how APT processes it
	if [ -n "$APT_TEST_GPGPUBKEY" ]; then
		rpm --import "$APT_TEST_GPGPUBKEY"
	fi

	# setup lua stuff
	mkdir -p $TMPWORKINGDIRECTORY/lua/scripts
	mkdir -p $TMPWORKINGDIRECTORY/lua/results
	echo "Dir::Bin::scripts \"$TMPWORKINGDIRECTORY/lua/scripts\";" > rootdir/etc/apt/apt.conf.d/90lua.conf

	# setup bash scripts
	mkdir -p $TMPWORKINGDIRECTORY/bash/scripts
	mkdir -p $TMPWORKINGDIRECTORY/bash/results

	# for rpmbuild
	mkdir -p $TMPWORKINGDIRECTORY/tmp

	# emulate CDROM (without affecting the config)
	MOCKUP_CDROM_STORAGE="$TMPWORKINGDIRECTORY/usr/src/RPM/DISK"
	CDROM_OPTS=(-o Acquire::CDROM::Mount="$MOCKUP_CDROM_STORAGE"
				-o Acquire::CDROM::"$MOCKUP_CDROM_STORAGE/"::Mount=/bin/true
				-o Acquire::CDROM::"$MOCKUP_CDROM_STORAGE/"::UMount=/bin/true
				# this is needed not to statvfs of the emulated CDROM FS:
				-o Debug::identcdrom=true
				# FIXME: a quick hack to add more debugging opts
				# for apt-get (non-cdrom-specific):
				-o Acquire::Verbose=yes
				-o Debug::pkgAcquire::Auth=yes
			   )

	msgdone 'info'
	msgdebug "$(rpm --eval '_dbpath: %_dbpath')"
	msgdebug "$(rpm --eval '_tmppath: %_tmppath')"
}

mk_intermediate() {
    local -r what="$1"; shift
    local -r build_cmd="$1"; shift
    # No additional args for build_cmd; nothing but $what shall determine
    # the result to ensure its stability across different invocations.

    local -r trap_before_mkdir="$CURRENTTRAP"

    # where it is being built and/or cached:
    local -r f="$APT_TEST_INTERMEDIATES/$what"
    local -r d="${f%/*}"
    mkdir -p "${d%/*}" # the prefix
    if ! mkdir "$d" &>/dev/null; then
	msginfo "Re-using ${what%/*}"
	wait_for_file "$f"
    else
	# After an error during the build procedure, we have to delete the dir
	# in order to make clear that one shouldn't start waiting for a result
	# on the other branch. So we push a temporary trap for this.
	# (Then restore from $trap_before_mkdir.)
	# (And waiting that has already started can be limited in wait_for_file.)
	# Note: This doesn't prevent trying the erroneous build once again.
	addtrap prefix 'rm -rf %q' "$d"

	# pass args: prefix name out_basename
	"$build_cmd" "${d%/*}" "${d##*/}" "${f##*/}".in-progress
	mv -T "$f"{.in-progress,}

	CURRENTTRAP="$trap_before_mkdir"
	addtrap ''
    fi
}

# The arch (the machine arch) as detected by rpm or apt
# (and as expected to appear in the APT conf by default).
#
# * `uname -m` gives the base value.
# * rpm-build has its own way to determine the arch, which refines `uname -m`.
#   See: rpmbuild --eval %_arch 2>/dev/null ||:
#   (Packages are built by default for this arch, i.e.,
#   when there is no explicit --target.)
# * rpm has its own way to determine the arch (with more incompatible refinements
#   on ARM compared to rpm-build).
# * APT has its own simplistic way to determine the arch based on `uname -m`.
#
# FIXME: APT should probably use the same value as rpm for APT::Architecture.
# FIXME: rpmbuild's and rpm's ways need to be synchronized, particularly on ARM.
if [ -z "$APT_TEST_DEFAULT_ARCH" ]; then
	APT_TEST_DEFAULT_ARCH="$(uname -m)"
fi

# The arch used for the built packages, repos, etc.
# (May be forced by $APT_TEST_TARGET together with --target.)
if [ -n "$APT_TEST_TARGET" ]; then
	# set by force
	APT_TEST_USED_ARCH="$APT_TEST_TARGET"
else
	# just set it to an appropriate value, if not set by force
	APT_TEST_USED_ARCH="$APT_TEST_DEFAULT_ARCH"
fi

buildpackage_() {
    local -r prefix="$1"; shift
    local -r NAME="$1"; shift
    local -r out_basename="$1"; shift

    msgmsg "Building package: ${NAME}"

    local target_opt=
    if [ -n "$APT_TEST_TARGET" ]; then
	target_opt=--target="$APT_TEST_TARGET"
    fi
    local -r target_opt

    local in_spec="$APT_TEST_INTERMEDIATES/specs/$NAME"/spec
    if ! [ -e "$in_spec" ]; then
	in_spec="${TESTDIRECTORY}/specs/${NAME}.spec"
    fi
    local -r in_spec
    if ! [ -e "$in_spec" ]; then
	msgdie "No spec for $NAME found in {APT_TEST_INTERMEDIATES,TESTDIRECTORY}/specs/"
    fi

    local -r d="$prefix/$NAME"

    local -r log="$d"/log

    HOME=/var/empty rpmbuild \
	${APT_TEST_PKG_FILENAME_BY_ALIAS:+--define "_rpmfilename %{ARCH}/${NAME}.%{ARCH}.rpm"} \
	--define 'EVR %{?epoch:%epoch:}%{version}-%{release}' \
	--define 'packager test@example.net' \
	--define="_tmppath $d/tmp" \
	--define="_topdir $d/RPM" \
	$target_opt \
	-bb "$in_spec" \
	1>"$log"

    local builtpackagefile
    builtpackagefile="$(sed -nEe '$ s/Wrote: (.*) \(.*\)$/\1/p' <"$log")"
    local -r builtpackagefile
    {
	printf 'local -r builtpackagefile=%q\n' \
	       "$builtpackagefile"
	HOME=/var/empty rpmquery \
	    -p "$builtpackagefile" \
	    --qf '
		local -r name=%{NAME:shescape}
		local -r arch=%{ARCH:shescape}
		local -r epoch=%{EPOCH:shescape}
		local -r version=%{VERSION:shescape}
		local -r release=%{RELEASE:shescape}
		'
    } >"$d/$out_basename"
}

buildpackage() {
	local -r NAME="$1"

	mk_intermediate built-pkg/"$NAME"/metainfo buildpackage_

	# Copy out files from the "cache" to where they are expected by our framework
	# (FIXME: this will silently overwrite existing files with the same name,
	# but we should report this as an error in the test.)
	mkdir -p $TMPWORKINGDIRECTORY/usr/src/RPM
	cp -l -a "$APT_TEST_INTERMEDIATES"/built-pkg/"$NAME"/RPM \
	   -T $TMPWORKINGDIRECTORY/usr/src/RPM
}

builtpackagefile() {
	local -r NAME="$1"

	. "$APT_TEST_INTERMEDIATES"/built-pkg/"$NAME"/metainfo

	if [ -n "$builtpackagefile" ]; then
	    echo "$builtpackagefile"
	    return
	fi

	# The rest is actually never needed now, with auto-detection of
	# the built pkg file and auto-extraction of the metainfo from it.

	# To type less in *.metainfo, assume that the spec's filename coincides with
	# the package's name by default:
	if [ -z "${name-}" ]; then
		name="$NAME"
	fi
	# If arch is not specified in the spec, we know what was given as --target.
	if [ -z "${arch-}" ]; then
		arch="$APT_TEST_USED_ARCH"
	fi

	if [ -n "$APT_TEST_PKG_FILENAME_BY_ALIAS" ]; then
	    echo "$TMPWORKINGDIRECTORY/usr/src/RPM/RPMS/${arch}/${name}-${version}-${release}.${arch}.rpm"
	else
	    echo "$TMPWORKINGDIRECTORY/usr/src/RPM/RPMS/${arch}/${NAME}.${arch}.rpm"
	fi
}

builtpackagebasename_re() {
    local fname

    fname="$(builtpackagefile "$1")"
    fname="${fname##*/}"

    local -r fname

    regex_quote "$fname"
}

builtpackagearch() {
	local -r NAME="$1"

	. "$APT_TEST_INTERMEDIATES"/built-pkg/"$NAME"/metainfo
	# If arch is not specified in the spec, we know what was given as --target.
	if [ -z "${arch-}" ]; then
		arch="$APT_TEST_USED_ARCH"
	fi

	echo "${arch}"
}

builtpackageversion() {
	local -r NAME="$1"

	. "$APT_TEST_INTERMEDIATES"/built-pkg/"$NAME"/metainfo

	echo "${version}-${release}"
}

installpackage() {
	local NAME="$1"

	local PKGFILE="$(builtpackagefile "$NAME")"

	msgmsg "Installing package via rpm: $(basename "$PKGFILE")"
	rpm \
		--justdb \
		--nodeps \
		-i "$PKGFILE" \
		1>/dev/null
}

aptgetinstallpackage() {
	local NAME="$1"

	local PKGFILE="$(builtpackagefile "$NAME")"

	msgmsg "Installing package via apt-get (as a file): $(basename "$PKGFILE")"
	# does no access to the repo
	aptget install "$PKGFILE" \
		   1>/dev/null
}

checkdiff() {
	DIFFTEXT="$(command diff -Nu "$@")" || {
		[ $? -eq 1 ] || exit 1
		DIFFTEXT="$(sed -e '/^---/ d' -e '/^+++/ d' -e '/^@@/ d' <<<"$DIFFTEXT")" || exit 1
		return 1
	}
}

testfileequal() {
	local MSG='Test for correctness of file'
	if [ "$1" = '--nomsg' ]; then
		MSG=''
		shift
	fi

	local FILE="$1"
	shift
	if [ -n "$MSG" ]; then
		msgtest "$MSG" "$FILE"
	fi
	checkdiff - $FILE <<<"${*:+$*}" \
	    && msgpass \
	    || {
		msgfail
		echo >&6 "$DIFFTEXT"
	    }

}

testempty() {
	msgtest 'Test for no output of' "$*"
	local COMPAREFILE="${TMPWORKINGDIRECTORY}/rootdir/tmp/testempty.comparefile"
	if $* >$COMPAREFILE 2>&1 && test ! -s $COMPAREFILE; then
		msgpass
	else
		msgfail
		cat >&6 $COMPAREFILE
	fi
}

testequal() {
	local MSG='Test of equality of'
	if [ "$1" = '--nomsg' ]; then
		MSG=''
		shift
	fi

	local -r COMPARE="$1"
	shift

	if [ -n "$MSG" ]; then
		msgtest "$MSG" "$*"
	fi
	local -r RESULTFILE="${TMPWORKINGDIRECTORY}/rootdir/tmp/testequal.resultfile"
	"$@" >|"$RESULTFILE" 2>&1
	checkdiff - "$RESULTFILE" <<<"$COMPARE" \
	    && msgpass \
	    || {
		msgfail
		echo >&6 "$DIFFTEXT"
	    }
}

testscriptoutput() {
	local MSG="$1"
	shift

	msgtest 'Test of output of' "$MSG"

	testfileequal --nomsg "$2" "$1"
}

testscriptnooutput() {
	local MSG="$1"
	shift

	msgtest 'Test for no output of' "$MSG"

	testfileequal --nomsg "$1"
}

# Effects:
#
# * global vars OUTPUT, RESULT
#   -- so that it can be followed up by test{success,failure}
testregexmatch() {
	local MSG='Test of regex match of'
	if [ "$1" = '--nomsg' ]; then
		MSG=''
		shift
	fi

	local COMPAREMSG="$1"
	shift

	if [ -n "$MSG" ]; then
		msgtest "$MSG" "$*"
	fi

	OUTPUT="${TMPWORKINGDIRECTORY}/rootdir/tmp/testregexmatch.output"

	local result=0 # not to be overwritten inside the tested command ("$@")
	"$@" &> $OUTPUT || result=$?
	RESULT=$result # the global var to hold this value (could be overwritten)
	local OUTPUT_STR
	# disable IFS not to strip spaces at the beginning and the end of file
	IFS= read -rd '' OUTPUT_STR <"$OUTPUT" ||
	    : # non-zero status always (because of EOF)
	local -r OUTPUT_STR

	if [[ "$OUTPUT_STR" =~ ^$COMPAREMSG$ ]] ; then
		msgpass
	else
		msgfail
		local COMPAREFILE="${TMPWORKINGDIRECTORY}/rootdir/tmp/testregexmatch.comparefile"
		printf '%s' "$COMPAREMSG" > $COMPAREFILE
		checkdiff $COMPAREFILE $OUTPUT || true
		echo >&6 "$DIFFTEXT"
	fi
}

match_and_delete() {
	local -r PATTERN="$1"; shift
	local -r VARNAME="$1"; shift

	[[ "${!VARNAME}" =~ ^(|.*$'\n')$PATTERN(|$'\n'(.*))$ ]] &&
		printf -v "$VARNAME" '%s%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[-1]}"
}

# Effects:
#
# * global vars OUTPUT, RESULT
#   -- so that it can be followed up by test{success,failure}
testpartsexhaust() {
	local MSG='Test that regex parts exhaust the output of'
	if [ "$1" = '--nomsg' ]; then
		MSG=''
		shift
	fi

	local -a REGEXPARTS
	while [ "$1" != '--' ]; do
		REGEXPARTS=("${REGEXPARTS[@]}" "$1"); shift
		: ${1?"Missing '--' arg." 'Usage: testpartsexhaust REGEX.. -- CMD'}
	done
	# The loop would fail if there was no '--' arg.
	shift # the '--' arg

	if [ -n "$MSG" ]; then
		msgtest "$MSG" "$*"
	fi

	OUTPUT="${TMPWORKINGDIRECTORY}/rootdir/tmp/testregexmatch.output"

	local result=0 # not to be overwritten inside the tested command ("$@")
	"$@" &> $OUTPUT || result=$?
	RESULT=$result # the global var to hold this value (could be overwritten)
	local OUTPUT_STR
	# disable IFS not to strip spaces at the beginning and the end of file
	read -rd '' OUTPUT_STR <"$OUTPUT" ||
	    : # non-zero status always (because of EOF)

	local REGEXPART
	for REGEXPART in "${REGEXPARTS[@]}"; do
		if ! match_and_delete "$REGEXPART" OUTPUT_STR; then
			msgfail
			echo "${CINFO}*** During matching parts with the output result, a part couldn't be matched.${CNORMAL}"
			echo "${CINFO}The remaining unmatched output:${CNORMAL}"
			echo "${CINFO}----------------------------------------${CNORMAL}"
			printf '%s' "$OUTPUT_STR"
			echo "${CINFO}----------------------------------------${CNORMAL}"
			echo "${CINFO}The part that didn't match:${CNORMAL}"
			echo "${CINFO}----------------------------------------${CNORMAL}"
			printf '%s' "$REGEXPART"
			echo "${CINFO}----------------------------------------${CNORMAL}"
			return
		fi
	done
	if [ "$OUTPUT_STR" != '' ]; then
		msgfail
		echo "${CINFO}The remaining unmatched output:"
		echo "$OUTPUT_STR"
	else
		msgpass
	fi
}

skiplines() {
	local count="$1"
	shift

	"$@" 2>&1 | tail +"$count"
}

testequalor2() {
	local COMPAREFILE1="${TMPWORKINGDIRECTORY}/rootdir/tmp/testequalor2.comparefile1"
	local COMPAREFILE2="${TMPWORKINGDIRECTORY}/rootdir/tmp/testequalor2.comparefile2"
	local COMPAREAGAINST="${TMPWORKINGDIRECTORY}/rootdir/tmp/testequalor2.compareagainst"
	echo "$1" > $COMPAREFILE1
	echo "$2" > $COMPAREFILE2
	shift 2
	msgtest 'Test for equality OR of' "$*"
	$* >$COMPAREAGAINST 2>&1 || true
	if checkdiff $COMPAREFILE1 $COMPAREAGAINST >/dev/null 2>&1 || \
		checkdiff $COMPAREFILE2 $COMPAREAGAINST >/dev/null 2>&1
	then
		msgpass
	else
		msgfail
		echo "${CINFO}Diff against OR 1${CNORMAL}"
		checkdiff $COMPAREFILE1 $COMPAREAGAINST || true
		echo >&6 "$DIFFTEXT"
		echo "${CINFO}Diff against OR 2${CNORMAL}"
		checkdiff $COMPAREFILE2 $COMPAREAGAINST || true
		echo >&6 "$DIFFTEXT"
	fi
}

testshowvirtual() {
	local VIRTUAL="N: Can't select versions from package '$1' as it is purely virtual"
	local PACKAGE="$1"
	shift
	while [ -n "$1" ]; do
		VIRTUAL="${VIRTUAL}
N: Can't select versions from package '$1' as it is purely virtual"
		PACKAGE="${PACKAGE} $1"
		shift
	done
	msgtest 'Test for virtual packages' "apt-cache show $PACKAGE"
	VIRTUAL="${VIRTUAL}
N: No packages found"
	local COMPAREFILE="${TMPWORKINGDIRECTORY}/rootdir/tmp/testshowvirtual.comparefile"
	aptcache show -q=0 $PACKAGE 2>&1 | {
		checkdiff $COMPAREFILE - \
		    && msgpass \
		    || {
			msgfail
			echo >&6 "$DIFFTEXT"
		    }
	}
}

testnopackage() {
	msgtest 'Test for non-existent packages' "apt-cache show $*"
	local SHOWPKG="$(aptcache show "$@" 2>&1 | grep '^Package: ')"
	if [ -n "$SHOWPKG" ]; then
		msgfail
		echo >&6 "$SHOWPKG"
	else
		msgpass
	fi
}

testpkginstalled() {
	msgtest 'Test that package(s) are installed with' "rpm -q $*"

	testsuccess --nomsg rpm -q "$@"
}

getpackageversion() {
	local result=

	result=$(rpm \
		 -q --qf '%{EVR}\n' "$@" \
		 2>/dev/null ||:)

	echo $result
}

testpkgnotinstalled() {
	msgtest 'Test that package(s) are not installed with' "rpm -q $*"

	testfailure --nomsg rpm -q "$@"
}

# If no args, don't run a command, but examine global vars OUTPUT, RESULT
# (as a follow-up test after another test).
testsuccess() {
	if [ $# -gt 0 ] && [ "$1" = '--nomsg' ]; then
		shift
	else
		if [ $# -gt 0 ]; then
			msgtest 'Test for successful execution of' "$*"
		else
			msgtest 'Test for successful execution of the last cmd'
		fi
	fi

	if [ $# -gt 0 ]; then
		OUTPUT="${TMPWORKINGDIRECTORY}/rootdir/tmp/testsuccess.output"
		local result=0 # not to be overwritten inside the tested command ("$@")
		"$@" &> $OUTPUT || result=$?
		RESULT=$result # the global var to hold this value (could be overwritten)
	fi

	if [ $RESULT -eq 0 ]; then
		msgpass
		if [ $MSGLEVEL -gt 3 ]; then # info
			cat >&6 $OUTPUT
		fi
	else
		msgfail
		cat >&6 $OUTPUT
	fi
}

# If no args, don't run a command, but examine global vars OUTPUT, RESULT
# (as a follow-up test after another test).
testfailure() {
	if [ $# -gt 0 ] && [ "$1" = '--nomsg' ]; then
		shift
	else
		if [ $# -gt 0 ]; then
			msgtest 'Test for failure in execution of' "$*"
		else
			msgtest 'Test for failure in execution of the last cmd'
		fi
	fi

	if [ $# -gt 0 ]; then
		OUTPUT="${TMPWORKINGDIRECTORY}/rootdir/tmp/testfailure.output"
		local result=0 # not to be overwritten inside the tested command ("$@")
		"$@" &> $OUTPUT || result=$?
		RESULT=$result # the global var to hold this value (could be overwritten)
	fi

	if [ $RESULT -eq 0 ]; then
		msgfail
		cat >&6 $OUTPUT
	else
		msgpass
		if [ $MSGLEVEL -gt 3 ]; then # info
			cat >&6 $OUTPUT
		fi
	fi
}

createluascript() {
	local APTNAME="$1"
	local SCRIPTFILENAME="$2"

	echo "$APTNAME:: \"${SCRIPTFILENAME}.lua\";" > rootdir/etc/apt/apt.conf.d/91lua-${SCRIPTFILENAME}.conf
	cat > $TMPWORKINGDIRECTORY/lua/scripts/${SCRIPTFILENAME}.lua << ENDSCRIPT
f = io.open("$TMPWORKINGDIRECTORY/lua/results/${SCRIPTFILENAME}.result", "a")
f:write("$APTNAME called\n")
f:close()
ENDSCRIPT
}

createbashscript() {
	local APTNAME="$1"
	local SCRIPTFILENAME="$2"

	echo "$APTNAME:: \"$TMPWORKINGDIRECTORY/bash/scripts/${SCRIPTFILENAME}.sh\";" > rootdir/etc/apt/apt.conf.d/92bash-${SCRIPTFILENAME}.conf
	cat > $TMPWORKINGDIRECTORY/bash/scripts/${SCRIPTFILENAME}.sh << ENDSCRIPT
#!/bin/bash
echo "$APTNAME called" >> "$TMPWORKINGDIRECTORY/bash/results/${SCRIPTFILENAME}.result"
ENDSCRIPT

	chmod +x $TMPWORKINGDIRECTORY/bash/scripts/${SCRIPTFILENAME}.sh
}

encode_uri_for_apt_lists() {
	local -r uri="$1"; shift

	sed -Ee 's: :%20:g; s:_:%5f:g; s:/:_:g;' <<<"$uri"
}

encode_auth_in_uri() {
	local -r uri="$1"; shift

	sed -Ee 's: :%20:g; s:@:%40:g; s|:|%3a|g;' <<<"$uri"
}

set_regex_quote_str() {
	local -r regex_var="$1"; shift
	local -r str="$1"; shift

	# FIXME: %q is not the correct quotation for regexes.
	if [ -n "$str" ]; then
		printf -v "$regex_var" '%q' "$str"
	else
		# workaround a problem of %q
		printf -v "$regex_var" ''
	fi
}

regex_quote() {
    local re

    set_regex_quote_str re "$1"
    local -r re

    printf '%s' "$re"
}

# for mk_intermediate
ssl_keygen() {
    local -r prefix="$1"; shift
    local -r name="$1"; shift
    local -r out_basename="$1"; shift
    local -r hostname="${name##*@}"

    local ip=
    case "$hostname" in
	localhost)
	    ip=,IP:127.0.0.1
	    ;;
	localhost6)
	    ip=,IP:::1
	    ;;
    esac	

    msginfo "Generating SSL key+cert: ${name}"

    openssl req -x509 \
	    -newkey rsa:4096 \
	    -nodes \
	    -days 365 \
	    -subj "/CN=$hostname" \
	    -addext "subjectAltName=DNS:$hostname$ip" \
	    -keyout "$prefix/$name"/key \
	    -out "$prefix/$name/$out_basename" \
	&>/dev/null
}

############################################################
# Helpers for a background apt-shell process.
# The process will be used to run commands.

# Output/effects:
# * global vars with the paths used to control the bg process (via "empty")
#   (convenient for use in other scripts):
#   BG_APTSHELL_{PIDFILE,IN,OUT,LOG}
bg_aptshell_setup() {
	mkdir -p $TMPWORKINGDIRECTORY/bg_aptshell
	BG_APTSHELL_LOG=$TMPWORKINGDIRECTORY/bg_aptshell/bg_aptshell.log
}

# Read until the prompt; consume and drop the prompt.
# The read output is output to the standard output.
bg_aptshell_read_response() {
	# "getline" actually gets the next "record" separated by RS,
	# so everything up to the prompt.
	awk -v RS='apt> ' \
	    -v ORS= \
	    'BEGIN { getline; print $0; quit; }' \
	    <&${BG_APTSHELL[0]}
}

# The FD and the PID vars associated with a coproc may start disappearing
# during exiting; so it's more convenient to bind function args (or local vars)
# to the values once. Therefore this helper function has been written.
# And it saves repeating the same code at several places.
bg_aptshell_exit_helper() {
	local -r old_input_fd="$1"; shift
	local -r old_pid="$1"; shift

	[ -z "$old_input_fd" ] || echo exit >&"$old_input_fd" ||:
	[ -z "$old_pid" ] ||
	    ensure_it_is_killed_in_a_while "$old_pid"
}

bg_aptshell_restart() {
	bg_aptshell_exit_helper "${BG_APTSHELL[1]-}" "${BG_APTSHELL_PID-}" \
	    &>>"$BG_APTSHELL_LOG"

	# Use the runapt() wrapper for APT_CONFIG (and the debug messages).
	# (NB: runapt() should have not yet been redefined to send commands
	# to the bg apt-shell!)
        coproc BG_APTSHELL {
	    runapt apt-shell # | tee "$BG_APTSHELL_LOG"
	}

	[ -n "${BG_APTSHELL_PID-}" ] ||
		{ { echo BG_APTSHELL_LOG:; cat "$BG_APTSHELL_LOG"; echo; } >&6 ||:
		  msgdie 'the bg apt-shell failed to start normally'
		}

	addtrap 'prefix' \
		'bg_aptshell_exit_helper %q %q &>%q||:; [ "$EXIT_CODE" = 0 ] || if [ -e %q ]; then echo BG_APTSHELL_LOG:; cat %q ||:; echo; fi >&6;' \
		"${BG_APTSHELL[1]}" \
		"$BG_APTSHELL_PID" \
		"$BG_APTSHELL_LOG" \
		"$BG_APTSHELL_LOG" \
		"$BG_APTSHELL_LOG"

	bg_aptshell_read_response >/dev/null
}

runaptcmd_in_bg_apt_shell() {
	msgdebug "CWD: $PWD"
	local -r CMD="$1"; shift
	msgdebug "Executing in bg apt-shell (normally $CMD): ${CCMD}$*${CDEBUG} "
	echo "$*" >&"${BG_APTSHELL[1]}"

	# Consume the echoed command (if any) in the output. (It seems that
	# readline usually echoes the input, but sometimes maybe not.)
	local the_rest_of_the_line
	read -r the_rest_of_the_line <&"${BG_APTSHELL[0]}"
	# Sanity check of our expectations:
	[ "$the_rest_of_the_line" = "$*" ] || [ "$the_rest_of_the_line" = '' ]

	bg_aptshell_read_response
	# TODO: try to detect the status (success/error) based on the output
}

# The commands of apt-config are not available in apt-shell.
#aptconfig_in_bg_apt_shell() { runaptcmd_in_bg_apt_shell apt-config "$@"; }
aptcache_in_bg_apt_shell() { runaptcmd_in_bg_apt_shell apt-cache "$@"; }
aptget_in_bg_apt_shell() { runaptcmd_in_bg_apt_shell apt-get "$@"; }
aptmark_in_bg_apt_shell() { runaptcmd_in_bg_apt_shell apt-mark "$@"; }
