#!/bin/bash

#
# Copyright (c) International Business Machines  Corp., 2005
# Authors: Avantika Mathur (mathurav@us.ibm.com)
#          Matt Helsley (matthltc@us.ibm.com)
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

if tst_kvcmp -lt "2.6.15"; then
       tst_resm TCONF "System kernel version is less than 2.6.15"
       tst_resm TCONF "Cannot execute test"
       exit 0
fi

test_setup()
{
	#######################################################################
	## Configure
	#######################################################################
	dopts='-dEBb'

	## Remove logged test state depending on results. 0 means do not remove,
	## 1 means OK to remove.
	# rm saved state from tests that appear to have cleaned up properly?
	rm_ok=1
	# rm saved state from tests that don't appear to have fully cleaned up?
	rm_err=0

	#######################################################################
	## Initialize some variables
	#######################################################################
	TCID="$0"
	TST_COUNT=0

	test_dirs=( move bind rbind regression )  #cloneNS
	nfailed=0
	nsucceeded=0

	# set the LTPROOT directory
	cd `dirname $0`
	LTPROOT="${PWD}"
	echo "${LTPROOT}" | grep testscripts > /dev/null 2>&1
	if [ $? -eq 0 ]; then
		cd ..
		LTPROOT="${PWD}"
	fi

	FS_BIND_ROOT="${LTPROOT}/testcases/bin/fs_bind"

	total=0 # total number of tests
	for dir in "${test_dirs[@]}" ; do
		((total += `ls "${FS_BIND_ROOT}/${dir}/test"* | wc -l`))
	done
	TST_TOTAL=${total}

	# set the PATH to include testcases/bin
	LTPBIN="${LTPROOT}/testcases/bin"
	PATH="${PATH}:/usr/sbin:${LTPBIN}:${FS_BIND_ROOT}/bin"

	# Results directory
	resdir="${LTPROOT}/results/fs_bind"
	if [ ! -d "${resdir}" ]; then
		mkdir -p "${resdir}" 2> /dev/null
	fi

	TMPDIR="${TMPDIR:-/tmp}"
	# A temporary directory where we can do stuff and that is
	# safe to remove
	sandbox="${TMPDIR}/sandbox"

	ERR_MSG=""

	export LTPBIN PATH FS_BIND_ROOT ERR_MSG TCID TST_COUNT TST_TOTAL

	if [ ! -d "${resdir}" ]; then
		tst_brkm TBROK true "$0: failed to make results directory"
		exit 1
	fi
}

test_prereqs()
{
	# Must be root to run the containers testsuite
	if [ $UID != 0 ]; then
		tst_brkm TBROK true "FAILED: Must be root to execute this script"
		exit 1
	fi

	mkdir "${sandbox}" >& /dev/null
	if [ ! -d "${sandbox}" -o ! -x "${sandbox}" ]; then
		tst_brkm TBROK true "$0: failed to make directory \"${sandbox}\""
		exit -1
	fi

	mount --bind "${sandbox}" "${sandbox}" >& /dev/null
	if [ $? -ne 0 ]; then
		tst_brkm TBROK true "$0: failed to perform bind mount on directory \"${sandbox}\""
		exit 1
	fi

	mount --make-private "${sandbox}" >& /dev/null
	if [ $? -ne 0 ]; then
		tst_brkm TBROK true "$0: failed to make private mountpoint on directory \"${sandbox}\""
		exit 1
	fi

	local mnt_bind=1
	local mnt_move=1

	pushd "${sandbox}" > /dev/null && {
		mkdir bind_test move_test && {
			mount --bind bind_test bind_test && {
				mnt_bind=0
				mount --move bind_test move_test && {
					mnt_move=0
					umount move_test
				} || {
					# bind mount succeeded but move mount
					# failed
					umount bind_test
				}
			} || {
				# mount failed -- check if it's because we
				# don't have privileges we need
				if [ $? -eq 32 ]; then
					tst_brkm TBROK true "$0 requires the privilege to use the mount command"
					exit 32
				fi
			}
			rmdir bind_test move_test
		}
		popd > /dev/null
	}

	if [ ${mnt_bind} -eq 1 -o ${mnt_move} -eq 1 ]; then
		tst_brkm TBROK true "$0: requires that mount support the --bind and --move options"
		exit 1
	fi

	if tst_kvcmp -lt "2.6.15"; then
		tst_resm TWARN "$0: the remaining tests require 2.6.15 or later"
		tst_exit 0
		exit
	else
		tst_resm TINFO "$0: kernel >= 2.6.15 detected -- continuing"
	fi

	mount --make-shared "${sandbox}" > /dev/null 2>&1 || "${FS_BIND_ROOT}/bin/smount" "${sandbox}" shared
	umount "${sandbox}" || {
		tst_resm TFAIL "$0: failed to umount simplest shared subtree"
		exit 1
	}
	tst_resm TPASS "$0: umounted simplest shared subtree"

}

# mounts we are concerned with in a well-defined order (helps diff)
# returns grep return codes
grep_proc_mounts()
{
	local rc=0

	# Save the pipefail shell option
	shopt -o -q pipefail
	local save=$?
	set -o pipefail

	# Grep /proc/mounts which is often more up-to-date than mounts
	# We use pipefail because if the grep fails we want to pass that along
	grep -F "${sandbox}" /proc/mounts | sort -b
	rc=$?

	# Restore the pipefail shell options
	[ $save -eq 0 ] && shopt -o -s pipefail || shopt -o -u pipefail

	return $rc
}

# Record the mount state
save_proc_mounts()
{
	touch "$2/proc_mounts.before" >& /dev/null
	if [ $? -ne 0 ]; then
		tst_brkm TBROK true "$1: failed to record proc mounts"
		return 1
	fi

	grep_proc_mounts 2> /dev/null > "$2/proc_mounts.before"
	return 0
}

# Compare mount list after the test with the list from before.
# If there are no differences then remove the before list and silently
# return 0. Else print the differences to stderr and return 1.
check_proc_mounts()
{
	local tname="$1"

	if [ ! -r "$2/proc_mounts.before" ]; then
		tst_brkm TBROK true "${tname}: Could not find pre-test proc mount list"
		return 1
	fi

	grep_proc_mounts 2> /dev/null > "$2/proc_mounts.after"
	# If the mounts are the same then just return
	diff ${dopts} -q "$2/proc_mounts.before" "$2/proc_mounts.after" >& /dev/null
	if [ $? -eq 0 ]; then
		[ $rm_ok -eq 1 ] && rm -f "$2/proc_mounts."{before,after}
		return 0
	fi

	tst_resm TWARN "${tname}: did not properly clean up its proc mounts"
	diff ${dopts} -U 0 "$2/proc_mounts.before" "$2/proc_mounts.after" | grep -vE '^\@\@' 1>&2
	[ $rm_err -eq 1 ] && rm -f "$2/proc_mounts."{before,after}
	return 1
}

# Undo leftover mounts
restore_proc_mounts()
{
	#local tname="$1"

	# do lazy umounts -- we're assuming that tests will only leave
	# new mounts around and will never remove mounts outside the test
	# directory
	( while grep_proc_mounts ; do
		grep_proc_mounts | awk '{print $2}' | xargs -r -n 1 umount -l
	done ) >& /dev/null

	# mount list and exit with 0
	[ $rm_err -eq 1 ] && rm -f "$2/proc_mounts."{before,after} 1>&2 # >& /dev/null
	return 0
	# if returning error do this:
	# tst_brkm TBROK true "${tname}: failed to restore mounts"
}

# mounts we are concerned with in a well-defined order (helps diff)
# returns grep return codes
grep_mounts()
{
	local rc=0

	# Save the pipefail shell option
	shopt -o -q pipefail
	local save=$?
	set -o pipefail

	# Grep mount command output (which tends to come from /etc/mtab)
	# We use pipefail because if the grep fails we want to pass that along
	mount | grep -F "${sandbox}" | sort -b
	rc=$?

	# Restore the pipefail shell options
	[ $save -eq 0 ] && shopt -o -s pipefail || shopt -o -u pipefail

	return $rc
}

# Record the mount state
save_mounts()
{
	touch "$2/mtab.before" >& /dev/null
	if [ $? -ne 0 ]; then
		tst_brkm TBROK true "$1: failed to record mtab mounts"
		return 1
	fi

	grep_mounts 2> /dev/null > "$2/mtab.before"
	return 0
}

# Compare mount list after the test with the list from before.
# If there are no differences then remove the before list and silently
# return 0. Else print the differences to stderr and return 1.
check_mounts()
{
	local tname="$1"

	if [ ! -r "$2/mtab.before" ]; then
		tst_brkm TBROK true "${tname}: Could not find pre-test mtab mount list"
		return 1
	fi

	grep_mounts 2> /dev/null > "$2/mtab.after"
	# If the mounts are the same then just return
	diff ${dopts} -q "$2/mtab.before" "$2/mtab.after" >& /dev/null
	if [ $? -eq 0 ]; then
		[ $rm_ok -eq 1 ] && rm -f "$2/mtab."{before,after}
		return 0
	fi

	tst_resm TWARN "${tname}: did not properly clean up its mtab mounts"
	diff ${dopts} -U 0 "$2/mtab.before" "$2/mtab.after" | grep -vE '^\@\@' 1>&2
	[ $rm_err -eq 1 ] && rm -f "$2/mtab."{before,after}
	return 1
}

# Undo leftover mounts
restore_mounts()
{
	#local tname="$1"

	# do lazy umounts -- we're assuming that tests will only leave
	# new mounts around and will never remove mounts outside the test
	# directory
	( while grep_mounts ; do
		grep_mounts | awk '{print $3}' | xargs -r -n 1 umount -l
	done ) >& /dev/null

	# mount list and exit with 0
	[ $rm_err -eq 1 ] && rm -f "$2/mtab."{before,after} 1>&2 # >& /dev/null
	return 0
	# if returning error do this:
	# tst_brkm TBROK true "${tname}: failed to restore mounts"
}

# Record the sandbox state
# We don't save full sandbox state -- just the names of files and dirs present
save_sandbox()
{
	local when="before"
	local tname="$1"

	if [ -e "$2/files.before" ]; then
		if [ -e "$2/files.after" ]; then
			tst_brkm TBROK true "${tname}: stale catalog of \"${sandbox}\""
			return 1
		fi
		when="after"
	fi

	( find "${sandbox}" -type d -print | sort > "$2/dirs.$when"
	  find "${sandbox}" -type f -print | sort | \
		grep -vE '^'"$2"'/(dirs|files)\.(before|after)$' > "$2/files.$when" ) >& /dev/null
	return 0
}

# Save sandbox after test and then compare. If the sandbox state is not
# clean then print the differences to stderr and return 1. Else remove all
# saved sandbox state and silently return 0
check_sandbox()
{
	local tname="$1"

	if [ ! -r "$2/files.before" -o ! -r "$2/dirs.before" ]; then
		tst_brkm TBROK true "${tname} missing saved catalog of \"${sandbox}\""
		return 1
	fi

	save_sandbox "${tname} (check)" "$2"

	( diff ${dopts} -q "$2/dirs.before" "$2/dirs.after" && \
	  diff ${dopts} -q "$2/files.before" "$2/files.after" )  >& /dev/null \
	  && {
		[ $rm_ok -eq 1 ] && rm -f "$2/"{files,dirs}.{before,after}
		return 0
	}

	tst_resm TWARN "${tname} did not properly clean up \"${sandbox}\""
	diff ${dopts} -U 0 "$2/dirs.before" "$2/dirs.after" 1>&2
	diff ${dopts} -U 0 "$2/files.before" "$2/files.after" 1>&2
	[ $rm_err -eq 1 ] && rm -f "$2/"{files,dirs}.{before,after} 1>&2
	return 1
}

# Robust sandbox cleanup
clean_sandbox()
{
	local tname="$1"

	{ rm -rf "${sandbox}" ; mkdir "${sandbox}" ; } >& /dev/null
	if [ ! -d "${sandbox}" -o ! -x "${sandbox}" ]; then
		tst_brkm TBROK true "$tname: failed to make directory \"${sandbox}\""
		return 1
	fi
	return 0
}

# Check file for non-whitespace chars
is_file_empty()
{
	awk '/^[[:space:]]*$/  { next }
	      { exit 1; }' < "$1"
}

#
# Run the specified test script.
#
# Return 1 if the test was broken but should not stop the remaining test
#	categories from being run.
# Return 2 if the test was broken and no further tests should be run.
# Return 0 otherwise (if the test was broken but it shouldn't affect other
#	test runs)
# Note that this means the return status is not the success or failure of the
#	test itself.
#
run_test()
{
	local t="$1"
	local tname="$(basename "$(dirname "$t")")/$(basename "$t")"
	local log="$resdir/$tname/log"
	local errlog="$resdir/$tname/err"
	local do_break=0

	ERR_MSG=""

	# Pre-test
	mkdir -p "$resdir/$tname"
	if [ ! -d "$resdir/$tname" -o ! -x "$resdir/$tname" ]; then
		tst_brkm TBROK true "$0: can't make or use \"$resdir/$tname\" as a log directory"
		return 1
	fi

	save_sandbox "$tname" "$resdir/$tname" || do_break=1
	save_mounts "$tname" "$resdir/$tname" || do_break=1
	save_proc_mounts "$tname" "$resdir/$tname" || do_break=1
	mount --bind "${sandbox}" "${sandbox}" >& /dev/null || do_break=1
	mount --make-private "${sandbox}" >& /dev/null || do_break=1

	if [ $do_break -eq 1 ]; then
		umount -l "${sandbox}" >& /dev/null
		tst_brkm TBROK true "$tname: failed to save pre-test state of \"${sandbox}\""
		return 2
	fi
	pushd "${sandbox}" > /dev/null

	# Run the test
	(
		TCID="$tname"
		declare -r TST_COUNT
		export LTPBIN PATH FS_BIND_ROOT ERR_MSG TCID TST_COUNT TST_TOTAL
		"$t" #> "$log" 2> "$errlog"
	)
	local rc=$?
	TCID="$0"

	# Post-test
	popd > /dev/null
	if [ $rc -ne 0 ]; then
		#echo "FAILED"
		((nfailed++))
	else
		#echo "SUCCEEDED"
		((nsucceeded++))
	fi
	umount -l "${sandbox}" >& /dev/null
	check_proc_mounts "$tname" "$resdir/$tname" || \
	restore_proc_mounts "$tname" "$resdir/$tname" || do_break=1
	check_mounts "$tname" "$resdir/$tname" || \
	restore_mounts "$tname" "$resdir/$tname" || do_break=1
	check_sandbox "$tname" "$resdir/$tname"
	clean_sandbox "$tname" || do_break=1
	if [ $do_break -eq 1 ]; then
		tst_brkm TBROK true "$tname: failed to restore pre-test state of \"${sandbox}\""
		return 2
	fi

	# If we succeeded and the error log is empty remove it
	if [ $rc -eq 0 -a -w "$errlog" ] && is_file_empty "$errlog" ; then
		rm -f "$errlog"
	fi
	return 0
}

main()
{
	TST_COUNT=1
	for dir in "${test_dirs[@]}" ; do
		tests=( $(find "${FS_BIND_ROOT}/${dir}" -type f -name 'test*') )
		clean_sandbox "$0" || break
		for t in "${tests[@]}" ; do
			run_test "$t"
			local rc=$?

			if [ $rc -ne 0 ]; then
				break $rc
			fi

			((TST_COUNT++))
		done
	done
	rm -rf "${sandbox}"
	return 0

	skipped=$((total - nsucceeded - nfailed))
	if [ $nfailed -eq 0 -a $skipped -eq 0 ]; then
		# Use PASSED for the summary rather than SUCCEEDED to make it
		# easy to determine 100% success from a calling script
		summary="PASSED"
	else
		# Use FAILED to make it easy to find > 0% failure from a
		# calling script
		summary="FAILED"
	fi
	cat - <<-EOF
		*********************************
		RESULTS SUMMARY:

			passed:  $nsucceeded/$total
			failed:  $nfailed/$total
			skipped: $skipped/$total
			summary: $summary

		*********************************
	EOF
}

test_setup || exit 1
test_prereqs || exit 1
declare -r FS_BIND_ROOT
declare -r TST_TOTAL
main  #2> "$resdir/errors" 1> "$resdir/summary"