# $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 }