#! /usr/bin/atf-sh
# $NetBSD: t_realpath.sh,v 1.1 2022/07/21 09:52:49 kre Exp $
#
# Copyright (c) 2022 The NetBSD Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

# ===========================================================
#
# Test data and expected results

# Note that the empty line calls realpath with no file arg
existing='.

../Dir/StdOut
./S1
./S1/../S4
./Snr/../S4
S1/S2/File
S1/S3/Link
Snr/HoHo
Snr/Link
L
/
/bin
Self
Self/
S4/S1/'

exist_results='Dir
Dir
Dir/StdOut
Dir/S1
Dir/S4
Dir/S4
Dir/S1/S2/File
Dir/S1/S2/File
Dir/Snr/HoHo
Dir/S1
Dir/StdOut
/
/bin
Dir
Dir
Dir/S1'

exist_root_only='Snx/HaHa
Snx/Link'

exist_root_results='Dir/Snx/HaHa
Dir/S1/S2/File'

nofile='-
trash
Snr/Haha
T1
T2
T3
T4
T5
../Dir/T2
../Dir/T3
/nonsense
/bin/../nonsense
./Self/Self/Self/Self/S1/../Self/../Dir/Self/T1
Self/nonsense'

nofile_results='Dir/-
Dir/trash
Dir/Snr/Haha
Dir/NoSuchFile
Dir/S1/NoSuchFile
Dir/S1/NoSuchFile
Dir/S1/S2/NoSuchFile
Dir/S1/S2/NoSuchFile
Dir/S1/NoSuchFile
Dir/S1/NoSuchFile
/nonsense
/nonsense
Dir/NoSuchFile
Dir/nonsense'

always_fail='StdOut/
StdOut/../StdErr
Loop
S1/S5/Link
Loop/../StdOut
BigLoop
U1
U2
U3
U4
U5
U6
U7
U8
U9
T1/NoSuchFile
T1/../NoSuchFile
U9/../NoSuchFile
U9/../StdOut'


# ===========================================================
# Helper functions
#

# Create the test environment
setup()
{
	atf_require_prog /usr/bin/mktemp
	atf_require_prog /bin/ln
	atf_require_prog /bin/cp
	atf_require_prog /bin/mkdir
	atf_require_prog /bin/chmod

	DIR=${PWD}/$(mktemp -d Dir.XXXXX) ||
		atf_fail "Did not make test directory"
	cd "${DIR}" || atf_fail "Unable to cd $DIR"

	ID=$( set -- $( type id ) && test "$1" = id && test "$2" = is &&
		test $# -eq 3 && printf %s "$3"  || printf no-id-program)

	mkdir Dir && cd Dir			|| atf_fail "enter Dir"

	>StdOut					|| atf_fail "setup StdOut"
	>StdErr					|| atf_fail "setup StdErr"
	ln -s ../Dir Dir			|| atf_fail "setup Dir"
	ln -s Loop Loop				|| atf_fail "setup Loop"
	ln -s . Self				|| atf_fail "setup Self"
	mkdir S1 S1/S2 S1/S3 S4 S4/S5		|| atf_fail "setup subdirs"
	echo S1/S2/File > S1/S2/File		|| atf_fail "setup File"
	ln -s ../S2/File S1/S3/Link		|| atf_fail "setup S3/Link"
	ln -s ../S1 S4/S1			|| atf_fail "setup S4/S1"
	ln -s StdOut L1				|| atf_fail "setup L1"
	ln -s L1 L2				|| atf_fail "setup L2"
	ln -s ../L2 S1/L3			|| atf_fail "setup L3"
	ln -s ../L3 S1/S2/L4			|| atf_fail "setup L4"
	ln -s ../S2/L4 S1/S3/L5			|| atf_fail "setup L5"
	ln -s S1/S3/L5 L			|| atf_fail "setup L"
	ln -s ${PWD}/S1 S4/PWDS1		|| atf_fail "setup PWDS1"
	ln -s ${PWD}/S9 S4/PWDS9		|| atf_fail "setup PWDS9"
	ln -s ${PWD}/S9/File S4/PWDS9F		|| atf_fail "setup PWDS9F"
	ln -s ../S4/BigLoop S1/BigLoop		|| atf_fail "setup S1/BigLoop"
	ln -s ../BigLoop S4/BigLoop		|| atf_fail "setup S4/BigLoop"
	ln -s "${DIR}"/Dir/S1/BigLoop BigLoop	|| atf_fail "setup BigLoop"
	mkdir Snx				|| atf_fail "setup Snx"
	cp /dev/null Snx/HaHa			|| atf_fail "setup Snx/HaHa"
	ln -s "${DIR}"/Dir/S1/S2/File Snx/Link	|| atf_fail "setup Snx/Link"
	mkdir Snr				|| atf_fail "setup Snr"
	cp /dev/null Snr/HoHo			|| atf_fail "setup Snr/HoHo"
	ln -s "${DIR}"/Dir/S4/PWDS1 Snr/Link	|| atf_fail "setup Snr/Link"
	ln -s ../Snx/HaHa Snr/HaHa		|| atf_fail "setup HaHa"
	ln -s "${DIR}"/Dir/NoSuchFile T1	|| atf_fail "setup T1"
	ln -s "${DIR}"/Dir/S1/NoSuchFile T2	|| atf_fail "setup T2"
	ln -s S1/NoSuchFile T3			|| atf_fail "setup T3"
	ln -s "${DIR}"/Dir/S1/S2/NoSuchFile T4	|| atf_fail "setup T4"
	ln -s S1/S2/NoSuchFile T5		|| atf_fail "setup T5"
	ln -s "${DIR}"/Dir/StdOut/CannotExist T6 || atf_fail "setup T6"
	ln -s "${DIR}"/Dir/NoDir/WhoKnows U1	|| atf_fail "setup U1"
	ln -s "${DIR}"/Dir/S1/NoDir/WhoKnows U2	|| atf_fail "setup U2"
	ln -s "${DIR}"/Dir/S1/S2/NoDir/WhoKnows U3 || atf_fail "setup U3"
	ln -s "${DIR}"/Dir/S1/../NoDir/WhoKnows U4 || atf_fail "setup U4"
	ln -s "${DIR}"/Dir/NoDir/../StdOut U5	|| atf_fail "setup U5"
	ln -s NoDir/../StdOut U6		|| atf_fail "setup U6"
	ln -s S1/NoDir/../../StdOut U7		|| atf_fail "setup U7"
	ln -s "${DIR}"/Dir/Missing/NoDir/WhoKnows U8 || atf_fail "setup U8"
	ln -s "${DIR}"/Dir/Missing/NoDir/../../StdOut U9 || atf_fail "setup U9"
	chmod a+r,a-x Snx			|| atf_fail "setup a-x "
	chmod a+x,a-r Snr			|| atf_fail "setup a-r"
}

# ATF will remove all the files we made, just ensure perms are OK
cleanup()
{
	chmod -R u+rwx .
	return 0
}

run_tests_pass()
{
	opt=$1
	tests=$2
	results=$3

	FAILS=
	FAILURES=0
	T=0

	while [ "${#tests}" -gt 0 ]
	do
		FILE=${tests%%$'\n'*}
		EXP=${results%%$'\n'*}

		tests=${tests#"${FILE}"};	tests=${tests#$'\n'}
		results=${results#"${EXP}"};	results=${results#$'\n'}

		test -z "${EXP}" && atf_fail "Too few results (test botch)"

		T=$(( $T + 1 ))

		GOT=$(realpath $opt -- ${FILE:+"${FILE}"})
		STATUS=$?

		case "${GOT}" in
		'')	;;		# nothing printed, deal with that below

		/*)			# Full Path (what we want)
			# Remove the unpredictable ATF dir prefix (if present)
			GOT=${GOT#"${DIR}/"}
			# Now it might be a relative path, that's OK
			# at least it can be compared (its prefix is known)
			;;

		*)			# a relative path was printed
			FAILURES=$(($FAILURES + 1))
			FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
			FAILS="${FAILS}${opt:+ $opt} '${FILE}'"
			FAILS="${FAILS}: output relative path '${GOT}'"
			FAILS="${FAILS}, and exit($STATUS)"
			continue
			;;
		esac


		if [ $STATUS -ne 0 ] || [ "${EXP}" != "${GOT}" ]
		then
			FAILURES=$(($FAILURES + 1))
			if [ $STATUS -ne 0 ]
			then
			    FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
			    FAILS="${FAILS}${opt:+ $opt} '${FILE}'"
			    FAILS="${FAILS} failed: status ${STATUS}"
			else
			    FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
			    FAILS="${FAILS}${opt:+ $opt} '${FILE}'"
			    FAILS="${FAILS} expected '${EXP}' received '${GOT}'"
			fi
		fi
	done

	if test  -n "${results}"
	then
		FAILURES=$(( $FAILURES + 1 ))

		N=$(( $(printf '%s\n' "${results}" | wc -l) ))
		s=s; if [ $N -eq 1 ]; then s=; fi
		FAILS=${FAILS:+"${FAILS}"$'\n'}"After $T tests"
		FAILS="still $N more result$s (test botch)"
	fi

	if [ $FAILURES -gt 0 ]
	then
		s=s
		if [ $FAILURES -eq 1 ]; then s=; fi
		printf >&2 '%d path%s resolved incorrectly:\n%s\n' \
			"$FAILURES" "$s" "${FAILS}"
		atf_fail "$FAILURES path$s resolved incorrectly; see stderr"
	fi
	return 0
}

run_tests_fail()
{
	opt=$1
	tests=$2

	FAILS=
	FAILURES=0
	T=0

	while [ "${#tests}" -gt 0 ]
	do
		FILE=${tests%%$'\n'*}

		tests=${tests#"${FILE}"};	tests=${tests#$'\n'}

		test -z "${FILE}" && continue

		T=$(( $T + 1 ))

		GOT=$(realpath $opt -- "${FILE}" 2>StdErr)
		STATUS=$?

		ERR=$(cat StdErr)

		if [ $STATUS -eq 0 ] || [ "${GOT}" ] || ! [ "${ERR}" ]
		then
			FAILURES=$(($FAILURES + 1))
			if [ "${STATUS}" -eq 0 ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T: "
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' worked;"
				FAILS="${FAILS} received: '${GOT}'}"

				if [ "${ERR}" ]; then
					FAILS="${FAILS} and on stderr '${ERR}'"
				fi
			elif [ "${GOT}" ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' failed,"
				FAILS="${FAILS} but with '${GOT}' on stdout"

				if [ "${ERR}" ]; then
					FAILS="${FAILS}, and on stderr '${ERR}'"
				else
					FAILS="${FAILS}, and empty stderr"
				fi
			else
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' failed,"
				FAILS="${FAILS} but with no error message"
			fi
		fi
	done
	if [ $FAILURES -gt 0 ]
	then
		S=s
		if [ $FAILURES -eq 1 ]; then s=; fi
		printf >&2 '%d path%s resolved incorrectly:\n%s\n' \
			"$FAILURES" "$s" "${FAILS}"
		atf_fail "$FAILURES path$s resolved incorrectly; see stderr"
	fi
	return 0
}

# ===================================================================
# Test cases setup follows (but almost all the work is earlier)

atf_test_case a__e_ok cleanup
realpath_e_ok_head()
{
	atf_set descr "Test realpath (with -e) cases which should work"
}
a__e_ok_body()
{
	setup
	run_tests_pass -e "${existing}" "${exist_results}"

	if [ -x "${ID}" ] && [ "$("$ID" -u)" = 0 ]
	then
		run_tests_pass -e "${exist_root_only}" "${exist_root_results}"
	fi
}
a__e_ok_cleanup()
{
	cleanup
}

atf_test_case b__E_ok cleanup
b__E_ok_head()
{
	atf_set descr "Test realpath (with -E) cases which should work"
}
b__E_ok_body() {
	setup
	# everything which works with -e should also work with -E
	run_tests_pass -E "${existing}" "${exist_results}"
	run_tests_pass -E "${nofile}" "${nofile_results}"

	if [ -x "${ID}" ] && [ "$("${ID}" -u)" = 0 ]
	then
		run_tests_pass -E "${exist_root_only}" "${exist_root_results}"
	fi
}
b__E_ok_cleanup()
{
	cleanup
}

atf_test_case c__ok cleanup
c__ok_head()
{
	atf_set descr "Test realpath (without -e or -E) cases which should work"
}
c__ok_body() {
	setup
	# Our default for realpath is -E, so the -E tests should work
	run_tests_pass '' "${existing}" "${exist_results}"
	# but more should work as well
	run_tests_pass '' "${nofile}" "${nofile_results}"

	if [ -x "${ID}" ] && [ "$("${ID}" -u)" = 0 ]
	then
		run_tests_pass '' "${exist_root_only}" "${exist_root_results}"
	fi
}
c__ok_cleanup()
{
	cleanup
}

atf_test_case d__E_fail
d__E_fail_head()
{
	atf_set descr "Test realpath -e cases which should not work"
}
d__E_fail_body()
{
	setup
	run_tests_fail -E "${always_fail}"
	if [ -x "${ID}" ] && [ "$("${ID}" -u)" != 0 ]
	then
		run_tests_fail -E "${exist_root_only}"
	fi
}
d__E_fail_cleanup()
{
	cleanup
}

atf_test_case e__e_fail
e__e_fail_head()
{
	atf_set descr "Test realpath -e cases which should not work"
}
e__e_fail_body()
{
	setup
	# Some -E tests that work should fail with -e
	run_tests_fail -e "${nofile}"
	run_tests_fail -e "${always_fail}"
	if [ -x "${ID}" ] && [ "$("${ID}" -u)" != 0 ]
	then
		run_tests_fail -e "${exist_root_only}"
	fi
}
e__e_fail_cleanup()
{
	cleanup
}

atf_test_case f__fail
f__fail_head()
{
	atf_set descr "Test realpath cases which should not work (w/o -[eE])"
}
f__fail_body()
{
	setup
	run_tests_fail '' "${always_fail}"
	if [ -x "${ID}" ] && [ "$("${ID}" -u)" != 0 ]
	then
		run_tests_fail '' "${exist_root_only}"
	fi
}
f__fail_cleanup()
{
	cleanup
}

atf_test_case g__q cleanup
g__q_head()
{
	atf_set descr "Test realpath's -q option; also test usage message"
}
g__q_body()
{
	setup

	# Just run these tests here, the paths have been tested
	# already, all we care about is that -q suppresses err messages
	# about the ones that fail, so just test those.  Since those
	# always fail, it is irrlevant which of -e or -E we would use,
	# so simply use neither.

	# This is adapted from run_tests_fail

	FAILURES=0
	FAILS=

	opt=-q

	T=0
	for FILE in ${always_fail}
	do

		test -z "${FILE}" && continue

		T=$(( $T + 1 ))

		GOT=$(realpath $opt -- "${FILE}" 2>StdErr)
		STATUS=$?

		ERR=$(cat StdErr)

		if [ $STATUS -eq 0 ] || [ "${GOT}" ] || [ "${ERR}" ]
		then
			FAILURES=$(($FAILURES + 1))
			if [ "${STATUS}" -eq 0 ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T: "
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' worked;"
				FAILS="${FAILS} received: '${GOT}'}"

				if [ "${ERR}" ]; then
					FAILS="${FAILS} and on stderr '${ERR}'"
				fi
			elif [ "${GOT}" ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' failed,"
				FAILS="${FAILS} but with '${GOT}' on stdout"

				if [ "${ERR}" ]; then
					FAILS="${FAILS}, and on stderr '${ERR}'"
				else
					FAILS="${FAILS}, and empty stderr"
				fi
			else
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} '${FILE}' failed,"
				FAILS="${FAILS} stderr: '${ERR}'"
			fi
		fi
	done

	# There are a couple of cases where -q does not suppress stderr

	for FILE in '' -wObBl@ --
	do

		T=$(( $T + 1 ))

		unset XTRA
		case "${FILE}" in
		'')	;;
		--)	XTRA=;;
		-*)	XTRA=/junk;;
		esac

		# Note lack of -- in the following, so $FILE can be either
		# a file name (well, kind of...), or options.

		GOT=$(realpath $opt "${FILE}" ${XTRA+"${XTRA}"} 2>StdErr)
		STATUS=$?

		ERR=$(cat StdErr)

		if [ $STATUS -eq 0 ] || [ "${GOT}" ] || ! [ "${ERR}" ]
		then
			FAILURES=$(($FAILURES + 1))
			if [ "${STATUS}" -eq 0 ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T: "
				FAILS="${FAILS}${opt:+ $opt} ${FILE:-''}"
				FAILS="${FAILS}${XTRA:+ $XTRA} worked;"
				FAILS="${FAILS} received: '${GOT}'}"

				if [ "${ERR}" ]; then
					FAILS="${FAILS} and on stderr '${ERR}'"
				fi
			elif [ "${GOT}" ]
			then
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} ${FILE:-''}"
				FAILS="${FAILS}${XTRA:+ ${XTRA}} failed,"
				FAILS="${FAILS} but with '${GOT}' on stdout"

				if [ "${ERR}" ]; then
					FAILS="${FAILS}, and on stderr '${ERR}'"
				else
					FAILS="${FAILS}, and empty stderr"
				fi
			else
				FAILS=${FAILS:+"${FAILS}"$'\n'}"Path $T:"
				FAILS="${FAILS}${opt:+ $opt} ${FILE:-''}"
				FAILS="${FAILS}${XTRA:+ ${XTRA}} failed,"
				FAILS="${FAILS} with stderr empty"
			fi
		fi
	done

	if [ $FAILURES -gt 0 ]
	then
		s=s
		if [ $FAILURES -eq 1 ]; then s=; fi
		printf >&2 '%d path%s resolved incorrectly:\n%s\n' \
			"$FAILURES" "$s" "${FAILS}"
		atf_fail "$FAILURES path$s resolved incorrectly; see stderr"
	fi
	return 0
}
g__q_cleanup()
{
	cleanup
}

atf_test_case h__n_args
h__n_args_head()
{
	atf_set descr "Test realpath with multiple file args"
}
h__n_args_body()
{
	setup

	# Since these paths have already (hopefully) tested and work
	# (if a__e_ok had any failures, fix those before even looking
	# at any failure here)

	# Since we are assuming that the test cases all work, simply
	# Count how many there are, and then expect the same number
	# of answers

	unset IFS
	set -- ${existing}
	# Note that any empty line (no args) case just vanished...
	# That would be meaningless here, removing it is correct.

 	GOT=$(realpath -e -- "$@" 2>StdErr)
	STATUS=$?

	ERR=$(cat StdErr; printf X)
	ERR=${ERR%X}

	NR=$(( $(printf '%s\n' "${GOT}" | wc -l) ))

	if [ $NR -ne $# ] || [ $STATUS -ne 0 ] || [ -s StdErr ]
	then
		printf >&2 'Stderr from test:\n%s\n' "${ERR}"
		if [ $STATUS -eq 0 ]; then S="OK"; else S="FAIL($STATUS)"; fi
		if [ ${#ERR} -ne 0 ]
		then
			E="${#ERR} bytes on stderr"
		else
			E="nothing on stderr"
		fi
		atf_fail 'Given %d args, got %d results; Status:%s; %s\n' \
			"$#" "${NR}" "${S}" "${E}"
	fi
	return 0
}
h__n_args_cleanup()
{
	cleanup
}

atf_init_test_cases()
{
	atf_add_test_case a__e_ok
	atf_add_test_case b__E_ok
	atf_add_test_case c__ok
	atf_add_test_case d__E_fail
	atf_add_test_case e__e_fail
	atf_add_test_case f__fail
	atf_add_test_case g__q
	atf_add_test_case h__n_args
}
